c11420b0c9468aea74c859717c5d3e1d622d701b
[pub/Android/ownCloud.git] / src / com / owncloud / android / ui / fragment / OCFileListFragment.java
1 /* ownCloud Android client application
2 * Copyright (C) 2011 Bartek Przybylski
3 * Copyright (C) 2012-2014 ownCloud Inc.
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 2,
7 * as published by the Free Software Foundation.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 */
18 package com.owncloud.android.ui.fragment;
19
20 import java.io.File;
21 import java.util.ArrayList;
22 import java.util.List;
23
24 import com.owncloud.android.R;
25 import com.owncloud.android.authentication.AccountUtils;
26 import com.owncloud.android.datamodel.FileDataStorageManager;
27 import com.owncloud.android.datamodel.OCFile;
28 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
29 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
30 import com.owncloud.android.lib.common.operations.OnRemoteOperationListener;
31 import com.owncloud.android.lib.common.operations.RemoteOperation;
32 import com.owncloud.android.operations.RemoveFileOperation;
33 import com.owncloud.android.operations.RenameFileOperation;
34 import com.owncloud.android.operations.SynchronizeFileOperation;
35 import com.owncloud.android.ui.activity.FileDisplayActivity;
36 import com.owncloud.android.ui.activity.TransferServiceGetter;
37 import com.owncloud.android.ui.adapter.FileListListAdapter;
38 import com.owncloud.android.ui.dialog.EditNameDialog;
39 import com.owncloud.android.ui.dialog.EditNameDialog.EditNameDialogListener;
40 import com.owncloud.android.ui.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
41 import com.owncloud.android.ui.preview.PreviewImageFragment;
42 import com.owncloud.android.ui.preview.PreviewMediaFragment;
43 import com.owncloud.android.utils.Log_OC;
44
45 import android.accounts.Account;
46 import android.app.Activity;
47 import android.database.Cursor;
48 import android.os.Bundle;
49 import android.os.Handler;
50 import android.view.ContextMenu;
51 import android.view.MenuInflater;
52 import android.view.MenuItem;
53 import android.view.View;
54 import android.widget.AdapterView;
55 import android.widget.AdapterView.AdapterContextMenuInfo;
56
57 /**
58 * A Fragment that lists all files and folders in a given path.
59 *
60 * @author Bartek Przybylski
61 *
62 */
63 public class OCFileListFragment extends ExtendedListFragment implements EditNameDialogListener, ConfirmationDialogFragmentListener {
64
65 private static final String TAG = OCFileListFragment.class.getSimpleName();
66
67 private static final String MY_PACKAGE = OCFileListFragment.class.getPackage() != null ? OCFileListFragment.class.getPackage().getName() : "com.owncloud.android.ui.fragment";
68 private static final String EXTRA_FILE = MY_PACKAGE + ".extra.FILE";
69
70 private static final String KEY_INDEXES = "INDEXES";
71 private static final String KEY_FIRST_POSITIONS= "FIRST_POSITIONS";
72 private static final String KEY_TOPS = "TOPS";
73 private static final String KEY_HEIGHT_CELL = "HEIGHT_CELL";
74
75 private OCFileListFragment.ContainerActivity mContainerActivity;
76
77 private OCFile mFile = null;
78 private FileListListAdapter mAdapter;
79
80 private Handler mHandler;
81 private OCFile mTargetFile;
82
83 // Save the state of the scroll in browsing
84 private ArrayList<Integer> mIndexes;
85 private ArrayList<Integer> mFirstPositions;
86 private ArrayList<Integer> mTops;
87
88 private int mHeightCell = 0;
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 = (ContainerActivity) activity;
99 } catch (ClassCastException e) {
100 throw new ClassCastException(activity.toString() + " must implement " + OCFileListFragment.ContainerActivity.class.getSimpleName());
101 }
102 }
103
104
105 /**
106 * {@inheritDoc}
107 */
108 @Override
109 public void onActivityCreated(Bundle savedInstanceState) {
110 super.onActivityCreated(savedInstanceState);
111 Log_OC.e(TAG, "onActivityCreated() start");
112
113 mAdapter = new FileListListAdapter(getActivity(), mContainerActivity);
114
115 if (savedInstanceState != null) {
116 mFile = savedInstanceState.getParcelable(EXTRA_FILE);
117 mIndexes = savedInstanceState.getIntegerArrayList(KEY_INDEXES);
118 mFirstPositions = savedInstanceState.getIntegerArrayList(KEY_FIRST_POSITIONS);
119 mTops = savedInstanceState.getIntegerArrayList(KEY_TOPS);
120 mHeightCell = savedInstanceState.getInt(KEY_HEIGHT_CELL);
121
122 } else {
123 mIndexes = new ArrayList<Integer>();
124 mFirstPositions = new ArrayList<Integer>();
125 mTops = new ArrayList<Integer>();
126 mHeightCell = 0;
127
128 }
129
130 setListAdapter(mAdapter);
131
132 registerForContextMenu(getListView());
133 getListView().setOnCreateContextMenuListener(this);
134
135 mHandler = new Handler();
136
137 }
138
139 /**
140 * Saves the current listed folder.
141 */
142 @Override
143 public void onSaveInstanceState (Bundle outState) {
144 super.onSaveInstanceState(outState);
145 outState.putParcelable(EXTRA_FILE, mFile);
146 outState.putIntegerArrayList(KEY_INDEXES, mIndexes);
147 outState.putIntegerArrayList(KEY_FIRST_POSITIONS, mFirstPositions);
148 outState.putIntegerArrayList(KEY_TOPS, mTops);
149 outState.putInt(KEY_HEIGHT_CELL, mHeightCell);
150 }
151
152 /**
153 * Call this, when the user presses the up button.
154 *
155 * Tries to move up the current folder one level. If the parent folder was removed from the database,
156 * it continues browsing up until finding an existing folders.
157 *
158 * return Count of folder levels browsed up.
159 */
160 public int onBrowseUp() {
161 OCFile parentDir = null;
162 int moveCount = 0;
163
164 if(mFile != null){
165 FileDataStorageManager storageManager = mContainerActivity.getStorageManager();
166
167 String parentPath = null;
168 if (mFile.getParentId() != FileDataStorageManager.ROOT_PARENT_ID) {
169 parentPath = new File(mFile.getRemotePath()).getParent();
170 parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ? parentPath : parentPath + OCFile.PATH_SEPARATOR;
171 parentDir = storageManager.getFileByPath(parentPath);
172 moveCount++;
173 } else {
174 parentDir = storageManager.getFileByPath(OCFile.ROOT_PATH); // never returns null; keep the path in root folder
175 }
176 while (parentDir == null) {
177 parentPath = new File(parentPath).getParent();
178 parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ? parentPath : parentPath + OCFile.PATH_SEPARATOR;
179 parentDir = storageManager.getFileByPath(parentPath);
180 moveCount++;
181 } // exit is granted because storageManager.getFileByPath("/") never returns null
182 mFile = parentDir;
183 }
184
185 if (mFile != null) {
186 listDirectory(mFile);
187
188 mContainerActivity.startSyncFolderOperation(mFile);
189
190 // restore index and top position
191 restoreIndexAndTopPosition();
192
193 } // else - should never happen now
194
195 return moveCount;
196 }
197
198 /*
199 * Restore index and position
200 */
201 private void restoreIndexAndTopPosition() {
202 if (mIndexes.size() > 0) {
203 // needs to be checked; not every browse-up had a browse-down before
204
205 int index = mIndexes.remove(mIndexes.size() - 1);
206
207 int firstPosition = mFirstPositions.remove(mFirstPositions.size() -1);
208
209 int top = mTops.remove(mTops.size() - 1);
210
211 mList.setSelectionFromTop(firstPosition, top);
212
213 // Move the scroll if the selection is not visible
214 int indexPosition = mHeightCell*index;
215 int height = mList.getHeight();
216
217 if (indexPosition > height) {
218 if (android.os.Build.VERSION.SDK_INT >= 11)
219 {
220 mList.smoothScrollToPosition(index);
221 }
222 else if (android.os.Build.VERSION.SDK_INT >= 8)
223 {
224 mList.setSelectionFromTop(index, 0);
225 }
226
227 }
228 }
229 }
230
231 /*
232 * Save index and top position
233 */
234 private void saveIndexAndTopPosition(int index) {
235
236 mIndexes.add(index);
237
238 int firstPosition = mList.getFirstVisiblePosition();
239 mFirstPositions.add(firstPosition);
240
241 View view = mList.getChildAt(0);
242 int top = (view == null) ? 0 : view.getTop() ;
243
244 mTops.add(top);
245
246 // Save the height of a cell
247 mHeightCell = (view == null || mHeightCell != 0) ? mHeightCell : view.getHeight();
248 }
249
250 @Override
251 public void onItemClick(AdapterView<?> l, View v, int position, long id) {
252 OCFile file = mContainerActivity.getStorageManager().createFileInstance(
253 (Cursor) mAdapter.getItem(position));
254 if (file != null) {
255 if (file.isFolder()) {
256 // update state and view of this fragment
257 listDirectory(file);
258 // then, notify parent activity to let it update its state and view, and other fragments
259 mContainerActivity.onBrowsedDownTo(file);
260 // save index and top position
261 saveIndexAndTopPosition(position);
262
263 } else { /// Click on a file
264 if (PreviewImageFragment.canBePreviewed(file)) {
265 // preview image - it handles the download, if needed
266 mContainerActivity.startImagePreview(file);
267
268 } else if (file.isDown()) {
269 if (PreviewMediaFragment.canBePreviewed(file)) {
270 // media preview
271 mContainerActivity.startMediaPreview(file, 0, true);
272 } else {
273 FileDisplayActivity activity = (FileDisplayActivity) getSherlockActivity();
274 activity.getFileOperationsHelper().openFile(file, activity);
275 }
276
277 } else {
278 // automatic download, preview on finish
279 mContainerActivity.startDownloadForPreview(file);
280 }
281
282 }
283
284 } else {
285 Log_OC.d(TAG, "Null object in ListAdapter!!");
286 }
287
288 }
289
290 /**
291 * {@inheritDoc}
292 */
293 @Override
294 public void onCreateContextMenu (ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
295 super.onCreateContextMenu(menu, v, menuInfo);
296 MenuInflater inflater = getActivity().getMenuInflater();
297 inflater.inflate(R.menu.file_actions_menu, menu);
298 AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
299 OCFile targetFile = mContainerActivity.getStorageManager().createFileInstance(
300 (Cursor) mAdapter.getItem(info.position));
301 List<Integer> toHide = new ArrayList<Integer>();
302 List<Integer> toDisable = new ArrayList<Integer>();
303
304 MenuItem item = null;
305 if (targetFile.isFolder()) {
306 // contextual menu for folders
307 toHide.add(R.id.action_open_file_with);
308 toHide.add(R.id.action_download_file);
309 toHide.add(R.id.action_cancel_download);
310 toHide.add(R.id.action_cancel_upload);
311 toHide.add(R.id.action_sync_file);
312 toHide.add(R.id.action_see_details);
313 toHide.add(R.id.action_send_file);
314 if ( mContainerActivity.getFileDownloaderBinder().isDownloading(AccountUtils.getCurrentOwnCloudAccount(getActivity()), targetFile) ||
315 mContainerActivity.getFileUploaderBinder().isUploading(AccountUtils.getCurrentOwnCloudAccount(getActivity()), targetFile) ) {
316 toDisable.add(R.id.action_rename_file);
317 toDisable.add(R.id.action_remove_file);
318
319 }
320
321 } else {
322 // contextual menu for regular files
323
324 // new design: 'download' and 'open with' won't be available anymore in context menu
325 toHide.add(R.id.action_download_file);
326 toHide.add(R.id.action_open_file_with);
327
328 if (targetFile.isDown()) {
329 toHide.add(R.id.action_cancel_download);
330 toHide.add(R.id.action_cancel_upload);
331
332 } else {
333 toHide.add(R.id.action_sync_file);
334 }
335 if ( mContainerActivity.getFileDownloaderBinder().isDownloading(AccountUtils.getCurrentOwnCloudAccount(getActivity()), targetFile)) {
336 toHide.add(R.id.action_cancel_upload);
337 toDisable.add(R.id.action_rename_file);
338 toDisable.add(R.id.action_remove_file);
339
340 } else if ( mContainerActivity.getFileUploaderBinder().isUploading(AccountUtils.getCurrentOwnCloudAccount(getActivity()), targetFile)) {
341 toHide.add(R.id.action_cancel_download);
342 toDisable.add(R.id.action_rename_file);
343 toDisable.add(R.id.action_remove_file);
344
345 } else {
346 toHide.add(R.id.action_cancel_download);
347 toHide.add(R.id.action_cancel_upload);
348 }
349 }
350
351 // Options shareLink
352 if (!targetFile.isShareByLink()) {
353 toHide.add(R.id.action_unshare_file);
354 }
355
356 // Send file
357 boolean sendEnabled = getString(R.string.send_files_to_other_apps).equalsIgnoreCase("on");
358 if (!sendEnabled) {
359 toHide.add(R.id.action_send_file);
360 }
361
362 for (int i : toHide) {
363 item = menu.findItem(i);
364 if (item != null) {
365 item.setVisible(false);
366 item.setEnabled(false);
367 }
368 }
369
370 for (int i : toDisable) {
371 item = menu.findItem(i);
372 if (item != null) {
373 item.setEnabled(false);
374 }
375 }
376 }
377
378
379 /**
380 * {@inhericDoc}
381 */
382 @Override
383 public boolean onContextItemSelected (MenuItem item) {
384 AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
385 mTargetFile = mContainerActivity.getStorageManager().createFileInstance(
386 (Cursor) mAdapter.getItem(info.position));
387 switch (item.getItemId()) {
388 case R.id.action_share_file: {
389 FileDisplayActivity activity = (FileDisplayActivity) getSherlockActivity();
390 activity.getFileOperationsHelper().shareFileWithLink(mTargetFile, activity);
391 return true;
392 }
393 case R.id.action_unshare_file: {
394 FileDisplayActivity activity = (FileDisplayActivity) getSherlockActivity();
395 activity.getFileOperationsHelper().unshareFileWithLink(mTargetFile, activity);
396 return true;
397 }
398 case R.id.action_rename_file: {
399 String fileName = mTargetFile.getFileName();
400 int extensionStart = mTargetFile.isFolder() ? -1 : fileName.lastIndexOf(".");
401 int selectionEnd = (extensionStart >= 0) ? extensionStart : fileName.length();
402 EditNameDialog dialog = EditNameDialog.newInstance(getString(R.string.rename_dialog_title), fileName, 0, selectionEnd, this);
403 dialog.show(getFragmentManager(), EditNameDialog.TAG);
404 return true;
405 }
406 case R.id.action_remove_file: {
407 int messageStringId = R.string.confirmation_remove_alert;
408 int posBtnStringId = R.string.confirmation_remove_remote;
409 int neuBtnStringId = -1;
410 if (mTargetFile.isFolder()) {
411 messageStringId = R.string.confirmation_remove_folder_alert;
412 posBtnStringId = R.string.confirmation_remove_remote_and_local;
413 neuBtnStringId = R.string.confirmation_remove_folder_local;
414 } else if (mTargetFile.isDown()) {
415 posBtnStringId = R.string.confirmation_remove_remote_and_local;
416 neuBtnStringId = R.string.confirmation_remove_local;
417 }
418 ConfirmationDialogFragment confDialog = ConfirmationDialogFragment.newInstance(
419 messageStringId,
420 new String[]{mTargetFile.getFileName()},
421 posBtnStringId,
422 neuBtnStringId,
423 R.string.common_cancel);
424 confDialog.setOnConfirmationListener(this);
425 confDialog.show(getFragmentManager(), FileDetailFragment.FTAG_CONFIRMATION);
426 return true;
427 }
428 case R.id.action_sync_file: {
429 Account account = AccountUtils.getCurrentOwnCloudAccount(getSherlockActivity());
430 RemoteOperation operation = new SynchronizeFileOperation(mTargetFile, null, mContainerActivity.getStorageManager(), account, true, getSherlockActivity());
431 operation.execute(account, getSherlockActivity(), mContainerActivity, mHandler, getSherlockActivity());
432 ((FileDisplayActivity) getSherlockActivity()).showLoadingDialog();
433 return true;
434 }
435 case R.id.action_cancel_download: {
436 FileDownloaderBinder downloaderBinder = mContainerActivity.getFileDownloaderBinder();
437 Account account = AccountUtils.getCurrentOwnCloudAccount(getActivity());
438 if (downloaderBinder != null && downloaderBinder.isDownloading(account, mTargetFile)) {
439 downloaderBinder.cancel(account, mTargetFile);
440 listDirectory();
441 mContainerActivity.onTransferStateChanged(mTargetFile, false, false);
442 }
443 return true;
444 }
445 case R.id.action_cancel_upload: {
446 FileUploaderBinder uploaderBinder = mContainerActivity.getFileUploaderBinder();
447 Account account = AccountUtils.getCurrentOwnCloudAccount(getActivity());
448 if (uploaderBinder != null && uploaderBinder.isUploading(account, mTargetFile)) {
449 uploaderBinder.cancel(account, mTargetFile);
450 listDirectory();
451 mContainerActivity.onTransferStateChanged(mTargetFile, false, false);
452 }
453 return true;
454 }
455 case R.id.action_see_details: {
456 ((FileFragment.ContainerActivity)getActivity()).showDetails(mTargetFile);
457 return true;
458 }
459 case R.id.action_send_file: {
460 // Obtain the file
461 if (!mTargetFile.isDown()) { // Download the file
462 Log_OC.d(TAG, mTargetFile.getRemotePath() + " : File must be downloaded");
463 mContainerActivity.startDownloadForSending(mTargetFile);
464
465 } else {
466
467 FileDisplayActivity activity = (FileDisplayActivity) getSherlockActivity();
468 activity.getFileOperationsHelper().sendDownloadedFile(mTargetFile, activity);
469 }
470 return true;
471 }
472 default:
473 return super.onContextItemSelected(item);
474 }
475 }
476
477
478 /**
479 * Use this to query the {@link OCFile} that is currently
480 * being displayed by this fragment
481 * @return The currently viewed OCFile
482 */
483 public OCFile getCurrentFile(){
484 return mFile;
485 }
486
487 /**
488 * Calls {@link OCFileListFragment#listDirectory(OCFile)} with a null parameter
489 */
490 public void listDirectory(){
491 listDirectory(null);
492 }
493
494 /**
495 * Lists the given directory on the view. When the input parameter is null,
496 * it will either refresh the last known directory. list the root
497 * if there never was a directory.
498 *
499 * @param directory File to be listed
500 */
501 public void listDirectory(OCFile directory) {
502 FileDataStorageManager storageManager = mContainerActivity.getStorageManager();
503 if (storageManager != null) {
504
505 // Check input parameters for null
506 if(directory == null){
507 if(mFile != null){
508 directory = mFile;
509 } else {
510 directory = storageManager.getFileByPath("/");
511 if (directory == null) return; // no files, wait for sync
512 }
513 }
514
515
516 // If that's not a directory -> List its parent
517 if(!directory.isFolder()){
518 Log_OC.w(TAG, "You see, that is not a directory -> " + directory.toString());
519 directory = storageManager.getFileById(directory.getParentId());
520 }
521
522 mAdapter.swapDirectory(directory, storageManager);
523 if (mFile == null || !mFile.equals(directory)) {
524 mList.setSelectionFromTop(0, 0);
525 }
526 mFile = directory;
527 }
528 }
529
530
531
532 /**
533 * Interface to implement by any Activity that includes some instance of FileListFragment
534 *
535 * @author David A. Velasco
536 */
537 public interface ContainerActivity extends TransferServiceGetter, OnRemoteOperationListener {
538
539 /**
540 * Callback method invoked when a the user browsed into a different folder through the list of files
541 *
542 * @param file
543 */
544 public void onBrowsedDownTo(OCFile folder);
545
546 public void startDownloadForPreview(OCFile file);
547
548 public void startMediaPreview(OCFile file, int i, boolean b);
549
550 public void startImagePreview(OCFile file);
551
552 public void startSyncFolderOperation(OCFile folder);
553
554 /**
555 * Getter for the current DataStorageManager in the container activity
556 */
557 public FileDataStorageManager getStorageManager();
558
559
560 /**
561 * Callback method invoked when a the 'transfer state' of a file changes.
562 *
563 * This happens when a download or upload is started or ended for a file.
564 *
565 * This method is necessary by now to update the user interface of the double-pane layout in tablets
566 * because methods {@link FileDownloaderBinder#isDownloading(Account, OCFile)} and {@link FileUploaderBinder#isUploading(Account, OCFile)}
567 * won't provide the needed response before the method where this is called finishes.
568 *
569 * TODO Remove this when the transfer state of a file is kept in the database (other thing TODO)
570 *
571 * @param file OCFile which state changed.
572 * @param downloading Flag signaling if the file is now downloading.
573 * @param uploading Flag signaling if the file is now uploading.
574 */
575 public void onTransferStateChanged(OCFile file, boolean downloading, boolean uploading);
576
577 void startDownloadForSending(OCFile file);
578
579 }
580
581
582 @Override
583 public void onDismiss(EditNameDialog dialog) {
584 if (dialog.getResult()) {
585 String newFilename = dialog.getNewFilename();
586 Log_OC.d(TAG, "name edit dialog dismissed with new name " + newFilename);
587 RemoteOperation operation = new RenameFileOperation(mTargetFile,
588 AccountUtils.getCurrentOwnCloudAccount(getActivity()),
589 newFilename,
590 mContainerActivity.getStorageManager());
591 operation.execute(AccountUtils.getCurrentOwnCloudAccount(getSherlockActivity()), getSherlockActivity(), mContainerActivity, mHandler, getSherlockActivity());
592 ((FileDisplayActivity) getActivity()).showLoadingDialog();
593 }
594 }
595
596
597 @Override
598 public void onConfirmation(String callerTag) {
599 if (callerTag.equals(FileDetailFragment.FTAG_CONFIRMATION)) {
600 if (mContainerActivity.getStorageManager().getFileById(mTargetFile.getFileId()) != null) {
601 RemoteOperation operation = new RemoveFileOperation( mTargetFile,
602 true,
603 mContainerActivity.getStorageManager());
604 operation.execute(AccountUtils.getCurrentOwnCloudAccount(getSherlockActivity()), getSherlockActivity(), mContainerActivity, mHandler, getSherlockActivity());
605
606 ((FileDisplayActivity) getActivity()).showLoadingDialog();
607 }
608 }
609 }
610
611 @Override
612 public void onNeutral(String callerTag) {
613 mContainerActivity.getStorageManager().removeFile(mTargetFile, false, true); // TODO perform in background task / new thread
614 listDirectory();
615 mContainerActivity.onTransferStateChanged(mTargetFile, false, false);
616 }
617
618 @Override
619 public void onCancel(String callerTag) {
620 Log_OC.d(TAG, "REMOVAL CANCELED");
621 }
622
623
624 }