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