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