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