• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.documentsui;
18 
19 import static com.android.documentsui.Shared.DEBUG;
20 import static com.android.documentsui.State.ACTION_CREATE;
21 import static com.android.documentsui.State.ACTION_GET_CONTENT;
22 import static com.android.documentsui.State.ACTION_OPEN;
23 import static com.android.documentsui.State.ACTION_OPEN_TREE;
24 import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION;
25 
26 import android.app.Activity;
27 import android.app.Fragment;
28 import android.app.FragmentManager;
29 import android.content.ClipData;
30 import android.content.ComponentName;
31 import android.content.ContentProviderClient;
32 import android.content.ContentResolver;
33 import android.content.ContentValues;
34 import android.content.Intent;
35 import android.content.pm.ResolveInfo;
36 import android.database.Cursor;
37 import android.net.Uri;
38 import android.os.Bundle;
39 import android.os.Parcelable;
40 import android.provider.DocumentsContract;
41 import android.support.design.widget.Snackbar;
42 import android.util.Log;
43 import android.view.Menu;
44 import android.view.MenuItem;
45 
46 import com.android.documentsui.RecentsProvider.RecentColumns;
47 import com.android.documentsui.RecentsProvider.ResumeColumns;
48 import com.android.documentsui.dirlist.AnimationView;
49 import com.android.documentsui.dirlist.DirectoryFragment;
50 import com.android.documentsui.dirlist.Model;
51 import com.android.documentsui.model.DocumentInfo;
52 import com.android.documentsui.model.DurableUtils;
53 import com.android.documentsui.model.RootInfo;
54 import com.android.documentsui.services.FileOperationService;
55 
56 import libcore.io.IoUtils;
57 
58 import java.io.FileNotFoundException;
59 import java.io.IOException;
60 import java.util.Arrays;
61 import java.util.Collection;
62 import java.util.List;
63 
64 public class DocumentsActivity extends BaseActivity {
65     private static final int CODE_FORWARD = 42;
66     private static final String TAG = "DocumentsActivity";
67 
DocumentsActivity()68     public DocumentsActivity() {
69         super(R.layout.documents_activity, TAG);
70     }
71 
72     @Override
onCreate(Bundle icicle)73     public void onCreate(Bundle icicle) {
74         super.onCreate(icicle);
75 
76         if (mState.action == ACTION_CREATE) {
77             final String mimeType = getIntent().getType();
78             final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
79             SaveFragment.show(getFragmentManager(), mimeType, title);
80         } else if (mState.action == ACTION_OPEN_TREE ||
81                    mState.action == ACTION_PICK_COPY_DESTINATION) {
82             PickFragment.show(getFragmentManager());
83         }
84 
85         if (mState.action == ACTION_GET_CONTENT) {
86             final Intent moreApps = new Intent(getIntent());
87             moreApps.setComponent(null);
88             moreApps.setPackage(null);
89             RootsFragment.show(getFragmentManager(), moreApps);
90         } else if (mState.action == ACTION_OPEN ||
91                    mState.action == ACTION_CREATE ||
92                    mState.action == ACTION_OPEN_TREE ||
93                    mState.action == ACTION_PICK_COPY_DESTINATION) {
94             RootsFragment.show(getFragmentManager(), null);
95         }
96 
97         if (mState.restored) {
98             if (DEBUG) Log.d(TAG, "Stack already resolved");
99         } else {
100             // We set the activity title in AsyncTask.onPostExecute().
101             // To prevent talkback from reading aloud the default title, we clear it here.
102             setTitle("");
103 
104             // As a matter of policy we don't load the last used stack for the copy
105             // destination picker (user is already in Files app).
106             // Concensus was that the experice was too confusing.
107             // In all other cases, where the user is visiting us from another app
108             // we restore the stack as last used from that app.
109             if (mState.action == ACTION_PICK_COPY_DESTINATION) {
110                 if (DEBUG) Log.d(TAG, "Launching directly into Home directory.");
111                 loadRoot(getDefaultRoot());
112             } else {
113                 if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package.");
114                 new LoadLastUsedStackTask(this).execute();
115             }
116         }
117     }
118 
119     @Override
includeState(State state)120     void includeState(State state) {
121         final Intent intent = getIntent();
122         final String action = intent.getAction();
123         if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
124             state.action = ACTION_OPEN;
125         } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
126             state.action = ACTION_CREATE;
127         } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
128             state.action = ACTION_GET_CONTENT;
129         } else if (Intent.ACTION_OPEN_DOCUMENT_TREE.equals(action)) {
130             state.action = ACTION_OPEN_TREE;
131         } else if (Shared.ACTION_PICK_COPY_DESTINATION.equals(action)) {
132             state.action = ACTION_PICK_COPY_DESTINATION;
133         }
134 
135         if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT) {
136             state.allowMultiple = intent.getBooleanExtra(
137                     Intent.EXTRA_ALLOW_MULTIPLE, false);
138         }
139 
140         if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT
141                 || state.action == ACTION_CREATE) {
142             state.openableOnly = intent.hasCategory(Intent.CATEGORY_OPENABLE);
143         }
144 
145         if (state.action == ACTION_PICK_COPY_DESTINATION) {
146             // Indicates that a copy operation (or move) includes a directory.
147             // Why? Directory creation isn't supported by some roots (like Downloads).
148             // This allows us to restrict available roots to just those with support.
149             state.directoryCopy = intent.getBooleanExtra(
150                     Shared.EXTRA_DIRECTORY_COPY, false);
151             state.copyOperationSubType = intent.getIntExtra(
152                     FileOperationService.EXTRA_OPERATION,
153                     FileOperationService.OPERATION_COPY);
154         }
155     }
156 
onAppPicked(ResolveInfo info)157     public void onAppPicked(ResolveInfo info) {
158         final Intent intent = new Intent(getIntent());
159         intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
160         intent.setComponent(new ComponentName(
161                 info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
162         startActivityForResult(intent, CODE_FORWARD);
163     }
164 
165     @Override
onActivityResult(int requestCode, int resultCode, Intent data)166     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
167         if (DEBUG) Log.d(TAG, "onActivityResult() code=" + resultCode);
168 
169         // Only relay back results when not canceled; otherwise stick around to
170         // let the user pick another app/backend.
171         if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) {
172 
173             // Remember that we last picked via external app
174             final String packageName = getCallingPackageMaybeExtra();
175             final ContentValues values = new ContentValues();
176             values.put(ResumeColumns.EXTERNAL, 1);
177             getContentResolver().insert(RecentsProvider.buildResume(packageName), values);
178 
179             // Pass back result to original caller
180             setResult(resultCode, data);
181             finish();
182         } else {
183             super.onActivityResult(requestCode, resultCode, data);
184         }
185     }
186 
187     @Override
onPostCreate(Bundle savedInstanceState)188     protected void onPostCreate(Bundle savedInstanceState) {
189         super.onPostCreate(savedInstanceState);
190         mDrawer.update();
191         mNavigator.update();
192     }
193 
194     @Override
getDrawerTitle()195     public String getDrawerTitle() {
196         String title = getIntent().getStringExtra(DocumentsContract.EXTRA_PROMPT);
197         if (title == null) {
198             if (mState.action == ACTION_OPEN ||
199                 mState.action == ACTION_GET_CONTENT ||
200                 mState.action == ACTION_OPEN_TREE) {
201                 title = getResources().getString(R.string.title_open);
202             } else if (mState.action == ACTION_CREATE ||
203                        mState.action == ACTION_PICK_COPY_DESTINATION) {
204                 title = getResources().getString(R.string.title_save);
205             } else {
206                 // If all else fails, just call it "Documents".
207                 title = getResources().getString(R.string.app_label);
208             }
209         }
210 
211         return title;
212     }
213 
214     @Override
onPrepareOptionsMenu(Menu menu)215     public boolean onPrepareOptionsMenu(Menu menu) {
216         super.onPrepareOptionsMenu(menu);
217 
218         final DocumentInfo cwd = getCurrentDirectory();
219 
220         boolean picking = mState.action == ACTION_CREATE
221                 || mState.action == ACTION_OPEN_TREE
222                 || mState.action == ACTION_PICK_COPY_DESTINATION;
223 
224         if (picking) {
225             // May already be hidden because the root
226             // doesn't support search.
227             mSearchManager.showMenu(false);
228         }
229 
230         final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
231         final MenuItem grid = menu.findItem(R.id.menu_grid);
232         final MenuItem list = menu.findItem(R.id.menu_list);
233         final MenuItem fileSize = menu.findItem(R.id.menu_file_size);
234 
235 
236         createDir.setVisible(picking);
237         createDir.setEnabled(canCreateDirectory());
238 
239         // No display options in recent directories
240         boolean inRecents = cwd == null;
241         if (picking && inRecents) {
242             grid.setVisible(false);
243             list.setVisible(false);
244         }
245 
246         fileSize.setVisible(fileSize.isVisible() && !picking);
247 
248         if (mState.action == ACTION_CREATE) {
249             final FragmentManager fm = getFragmentManager();
250             SaveFragment.get(fm).prepareForDirectory(cwd);
251         }
252 
253         Menus.disableHiddenItems(menu);
254 
255         return true;
256     }
257 
258     @Override
refreshDirectory(int anim)259     void refreshDirectory(int anim) {
260         final FragmentManager fm = getFragmentManager();
261         final RootInfo root = getCurrentRoot();
262         final DocumentInfo cwd = getCurrentDirectory();
263 
264         if (cwd == null) {
265             // No directory means recents
266             if (mState.action == ACTION_CREATE ||
267                 mState.action == ACTION_OPEN_TREE ||
268                 mState.action == ACTION_PICK_COPY_DESTINATION) {
269                 RecentsCreateFragment.show(fm);
270             } else {
271                 DirectoryFragment.showRecentsOpen(fm, anim);
272 
273                 // In recents we pick layout mode based on the mimetype,
274                 // picking GRID for visual types. We intentionally don't
275                 // consult a user's saved preferences here since they are
276                 // set per root (not per root and per mimetype).
277                 boolean visualMimes = MimePredicate.mimeMatches(
278                         MimePredicate.VISUAL_MIMES, mState.acceptMimes);
279                 mState.derivedMode = visualMimes ? State.MODE_GRID : State.MODE_LIST;
280             }
281         } else {
282                 // Normal boring directory
283                 DirectoryFragment.showDirectory(fm, root, cwd, anim);
284         }
285 
286         // Forget any replacement target
287         if (mState.action == ACTION_CREATE) {
288             final SaveFragment save = SaveFragment.get(fm);
289             if (save != null) {
290                 save.setReplaceTarget(null);
291             }
292         }
293 
294         if (mState.action == ACTION_OPEN_TREE ||
295             mState.action == ACTION_PICK_COPY_DESTINATION) {
296             final PickFragment pick = PickFragment.get(fm);
297             if (pick != null) {
298                 pick.setPickTarget(mState.action, mState.copyOperationSubType, cwd);
299             }
300         }
301     }
302 
onSaveRequested(DocumentInfo replaceTarget)303     void onSaveRequested(DocumentInfo replaceTarget) {
304         new ExistingFinishTask(this, replaceTarget.derivedUri)
305                 .executeOnExecutor(getExecutorForCurrentDirectory());
306     }
307 
308     @Override
onDirectoryCreated(DocumentInfo doc)309     void onDirectoryCreated(DocumentInfo doc) {
310         assert(doc.isDirectory());
311         openContainerDocument(doc);
312     }
313 
onSaveRequested(String mimeType, String displayName)314     void onSaveRequested(String mimeType, String displayName) {
315         new CreateFinishTask(this, mimeType, displayName)
316                 .executeOnExecutor(getExecutorForCurrentDirectory());
317     }
318 
319     @Override
onRootPicked(RootInfo root)320     void onRootPicked(RootInfo root) {
321         super.onRootPicked(root);
322         mNavigator.revealRootsDrawer(false);
323     }
324 
325     @Override
onDocumentPicked(DocumentInfo doc, Model model)326     public void onDocumentPicked(DocumentInfo doc, Model model) {
327         final FragmentManager fm = getFragmentManager();
328         if (doc.isContainer()) {
329             openContainerDocument(doc);
330         } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
331             // Explicit file picked, return
332             new ExistingFinishTask(this, doc.derivedUri)
333                     .executeOnExecutor(getExecutorForCurrentDirectory());
334         } else if (mState.action == ACTION_CREATE) {
335             // Replace selected file
336             SaveFragment.get(fm).setReplaceTarget(doc);
337         }
338     }
339 
340     @Override
onDocumentsPicked(List<DocumentInfo> docs)341     public void onDocumentsPicked(List<DocumentInfo> docs) {
342         if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
343             final int size = docs.size();
344             final Uri[] uris = new Uri[size];
345             for (int i = 0; i < size; i++) {
346                 uris[i] = docs.get(i).derivedUri;
347             }
348             new ExistingFinishTask(this, uris)
349                     .executeOnExecutor(getExecutorForCurrentDirectory());
350         }
351     }
352 
onPickRequested(DocumentInfo pickTarget)353     public void onPickRequested(DocumentInfo pickTarget) {
354         Uri result;
355         if (mState.action == ACTION_OPEN_TREE) {
356             result = DocumentsContract.buildTreeDocumentUri(
357                     pickTarget.authority, pickTarget.documentId);
358         } else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
359             result = pickTarget.derivedUri;
360         } else {
361             // Should not be reached.
362             throw new IllegalStateException("Invalid mState.action.");
363         }
364         new PickFinishTask(this, result).executeOnExecutor(getExecutorForCurrentDirectory());
365     }
366 
writeStackToRecentsBlocking()367     void writeStackToRecentsBlocking() {
368         final ContentResolver resolver = getContentResolver();
369         final ContentValues values = new ContentValues();
370 
371         final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
372         if (mState.action == ACTION_CREATE ||
373             mState.action == ACTION_OPEN_TREE ||
374             mState.action == ACTION_PICK_COPY_DESTINATION) {
375             // Remember stack for last create
376             values.clear();
377             values.put(RecentColumns.KEY, mState.stack.buildKey());
378             values.put(RecentColumns.STACK, rawStack);
379             resolver.insert(RecentsProvider.buildRecent(), values);
380         }
381 
382         // Remember location for next app launch
383         final String packageName = getCallingPackageMaybeExtra();
384         values.clear();
385         values.put(ResumeColumns.STACK, rawStack);
386         values.put(ResumeColumns.EXTERNAL, 0);
387         resolver.insert(RecentsProvider.buildResume(packageName), values);
388     }
389 
390     @Override
onTaskFinished(Uri... uris)391     void onTaskFinished(Uri... uris) {
392         if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris));
393 
394         final Intent intent = new Intent();
395         if (uris.length == 1) {
396             intent.setData(uris[0]);
397         } else if (uris.length > 1) {
398             final ClipData clipData = new ClipData(
399                     null, mState.acceptMimes, new ClipData.Item(uris[0]));
400             for (int i = 1; i < uris.length; i++) {
401                 clipData.addItem(new ClipData.Item(uris[i]));
402             }
403             intent.setClipData(clipData);
404         }
405 
406         if (mState.action == ACTION_GET_CONTENT) {
407             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
408         } else if (mState.action == ACTION_OPEN_TREE) {
409             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
410                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
411                     | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
412                     | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
413         } else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
414             // Picking a copy destination is only used internally by us, so we
415             // don't need to extend permissions to the caller.
416             intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
417             intent.putExtra(FileOperationService.EXTRA_OPERATION, mState.copyOperationSubType);
418         } else {
419             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
420                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
421                     | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
422         }
423 
424         setResult(Activity.RESULT_OK, intent);
425         finish();
426     }
427 
428 
get(Fragment fragment)429     public static DocumentsActivity get(Fragment fragment) {
430         return (DocumentsActivity) fragment.getActivity();
431     }
432 
433     /**
434      * Loads the last used path (stack) from Recents (history).
435      * The path selected is based on the calling package name. So the last
436      * path for an app like Gmail can be different than the last path
437      * for an app like DropBox.
438      */
439     private static final class LoadLastUsedStackTask
440             extends PairedTask<DocumentsActivity, Void, Void> {
441 
442         private volatile boolean mRestoredStack;
443         private volatile boolean mExternal;
444         private State mState;
445 
LoadLastUsedStackTask(DocumentsActivity activity)446         public LoadLastUsedStackTask(DocumentsActivity activity) {
447             super(activity);
448             mState = activity.mState;
449         }
450 
451         @Override
run(Void... params)452         protected Void run(Void... params) {
453             if (DEBUG && !mState.stack.isEmpty()) {
454                 Log.w(TAG, "Overwriting existing stack.");
455             }
456             RootsCache roots = DocumentsApplication.getRootsCache(mOwner);
457 
458             String packageName = mOwner.getCallingPackageMaybeExtra();
459             Uri resumeUri = RecentsProvider.buildResume(packageName);
460             Cursor cursor = mOwner.getContentResolver().query(resumeUri, null, null, null, null);
461             try {
462                 if (cursor.moveToFirst()) {
463                     mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0;
464                     final byte[] rawStack = cursor.getBlob(
465                             cursor.getColumnIndex(ResumeColumns.STACK));
466                     DurableUtils.readFromArray(rawStack, mState.stack);
467                     mRestoredStack = true;
468                 }
469             } catch (IOException e) {
470                 Log.w(TAG, "Failed to resume: " + e);
471             } finally {
472                 IoUtils.closeQuietly(cursor);
473             }
474 
475             if (mRestoredStack) {
476                 // Update the restored stack to ensure we have freshest data
477                 final Collection<RootInfo> matchingRoots = roots.getMatchingRootsBlocking(mState);
478                 try {
479                     mState.stack.updateRoot(matchingRoots);
480                     mState.stack.updateDocuments(mOwner.getContentResolver());
481                 } catch (FileNotFoundException e) {
482                     Log.w(TAG, "Failed to restore stack for package: " + packageName
483                             + " because of error: "+ e);
484                     mState.stack.reset();
485                     mRestoredStack = false;
486                 }
487             }
488 
489             return null;
490         }
491 
492         @Override
finish(Void result)493         protected void finish(Void result) {
494             mState.restored = true;
495             mState.external = mExternal;
496             mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
497         }
498     }
499 
500     private static final class PickFinishTask extends PairedTask<DocumentsActivity, Void, Void> {
501         private final Uri mUri;
502 
PickFinishTask(DocumentsActivity activity, Uri uri)503         public PickFinishTask(DocumentsActivity activity, Uri uri) {
504             super(activity);
505             mUri = uri;
506         }
507 
508         @Override
run(Void... params)509         protected Void run(Void... params) {
510             mOwner.writeStackToRecentsBlocking();
511             return null;
512         }
513 
514         @Override
finish(Void result)515         protected void finish(Void result) {
516             mOwner.onTaskFinished(mUri);
517         }
518     }
519 
520     private static final class ExistingFinishTask extends PairedTask<DocumentsActivity, Void, Void> {
521         private final Uri[] mUris;
522 
ExistingFinishTask(DocumentsActivity activity, Uri... uris)523         public ExistingFinishTask(DocumentsActivity activity, Uri... uris) {
524             super(activity);
525             mUris = uris;
526         }
527 
528         @Override
run(Void... params)529         protected Void run(Void... params) {
530             mOwner.writeStackToRecentsBlocking();
531             return null;
532         }
533 
534         @Override
finish(Void result)535         protected void finish(Void result) {
536             mOwner.onTaskFinished(mUris);
537         }
538     }
539 
540     /**
541      * Task that creates a new document in the background.
542      */
543     private static final class CreateFinishTask extends PairedTask<DocumentsActivity, Void, Uri> {
544         private final String mMimeType;
545         private final String mDisplayName;
546 
CreateFinishTask(DocumentsActivity activity, String mimeType, String displayName)547         public CreateFinishTask(DocumentsActivity activity, String mimeType, String displayName) {
548             super(activity);
549             mMimeType = mimeType;
550             mDisplayName = displayName;
551         }
552 
553         @Override
prepare()554         protected void prepare() {
555             mOwner.setPending(true);
556         }
557 
558         @Override
run(Void... params)559         protected Uri run(Void... params) {
560             final ContentResolver resolver = mOwner.getContentResolver();
561             final DocumentInfo cwd = mOwner.getCurrentDirectory();
562 
563             ContentProviderClient client = null;
564             Uri childUri = null;
565             try {
566                 client = DocumentsApplication.acquireUnstableProviderOrThrow(
567                         resolver, cwd.derivedUri.getAuthority());
568                 childUri = DocumentsContract.createDocument(
569                         client, cwd.derivedUri, mMimeType, mDisplayName);
570             } catch (Exception e) {
571                 Log.w(TAG, "Failed to create document", e);
572             } finally {
573                 ContentProviderClient.releaseQuietly(client);
574             }
575 
576             if (childUri != null) {
577                 mOwner.writeStackToRecentsBlocking();
578             }
579 
580             return childUri;
581         }
582 
583         @Override
finish(Uri result)584         protected void finish(Uri result) {
585             if (result != null) {
586                 mOwner.onTaskFinished(result);
587             } else {
588                 Snackbars.makeSnackbar(
589                         mOwner, R.string.save_error, Snackbar.LENGTH_SHORT).show();
590             }
591 
592             mOwner.setPending(false);
593         }
594     }
595 }
596