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