• 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.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 
FilesActivity()75     public FilesActivity() {
76         super(R.layout.files_activity, TAG);
77     }
78 
79     @Override
onCreate(Bundle icicle)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.
updateTaskDescription(final Intent intent)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 
presentFileErrors(Bundle icicle, final Intent intent)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
includeState(State state)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
onPostCreate(Bundle savedInstanceState)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
onResume()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
getDrawerTitle()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
onPrepareOptionsMenu(Menu menu)270     public boolean onPrepareOptionsMenu(Menu menu) {
271         super.onPrepareOptionsMenu(menu);
272         mInjector.menuManager.updateOptionMenu(menu);
273         return true;
274     }
275 
276     @Override
onOptionsItemSelected(MenuItem item)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
onProvideKeyboardShortcuts( List<KeyboardShortcutGroup> data, Menu menu, int deviceId)306     public void onProvideKeyboardShortcuts(
307             List<KeyboardShortcutGroup> data, Menu menu, int deviceId) {
308         mInjector.menuManager.updateKeyboardShortcutsMenu(data, this::getString);
309     }
310 
311     @Override
refreshDirectory(@nimationType int anim)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
onDocumentsPicked(List<DocumentInfo> docs)328     public void onDocumentsPicked(List<DocumentInfo> docs) {
329         throw new UnsupportedOperationException();
330     }
331 
332     @Override
onDocumentPicked(DocumentInfo doc)333     public void onDocumentPicked(DocumentInfo doc) {
334         throw new UnsupportedOperationException();
335     }
336 
337     @Override
onDirectoryCreated(DocumentInfo doc)338     public void onDirectoryCreated(DocumentInfo doc) {
339         assert(doc.isDirectory());
340         mInjector.focusManager.focusDocument(doc.documentId);
341     }
342 
343     @CallSuper
344     @Override
onKeyDown(int keyCode, KeyEvent event)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
getShadowBuilder()354     public DragShadowBuilder getShadowBuilder() {
355         return mShadowBuilder;
356     }
357 
358     @Override
onKeyShortcut(int keyCode, KeyEvent event)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
getInjector()385     public Injector<ActionHandler<FilesActivity>> getInjector() {
386         return mInjector;
387     }
388 }
389