Home | History | Annotate | Download | only in files
      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.files;
     18 
     19 import static com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_UNKNOWN;
     20 
     21 import android.app.ActivityManager.TaskDescription;
     22 import android.app.FragmentManager;
     23 import android.content.Intent;
     24 import android.graphics.drawable.BitmapDrawable;
     25 import android.net.Uri;
     26 import android.os.Bundle;
     27 import android.support.annotation.CallSuper;
     28 import android.view.KeyEvent;
     29 import android.view.KeyboardShortcutGroup;
     30 import android.view.Menu;
     31 import android.view.MenuItem;
     32 
     33 import com.android.documentsui.ActionModeController;
     34 import com.android.documentsui.BaseActivity;
     35 import com.android.documentsui.DocumentsApplication;
     36 import com.android.documentsui.DragShadowBuilder;
     37 import com.android.documentsui.FocusManager;
     38 import com.android.documentsui.Injector;
     39 import com.android.documentsui.MenuManager.DirectoryDetails;
     40 import com.android.documentsui.OperationDialogFragment;
     41 import com.android.documentsui.OperationDialogFragment.DialogType;
     42 import com.android.documentsui.ProviderExecutor;
     43 import com.android.documentsui.R;
     44 import com.android.documentsui.SharedInputHandler;
     45 import com.android.documentsui.base.DocumentInfo;
     46 import com.android.documentsui.base.Features;
     47 import com.android.documentsui.base.RootInfo;
     48 import com.android.documentsui.base.State;
     49 import com.android.documentsui.clipping.DocumentClipper;
     50 import com.android.documentsui.dirlist.AnimationView.AnimationType;
     51 import com.android.documentsui.dirlist.DirectoryFragment;
     52 import com.android.documentsui.prefs.ScopedPreferences;
     53 import com.android.documentsui.selection.SelectionManager;
     54 import com.android.documentsui.services.FileOperationService;
     55 import com.android.documentsui.sidebar.RootsFragment;
     56 import com.android.documentsui.ui.DialogController;
     57 import com.android.documentsui.ui.MessageBuilder;
     58 
     59 import java.util.ArrayList;
     60 import java.util.List;
     61 
     62 /**
     63  * Standalone file management activity.
     64  */
     65 public class FilesActivity extends BaseActivity implements ActionHandler.Addons {
     66 
     67     private static final String TAG = "FilesActivity";
     68     static final String PREFERENCES_SCOPE = "files";
     69 
     70     private Injector<ActionHandler<FilesActivity>> mInjector;
     71     private ActivityInputHandler mActivityInputHandler;
     72     private SharedInputHandler mSharedInputHandler;
     73     private DragShadowBuilder mShadowBuilder;
     74 
     75     public FilesActivity() {
     76         super(R.layout.files_activity, TAG);
     77     }
     78 
     79     @Override
     80     public void onCreate(Bundle icicle) {
     81 
     82         MessageBuilder messages = new MessageBuilder(this);
     83         Features features = Features.create(this);
     84         mInjector = new Injector<>(
     85                 features,
     86                 new Config(),
     87                 ScopedPreferences.create(this, PREFERENCES_SCOPE),
     88                 messages,
     89                 DialogController.create(this, messages));
     90 
     91         super.onCreate(icicle);
     92 
     93         DocumentClipper clipper = DocumentsApplication.getDocumentClipper(this);
     94         mInjector.selectionMgr = new SelectionManager(SelectionManager.MODE_MULTIPLE);
     95 
     96         mInjector.focusManager = new FocusManager(
     97                 mInjector.features,
     98                 mInjector.selectionMgr,
     99                 mDrawer,
    100                 this::focusSidebar,
    101                 getColor(R.color.accent_dark));
    102 
    103         mInjector.menuManager = new MenuManager(
    104                 mInjector.features,
    105                 mSearchManager,
    106                 mState,
    107                 new DirectoryDetails(this) {
    108                     @Override
    109                     public boolean hasItemsToPaste() {
    110                         return clipper.hasItemsToPaste();
    111                     }
    112                 },
    113                 getApplicationContext(),
    114                 mInjector.selectionMgr,
    115                 mProviders::getApplicationName,
    116                 mInjector.getModel()::getItemUri);
    117 
    118         mShadowBuilder = new DragShadowBuilder(this);
    119         mInjector.actionModeController = new ActionModeController(
    120                 this,
    121                 mInjector.selectionMgr,
    122                 mInjector.menuManager,
    123                 mInjector.messages);
    124 
    125         mInjector.actions = new ActionHandler<>(
    126                 this,
    127                 mState,
    128                 mProviders,
    129                 mDocs,
    130                 mSearchManager,
    131                 ProviderExecutor::forAuthority,
    132                 mInjector.actionModeController,
    133                 clipper,
    134                 DocumentsApplication.getClipStore(this),
    135                 mInjector);
    136 
    137         mInjector.searchManager = mSearchManager;
    138 
    139         mActivityInputHandler =
    140                 new ActivityInputHandler(mInjector.actions::deleteSelectedDocuments);
    141         mSharedInputHandler =
    142                 new SharedInputHandler(
    143                         mInjector.focusManager,
    144                         mInjector.selectionMgr,
    145                         mInjector.searchManager::cancelSearch,
    146                         this::popDir,
    147                         mInjector.features);
    148 
    149         RootsFragment.show(getFragmentManager(), null);
    150 
    151         final Intent intent = getIntent();
    152 
    153         mInjector.actions.initLocation(intent);
    154 
    155         // Allow the activity to masquerade as another, so we can look both like
    156         // Downloads and Files, but with only a single underlying activity.
    157         if (intent.hasExtra(LauncherActivity.TASK_LABEL_RES)
    158                 && intent.hasExtra(LauncherActivity.TASK_ICON_RES)) {
    159             updateTaskDescription(intent);
    160         }
    161 
    162         presentFileErrors(icicle, intent);
    163     }
    164 
    165     // This is called in the intent contains label and icon resources.
    166     // When that is true, the launcher activity has supplied them so we
    167     // can adapt our presentation to how we were launched.
    168     // Without this code, overlaying launcher_icon and launcher_label
    169     // resources won't create a complete illusion of the activity being renamed.
    170     // E.g. if we re-brand Files to Downloads by overlaying label and icon
    171     // when the user tapped recents they'd see not "Downloads", but the
    172     // underlying Activity description...Files.
    173     // Alternate if we rename this activity, when launching other ways
    174     // like when browsing files on a removable disk, the app would be
    175     // called Downloads, which is also not the desired behavior.
    176     private void updateTaskDescription(final Intent intent) {
    177         int labelRes = intent.getIntExtra(LauncherActivity.TASK_LABEL_RES, -1);
    178         assert(labelRes > -1);
    179         String label = getResources().getString(labelRes);
    180 
    181         int iconRes = intent.getIntExtra(LauncherActivity.TASK_ICON_RES, -1);
    182         assert(iconRes > -1);
    183 
    184         BitmapDrawable drawable = (BitmapDrawable) getResources().getDrawable(
    185                 iconRes,
    186                 null  // we don't care about theme, since the supplier should have handled that.
    187                 );
    188 
    189         setTaskDescription(new TaskDescription(label, drawable.getBitmap()));
    190     }
    191 
    192     private void presentFileErrors(Bundle icicle, final Intent intent) {
    193         final @DialogType int dialogType = intent.getIntExtra(
    194                 FileOperationService.EXTRA_DIALOG_TYPE, DIALOG_TYPE_UNKNOWN);
    195         // DialogFragment takes care of restoring the dialog on configuration change.
    196         // Only show it manually for the first time (icicle is null).
    197         if (icicle == null && dialogType != DIALOG_TYPE_UNKNOWN) {
    198             final int opType = intent.getIntExtra(
    199                     FileOperationService.EXTRA_OPERATION_TYPE,
    200                     FileOperationService.OPERATION_COPY);
    201             final ArrayList<DocumentInfo> docList =
    202                     intent.getParcelableArrayListExtra(FileOperationService.EXTRA_FAILED_DOCS);
    203             final ArrayList<Uri> uriList =
    204                     intent.getParcelableArrayListExtra(FileOperationService.EXTRA_FAILED_URIS);
    205             OperationDialogFragment.show(
    206                     getFragmentManager(),
    207                     dialogType,
    208                     docList,
    209                     uriList,
    210                     mState.stack,
    211                     opType);
    212         }
    213     }
    214 
    215     @Override
    216     public void includeState(State state) {
    217         final Intent intent = getIntent();
    218 
    219         // This is a remnant of old logic where we used to initialize accept MIME types in
    220         // BaseActivity. ProvidersAccess still rely on this being correctly initialized so we still have
    221         // to initialize it in FilesActivity.
    222         state.initAcceptMimes(intent, "*/*");
    223         state.action = State.ACTION_BROWSE;
    224         state.allowMultiple = true;
    225 
    226         // Options specific to the DocumentsActivity.
    227         assert(!intent.hasExtra(Intent.EXTRA_LOCAL_ONLY));
    228     }
    229 
    230     @Override
    231     protected void onPostCreate(Bundle savedInstanceState) {
    232         super.onPostCreate(savedInstanceState);
    233         // This check avoids a flicker from "Recents" to "Home".
    234         // Only update action bar at this point if there is an active
    235         // serach. Why? Because this avoid an early (undesired) load of
    236         // the recents root...which is the default root in other activities.
    237         // In Files app "Home" is the default, but it is loaded async.
    238         // update will be called once Home root is loaded.
    239         // Except while searching we need this call to ensure the
    240         // search bits get layed out correctly.
    241         if (mSearchManager.isSearching()) {
    242             mNavigator.update();
    243         }
    244     }
    245 
    246     @Override
    247     public void onResume() {
    248         super.onResume();
    249 
    250         final RootInfo root = getCurrentRoot();
    251 
    252         // If we're browsing a specific root, and that root went away, then we
    253         // have no reason to hang around.
    254         // TODO: Rather than just disappearing, maybe we should inform
    255         // the user what has happened, let them close us. Less surprising.
    256         if (mProviders.getRootBlocking(root.authority, root.rootId) == null) {
    257             finish();
    258         }
    259     }
    260 
    261     @Override
    262     public String getDrawerTitle() {
    263         Intent intent = getIntent();
    264         return (intent != null && intent.hasExtra(Intent.EXTRA_TITLE))
    265                 ? intent.getStringExtra(Intent.EXTRA_TITLE)
    266                 : getString(R.string.app_label);
    267     }
    268 
    269     @Override
    270     public boolean onPrepareOptionsMenu(Menu menu) {
    271         super.onPrepareOptionsMenu(menu);
    272         mInjector.menuManager.updateOptionMenu(menu);
    273         return true;
    274     }
    275 
    276     @Override
    277     public boolean onOptionsItemSelected(MenuItem item) {
    278         DirectoryFragment dir;
    279         switch (item.getItemId()) {
    280             case R.id.menu_create_dir:
    281                 assert(canCreateDirectory());
    282                 showCreateDirectoryDialog();
    283                 break;
    284             case R.id.menu_new_window:
    285                 mInjector.actions.openInNewWindow(mState.stack);
    286                 break;
    287             case R.id.menu_paste_from_clipboard:
    288                 dir = getDirectoryFragment();
    289                 if (dir != null) {
    290                     dir.pasteFromClipboard();
    291                 }
    292                 break;
    293             case R.id.menu_settings:
    294                 mInjector.actions.openSettings(getCurrentRoot());
    295                 break;
    296             case R.id.menu_select_all:
    297                 mInjector.actions.selectAllFiles();
    298                 break;
    299             default:
    300                 return super.onOptionsItemSelected(item);
    301         }
    302         return true;
    303     }
    304 
    305     @Override
    306     public void onProvideKeyboardShortcuts(
    307             List<KeyboardShortcutGroup> data, Menu menu, int deviceId) {
    308         mInjector.menuManager.updateKeyboardShortcutsMenu(data, this::getString);
    309     }
    310 
    311     @Override
    312     public void refreshDirectory(@AnimationType int anim) {
    313         final FragmentManager fm = getFragmentManager();
    314         final RootInfo root = getCurrentRoot();
    315         final DocumentInfo cwd = getCurrentDirectory();
    316 
    317         assert(!mSearchManager.isSearching());
    318 
    319         if (mState.stack.isRecents()) {
    320             DirectoryFragment.showRecentsOpen(fm, anim);
    321         } else {
    322             // Normal boring directory
    323             DirectoryFragment.showDirectory(fm, root, cwd, anim);
    324         }
    325     }
    326 
    327     @Override
    328     public void onDocumentsPicked(List<DocumentInfo> docs) {
    329         throw new UnsupportedOperationException();
    330     }
    331 
    332     @Override
    333     public void onDocumentPicked(DocumentInfo doc) {
    334         throw new UnsupportedOperationException();
    335     }
    336 
    337     @Override
    338     public void onDirectoryCreated(DocumentInfo doc) {
    339         assert(doc.isDirectory());
    340         mInjector.focusManager.focusDocument(doc.documentId);
    341     }
    342 
    343     @CallSuper
    344     @Override
    345     public boolean onKeyDown(int keyCode, KeyEvent event) {
    346         return mActivityInputHandler.onKeyDown(keyCode, event)
    347                 || mSharedInputHandler.onKeyDown(
    348                         keyCode,
    349                         event)
    350                 || super.onKeyDown(keyCode, event);
    351     }
    352 
    353     @Override
    354     public DragShadowBuilder getShadowBuilder() {
    355         return mShadowBuilder;
    356     }
    357 
    358     @Override
    359     public boolean onKeyShortcut(int keyCode, KeyEvent event) {
    360         DirectoryFragment dir;
    361         // TODO: All key events should be statically bound using alphabeticShortcut.
    362         // But not working.
    363         switch (keyCode) {
    364             case KeyEvent.KEYCODE_A:
    365                 mInjector.actions.selectAllFiles();
    366                 return true;
    367             case KeyEvent.KEYCODE_X:
    368                 mInjector.actions.cutToClipboard();
    369                 return true;
    370             case KeyEvent.KEYCODE_C:
    371                 mInjector.actions.copyToClipboard();
    372                 return true;
    373             case KeyEvent.KEYCODE_V:
    374                 dir = getDirectoryFragment();
    375                 if (dir != null) {
    376                     dir.pasteFromClipboard();
    377                 }
    378                 return true;
    379             default:
    380                 return super.onKeyShortcut(keyCode, event);
    381         }
    382     }
    383 
    384     @Override
    385     public Injector<ActionHandler<FilesActivity>> getInjector() {
    386         return mInjector;
    387     }
    388 }
    389