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