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