8a8589da1c96881bce8a9c5ee7917e19b26f70d2
[pub/Android/ownCloud.git] / src / com / owncloud / android / ui / fragment / OCFileListFragment.java
1 /**
2 * ownCloud Android client application
3 *
4 * @author Bartek Przybylski
5 * @author masensio
6 * @author David A. Velasco
7 * Copyright (C) 2011 Bartek Przybylski
8 * Copyright (C) 2015 ownCloud Inc.
9 *
10 * This program is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 *
22 */
23 package com.owncloud.android.ui.fragment;
24
25 import android.app.Activity;
26 import android.content.Intent;
27 import android.os.Bundle;
28 import android.os.Parcelable;
29 import android.support.v4.widget.SwipeRefreshLayout;
30 import android.view.ActionMode;
31 import android.view.ContextMenu;
32 import android.view.Menu;
33 import android.view.MenuInflater;
34 import android.view.MenuItem;
35 import android.view.View;
36 import android.widget.AbsListView;
37 import android.widget.AdapterView;
38 import android.widget.AdapterView.AdapterContextMenuInfo;
39 import android.widget.PopupMenu;
40
41 import com.owncloud.android.R;
42 import com.owncloud.android.authentication.AccountUtils;
43 import com.owncloud.android.datamodel.FileDataStorageManager;
44 import com.owncloud.android.datamodel.OCFile;
45 import com.owncloud.android.files.FileMenuFilter;
46 import com.owncloud.android.lib.common.utils.Log_OC;
47 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
48 import com.owncloud.android.ui.activity.FileActivity;
49 import com.owncloud.android.ui.activity.FileDisplayActivity;
50 import com.owncloud.android.ui.activity.FolderPickerActivity;
51 import com.owncloud.android.ui.activity.OnEnforceableRefreshListener;
52 import com.owncloud.android.ui.adapter.FileListListAdapter;
53 import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
54 import com.owncloud.android.ui.dialog.FileActionsDialogFragment;
55 import com.owncloud.android.ui.dialog.RemoveFileDialogFragment;
56 import com.owncloud.android.ui.dialog.RenameFileDialogFragment;
57 import com.owncloud.android.ui.preview.PreviewImageFragment;
58 import com.owncloud.android.ui.preview.PreviewMediaFragment;
59 import com.owncloud.android.utils.FileStorageUtils;
60 import com.owncloud.android.ui.preview.PreviewTextFragment;
61
62 import java.io.File;
63 import java.util.ArrayList;
64
65 /**
66 * A Fragment that lists all files and folders in a given path.
67 *
68 * TODO refactor to get rid of direct dependency on FileDisplayActivity
69 */
70 public class OCFileListFragment extends ExtendedListFragment {
71
72 private static final String TAG = OCFileListFragment.class.getSimpleName();
73
74 private static final String MY_PACKAGE = OCFileListFragment.class.getPackage() != null ?
75 OCFileListFragment.class.getPackage().getName() : "com.owncloud.android.ui.fragment";
76
77 public final static String ARG_JUST_FOLDERS = MY_PACKAGE + ".JUST_FOLDERS";
78 public final static String ARG_ALLOW_CONTEXTUAL_ACTIONS = MY_PACKAGE + ".ALLOW_CONTEXTUAL";
79
80 private static final String KEY_FILE = MY_PACKAGE + ".extra.FILE";
81
82 private FileFragment.ContainerActivity mContainerActivity;
83
84 private OCFile mFile = null;
85 private FileListListAdapter mAdapter;
86 private boolean mJustFolders;
87
88 /**
89 * {@inheritDoc}
90 */
91 @Override
92 public void onAttach(Activity activity) {
93 super.onAttach(activity);
94 Log_OC.e(TAG, "onAttach");
95 try {
96 mContainerActivity = (FileFragment.ContainerActivity) activity;
97
98 } catch (ClassCastException e) {
99 throw new ClassCastException(activity.toString() + " must implement " +
100 FileFragment.ContainerActivity.class.getSimpleName());
101 }
102 try {
103 setOnRefreshListener((OnEnforceableRefreshListener) activity);
104
105 } catch (ClassCastException e) {
106 throw new ClassCastException(activity.toString() + " must implement " +
107 SwipeRefreshLayout.OnRefreshListener.class.getSimpleName());
108 }
109 }
110
111
112 @Override
113 public void onDetach() {
114 setOnRefreshListener(null);
115 mContainerActivity = null;
116 super.onDetach();
117 }
118
119 /**
120 * {@inheritDoc}
121 */
122 @Override
123 public void onActivityCreated(Bundle savedInstanceState) {
124 super.onActivityCreated(savedInstanceState);
125 Log_OC.e(TAG, "onActivityCreated() start");
126
127 if (savedInstanceState != null) {
128 mFile = savedInstanceState.getParcelable(KEY_FILE);
129 }
130
131 if (mJustFolders) {
132 setFooterEnabled(false);
133 } else {
134 setFooterEnabled(true);
135 }
136
137 Bundle args = getArguments();
138 mJustFolders = (args == null) ? false : args.getBoolean(ARG_JUST_FOLDERS, false);
139 mAdapter = new FileListListAdapter(
140 mJustFolders,
141 getActivity(),
142 mContainerActivity
143 );
144 setListAdapter(mAdapter);
145
146 registerLongClickListener();
147 }
148
149 private void registerLongClickListener() {
150 getListView().setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
151 private Menu menu;
152
153 @Override
154 public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
155 final int checkedCount = getListView().getCheckedItemCount();
156 // TODO Tobi extract to values
157 mode.setTitle(checkedCount + " selected");
158
159 if (checked) {
160 mAdapter.setNewSelection(position, checked);
161 } else {
162 mAdapter.removeSelection(position);
163 }
164
165 // TODO maybe change: only recreate menu if count changes
166 menu.clear();
167 if (checkedCount == 1) {
168 createContextMenu(menu);
169 } else {
170 // download, move, copy, delete
171 getActivity().getMenuInflater().inflate(R.menu.multiple_file_actions_menu, menu);
172 }
173
174 }
175
176 @Override
177 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
178 this.menu = menu;
179 return true;
180 }
181
182 @Override
183 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
184 return false;
185 }
186
187 @Override
188 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
189 return onFileActionChosen(item.getItemId());
190 }
191
192 @Override
193 public void onDestroyActionMode(ActionMode mode) {
194 mAdapter.removeSelection();
195 }
196 });
197 }
198
199 // TODO Tobi needed?
200 private void showFileAction(int fileIndex) {
201 Bundle args = getArguments();
202 PopupMenu pm = new PopupMenu(getActivity(),null);
203 Menu menu = pm.getMenu();
204
205 boolean allowContextualActions =
206 (args == null) ? true : args.getBoolean(ARG_ALLOW_CONTEXTUAL_ACTIONS, true);
207
208 if (allowContextualActions) {
209 MenuInflater inflater = getActivity().getMenuInflater();
210
211 inflater.inflate(R.menu.file_actions_menu, menu);
212 OCFile targetFile = (OCFile) mAdapter.getItem(fileIndex);
213
214 if (mContainerActivity.getStorageManager() != null) {
215 FileMenuFilter mf = new FileMenuFilter(
216 targetFile,
217 mContainerActivity.getStorageManager().getAccount(),
218 mContainerActivity,
219 getActivity()
220 );
221 mf.filter(menu);
222 }
223
224 /// TODO break this direct dependency on FileDisplayActivity... if possible
225 MenuItem item = menu.findItem(R.id.action_open_file_with);
226 FileFragment frag = ((FileDisplayActivity)getActivity()).getSecondFragment();
227 if (frag != null && frag instanceof FileDetailFragment &&
228 frag.getFile().getFileId() == targetFile.getFileId()) {
229 item = menu.findItem(R.id.action_see_details);
230 if (item != null) {
231 item.setVisible(false);
232 item.setEnabled(false);
233 }
234 }
235
236 FileActionsDialogFragment dialog = FileActionsDialogFragment.newInstance(menu, fileIndex, targetFile.getFileName());
237 dialog.setTargetFragment(this, 0);
238 dialog.show(getFragmentManager(), FileActionsDialogFragment.FTAG_FILE_ACTIONS);
239 }
240 }
241
242 /**
243 * Saves the current listed folder.
244 */
245 @Override
246 public void onSaveInstanceState(Bundle outState) {
247 super.onSaveInstanceState(outState);
248 outState.putParcelable(KEY_FILE, mFile);
249 }
250
251 /**
252 * Call this, when the user presses the up button.
253 *
254 * Tries to move up the current folder one level. If the parent folder was removed from the
255 * database, it continues browsing up until finding an existing folders.
256 * <p/>
257 * return Count of folder levels browsed up.
258 */
259 public int onBrowseUp() {
260 OCFile parentDir = null;
261 int moveCount = 0;
262
263 if (mFile != null) {
264 FileDataStorageManager storageManager = mContainerActivity.getStorageManager();
265
266 String parentPath = null;
267 if (mFile.getParentId() != FileDataStorageManager.ROOT_PARENT_ID) {
268 parentPath = new File(mFile.getRemotePath()).getParent();
269 parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ? parentPath :
270 parentPath + OCFile.PATH_SEPARATOR;
271 parentDir = storageManager.getFileByPath(parentPath);
272 moveCount++;
273 } else {
274 parentDir = storageManager.getFileByPath(OCFile.ROOT_PATH);
275 }
276 while (parentDir == null) {
277 parentPath = new File(parentPath).getParent();
278 parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ? parentPath :
279 parentPath + OCFile.PATH_SEPARATOR;
280 parentDir = storageManager.getFileByPath(parentPath);
281 moveCount++;
282 } // exit is granted because storageManager.getFileByPath("/") never returns null
283 mFile = parentDir;
284
285 // TODO Enable when "On Device" is recovered ?
286 listDirectory(mFile /*, MainApp.getOnlyOnDevice()*/);
287
288 onRefresh(false);
289
290 // restore index and top position
291 restoreIndexAndTopPosition();
292
293 } // else - should never happen now
294
295 return moveCount;
296 }
297
298 @Override
299 public void onItemClick(AdapterView<?> l, View v, int position, long id) {
300 OCFile file = (OCFile) mAdapter.getItem(position);
301 if (file != null) {
302 if (file.isFolder()) {
303 // update state and view of this fragment
304 // TODO Enable when "On Device" is recovered ?
305 listDirectory(file/*, MainApp.getOnlyOnDevice()*/);
306 // then, notify parent activity to let it update its state and view
307 mContainerActivity.onBrowsedDownTo(file);
308 // save index and top position
309 saveIndexAndTopPosition(position);
310
311 } else { /// Click on a file
312 if (PreviewImageFragment.canBePreviewed(file)) {
313 // preview image - it handles the download, if needed
314 ((FileDisplayActivity)mContainerActivity).startImagePreview(file);
315 } else if (PreviewTextFragment.canBePreviewed(file)){
316 ((FileDisplayActivity)mContainerActivity).startTextPreview(file);
317 } else if (file.isDown()) {
318 if (PreviewMediaFragment.canBePreviewed(file)) {
319 // media preview
320 ((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true);
321 } else {
322 mContainerActivity.getFileOperationsHelper().openFile(file);
323 }
324
325 } else {
326 // automatic download, preview on finish
327 ((FileDisplayActivity) mContainerActivity).startDownloadForPreview(file);
328 }
329
330 }
331
332 } else {
333 Log_OC.d(TAG, "Null object in ListAdapter!!");
334 }
335
336 }
337
338 /**
339 * {@inheritDoc}
340 */
341 // TODO Tobi needed?
342 public void createContextMenu(Menu menu) {
343 Bundle args = getArguments();
344 boolean allowContextualActions =
345 (args == null) ? true : args.getBoolean(ARG_ALLOW_CONTEXTUAL_ACTIONS, true);
346 if (allowContextualActions) {
347 MenuInflater inflater = getActivity().getMenuInflater();
348 inflater.inflate(R.menu.file_actions_menu, menu);
349 OCFile targetFile = null;
350 if (mAdapter.getCheckedItems().size() == 1){
351 targetFile = mAdapter.getCheckedItems().get(0);
352 }
353
354 if (mContainerActivity.getStorageManager() != null) {
355 FileMenuFilter mf = new FileMenuFilter(
356 targetFile,
357 mContainerActivity.getStorageManager().getAccount(),
358 mContainerActivity,
359 getActivity()
360 );
361 mf.filter(menu);
362 }
363
364 /// TODO break this direct dependency on FileDisplayActivity... if possible
365 MenuItem item = menu.findItem(R.id.action_open_file_with);
366 FileFragment frag = ((FileDisplayActivity)getActivity()).getSecondFragment();
367 if (frag != null && frag instanceof FileDetailFragment &&
368 frag.getFile().getFileId() == targetFile.getFileId()) {
369 item = menu.findItem(R.id.action_see_details);
370 if (item != null) {
371 item.setVisible(false);
372 item.setEnabled(false);
373 }
374 }
375 }
376 }
377
378 public boolean onFileActionChosen(int menuId) {
379 if (mAdapter.getCheckedItems().size() == 1){
380 OCFile mTargetFile = mAdapter.getCheckedItems().get(0);
381
382 switch (menuId) {
383 case R.id.action_share_file: {
384 mContainerActivity.getFileOperationsHelper().shareFileWithLink(mTargetFile);
385 return true;
386 }
387 case R.id.action_open_file_with: {
388 mContainerActivity.getFileOperationsHelper().openFile(mTargetFile);
389 return true;
390 }
391 case R.id.action_unshare_file: {
392 mContainerActivity.getFileOperationsHelper().unshareFileWithLink(mTargetFile);
393 return true;
394 }
395 case R.id.action_rename_file: {
396 RenameFileDialogFragment dialog = RenameFileDialogFragment.newInstance(mTargetFile);
397 dialog.show(getFragmentManager(), FileDetailFragment.FTAG_RENAME_FILE);
398 return true;
399 }
400 case R.id.action_remove_file: {
401 RemoveFileDialogFragment dialog = RemoveFileDialogFragment.newInstance(mTargetFile);
402 dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
403 return true;
404 }
405 case R.id.action_download_file:
406 case R.id.action_sync_file: {
407 mContainerActivity.getFileOperationsHelper().syncFile(mTargetFile);
408 return true;
409 }
410 case R.id.action_cancel_download:
411 case R.id.action_cancel_upload: {
412 ((FileDisplayActivity) mContainerActivity).cancelTransference(mTargetFile);
413 return true;
414 }
415 case R.id.action_see_details: {
416 mContainerActivity.showDetails(mTargetFile);
417 return true;
418 }
419 case R.id.action_send_file: {
420 // Obtain the file
421 if (!mTargetFile.isDown()) { // Download the file
422 Log_OC.d(TAG, mTargetFile.getRemotePath() + " : File must be downloaded");
423 ((FileDisplayActivity) mContainerActivity).startDownloadForSending(mTargetFile);
424
425 } else {
426 mContainerActivity.getFileOperationsHelper().sendDownloadedFile(mTargetFile);
427 }
428 return true;
429 }
430 case R.id.action_move: {
431 Intent action = new Intent(getActivity(), FolderPickerActivity.class);
432
433 // Pass mTargetFile that contains info of selected file/folder
434 ArrayList files = new ArrayList();
435 files.add(mTargetFile);
436 action.putParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES, files);
437 getActivity().startActivityForResult(action, FileDisplayActivity.ACTION_MOVE_FILES);
438 return true;
439 }
440 case R.id.action_favorite_file: {
441 mContainerActivity.getFileOperationsHelper().toggleFavorite(mTargetFile, true);
442 return true;
443 }
444 case R.id.action_unfavorite_file: {
445 mContainerActivity.getFileOperationsHelper().toggleFavorite(mTargetFile, false);
446 return true;
447 }
448 case R.id.action_copy:
449 Intent action = new Intent(getActivity(), FolderPickerActivity.class);
450
451 // Pass mTargetFile that contains info of selected file/folder
452 action.putExtra(FolderPickerActivity.EXTRA_FILE, mTargetFile);
453 getActivity().startActivityForResult(action, FileDisplayActivity.ACTION_COPY_FILES);
454 return true;
455 default:
456 return false;
457 }
458 } else {
459 ArrayList<OCFile> mTargetFiles = mAdapter.getCheckedItems();
460
461 switch (menuId) {
462 // case R.id.action_remove_file: {
463 // RemoveFileDialogFragment dialog = RemoveFileDialogFragment.newInstance(mTargetFile);
464 // dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
465 // return true;
466 // }
467 case R.id.action_download_file:
468 case R.id.action_sync_file: {
469 mContainerActivity.getFileOperationsHelper().syncFiles(mTargetFiles);
470 return true;
471 }
472 case R.id.action_move: {
473 Intent action = new Intent(getActivity(), FolderPickerActivity.class);
474 action.putParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES, mTargetFiles);
475 getActivity().startActivityForResult(action, FileDisplayActivity.ACTION_MOVE_FILES);
476 return true;
477 }
478 case R.id.action_favorite_file: {
479 mContainerActivity.getFileOperationsHelper().toggleFavorites(mTargetFiles, true);
480 return true;
481 }
482 case R.id.action_unfavorite_file: {
483 mContainerActivity.getFileOperationsHelper().toggleFavorites(mTargetFiles, false);
484 return true;
485 }
486 case R.id.action_copy:
487 Intent action = new Intent(getActivity(), FolderPickerActivity.class);
488 action.putParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES, mTargetFiles);
489 getActivity().startActivityForResult(action, FileDisplayActivity.ACTION_COPY_FILES);
490 return true;
491 default:
492 return false;
493 }
494 }
495
496 }
497
498 /**
499 * {@inhericDoc}
500 */
501 @Override
502 public boolean onContextItemSelected (MenuItem item) {
503 AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
504 boolean matched = onFileActionChosen(item.getItemId());
505 if(!matched) {
506 return super.onContextItemSelected(item);
507 } else {
508 return matched;
509 }
510 }
511
512
513 /**
514 * Use this to query the {@link OCFile} that is currently
515 * being displayed by this fragment
516 *
517 * @return The currently viewed OCFile
518 */
519 public OCFile getCurrentFile() {
520 return mFile;
521 }
522
523 /**
524 * Calls {@link OCFileListFragment#listDirectory(OCFile)} with a null parameter
525 */
526 public void listDirectory(/*boolean onlyOnDevice*/){
527 listDirectory(null);
528 // TODO Enable when "On Device" is recovered ?
529 // listDirectory(null, onlyOnDevice);
530 }
531
532 public void refreshDirectory(){
533 // TODO Enable when "On Device" is recovered ?
534 listDirectory(getCurrentFile()/*, MainApp.getOnlyOnDevice()*/);
535 }
536
537 /**
538 * Lists the given directory on the view. When the input parameter is null,
539 * it will either refresh the last known directory. list the root
540 * if there never was a directory.
541 *
542 * @param directory File to be listed
543 */
544 public void listDirectory(OCFile directory/*, boolean onlyOnDevice*/) {
545 FileDataStorageManager storageManager = mContainerActivity.getStorageManager();
546 if (storageManager != null) {
547
548 // Check input parameters for null
549 if (directory == null) {
550 if (mFile != null) {
551 directory = mFile;
552 } else {
553 directory = storageManager.getFileByPath("/");
554 if (directory == null) return; // no files, wait for sync
555 }
556 }
557
558
559 // If that's not a directory -> List its parent
560 if (!directory.isFolder()) {
561 Log_OC.w(TAG, "You see, that is not a directory -> " + directory.toString());
562 directory = storageManager.getFileById(directory.getParentId());
563 }
564
565 // TODO Enable when "On Device" is recovered ?
566 mAdapter.swapDirectory(directory, storageManager/*, onlyOnDevice*/);
567 if (mFile == null || !mFile.equals(directory)) {
568 mCurrentListView.setSelection(0);
569 }
570 mFile = directory;
571
572 updateLayout();
573
574 }
575 }
576
577 private void updateLayout() {
578 if (!mJustFolders) {
579 int filesCount = 0, foldersCount = 0, imagesCount = 0;
580 int count = mAdapter.getCount();
581 OCFile file;
582 for (int i=0; i < count ; i++) {
583 file = (OCFile) mAdapter.getItem(i);
584 if (file.isFolder()) {
585 foldersCount++;
586 } else {
587 if (!file.isHidden()) {
588 filesCount++;
589
590 if (file.isImage()) {
591 imagesCount++;
592 }
593 }
594 }
595 }
596 // set footer text
597 setFooterText(generateFooterText(filesCount, foldersCount));
598
599 // decide grid vs list view
600 OwnCloudVersion version = AccountUtils.getServerVersion(
601 ((FileActivity)mContainerActivity).getAccount());
602 if (version != null && version.supportsRemoteThumbnails() &&
603 imagesCount > 0 && imagesCount == filesCount) {
604 switchToGridView();
605 registerLongClickListener();
606 } else {
607 switchToListView();
608 }
609 }
610 }
611
612 private String generateFooterText(int filesCount, int foldersCount) {
613 String output;
614 if (filesCount <= 0) {
615 if (foldersCount <= 0) {
616 output = "";
617
618 } else if (foldersCount == 1) {
619 output = getResources().getString(R.string.file_list__footer__folder);
620
621 } else { // foldersCount > 1
622 output = getResources().getString(R.string.file_list__footer__folders, foldersCount);
623 }
624
625 } else if (filesCount == 1) {
626 if (foldersCount <= 0) {
627 output = getResources().getString(R.string.file_list__footer__file);
628
629 } else if (foldersCount == 1) {
630 output = getResources().getString(R.string.file_list__footer__file_and_folder);
631
632 } else { // foldersCount > 1
633 output = getResources().getString(R.string.file_list__footer__file_and_folders, foldersCount);
634 }
635 } else { // filesCount > 1
636 if (foldersCount <= 0) {
637 output = getResources().getString(R.string.file_list__footer__files, filesCount);
638
639 } else if (foldersCount == 1) {
640 output = getResources().getString(R.string.file_list__footer__files_and_folder, filesCount);
641
642 } else { // foldersCount > 1
643 output = getResources().getString(
644 R.string.file_list__footer__files_and_folders, filesCount, foldersCount
645 );
646
647 }
648 }
649 return output;
650 }
651
652 public void sortByName(boolean descending) {
653 mAdapter.setSortOrder(FileStorageUtils.SORT_NAME, descending);
654 }
655
656 public void sortByDate(boolean descending) {
657 mAdapter.setSortOrder(FileStorageUtils.SORT_DATE, descending);
658 }
659
660 public void sortBySize(boolean descending) {
661 mAdapter.setSortOrder(FileStorageUtils.SORT_SIZE, descending);
662 }
663 }