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