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