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