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.Bitmap; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.Paint; 28 import android.graphics.drawable.AdaptiveIconDrawable; 29 import android.graphics.drawable.BitmapDrawable; 30 import android.graphics.drawable.Drawable; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.support.annotation.CallSuper; 34 import android.view.KeyEvent; 35 import android.view.KeyboardShortcutGroup; 36 import android.view.Menu; 37 import android.view.MenuItem; 38 39 import com.android.documentsui.ActionModeController; 40 import com.android.documentsui.BaseActivity; 41 import com.android.documentsui.DocsSelectionHelper; 42 import com.android.documentsui.DocumentsApplication; 43 import com.android.documentsui.FocusManager; 44 import com.android.documentsui.Injector; 45 import com.android.documentsui.MenuManager.DirectoryDetails; 46 import com.android.documentsui.OperationDialogFragment; 47 import com.android.documentsui.OperationDialogFragment.DialogType; 48 import com.android.documentsui.ProviderExecutor; 49 import com.android.documentsui.R; 50 import com.android.documentsui.SharedInputHandler; 51 import com.android.documentsui.ShortcutsUpdater; 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.DirectoryFragment; 59 import com.android.documentsui.prefs.ScopedPreferences; 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 ActionHandler.Addons { 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 80 public FilesActivity() { 81 super(R.layout.files_activity, TAG); 82 } 83 84 // make these methods visible in this package to work around compiler bug http://b/62218600 85 @Override protected boolean focusSidebar() { return super.focusSidebar(); } 86 @Override protected boolean popDir() { return super.popDir(); } 87 88 @Override 89 public void onCreate(Bundle icicle) { 90 91 MessageBuilder messages = new MessageBuilder(this); 92 Features features = Features.create(this); 93 ScopedPreferences prefs = ScopedPreferences.create(this, PREFERENCES_SCOPE); 94 95 mInjector = new Injector<>( 96 features, 97 new Config(), 98 ScopedPreferences.create(this, PREFERENCES_SCOPE), 99 messages, 100 DialogController.create(features, this, messages), 101 DocumentsApplication.getFileTypeLookup(this), 102 new ShortcutsUpdater(this, prefs)::update); 103 104 super.onCreate(icicle); 105 106 DocumentClipper clipper = DocumentsApplication.getDocumentClipper(this); 107 mInjector.selectionMgr = DocsSelectionHelper.createMultiSelect(); 108 109 mInjector.focusManager = new FocusManager( 110 mInjector.features, 111 mInjector.selectionMgr, 112 mDrawer, 113 this::focusSidebar, 114 getColor(R.color.accent_dark)); 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 131 mInjector.actionModeController = new ActionModeController( 132 this, 133 mInjector.selectionMgr, 134 mInjector.menuManager, 135 mInjector.messages); 136 137 mInjector.actions = new ActionHandler<>( 138 this, 139 mState, 140 mProviders, 141 mDocs, 142 mSearchManager, 143 ProviderExecutor::forAuthority, 144 mInjector.actionModeController, 145 clipper, 146 DocumentsApplication.getClipStore(this), 147 DocumentsApplication.getDragAndDropManager(this), 148 mInjector); 149 150 mInjector.searchManager = mSearchManager; 151 152 mActivityInputHandler = 153 new ActivityInputHandler(mInjector.actions::deleteSelectedDocuments); 154 mSharedInputHandler = 155 new SharedInputHandler( 156 mInjector.focusManager, 157 mInjector.selectionMgr, 158 mInjector.searchManager::cancelSearch, 159 this::popDir, 160 mInjector.features); 161 162 RootsFragment.show(getFragmentManager(), null); 163 164 final Intent intent = getIntent(); 165 166 mInjector.actions.initLocation(intent); 167 168 // Allow the activity to masquerade as another, so we can look both like 169 // Downloads and Files, but with only a single underlying activity. 170 if (intent.hasExtra(LauncherActivity.TASK_LABEL_RES) 171 && intent.hasExtra(LauncherActivity.TASK_ICON_RES)) { 172 updateTaskDescription(intent); 173 } 174 175 presentFileErrors(icicle, intent); 176 } 177 178 // This is called in the intent contains label and icon resources. 179 // When that is true, the launcher activity has supplied them so we 180 // can adapt our presentation to how we were launched. 181 // Without this code, overlaying launcher_icon and launcher_label 182 // resources won't create a complete illusion of the activity being renamed. 183 // E.g. if we re-brand Files to Downloads by overlaying label and icon 184 // when the user tapped recents they'd see not "Downloads", but the 185 // underlying Activity description...Files. 186 // Alternate if we rename this activity, when launching other ways 187 // like when browsing files on a removable disk, the app would be 188 // called Downloads, which is also not the desired behavior. 189 private void updateTaskDescription(final Intent intent) { 190 int labelRes = intent.getIntExtra(LauncherActivity.TASK_LABEL_RES, -1); 191 assert(labelRes > -1); 192 String label = getResources().getString(labelRes); 193 194 int iconRes = intent.getIntExtra(LauncherActivity.TASK_ICON_RES, -1); 195 assert(iconRes > -1); 196 197 Drawable drawable = getResources().getDrawable( 198 iconRes, 199 null // we don't care about theme, since the supplier should have handled that. 200 ); 201 202 setTaskDescription(new TaskDescription(label, flattenDrawableToBitmap(drawable))); 203 } 204 205 // AdaptiveIconDrawable assumes that the consumer of the icon applies the shadow and 206 // recents assume that the provider of the task description handles these. Hence, 207 // we apply the shadow treatment same as Launcher3 implementation. 208 private Bitmap flattenDrawableToBitmap(Drawable d) { 209 // Percent of actual icon size 210 float ICON_SIZE_BLUR_FACTOR = 0.5f/48; 211 // Percent of actual icon size 212 float ICON_SIZE_KEY_SHADOW_DELTA_FACTOR = 1f/48; 213 int KEY_SHADOW_ALPHA = 61; 214 int AMBIENT_SHADOW_ALPHA = 30; 215 if (d instanceof BitmapDrawable) { 216 return ((BitmapDrawable) d).getBitmap(); 217 } else if (d instanceof AdaptiveIconDrawable) { 218 AdaptiveIconDrawable aid = (AdaptiveIconDrawable) d; 219 int iconSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); 220 int shadowSize = Math.max(iconSize, aid.getIntrinsicHeight()); 221 aid.setBounds(0, 0, shadowSize, shadowSize); 222 223 float blur = ICON_SIZE_BLUR_FACTOR * shadowSize; 224 float keyShadowDistance = ICON_SIZE_KEY_SHADOW_DELTA_FACTOR * shadowSize; 225 226 int bitmapSize = (int) (shadowSize + 2 * blur + keyShadowDistance); 227 Bitmap shadow = Bitmap.createBitmap(bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888); 228 229 Canvas canvas = new Canvas(shadow); 230 canvas.translate(blur + keyShadowDistance / 2, blur); 231 232 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 233 paint.setColor(Color.TRANSPARENT); 234 235 // Draw ambient shadow 236 paint.setShadowLayer(blur, 0, 0, AMBIENT_SHADOW_ALPHA << 24); 237 canvas.drawPath(aid.getIconMask(), paint); 238 239 // Draw key shadow 240 canvas.translate(0, keyShadowDistance); 241 paint.setShadowLayer(blur, 0, 0, KEY_SHADOW_ALPHA << 24); 242 canvas.drawPath(aid.getIconMask(), paint); 243 244 // Draw original drawable 245 aid.draw(canvas); 246 247 canvas.setBitmap(null); 248 return shadow; 249 } 250 return null; 251 } 252 253 private void presentFileErrors(Bundle icicle, final Intent intent) { 254 final @DialogType int dialogType = intent.getIntExtra( 255 FileOperationService.EXTRA_DIALOG_TYPE, DIALOG_TYPE_UNKNOWN); 256 // DialogFragment takes care of restoring the dialog on configuration change. 257 // Only show it manually for the first time (icicle is null). 258 if (icicle == null && dialogType != DIALOG_TYPE_UNKNOWN) { 259 final int opType = intent.getIntExtra( 260 FileOperationService.EXTRA_OPERATION_TYPE, 261 FileOperationService.OPERATION_COPY); 262 final ArrayList<DocumentInfo> docList = 263 intent.getParcelableArrayListExtra(FileOperationService.EXTRA_FAILED_DOCS); 264 final ArrayList<Uri> uriList = 265 intent.getParcelableArrayListExtra(FileOperationService.EXTRA_FAILED_URIS); 266 OperationDialogFragment.show( 267 getFragmentManager(), 268 dialogType, 269 docList, 270 uriList, 271 mState.stack, 272 opType); 273 } 274 } 275 276 @Override 277 public void includeState(State state) { 278 final Intent intent = getIntent(); 279 280 // This is a remnant of old logic where we used to initialize accept MIME types in 281 // BaseActivity. ProvidersAccess still rely on this being correctly initialized so we still have 282 // to initialize it in FilesActivity. 283 state.initAcceptMimes(intent, "*/*"); 284 state.action = State.ACTION_BROWSE; 285 state.allowMultiple = true; 286 287 // Options specific to the DocumentsActivity. 288 assert(!intent.hasExtra(Intent.EXTRA_LOCAL_ONLY)); 289 } 290 291 @Override 292 protected void onPostCreate(Bundle savedInstanceState) { 293 super.onPostCreate(savedInstanceState); 294 // This check avoids a flicker from "Recents" to "Home". 295 // Only update action bar at this point if there is an active 296 // search. Why? Because this avoid an early (undesired) load of 297 // the recents root...which is the default root in other activities. 298 // In Files app "Home" is the default, but it is loaded async. 299 // update will be called once Home root is loaded. 300 // Except while searching we need this call to ensure the 301 // search bits get laid out correctly. 302 if (mSearchManager.isSearching()) { 303 mNavigator.update(); 304 } 305 } 306 307 @Override 308 public void onResume() { 309 super.onResume(); 310 311 final RootInfo root = getCurrentRoot(); 312 313 // If we're browsing a specific root, and that root went away, then we 314 // have no reason to hang around. 315 // TODO: Rather than just disappearing, maybe we should inform 316 // the user what has happened, let them close us. Less surprising. 317 if (mProviders.getRootBlocking(root.authority, root.rootId) == null) { 318 finish(); 319 } 320 } 321 322 @Override 323 public String getDrawerTitle() { 324 Intent intent = getIntent(); 325 return (intent != null && intent.hasExtra(Intent.EXTRA_TITLE)) 326 ? intent.getStringExtra(Intent.EXTRA_TITLE) 327 : getString(R.string.app_label); 328 } 329 330 @Override 331 public boolean onPrepareOptionsMenu(Menu menu) { 332 super.onPrepareOptionsMenu(menu); 333 mInjector.menuManager.updateOptionMenu(menu); 334 return true; 335 } 336 337 @Override 338 public boolean onOptionsItemSelected(MenuItem item) { 339 DirectoryFragment dir; 340 switch (item.getItemId()) { 341 case R.id.option_menu_create_dir: 342 assert(canCreateDirectory()); 343 mInjector.actions.showCreateDirectoryDialog(); 344 break; 345 case R.id.option_menu_new_window: 346 mInjector.actions.openInNewWindow(mState.stack); 347 break; 348 case R.id.option_menu_settings: 349 mInjector.actions.openSettings(getCurrentRoot()); 350 break; 351 case R.id.option_menu_select_all: 352 mInjector.actions.selectAllFiles(); 353 break; 354 case R.id.option_menu_inspect: 355 mInjector.actions.showInspector(getCurrentDirectory()); 356 break; 357 default: 358 return super.onOptionsItemSelected(item); 359 } 360 return true; 361 } 362 363 @Override 364 public void onProvideKeyboardShortcuts( 365 List<KeyboardShortcutGroup> data, Menu menu, int deviceId) { 366 mInjector.menuManager.updateKeyboardShortcutsMenu(data, this::getString); 367 } 368 369 @Override 370 public void refreshDirectory(@AnimationType int anim) { 371 final FragmentManager fm = getFragmentManager(); 372 final RootInfo root = getCurrentRoot(); 373 final DocumentInfo cwd = getCurrentDirectory(); 374 375 assert(!mSearchManager.isSearching()); 376 377 if (mState.stack.isRecents()) { 378 DirectoryFragment.showRecentsOpen(fm, anim); 379 } else { 380 // Normal boring directory 381 DirectoryFragment.showDirectory(fm, root, cwd, anim); 382 } 383 } 384 385 @Override 386 public void onDocumentsPicked(List<DocumentInfo> docs) { 387 throw new UnsupportedOperationException(); 388 } 389 390 @Override 391 public void onDocumentPicked(DocumentInfo doc) { 392 throw new UnsupportedOperationException(); 393 } 394 395 @Override 396 public void onDirectoryCreated(DocumentInfo doc) { 397 assert(doc.isDirectory()); 398 mInjector.focusManager.focusDocument(doc.documentId); 399 } 400 401 @CallSuper 402 @Override 403 public boolean onKeyDown(int keyCode, KeyEvent event) { 404 return mActivityInputHandler.onKeyDown(keyCode, event) 405 || mSharedInputHandler.onKeyDown( 406 keyCode, 407 event) 408 || super.onKeyDown(keyCode, event); 409 } 410 411 @Override 412 public boolean onKeyShortcut(int keyCode, KeyEvent event) { 413 DirectoryFragment dir; 414 // TODO: All key events should be statically bound using alphabeticShortcut. 415 // But not working. 416 switch (keyCode) { 417 case KeyEvent.KEYCODE_A: 418 mInjector.actions.selectAllFiles(); 419 return true; 420 case KeyEvent.KEYCODE_X: 421 mInjector.actions.cutToClipboard(); 422 return true; 423 case KeyEvent.KEYCODE_C: 424 mInjector.actions.copyToClipboard(); 425 return true; 426 case KeyEvent.KEYCODE_V: 427 dir = getDirectoryFragment(); 428 if (dir != null) { 429 dir.pasteFromClipboard(); 430 } 431 return true; 432 default: 433 return super.onKeyShortcut(keyCode, event); 434 } 435 } 436 437 @Override 438 public Injector<ActionHandler<FilesActivity>> getInjector() { 439 return mInjector; 440 } 441 } 442