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