b2e4b5034abeb87c22811c0939b73d4eb34fa699
[pub/Android/ownCloud.git] / src / com / owncloud / android / ui / fragment / OCFileListFragment.java
1 /* ownCloud Android client application
2 * Copyright (C) 2011 Bartek Przybylski
3 * Copyright (C) 2012-2014 ownCloud Inc.
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 2,
7 * as published by the Free Software Foundation.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 */
18 package com.owncloud.android.ui.fragment;
19
20 import java.io.File;
21 import java.util.ArrayList;
22 import java.util.List;
23
24 import com.owncloud.android.R;
25 import com.owncloud.android.authentication.AccountUtils;
26 import com.owncloud.android.datamodel.FileDataStorageManager;
27 import com.owncloud.android.datamodel.OCFile;
28 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
29 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
30 import com.owncloud.android.lib.common.operations.OnRemoteOperationListener;
31 import com.owncloud.android.lib.common.operations.RemoteOperation;
32 import com.owncloud.android.operations.RemoveFileOperation;
33 import com.owncloud.android.operations.RenameFileOperation;
34 import com.owncloud.android.operations.SynchronizeFileOperation;
35 import com.owncloud.android.ui.activity.FileDisplayActivity;
36 import com.owncloud.android.ui.activity.TransferServiceGetter;
37 import com.owncloud.android.ui.adapter.FileListListAdapter;
38 import com.owncloud.android.ui.dialog.EditNameDialog;
39 import com.owncloud.android.ui.dialog.EditNameDialog.EditNameDialogListener;
40 import com.owncloud.android.ui.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
41 import com.owncloud.android.ui.preview.PreviewImageFragment;
42 import com.owncloud.android.ui.preview.PreviewMediaFragment;
43 import com.owncloud.android.utils.Log_OC;
44
45 import android.accounts.Account;
46 import android.app.Activity;
47 import android.os.Bundle;
48 import android.os.Handler;
49 import android.view.ContextMenu;
50 import android.view.MenuInflater;
51 import android.view.MenuItem;
52 import android.view.View;
53 import android.widget.AdapterView;
54 import android.widget.AdapterView.AdapterContextMenuInfo;
55
56 /**
57 * A Fragment that lists all files and folders in a given path.
58 *
59 * @author Bartek Przybylski
60 *
61 */
62 public class OCFileListFragment extends ExtendedListFragment implements EditNameDialogListener, ConfirmationDialogFragmentListener {
63
64 private static final String TAG = OCFileListFragment.class.getSimpleName();
65
66 private static final String MY_PACKAGE = OCFileListFragment.class.getPackage() != null ? OCFileListFragment.class.getPackage().getName() : "com.owncloud.android.ui.fragment";
67 private static final String EXTRA_FILE = MY_PACKAGE + ".extra.FILE";
68
69 private static final String KEY_INDEXES = "INDEXES";
70 private static final String KEY_FIRST_POSITIONS= "FIRST_POSITIONS";
71 private static final String KEY_TOPS = "TOPS";
72
73 private OCFileListFragment.ContainerActivity mContainerActivity;
74
75 private OCFile mFile = null;
76 private FileListListAdapter mAdapter;
77
78 private Handler mHandler;
79 private OCFile mTargetFile;
80
81 private ArrayList<Integer> mIndexes;
82 private ArrayList<Integer> mFirstPositions;
83 private ArrayList<Integer> mTops;
84
85
86 /**
87 * {@inheritDoc}
88 */
89 @Override
90 public void onAttach(Activity activity) {
91 super.onAttach(activity);
92 Log_OC.e(TAG, "onAttach");
93 try {
94 mContainerActivity = (ContainerActivity) activity;
95 } catch (ClassCastException e) {
96 throw new ClassCastException(activity.toString() + " must implement " + OCFileListFragment.ContainerActivity.class.getSimpleName());
97 }
98 }
99
100
101 /**
102 * {@inheritDoc}
103 */
104 @Override
105 public void onActivityCreated(Bundle savedInstanceState) {
106 super.onActivityCreated(savedInstanceState);
107 Log_OC.e(TAG, "onActivityCreated() start");
108 mAdapter = new FileListListAdapter(getActivity(), mContainerActivity);
109 if (savedInstanceState != null) {
110 mFile = savedInstanceState.getParcelable(EXTRA_FILE);
111 mIndexes = savedInstanceState.getIntegerArrayList(KEY_INDEXES);
112 mFirstPositions = savedInstanceState.getIntegerArrayList(KEY_FIRST_POSITIONS);
113 mTops = savedInstanceState.getIntegerArrayList(KEY_TOPS);
114
115 } else {
116 mIndexes = new ArrayList<Integer>();
117 mFirstPositions = new ArrayList<Integer>();
118 mTops = new ArrayList<Integer>();
119
120 }
121
122 setListAdapter(mAdapter);
123
124 registerForContextMenu(getListView());
125 getListView().setOnCreateContextMenuListener(this);
126
127 mHandler = new Handler();
128
129 }
130
131 /**
132 * Saves the current listed folder.
133 */
134 @Override
135 public void onSaveInstanceState (Bundle outState) {
136 super.onSaveInstanceState(outState);
137 outState.putParcelable(EXTRA_FILE, mFile);
138 outState.putIntegerArrayList(KEY_INDEXES, mIndexes);
139 outState.putIntegerArrayList(KEY_FIRST_POSITIONS, mFirstPositions);
140 outState.putIntegerArrayList(KEY_TOPS, mTops);
141 }
142
143 /**
144 * Call this, when the user presses the up button.
145 *
146 * Tries to move up the current folder one level. If the parent folder was removed from the database,
147 * it continues browsing up until finding an existing folders.
148 *
149 * return Count of folder levels browsed up.
150 */
151 public int onBrowseUp() {
152 OCFile parentDir = null;
153 int moveCount = 0;
154
155 if(mFile != null){
156 FileDataStorageManager storageManager = mContainerActivity.getStorageManager();
157
158 String parentPath = null;
159 if (mFile.getParentId() != FileDataStorageManager.ROOT_PARENT_ID) {
160 parentPath = new File(mFile.getRemotePath()).getParent();
161 parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ? parentPath : parentPath + OCFile.PATH_SEPARATOR;
162 parentDir = storageManager.getFileByPath(parentPath);
163 moveCount++;
164 } else {
165 parentDir = storageManager.getFileByPath(OCFile.ROOT_PATH); // never returns null; keep the path in root folder
166 }
167 while (parentDir == null) {
168 parentPath = new File(parentPath).getParent();
169 parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ? parentPath : parentPath + OCFile.PATH_SEPARATOR;
170 parentDir = storageManager.getFileByPath(parentPath);
171 moveCount++;
172 } // exit is granted because storageManager.getFileByPath("/") never returns null
173 mFile = parentDir;
174 }
175
176 if (mFile != null) {
177 listDirectory(mFile);
178
179 mContainerActivity.startSyncFolderOperation(mFile);
180
181 // restore index and top position
182 restoreIndexAndTopPosition();
183
184 } // else - should never happen now
185
186 return moveCount;
187 }
188
189 /*
190 * Restore index and position
191 */
192 private void restoreIndexAndTopPosition() {
193 int index = mIndexes.get(mIndexes.size() - 1);
194 mIndexes.remove(mIndexes.size() - 1);
195
196 int firstPosition = mFirstPositions.get(mFirstPositions.size() - 1);
197 mFirstPositions.remove(mFirstPositions.size() -1);
198
199 int top = mTops.get(mTops.size() - 1);
200 mTops.remove(mTops.size() - 1);
201
202 mList.setSelectionFromTop(firstPosition, top);
203
204 // Move the scroll if the selection is not visible
205 View view = mList.getChildAt(0);
206 int indexPosition = view.getHeight()*index;
207 int height = mList.getHeight();
208
209 if (indexPosition > height) {
210 mList.smoothScrollToPosition(index);
211 }
212 }
213
214 /*
215 * Save index and top position
216 */
217 private void saveIndexAndTopPosition(int index) {
218
219 mIndexes.add(index);
220
221 int firstPosition = mList.getFirstVisiblePosition();
222 mFirstPositions.add(firstPosition);
223
224 View view = mList.getChildAt(0);
225 int top = (view == null) ? 0 : view.getTop() ;
226
227 mTops.add(top);
228 }
229
230 @Override
231 public void onItemClick(AdapterView<?> l, View v, int position, long id) {
232 OCFile file = (OCFile) mAdapter.getItem(position);
233 if (file != null) {
234 if (file.isFolder()) {
235 // update state and view of this fragment
236 listDirectory(file);
237 // then, notify parent activity to let it update its state and view, and other fragments
238 mContainerActivity.onBrowsedDownTo(file);
239 // save index and top position
240 saveIndexAndTopPosition(position);
241
242 } else { /// Click on a file
243 if (PreviewImageFragment.canBePreviewed(file)) {
244 // preview image - it handles the download, if needed
245 mContainerActivity.startImagePreview(file);
246
247 } else if (file.isDown()) {
248 if (PreviewMediaFragment.canBePreviewed(file)) {
249 // media preview
250 mContainerActivity.startMediaPreview(file, 0, true);
251 } else {
252 FileDisplayActivity activity = (FileDisplayActivity) getSherlockActivity();
253 activity.getFileOperationsHelper().openFile(file, activity);
254 }
255
256 } else {
257 // automatic download, preview on finish
258 mContainerActivity.startDownloadForPreview(file);
259 }
260
261 }
262
263 } else {
264 Log_OC.d(TAG, "Null object in ListAdapter!!");
265 }
266
267 }
268
269 /**
270 * {@inheritDoc}
271 */
272 @Override
273 public void onCreateContextMenu (ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
274 super.onCreateContextMenu(menu, v, menuInfo);
275 MenuInflater inflater = getActivity().getMenuInflater();
276 inflater.inflate(R.menu.file_actions_menu, menu);
277 AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
278 OCFile targetFile = (OCFile) mAdapter.getItem(info.position);
279 List<Integer> toHide = new ArrayList<Integer>();
280 List<Integer> toDisable = new ArrayList<Integer>();
281
282 MenuItem item = null;
283 if (targetFile.isFolder()) {
284 // contextual menu for folders
285 toHide.add(R.id.action_open_file_with);
286 toHide.add(R.id.action_download_file);
287 toHide.add(R.id.action_cancel_download);
288 toHide.add(R.id.action_cancel_upload);
289 toHide.add(R.id.action_sync_file);
290 toHide.add(R.id.action_see_details);
291 toHide.add(R.id.action_send_file);
292 if ( mContainerActivity.getFileDownloaderBinder().isDownloading(AccountUtils.getCurrentOwnCloudAccount(getActivity()), targetFile) ||
293 mContainerActivity.getFileUploaderBinder().isUploading(AccountUtils.getCurrentOwnCloudAccount(getActivity()), targetFile) ) {
294 toDisable.add(R.id.action_rename_file);
295 toDisable.add(R.id.action_remove_file);
296
297 }
298
299 } else {
300 // contextual menu for regular files
301
302 // new design: 'download' and 'open with' won't be available anymore in context menu
303 toHide.add(R.id.action_download_file);
304 toHide.add(R.id.action_open_file_with);
305
306 if (targetFile.isDown()) {
307 toHide.add(R.id.action_cancel_download);
308 toHide.add(R.id.action_cancel_upload);
309
310 } else {
311 toHide.add(R.id.action_sync_file);
312 }
313 if ( mContainerActivity.getFileDownloaderBinder().isDownloading(AccountUtils.getCurrentOwnCloudAccount(getActivity()), targetFile)) {
314 toHide.add(R.id.action_cancel_upload);
315 toDisable.add(R.id.action_rename_file);
316 toDisable.add(R.id.action_remove_file);
317
318 } else if ( mContainerActivity.getFileUploaderBinder().isUploading(AccountUtils.getCurrentOwnCloudAccount(getActivity()), targetFile)) {
319 toHide.add(R.id.action_cancel_download);
320 toDisable.add(R.id.action_rename_file);
321 toDisable.add(R.id.action_remove_file);
322
323 } else {
324 toHide.add(R.id.action_cancel_download);
325 toHide.add(R.id.action_cancel_upload);
326 }
327 }
328
329 // Options shareLink
330 if (!targetFile.isShareByLink()) {
331 toHide.add(R.id.action_unshare_file);
332 }
333
334 // Send file
335 boolean sendEnabled = getString(R.string.send_files_to_other_apps).equalsIgnoreCase("on");
336 if (!sendEnabled) {
337 toHide.add(R.id.action_send_file);
338 }
339
340 for (int i : toHide) {
341 item = menu.findItem(i);
342 if (item != null) {
343 item.setVisible(false);
344 item.setEnabled(false);
345 }
346 }
347
348 for (int i : toDisable) {
349 item = menu.findItem(i);
350 if (item != null) {
351 item.setEnabled(false);
352 }
353 }
354 }
355
356
357 /**
358 * {@inhericDoc}
359 */
360 @Override
361 public boolean onContextItemSelected (MenuItem item) {
362 AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
363 mTargetFile = (OCFile) mAdapter.getItem(info.position);
364 switch (item.getItemId()) {
365 case R.id.action_share_file: {
366 FileDisplayActivity activity = (FileDisplayActivity) getSherlockActivity();
367 activity.getFileOperationsHelper().shareFileWithLink(mTargetFile, activity);
368 return true;
369 }
370 case R.id.action_unshare_file: {
371 FileDisplayActivity activity = (FileDisplayActivity) getSherlockActivity();
372 activity.getFileOperationsHelper().unshareFileWithLink(mTargetFile, activity);
373 return true;
374 }
375 case R.id.action_rename_file: {
376 String fileName = mTargetFile.getFileName();
377 int extensionStart = mTargetFile.isFolder() ? -1 : fileName.lastIndexOf(".");
378 int selectionEnd = (extensionStart >= 0) ? extensionStart : fileName.length();
379 EditNameDialog dialog = EditNameDialog.newInstance(getString(R.string.rename_dialog_title), fileName, 0, selectionEnd, this);
380 dialog.show(getFragmentManager(), EditNameDialog.TAG);
381 return true;
382 }
383 case R.id.action_remove_file: {
384 int messageStringId = R.string.confirmation_remove_alert;
385 int posBtnStringId = R.string.confirmation_remove_remote;
386 int neuBtnStringId = -1;
387 if (mTargetFile.isFolder()) {
388 messageStringId = R.string.confirmation_remove_folder_alert;
389 posBtnStringId = R.string.confirmation_remove_remote_and_local;
390 neuBtnStringId = R.string.confirmation_remove_folder_local;
391 } else if (mTargetFile.isDown()) {
392 posBtnStringId = R.string.confirmation_remove_remote_and_local;
393 neuBtnStringId = R.string.confirmation_remove_local;
394 }
395 ConfirmationDialogFragment confDialog = ConfirmationDialogFragment.newInstance(
396 messageStringId,
397 new String[]{mTargetFile.getFileName()},
398 posBtnStringId,
399 neuBtnStringId,
400 R.string.common_cancel);
401 confDialog.setOnConfirmationListener(this);
402 confDialog.show(getFragmentManager(), FileDetailFragment.FTAG_CONFIRMATION);
403 return true;
404 }
405 case R.id.action_sync_file: {
406 Account account = AccountUtils.getCurrentOwnCloudAccount(getSherlockActivity());
407 RemoteOperation operation = new SynchronizeFileOperation(mTargetFile, null, mContainerActivity.getStorageManager(), account, true, getSherlockActivity());
408 operation.execute(account, getSherlockActivity(), mContainerActivity, mHandler, getSherlockActivity());
409 ((FileDisplayActivity) getSherlockActivity()).showLoadingDialog();
410 return true;
411 }
412 case R.id.action_cancel_download: {
413 FileDownloaderBinder downloaderBinder = mContainerActivity.getFileDownloaderBinder();
414 Account account = AccountUtils.getCurrentOwnCloudAccount(getActivity());
415 if (downloaderBinder != null && downloaderBinder.isDownloading(account, mTargetFile)) {
416 downloaderBinder.cancel(account, mTargetFile);
417 listDirectory();
418 mContainerActivity.onTransferStateChanged(mTargetFile, false, false);
419 }
420 return true;
421 }
422 case R.id.action_cancel_upload: {
423 FileUploaderBinder uploaderBinder = mContainerActivity.getFileUploaderBinder();
424 Account account = AccountUtils.getCurrentOwnCloudAccount(getActivity());
425 if (uploaderBinder != null && uploaderBinder.isUploading(account, mTargetFile)) {
426 uploaderBinder.cancel(account, mTargetFile);
427 listDirectory();
428 mContainerActivity.onTransferStateChanged(mTargetFile, false, false);
429 }
430 return true;
431 }
432 case R.id.action_see_details: {
433 ((FileFragment.ContainerActivity)getActivity()).showDetails(mTargetFile);
434 return true;
435 }
436 case R.id.action_send_file: {
437 // Obtain the file
438 if (!mTargetFile.isDown()) { // Download the file
439 Log_OC.d(TAG, mTargetFile.getRemotePath() + " : File must be downloaded");
440 mContainerActivity.startDownloadForSending(mTargetFile);
441
442 } else {
443
444 FileDisplayActivity activity = (FileDisplayActivity) getSherlockActivity();
445 activity.getFileOperationsHelper().sendDownloadedFile(mTargetFile, activity);
446 }
447 return true;
448 }
449 default:
450 return super.onContextItemSelected(item);
451 }
452 }
453
454
455 /**
456 * Use this to query the {@link OCFile} that is currently
457 * being displayed by this fragment
458 * @return The currently viewed OCFile
459 */
460 public OCFile getCurrentFile(){
461 return mFile;
462 }
463
464 /**
465 * Calls {@link OCFileListFragment#listDirectory(OCFile)} with a null parameter
466 */
467 public void listDirectory(){
468 listDirectory(null);
469 }
470
471 /**
472 * Lists the given directory on the view. When the input parameter is null,
473 * it will either refresh the last known directory. list the root
474 * if there never was a directory.
475 *
476 * @param directory File to be listed
477 */
478 public void listDirectory(OCFile directory) {
479 FileDataStorageManager storageManager = mContainerActivity.getStorageManager();
480 if (storageManager != null) {
481
482 // Check input parameters for null
483 if(directory == null){
484 if(mFile != null){
485 directory = mFile;
486 } else {
487 directory = storageManager.getFileByPath("/");
488 if (directory == null) return; // no files, wait for sync
489 }
490 }
491
492
493 // If that's not a directory -> List its parent
494 if(!directory.isFolder()){
495 Log_OC.w(TAG, "You see, that is not a directory -> " + directory.toString());
496 directory = storageManager.getFileById(directory.getParentId());
497 }
498
499 mAdapter.swapDirectory(directory, storageManager);
500 if (mFile == null || !mFile.equals(directory)) {
501 mList.setSelectionFromTop(0, 0);
502 }
503 mFile = directory;
504 }
505 }
506
507
508
509 /**
510 * Interface to implement by any Activity that includes some instance of FileListFragment
511 *
512 * @author David A. Velasco
513 */
514 public interface ContainerActivity extends TransferServiceGetter, OnRemoteOperationListener {
515
516 /**
517 * Callback method invoked when a the user browsed into a different folder through the list of files
518 *
519 * @param file
520 */
521 public void onBrowsedDownTo(OCFile folder);
522
523 public void startDownloadForPreview(OCFile file);
524
525 public void startMediaPreview(OCFile file, int i, boolean b);
526
527 public void startImagePreview(OCFile file);
528
529 public void startSyncFolderOperation(OCFile folder);
530
531 /**
532 * Getter for the current DataStorageManager in the container activity
533 */
534 public FileDataStorageManager getStorageManager();
535
536
537 /**
538 * Callback method invoked when a the 'transfer state' of a file changes.
539 *
540 * This happens when a download or upload is started or ended for a file.
541 *
542 * This method is necessary by now to update the user interface of the double-pane layout in tablets
543 * because methods {@link FileDownloaderBinder#isDownloading(Account, OCFile)} and {@link FileUploaderBinder#isUploading(Account, OCFile)}
544 * won't provide the needed response before the method where this is called finishes.
545 *
546 * TODO Remove this when the transfer state of a file is kept in the database (other thing TODO)
547 *
548 * @param file OCFile which state changed.
549 * @param downloading Flag signaling if the file is now downloading.
550 * @param uploading Flag signaling if the file is now uploading.
551 */
552 public void onTransferStateChanged(OCFile file, boolean downloading, boolean uploading);
553
554 void startDownloadForSending(OCFile file);
555
556 }
557
558
559 @Override
560 public void onDismiss(EditNameDialog dialog) {
561 if (dialog.getResult()) {
562 String newFilename = dialog.getNewFilename();
563 Log_OC.d(TAG, "name edit dialog dismissed with new name " + newFilename);
564 RemoteOperation operation = new RenameFileOperation(mTargetFile,
565 AccountUtils.getCurrentOwnCloudAccount(getActivity()),
566 newFilename,
567 mContainerActivity.getStorageManager());
568 operation.execute(AccountUtils.getCurrentOwnCloudAccount(getSherlockActivity()), getSherlockActivity(), mContainerActivity, mHandler, getSherlockActivity());
569 ((FileDisplayActivity) getActivity()).showLoadingDialog();
570 }
571 }
572
573
574 @Override
575 public void onConfirmation(String callerTag) {
576 if (callerTag.equals(FileDetailFragment.FTAG_CONFIRMATION)) {
577 if (mContainerActivity.getStorageManager().getFileById(mTargetFile.getFileId()) != null) {
578 RemoteOperation operation = new RemoveFileOperation( mTargetFile,
579 true,
580 mContainerActivity.getStorageManager());
581 operation.execute(AccountUtils.getCurrentOwnCloudAccount(getSherlockActivity()), getSherlockActivity(), mContainerActivity, mHandler, getSherlockActivity());
582
583 ((FileDisplayActivity) getActivity()).showLoadingDialog();
584 }
585 }
586 }
587
588 @Override
589 public void onNeutral(String callerTag) {
590 mContainerActivity.getStorageManager().removeFile(mTargetFile, false, true); // TODO perform in background task / new thread
591 listDirectory();
592 mContainerActivity.onTransferStateChanged(mTargetFile, false, false);
593 }
594
595 @Override
596 public void onCancel(String callerTag) {
597 Log_OC.d(TAG, "REMOVAL CANCELED");
598 }
599
600
601 }