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