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