Home | History | Annotate | Download | only in documentsui
      1 /*
      2  * Copyright (C) 2015 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.OperationDialogFragment.DIALOG_TYPE_UNKNOWN;
     20 import static com.android.documentsui.Shared.DEBUG;
     21 
     22 import android.app.Activity;
     23 import android.app.FragmentManager;
     24 import android.content.ActivityNotFoundException;
     25 import android.content.ClipData;
     26 import android.content.ContentResolver;
     27 import android.content.ContentValues;
     28 import android.content.Intent;
     29 import android.net.Uri;
     30 import android.os.Bundle;
     31 import android.os.Parcelable;
     32 import android.provider.DocumentsContract;
     33 import android.support.design.widget.Snackbar;
     34 import android.util.Log;
     35 import android.view.KeyEvent;
     36 import android.view.Menu;
     37 import android.view.MenuItem;
     38 
     39 import com.android.documentsui.OperationDialogFragment.DialogType;
     40 import com.android.documentsui.RecentsProvider.ResumeColumns;
     41 import com.android.documentsui.dirlist.AnimationView;
     42 import com.android.documentsui.dirlist.DirectoryFragment;
     43 import com.android.documentsui.dirlist.Model;
     44 import com.android.documentsui.model.DocumentInfo;
     45 import com.android.documentsui.model.DocumentStack;
     46 import com.android.documentsui.model.DurableUtils;
     47 import com.android.documentsui.model.RootInfo;
     48 import com.android.documentsui.services.FileOperationService;
     49 
     50 import java.io.FileNotFoundException;
     51 import java.util.ArrayList;
     52 import java.util.Arrays;
     53 import java.util.Collection;
     54 import java.util.List;
     55 
     56 /**
     57  * Standalone file management activity.
     58  */
     59 public class FilesActivity extends BaseActivity {
     60 
     61     public static final String TAG = "FilesActivity";
     62 
     63     private DocumentClipper mClipper;
     64 
     65     public FilesActivity() {
     66         super(R.layout.files_activity, TAG);
     67     }
     68 
     69     @Override
     70     public void onCreate(Bundle icicle) {
     71         super.onCreate(icicle);
     72 
     73         mClipper = new DocumentClipper(this);
     74 
     75         RootsFragment.show(getFragmentManager(), null);
     76 
     77         final Intent intent = getIntent();
     78         final Uri uri = intent.getData();
     79 
     80         if (mState.restored) {
     81             if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData());
     82         } else if (!mState.stack.isEmpty()) {
     83             // If a non-empty stack is present in our state, it was read (presumably)
     84             // from EXTRA_STACK intent extra. In this case, we'll skip other means of
     85             // loading or restoring the stack (like URI).
     86             //
     87             // When restoring from a stack, if a URI is present, it should only ever be:
     88             // -- a launch URI: Launch URIs support sensible activity management,
     89             //    but don't specify a real content target)
     90             // -- a fake Uri from notifications. These URIs have no authority (TODO: details).
     91             //
     92             // Any other URI is *sorta* unexpected...except when browsing an archive
     93             // in downloads.
     94             if(uri != null
     95                     && uri.getAuthority() != null
     96                     && !uri.equals(mState.stack.peek())
     97                     && !LauncherActivity.isLaunchUri(uri)) {
     98                 if (DEBUG) Log.w(TAG,
     99                         "Launching with non-empty stack. Ignoring unexpected uri: " + uri);
    100             } else {
    101                 if (DEBUG) Log.d(TAG, "Launching with non-empty stack.");
    102             }
    103             refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
    104         } else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
    105             assert(uri != null);
    106             new OpenUriForViewTask(this).executeOnExecutor(
    107                     ProviderExecutor.forAuthority(uri.getAuthority()), uri);
    108         } else if (DocumentsContract.isRootUri(this, uri)) {
    109             if (DEBUG) Log.d(TAG, "Launching with root URI.");
    110             // If we've got a specific root to display, restore that root using a dedicated
    111             // authority. That way a misbehaving provider won't result in an ANR.
    112             loadRoot(uri);
    113         } else {
    114             if (DEBUG) Log.d(TAG, "All other means skipped. Launching into default directory.");
    115             loadRoot(getDefaultRoot());
    116         }
    117 
    118         final @DialogType int dialogType = intent.getIntExtra(
    119                 FileOperationService.EXTRA_DIALOG_TYPE, DIALOG_TYPE_UNKNOWN);
    120         // DialogFragment takes care of restoring the dialog on configuration change.
    121         // Only show it manually for the first time (icicle is null).
    122         if (icicle == null && dialogType != DIALOG_TYPE_UNKNOWN) {
    123             final int opType = intent.getIntExtra(
    124                     FileOperationService.EXTRA_OPERATION,
    125                     FileOperationService.OPERATION_COPY);
    126             final ArrayList<DocumentInfo> srcList =
    127                     intent.getParcelableArrayListExtra(FileOperationService.EXTRA_SRC_LIST);
    128             OperationDialogFragment.show(
    129                     getFragmentManager(),
    130                     dialogType,
    131                     srcList,
    132                     mState.stack,
    133                     opType);
    134         }
    135     }
    136 
    137     @Override
    138     void includeState(State state) {
    139         final Intent intent = getIntent();
    140 
    141         state.action = State.ACTION_BROWSE;
    142         state.allowMultiple = true;
    143 
    144         // Options specific to the DocumentsActivity.
    145         assert(!intent.hasExtra(Intent.EXTRA_LOCAL_ONLY));
    146 
    147         final DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
    148         if (stack != null) {
    149             state.stack = stack;
    150         }
    151     }
    152 
    153     @Override
    154     protected void onPostCreate(Bundle savedInstanceState) {
    155         super.onPostCreate(savedInstanceState);
    156         // This check avoids a flicker from "Recents" to "Home".
    157         // Only update action bar at this point if there is an active
    158         // serach. Why? Because this avoid an early (undesired) load of
    159         // the recents root...which is the default root in other activities.
    160         // In Files app "Home" is the default, but it is loaded async.
    161         // update will be called once Home root is loaded.
    162         // Except while searching we need this call to ensure the
    163         // search bits get layed out correctly.
    164         if (mSearchManager.isSearching()) {
    165             mNavigator.update();
    166         }
    167     }
    168 
    169     @Override
    170     public void onResume() {
    171         super.onResume();
    172 
    173         final RootInfo root = getCurrentRoot();
    174 
    175         // If we're browsing a specific root, and that root went away, then we
    176         // have no reason to hang around.
    177         // TODO: Rather than just disappearing, maybe we should inform
    178         // the user what has happened, let them close us. Less surprising.
    179         if (mRoots.getRootBlocking(root.authority, root.rootId) == null) {
    180             finish();
    181         }
    182     }
    183 
    184     @Override
    185     public String getDrawerTitle() {
    186         Intent intent = getIntent();
    187         return (intent != null && intent.hasExtra(Intent.EXTRA_TITLE))
    188                 ? intent.getStringExtra(Intent.EXTRA_TITLE)
    189                 : getTitle().toString();
    190     }
    191 
    192     @Override
    193     public boolean onPrepareOptionsMenu(Menu menu) {
    194         super.onPrepareOptionsMenu(menu);
    195 
    196         final RootInfo root = getCurrentRoot();
    197 
    198         final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
    199         final MenuItem pasteFromCb = menu.findItem(R.id.menu_paste_from_clipboard);
    200         final MenuItem settings = menu.findItem(R.id.menu_settings);
    201         final MenuItem newWindow = menu.findItem(R.id.menu_new_window);
    202 
    203         createDir.setVisible(true);
    204         createDir.setEnabled(canCreateDirectory());
    205         pasteFromCb.setEnabled(mClipper.hasItemsToPaste());
    206         settings.setVisible(root.hasSettings());
    207         newWindow.setVisible(Shared.shouldShowFancyFeatures(this));
    208 
    209         Menus.disableHiddenItems(menu, pasteFromCb);
    210         // It hides icon if searching in progress
    211         mSearchManager.updateMenu();
    212         return true;
    213     }
    214 
    215     @Override
    216     public boolean onOptionsItemSelected(MenuItem item) {
    217         switch (item.getItemId()) {
    218             case R.id.menu_create_dir:
    219                 assert(canCreateDirectory());
    220                 showCreateDirectoryDialog();
    221                 break;
    222             case R.id.menu_new_window:
    223                 createNewWindow();
    224                 break;
    225             case R.id.menu_paste_from_clipboard:
    226                 DirectoryFragment dir = getDirectoryFragment();
    227                 if (dir != null) {
    228                     dir.pasteFromClipboard();
    229                 }
    230                 break;
    231             default:
    232                 return super.onOptionsItemSelected(item);
    233         }
    234         return true;
    235     }
    236 
    237     private void createNewWindow() {
    238         Metrics.logUserAction(this, Metrics.USER_ACTION_NEW_WINDOW);
    239 
    240         Intent intent = LauncherActivity.createLaunchIntent(this);
    241         intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
    242 
    243         // With new multi-window mode we have to pick how we are launched.
    244         // By default we'd be launched in-place above the existing app.
    245         // By setting launch-to-side ActivityManager will open us to side.
    246         if (isInMultiWindowMode()) {
    247             intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
    248         }
    249 
    250         startActivity(intent);
    251     }
    252 
    253     @Override
    254     void refreshDirectory(int anim) {
    255         final FragmentManager fm = getFragmentManager();
    256         final RootInfo root = getCurrentRoot();
    257         final DocumentInfo cwd = getCurrentDirectory();
    258 
    259         assert(!mSearchManager.isSearching());
    260 
    261         if (cwd == null) {
    262             DirectoryFragment.showRecentsOpen(fm, anim);
    263         } else {
    264             // Normal boring directory
    265             DirectoryFragment.showDirectory(fm, root, cwd, anim);
    266         }
    267     }
    268 
    269     @Override
    270     void onRootPicked(RootInfo root) {
    271         super.onRootPicked(root);
    272         mDrawer.setOpen(false);
    273     }
    274 
    275     @Override
    276     public void onDocumentsPicked(List<DocumentInfo> docs) {
    277         throw new UnsupportedOperationException();
    278     }
    279 
    280     @Override
    281     public void onDocumentPicked(DocumentInfo doc, Model model) {
    282         // Anything on downloads goes through the back through downloads manager
    283         // (that's the MANAGE_DOCUMENT bit).
    284         // This is done for two reasons:
    285         // 1) The file in question might be a failed/queued or otherwise have some
    286         //    specialized download handling.
    287         // 2) For APKs, the download manager will add on some important security stuff
    288         //    like origin URL.
    289         // All other files not on downloads, event APKs, would get no benefit from this
    290         // treatment, thusly the "isDownloads" check.
    291 
    292         // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for
    293         // files in archives. Also, if the activity is already browsing a ZIP from downloads,
    294         // then skip MANAGE_DOCUMENTS.
    295         final boolean isViewing = Intent.ACTION_VIEW.equals(getIntent().getAction());
    296         final boolean isInArchive = mState.stack.size() > 1;
    297         if (getCurrentRoot().isDownloads() && !isInArchive && !isViewing) {
    298             // First try managing the document; we expect manager to filter
    299             // based on authority, so we don't grant.
    300             final Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
    301             manage.setData(doc.derivedUri);
    302 
    303             try {
    304                 startActivity(manage);
    305                 return;
    306             } catch (ActivityNotFoundException ex) {
    307                 // fall back to regular handling below.
    308             }
    309         }
    310 
    311         if (doc.isContainer()) {
    312             openContainerDocument(doc);
    313         } else {
    314             openDocument(doc, model);
    315         }
    316     }
    317 
    318     /**
    319      * Launches an intent to view the specified document.
    320      */
    321     private void openDocument(DocumentInfo doc, Model model) {
    322         Intent intent = new QuickViewIntentBuilder(
    323                 getPackageManager(), getResources(), doc, model).build();
    324 
    325         if (intent != null) {
    326             // TODO: un-work around issue b/24963914. Should be fixed soon.
    327             try {
    328                 startActivity(intent);
    329                 return;
    330             } catch (SecurityException e) {
    331                 // Carry on to regular view mode.
    332                 Log.e(TAG, "Caught security error: " + e.getLocalizedMessage());
    333             }
    334         }
    335 
    336         // Fall back to traditional VIEW action...
    337         intent = new Intent(Intent.ACTION_VIEW);
    338         intent.setDataAndType(doc.derivedUri, doc.mimeType);
    339 
    340         // Downloads has traditionally added the WRITE permission
    341         // in the TrampolineActivity. Since this behavior is long
    342         // established, we set the same permission for non-managed files
    343         // This ensures consistent behavior between the Downloads root
    344         // and other roots.
    345         int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
    346         if (doc.isWriteSupported()) {
    347             flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
    348         }
    349         intent.setFlags(flags);
    350 
    351         if (DEBUG && intent.getClipData() != null) {
    352             Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData());
    353         }
    354 
    355         try {
    356             startActivity(intent);
    357         } catch (ActivityNotFoundException e) {
    358             Snackbars.makeSnackbar(
    359                     this, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
    360         }
    361     }
    362 
    363     @Override
    364     public boolean onKeyShortcut(int keyCode, KeyEvent event) {
    365         DirectoryFragment dir;
    366         // TODO: All key events should be statically bound using alphabeticShortcut.
    367         // But not working.
    368         switch (keyCode) {
    369             case KeyEvent.KEYCODE_A:
    370                 dir = getDirectoryFragment();
    371                 if (dir != null) {
    372                     dir.selectAllFiles();
    373                 }
    374                 return true;
    375             case KeyEvent.KEYCODE_C:
    376                 dir = getDirectoryFragment();
    377                 if (dir != null) {
    378                     dir.copySelectedToClipboard();
    379                 }
    380                 return true;
    381             case KeyEvent.KEYCODE_V:
    382                 dir = getDirectoryFragment();
    383                 if (dir != null) {
    384                     dir.pasteFromClipboard();
    385                 }
    386                 return true;
    387             default:
    388                 return super.onKeyShortcut(keyCode, event);
    389         }
    390     }
    391 
    392     // Turns out only DocumentsActivity was ever calling saveStackBlocking.
    393     // There may be a  case where we want to contribute entries from
    394     // Behavior here in FilesActivity, but it isn't yet obvious.
    395     // TODO: Contribute to recents, or remove this.
    396     void writeStackToRecentsBlocking() {
    397         final ContentResolver resolver = getContentResolver();
    398         final ContentValues values = new ContentValues();
    399 
    400         final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
    401 
    402         // Remember location for next app launch
    403         final String packageName = getCallingPackageMaybeExtra();
    404         values.clear();
    405         values.put(ResumeColumns.STACK, rawStack);
    406         values.put(ResumeColumns.EXTERNAL, 0);
    407         resolver.insert(RecentsProvider.buildResume(packageName), values);
    408     }
    409 
    410     @Override
    411     void onTaskFinished(Uri... uris) {
    412         if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris));
    413 
    414         final Intent intent = new Intent();
    415         if (uris.length == 1) {
    416             intent.setData(uris[0]);
    417         } else if (uris.length > 1) {
    418             final ClipData clipData = new ClipData(
    419                     null, mState.acceptMimes, new ClipData.Item(uris[0]));
    420             for (int i = 1; i < uris.length; i++) {
    421                 clipData.addItem(new ClipData.Item(uris[i]));
    422             }
    423             intent.setClipData(clipData);
    424         }
    425 
    426         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
    427                 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    428                 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
    429 
    430         setResult(Activity.RESULT_OK, intent);
    431         finish();
    432     }
    433 
    434     /**
    435      * Builds a stack for the specific Uris. Multi roots are not supported, as it's impossible
    436      * to know which root to select. Also, the stack doesn't contain intermediate directories.
    437      * It's primarly used for opening ZIP archives from Downloads app.
    438      */
    439     private static final class OpenUriForViewTask extends PairedTask<FilesActivity, Uri, Void> {
    440 
    441         private final State mState;
    442         public OpenUriForViewTask(FilesActivity activity) {
    443             super(activity);
    444             mState = activity.mState;
    445         }
    446 
    447         @Override
    448         protected Void run(Uri... params) {
    449             final Uri uri = params[0];
    450 
    451             final RootsCache rootsCache = DocumentsApplication.getRootsCache(mOwner);
    452             final String authority = uri.getAuthority();
    453 
    454             final Collection<RootInfo> roots =
    455                     rootsCache.getRootsForAuthorityBlocking(authority);
    456             if (roots.isEmpty()) {
    457                 Log.e(TAG, "Failed to find root for the requested Uri: " + uri);
    458                 return null;
    459             }
    460 
    461             final RootInfo root = roots.iterator().next();
    462             mState.stack.root = root;
    463             try {
    464                 mState.stack.add(DocumentInfo.fromUri(mOwner.getContentResolver(), uri));
    465             } catch (FileNotFoundException e) {
    466                 Log.e(TAG, "Failed to resolve DocumentInfo from Uri: " + uri);
    467             }
    468             mState.stack.add(mOwner.getRootDocumentBlocking(root));
    469             return null;
    470         }
    471 
    472         @Override
    473         protected void finish(Void result) {
    474             mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
    475         }
    476     }
    477 }
    478