• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
FilesActivity()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
focusSidebar()85     @Override protected boolean focusSidebar() { return super.focusSidebar(); }
popDir()86     @Override protected boolean popDir() { return super.popDir(); }
87 
88     @Override
onCreate(Bundle icicle)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.
updateTaskDescription(final Intent intent)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.
flattenDrawableToBitmap(Drawable d)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 
presentFileErrors(Bundle icicle, final Intent intent)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
includeState(State state)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
onPostCreate(Bundle savedInstanceState)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
onResume()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
getDrawerTitle()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
onPrepareOptionsMenu(Menu menu)331     public boolean onPrepareOptionsMenu(Menu menu) {
332         super.onPrepareOptionsMenu(menu);
333         mInjector.menuManager.updateOptionMenu(menu);
334         return true;
335     }
336 
337     @Override
onOptionsItemSelected(MenuItem item)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
onProvideKeyboardShortcuts( List<KeyboardShortcutGroup> data, Menu menu, int deviceId)364     public void onProvideKeyboardShortcuts(
365             List<KeyboardShortcutGroup> data, Menu menu, int deviceId) {
366         mInjector.menuManager.updateKeyboardShortcutsMenu(data, this::getString);
367     }
368 
369     @Override
refreshDirectory(@nimationType int anim)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
onDocumentsPicked(List<DocumentInfo> docs)386     public void onDocumentsPicked(List<DocumentInfo> docs) {
387         throw new UnsupportedOperationException();
388     }
389 
390     @Override
onDocumentPicked(DocumentInfo doc)391     public void onDocumentPicked(DocumentInfo doc) {
392         throw new UnsupportedOperationException();
393     }
394 
395     @Override
onDirectoryCreated(DocumentInfo doc)396     public void onDirectoryCreated(DocumentInfo doc) {
397         assert(doc.isDirectory());
398         mInjector.focusManager.focusDocument(doc.documentId);
399     }
400 
401     @CallSuper
402     @Override
onKeyDown(int keyCode, KeyEvent event)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
onKeyShortcut(int keyCode, KeyEvent event)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
getInjector()438     public Injector<ActionHandler<FilesActivity>> getInjector() {
439         return mInjector;
440     }
441 }
442