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