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