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