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