Add new activity for selecting the destination folder when moving files, including...
[pub/Android/ownCloud.git] / src / com / owncloud / android / ui / activity / MoveActivity.java
1 package com.owncloud.android.ui.activity;
2
3 import java.io.IOException;
4
5 import android.accounts.Account;
6 import android.accounts.AccountManager;
7 import android.accounts.AuthenticatorException;
8 import android.accounts.OperationCanceledException;
9 import android.content.BroadcastReceiver;
10 import android.content.Context;
11 import android.content.Intent;
12 import android.content.IntentFilter;
13 import android.os.Bundle;
14 import android.support.v4.app.Fragment;
15 import android.support.v4.app.FragmentTransaction;
16 import android.util.Log;
17 import android.view.View;
18 import android.view.View.OnClickListener;
19 import android.view.ViewGroup;
20 import android.widget.ArrayAdapter;
21 import android.widget.Button;
22 import android.widget.TextView;
23 import android.widget.Toast;
24
25 import com.actionbarsherlock.app.ActionBar;
26 import com.actionbarsherlock.app.ActionBar.OnNavigationListener;
27 import com.actionbarsherlock.view.Menu;
28 import com.actionbarsherlock.view.MenuInflater;
29 import com.actionbarsherlock.view.MenuItem;
30 import com.actionbarsherlock.view.Window;
31 import com.owncloud.android.R;
32 import com.owncloud.android.datamodel.OCFile;
33 import com.owncloud.android.lib.common.OwnCloudAccount;
34 import com.owncloud.android.lib.common.OwnCloudClient;
35 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
36 import com.owncloud.android.lib.common.OwnCloudCredentials;
37 import com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException;
38 import com.owncloud.android.lib.common.operations.RemoteOperation;
39 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
40 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
41 import com.owncloud.android.operations.SynchronizeFolderOperation;
42 import com.owncloud.android.services.observer.FileObserverService;
43 import com.owncloud.android.syncadapter.FileSyncAdapter;
44 import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
45 import com.owncloud.android.ui.fragment.FileFragment;
46 import com.owncloud.android.ui.fragment.MoveFileListFragment;
47 import com.owncloud.android.utils.DisplayUtils;
48 import com.owncloud.android.utils.Log_OC;
49
50 public class MoveActivity extends HookActivity implements FileFragment.ContainerActivity,
51 OnNavigationListener, OnClickListener{
52
53 private ArrayAdapter<String> mDirectories;
54
55 private SyncBroadcastReceiver mSyncBroadcastReceiver;
56
57 public static final int DIALOG_SHORT_WAIT = 0;
58
59 public static final String ACTION_DETAILS = "com.owncloud.android.ui.activity.action.DETAILS";
60
61 private static final String TAG = MoveActivity.class.getSimpleName();
62
63 private static final String TAG_LIST_OF_FILES = "LIST_OF_FILES";
64
65 private boolean mSyncInProgress = false;
66
67 private Button mCancelBtn;
68 private Button mChooseBtn;
69
70
71 @Override
72 protected void onCreate(Bundle savedInstanceState) {
73 Log_OC.d(TAG, "onCreate() start");
74 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
75
76 super.onCreate(savedInstanceState); // this calls onAccountChanged() when ownCloud Account is valid
77
78 /// grant that FileObserverService is watching favourite files
79 if (savedInstanceState == null) {
80 Intent initObserversIntent = FileObserverService.makeInitIntent(this);
81 startService(initObserversIntent);
82 }
83
84 /// USER INTERFACE
85
86 // Inflate and set the layout view
87 setContentView(R.layout.files_move);
88 if (savedInstanceState == null) {
89 createMinFragments();
90 }
91
92 initControls();
93
94 // Action bar setup
95 mDirectories = new CustomArrayAdapter<String>(this, R.layout.sherlock_spinner_dropdown_item);
96 getSupportActionBar().setHomeButtonEnabled(true); // mandatory since Android ICS, according to the official documentation
97 setSupportProgressBarIndeterminateVisibility(mSyncInProgress /*|| mRefreshSharesInProgress*/); // always AFTER setContentView(...) ; to work around bug in its implementation
98
99 setBackgroundText();
100
101 Log_OC.d(TAG, "onCreate() end");
102
103 }
104
105 private void createMinFragments() {
106 MoveFileListFragment listOfFiles = new MoveFileListFragment();
107 FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
108 transaction.add(R.id.fragment_container, listOfFiles, TAG_LIST_OF_FILES);
109 transaction.commit();
110 }
111
112 /**
113 * Show a text message on screen view for notifying user if content is
114 * loading or folder is empty
115 */
116 private void setBackgroundText() {
117 MoveFileListFragment MoveFileListFragment = getListOfFilesFragment();
118 if (MoveFileListFragment != null) {
119 int message = R.string.file_list_loading;
120 if (!mSyncInProgress) {
121 // In case folder list is empty
122 message = R.string.file_list_empty_moving;
123 }
124 MoveFileListFragment.setMessageForEmptyList(getString(message));
125 } else {
126 Log.e(TAG, "MoveFileListFragment is null");
127 }
128 }
129
130 private MoveFileListFragment getListOfFilesFragment() {
131 Fragment listOfFiles = getSupportFragmentManager().findFragmentByTag(MoveActivity.TAG_LIST_OF_FILES);
132 if (listOfFiles != null) {
133 return (MoveFileListFragment)listOfFiles;
134 }
135 Log_OC.wtf(TAG, "Access to unexisting list of files fragment!!");
136 return null;
137 }
138
139 // Custom array adapter to override text colors
140 private class CustomArrayAdapter<T> extends ArrayAdapter<T> {
141
142 public CustomArrayAdapter(MoveActivity ctx, int view) {
143 super(ctx, view);
144 }
145
146 public View getView(int position, View convertView, ViewGroup parent) {
147 View v = super.getView(position, convertView, parent);
148
149 ((TextView) v).setTextColor(getResources().getColorStateList(
150 android.R.color.white));
151
152 fixRoot((TextView) v );
153 return v;
154 }
155
156 public View getDropDownView(int position, View convertView,
157 ViewGroup parent) {
158 View v = super.getDropDownView(position, convertView, parent);
159
160 ((TextView) v).setTextColor(getResources().getColorStateList(
161 android.R.color.white));
162
163 fixRoot((TextView) v );
164 return v;
165 }
166
167 private void fixRoot(TextView v) {
168 if (v.getText().equals(OCFile.PATH_SEPARATOR)) {
169 v.setText(R.string.default_display_name_for_root_folder);
170 }
171 }
172
173 }
174
175 /**
176 * {@inheritDoc}
177 *
178 * Updates action bar and second fragment, if in dual pane mode.
179 */
180 @Override
181 public void onBrowsedDownTo(OCFile directory) {
182 pushDirname(directory);
183
184 // Sync Folder
185 startSyncFolderOperation(directory);
186
187 }
188
189 /**
190 * Shows the information of the {@link OCFile} received as a
191 * parameter in the second fragment.
192 *
193 * @param file {@link OCFile} whose details will be shown
194 */
195 @Override
196 public void showDetails(OCFile file) {
197
198 }
199
200 /**
201 * {@inheritDoc}
202 */
203 @Override
204 public void onTransferStateChanged(OCFile file, boolean downloading, boolean uploading) {
205
206 }
207
208 /**
209 * Pushes a directory to the drop down list
210 * @param directory to push
211 * @throws IllegalArgumentException If the {@link OCFile#isFolder()} returns false.
212 */
213 public void pushDirname(OCFile directory) {
214 if(!directory.isFolder()){
215 throw new IllegalArgumentException("Only directories may be pushed!");
216 }
217 mDirectories.insert(directory.getFileName(), 0);
218 setFile(directory);
219 }
220
221 public void startSyncFolderOperation(OCFile folder) {
222 long currentSyncTime = System.currentTimeMillis();
223
224 mSyncInProgress = true;
225
226 // perform folder synchronization
227 RemoteOperation synchFolderOp = new SynchronizeFolderOperation( folder,
228 currentSyncTime,
229 false,
230 getFileOperationsHelper().isSharedSupported(),
231 getStorageManager(),
232 getAccount(),
233 getApplicationContext()
234 );
235 synchFolderOp.execute(getAccount(), this, null, null);
236
237 setSupportProgressBarIndeterminateVisibility(true);
238
239 setBackgroundText();
240 }
241
242 @Override
243 protected void onResume() {
244 super.onResume();
245 Log_OC.e(TAG, "onResume() start");
246
247 // refresh list of files
248 refreshListOfFilesFragment();
249
250 // Listen for sync messages
251 IntentFilter syncIntentFilter = new IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START);
252 syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_END);
253 syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED);
254 syncIntentFilter.addAction(SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED);
255 syncIntentFilter.addAction(SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED);
256 mSyncBroadcastReceiver = new SyncBroadcastReceiver();
257 registerReceiver(mSyncBroadcastReceiver, syncIntentFilter);
258
259 Log_OC.d(TAG, "onResume() end");
260 }
261
262 @Override
263 protected void onStart() {
264 super.onStart();
265 getSupportActionBar().setIcon(DisplayUtils.getSeasonalIconId());
266 }
267
268 @Override
269 protected void onDestroy() {
270 super.onDestroy();
271 }
272
273 @Override
274 public boolean onCreateOptionsMenu(Menu menu) {
275 MenuInflater inflater = getSherlock().getMenuInflater();
276 inflater.inflate(R.menu.main_menu, menu);
277 menu.findItem(R.id.action_upload).setVisible(false);
278 menu.findItem(R.id.action_settings).setVisible(false);
279 menu.findItem(R.id.action_sync_account).setVisible(false);
280 return true;
281 }
282
283 @Override
284 public boolean onOptionsItemSelected(MenuItem item) {
285 boolean retval = true;
286 switch (item.getItemId()) {
287 case R.id.action_create_dir: {
288 CreateFolderDialogFragment dialog =
289 CreateFolderDialogFragment.newInstance(getCurrentDir());
290 dialog.show(getSupportFragmentManager(), "createdirdialog");
291 break;
292 }
293 case android.R.id.home: {
294 OCFile currentDir = getCurrentDir();
295 if(currentDir != null && currentDir.getParentId() != 0) {
296 onBackPressed();
297 }
298 break;
299 }
300 default:
301 retval = super.onOptionsItemSelected(item);
302 }
303 return retval;
304 }
305
306 private OCFile getCurrentDir() {
307 OCFile file = getFile();
308 if (file != null) {
309 if (file.isFolder()) {
310 return file;
311 } else if (getStorageManager() != null) {
312 String parentPath = file.getRemotePath().substring(0, file.getRemotePath().lastIndexOf(file.getFileName()));
313 return getStorageManager().getFileByPath(parentPath);
314 }
315 }
316 return null;
317 }
318
319 protected void refreshListOfFilesFragment() {
320 MoveFileListFragment fileListFragment = getListOfFilesFragment();
321 if (fileListFragment != null) {
322 fileListFragment.listDirectory();
323 }
324 }
325
326 private class SyncBroadcastReceiver extends BroadcastReceiver {
327
328 /**
329 * {@link BroadcastReceiver} to enable syncing feedback in UI
330 */
331 @Override
332 public void onReceive(Context context, Intent intent) {
333 try {
334 String event = intent.getAction();
335 Log_OC.d(TAG, "Received broadcast " + event);
336 String accountName = intent.getStringExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME);
337 String synchFolderRemotePath = intent.getStringExtra(FileSyncAdapter.EXTRA_FOLDER_PATH);
338 RemoteOperationResult synchResult = (RemoteOperationResult)intent.getSerializableExtra(FileSyncAdapter.EXTRA_RESULT);
339 boolean sameAccount = (getAccount() != null && accountName.equals(getAccount().name) && getStorageManager() != null);
340
341 if (sameAccount) {
342
343 if (FileSyncAdapter.EVENT_FULL_SYNC_START.equals(event)) {
344 mSyncInProgress = true;
345
346 } else {
347 OCFile currentFile = (getFile() == null) ? null : getStorageManager().getFileByPath(getFile().getRemotePath());
348 OCFile currentDir = (getCurrentDir() == null) ? null : getStorageManager().getFileByPath(getCurrentDir().getRemotePath());
349
350 if (currentDir == null) {
351 // current folder was removed from the server
352 Toast.makeText( MoveActivity.this,
353 String.format(getString(R.string.sync_current_folder_was_removed), mDirectories.getItem(0)),
354 Toast.LENGTH_LONG)
355 .show();
356 browseToRoot();
357
358 } else {
359 if (currentFile == null && !getFile().isFolder()) {
360 // currently selected file was removed in the server, and now we know it
361 currentFile = currentDir;
362 }
363
364 if (synchFolderRemotePath != null && currentDir.getRemotePath().equals(synchFolderRemotePath)) {
365 MoveFileListFragment fileListFragment = getListOfFilesFragment();
366 if (fileListFragment != null) {
367 fileListFragment.listDirectory(currentDir);
368 }
369 }
370 setFile(currentFile);
371 }
372
373 mSyncInProgress = (!FileSyncAdapter.EVENT_FULL_SYNC_END.equals(event) && !SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event));
374
375 if (SynchronizeFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED.
376 equals(event) &&
377 /// TODO refactor and make common
378 synchResult != null && !synchResult.isSuccess() &&
379 (synchResult.getCode() == ResultCode.UNAUTHORIZED ||
380 synchResult.isIdPRedirection() ||
381 (synchResult.isException() && synchResult.getException()
382 instanceof AuthenticatorException))) {
383
384 OwnCloudClient client = null;
385 try {
386 OwnCloudAccount ocAccount =
387 new OwnCloudAccount(getAccount(), context);
388 client = (OwnCloudClientManagerFactory.getDefaultSingleton().
389 removeClientFor(ocAccount));
390 // TODO get rid of these exceptions
391 } catch (AccountNotFoundException e) {
392 e.printStackTrace();
393 } catch (AuthenticatorException e) {
394 e.printStackTrace();
395 } catch (OperationCanceledException e) {
396 e.printStackTrace();
397 } catch (IOException e) {
398 e.printStackTrace();
399 }
400
401 if (client != null) {
402 OwnCloudCredentials cred = client.getCredentials();
403 if (cred != null) {
404 AccountManager am = AccountManager.get(context);
405 if (cred.authTokenExpires()) {
406 am.invalidateAuthToken(
407 getAccount().type,
408 cred.getAuthToken()
409 );
410 } else {
411 am.clearPassword(getAccount());
412 }
413 }
414 }
415
416 requestCredentialsUpdate();
417
418 }
419 }
420 removeStickyBroadcast(intent);
421 Log_OC.d(TAG, "Setting progress visibility to " + mSyncInProgress);
422 setSupportProgressBarIndeterminateVisibility(mSyncInProgress /*|| mRefreshSharesInProgress*/);
423
424 setBackgroundText();
425
426 }
427
428 } catch (RuntimeException e) {
429 // avoid app crashes after changing the serial id of RemoteOperationResult
430 // in owncloud library with broadcast notifications pending to process
431 removeStickyBroadcast(intent);
432 }
433 }
434 }
435
436 public void browseToRoot() {
437 MoveFileListFragment listOfFiles = getListOfFilesFragment();
438 if (listOfFiles != null) { // should never be null, indeed
439 while (mDirectories.getCount() > 1) {
440 popDirname();
441 }
442 OCFile root = getStorageManager().getFileByPath(OCFile.ROOT_PATH);
443 listOfFiles.listDirectory(root);
444 setFile(listOfFiles.getCurrentFile());
445 startSyncFolderOperation(root);
446 }
447 }
448
449 /**
450 * Pops a directory name from the drop down list
451 * @return True, unless the stack is empty
452 */
453 public boolean popDirname() {
454 mDirectories.remove(mDirectories.getItem(0));
455 return !mDirectories.isEmpty();
456 }
457
458 private void setNavigationListWithFolder(OCFile file) {
459 mDirectories.clear();
460 OCFile fileIt = file;
461 String parentPath;
462 while(fileIt != null && fileIt.getFileName() != OCFile.ROOT_PATH) {
463 if (fileIt.isFolder()) {
464 mDirectories.add(fileIt.getFileName());
465 }
466 // get parent from path
467 parentPath = fileIt.getRemotePath().substring(0, fileIt.getRemotePath().lastIndexOf(fileIt.getFileName()));
468 fileIt = getStorageManager().getFileByPath(parentPath);
469 }
470 mDirectories.add(OCFile.PATH_SEPARATOR);
471 }
472
473 @Override
474 public boolean onNavigationItemSelected(int itemPosition, long itemId) {
475 if (itemPosition != 0) {
476 String targetPath = "";
477 for (int i=itemPosition; i < mDirectories.getCount() - 1; i++) {
478 targetPath = mDirectories.getItem(i) + OCFile.PATH_SEPARATOR + targetPath;
479 }
480 targetPath = OCFile.PATH_SEPARATOR + targetPath;
481 OCFile targetFolder = getStorageManager().getFileByPath(targetPath);
482 if (targetFolder != null) {
483 browseTo(targetFolder);
484 }
485
486 // the next operation triggers a new call to this method, but it's necessary to
487 // ensure that the name exposed in the action bar is the current directory when the
488 // user selected it in the navigation list
489 if (getSupportActionBar().getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST && itemPosition != 0)
490 getSupportActionBar().setSelectedNavigationItem(0);
491 }
492 return true;
493 }
494
495 public void browseTo(OCFile folder) {
496 if (folder == null || !folder.isFolder()) {
497 throw new IllegalArgumentException("Trying to browse to invalid folder " + folder);
498 }
499 MoveFileListFragment listOfFiles = getListOfFilesFragment();
500 if (listOfFiles != null) {
501 setNavigationListWithFolder(folder);
502 listOfFiles.listDirectory(folder);
503 setFile(listOfFiles.getCurrentFile());
504 startSyncFolderOperation(folder);
505 } else {
506 Log_OC.e(TAG, "Unexpected null when accessing list fragment");
507 }
508 }
509
510 @Override
511 public void onBackPressed() {
512 MoveFileListFragment listOfFiles = getListOfFilesFragment();
513 if (listOfFiles != null) { // should never be null, indeed
514 if (mDirectories.getCount() <= 1) {
515 finish();
516 return;
517 }
518 int levelsUp = listOfFiles.onBrowseUp();
519 for (int i=0; i < levelsUp && mDirectories.getCount() > 1 ; i++) {
520 popDirname();
521 }
522 }
523 if (listOfFiles != null) { // should never be null, indeed
524 setFile(listOfFiles.getCurrentFile());
525 }
526 }
527
528 private void updateNavigationElementsInActionBar(OCFile chosenFile) {
529 ActionBar actionBar = getSupportActionBar();
530 if (chosenFile == null) {
531 // only list of files - set for browsing through folders
532 OCFile currentDir = getCurrentDir();
533 boolean noRoot = (currentDir != null && currentDir.getParentId() != 0);
534 actionBar.setDisplayHomeAsUpEnabled(noRoot);
535 actionBar.setDisplayShowTitleEnabled(!noRoot);
536 if (!noRoot) {
537 actionBar.setTitle(getString(R.string.default_display_name_for_root_folder));
538 }
539 actionBar.setNavigationMode(!noRoot ? ActionBar.NAVIGATION_MODE_STANDARD : ActionBar.NAVIGATION_MODE_LIST);
540 actionBar.setListNavigationCallbacks(mDirectories, this); // assuming mDirectories is updated
541
542 } else {
543 actionBar.setDisplayHomeAsUpEnabled(true);
544 actionBar.setDisplayShowTitleEnabled(true);
545 actionBar.setTitle(chosenFile.getFileName());
546 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
547 }
548 }
549
550 /**
551 * Called when the ownCloud {@link Account} associated to the Activity was just updated.
552 */
553 @Override
554 protected void onAccountSet(boolean stateWasRecovered) {
555 super.onAccountSet(stateWasRecovered);
556 if (getAccount() != null) {
557 /// Check whether the 'main' OCFile handled by the Activity is contained in the current Account
558 OCFile file = getFile();
559 // get parent from path
560 String parentPath = "";
561 if (file != null) {
562 if (file.isDown() && file.getLastSyncDateForProperties() == 0) {
563 // upload in progress - right now, files are not inserted in the local cache until the upload is successful
564 // get parent from path
565 parentPath = file.getRemotePath().substring(0, file.getRemotePath().lastIndexOf(file.getFileName()));
566 if (getStorageManager().getFileByPath(parentPath) == null)
567 file = null; // not able to know the directory where the file is uploading
568 } else {
569 file = getStorageManager().getFileByPath(file.getRemotePath()); // currentDir = null if not in the current Account
570 }
571 }
572 if (file == null) {
573 // fall back to root folder
574 file = getStorageManager().getFileByPath(OCFile.ROOT_PATH); // never returns null
575 }
576 setFile(file);
577 setNavigationListWithFolder(file);
578
579 if (!stateWasRecovered) {
580 Log_OC.e(TAG, "Initializing Fragments in onAccountChanged..");
581 if (file.isFolder()) {
582 startSyncFolderOperation(file);
583 }
584 } else {
585 updateNavigationElementsInActionBar(file.isFolder() ? null : file);
586 }
587 }
588 }
589
590 /**
591 * Set controllers
592 */
593 private void initControls(){
594 mCancelBtn = (Button) findViewById(R.id.move_files_btn_cancel);
595 mCancelBtn.setOnClickListener(this);
596 mChooseBtn = (Button) findViewById(R.id.move_files_btn_choose);
597 mChooseBtn.setOnClickListener(this);
598 }
599
600 @Override
601 public void onClick(View v) {
602 if (v == mCancelBtn) {
603 finish();
604 }
605 }
606 }