Home | History | Annotate | Download | only in documentsui
      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 
     68     public DocumentsActivity() {
     69         super(R.layout.documents_activity, TAG);
     70     }
     71 
     72     @Override
     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
    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 
    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
    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
    188     protected void onPostCreate(Bundle savedInstanceState) {
    189         super.onPostCreate(savedInstanceState);
    190         mDrawer.update();
    191         mNavigator.update();
    192     }
    193 
    194     @Override
    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
    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
    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 
    303     void onSaveRequested(DocumentInfo replaceTarget) {
    304         new ExistingFinishTask(this, replaceTarget.derivedUri)
    305                 .executeOnExecutor(getExecutorForCurrentDirectory());
    306     }
    307 
    308     @Override
    309     void onDirectoryCreated(DocumentInfo doc) {
    310         assert(doc.isDirectory());
    311         openContainerDocument(doc);
    312     }
    313 
    314     void onSaveRequested(String mimeType, String displayName) {
    315         new CreateFinishTask(this, mimeType, displayName)
    316                 .executeOnExecutor(getExecutorForCurrentDirectory());
    317     }
    318 
    319     @Override
    320     void onRootPicked(RootInfo root) {
    321         super.onRootPicked(root);
    322         mNavigator.revealRootsDrawer(false);
    323     }
    324 
    325     @Override
    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
    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 
    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 
    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
    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 
    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 
    446         public LoadLastUsedStackTask(DocumentsActivity activity) {
    447             super(activity);
    448             mState = activity.mState;
    449         }
    450 
    451         @Override
    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
    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 
    503         public PickFinishTask(DocumentsActivity activity, Uri uri) {
    504             super(activity);
    505             mUri = uri;
    506         }
    507 
    508         @Override
    509         protected Void run(Void... params) {
    510             mOwner.writeStackToRecentsBlocking();
    511             return null;
    512         }
    513 
    514         @Override
    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 
    523         public ExistingFinishTask(DocumentsActivity activity, Uri... uris) {
    524             super(activity);
    525             mUris = uris;
    526         }
    527 
    528         @Override
    529         protected Void run(Void... params) {
    530             mOwner.writeStackToRecentsBlocking();
    531             return null;
    532         }
    533 
    534         @Override
    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 
    547         public CreateFinishTask(DocumentsActivity activity, String mimeType, String displayName) {
    548             super(activity);
    549             mMimeType = mimeType;
    550             mDisplayName = displayName;
    551         }
    552 
    553         @Override
    554         protected void prepare() {
    555             mOwner.setPending(true);
    556         }
    557 
    558         @Override
    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
    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