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; 18 19 import static com.android.documentsui.Shared.DEBUG; 20 import static com.android.documentsui.Shared.EXTRA_BENCHMARK; 21 import static com.android.documentsui.State.ACTION_CREATE; 22 import static com.android.documentsui.State.ACTION_GET_CONTENT; 23 import static com.android.documentsui.State.ACTION_OPEN; 24 import static com.android.documentsui.State.ACTION_OPEN_TREE; 25 import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION; 26 import static com.android.documentsui.State.MODE_GRID; 27 28 import android.app.Activity; 29 import android.app.Fragment; 30 import android.app.FragmentManager; 31 import android.content.Intent; 32 import android.content.pm.ApplicationInfo; 33 import android.content.pm.PackageInfo; 34 import android.content.pm.PackageManager; 35 import android.content.pm.ProviderInfo; 36 import android.database.ContentObserver; 37 import android.net.Uri; 38 import android.os.AsyncTask; 39 import android.os.Bundle; 40 import android.os.Handler; 41 import android.os.MessageQueue.IdleHandler; 42 import android.provider.DocumentsContract; 43 import android.provider.DocumentsContract.Root; 44 import android.support.annotation.CallSuper; 45 import android.support.annotation.LayoutRes; 46 import android.support.annotation.Nullable; 47 import android.util.Log; 48 import android.view.KeyEvent; 49 import android.view.Menu; 50 import android.view.MenuItem; 51 import android.widget.Spinner; 52 53 import com.android.documentsui.SearchViewManager.SearchManagerListener; 54 import com.android.documentsui.State.ViewMode; 55 import com.android.documentsui.dirlist.AnimationView; 56 import com.android.documentsui.dirlist.DirectoryFragment; 57 import com.android.documentsui.dirlist.Model; 58 import com.android.documentsui.model.DocumentInfo; 59 import com.android.documentsui.model.DocumentStack; 60 import com.android.documentsui.model.RootInfo; 61 62 import java.io.FileNotFoundException; 63 import java.util.ArrayList; 64 import java.util.Collection; 65 import java.util.Date; 66 import java.util.List; 67 import java.util.concurrent.Executor; 68 69 public abstract class BaseActivity extends Activity 70 implements SearchManagerListener, NavigationView.Environment { 71 72 private static final String BENCHMARK_TESTING_PACKAGE = "com.android.documentsui.appperftests"; 73 74 State mState; 75 RootsCache mRoots; 76 SearchViewManager mSearchManager; 77 DrawerController mDrawer; 78 NavigationView mNavigator; 79 List<EventListener> mEventListeners = new ArrayList<>(); 80 81 private final String mTag; 82 private final ContentObserver mRootsCacheObserver = new ContentObserver(new Handler()) { 83 @Override 84 public void onChange(boolean selfChange) { 85 new HandleRootsChangedTask(BaseActivity.this).execute(getCurrentRoot()); 86 } 87 }; 88 89 @LayoutRes 90 private int mLayoutId; 91 92 private boolean mNavDrawerHasFocus; 93 private long mStartTime; 94 onDocumentPicked(DocumentInfo doc, Model model)95 public abstract void onDocumentPicked(DocumentInfo doc, Model model); onDocumentsPicked(List<DocumentInfo> docs)96 public abstract void onDocumentsPicked(List<DocumentInfo> docs); 97 onTaskFinished(Uri... uris)98 abstract void onTaskFinished(Uri... uris); refreshDirectory(int anim)99 abstract void refreshDirectory(int anim); 100 /** Allows sub-classes to include information in a newly created State instance. */ includeState(State initialState)101 abstract void includeState(State initialState); 102 BaseActivity(@ayoutRes int layoutId, String tag)103 public BaseActivity(@LayoutRes int layoutId, String tag) { 104 mLayoutId = layoutId; 105 mTag = tag; 106 } 107 108 @CallSuper 109 @Override onCreate(Bundle icicle)110 public void onCreate(Bundle icicle) { 111 // Record the time when onCreate is invoked for metric. 112 mStartTime = new Date().getTime(); 113 114 super.onCreate(icicle); 115 116 final Intent intent = getIntent(); 117 118 addListenerForLaunchCompletion(); 119 120 setContentView(mLayoutId); 121 122 mDrawer = DrawerController.create(this); 123 mState = getState(icicle); 124 Metrics.logActivityLaunch(this, mState, intent); 125 126 mRoots = DocumentsApplication.getRootsCache(this); 127 128 getContentResolver().registerContentObserver( 129 RootsCache.sNotificationUri, false, mRootsCacheObserver); 130 131 mSearchManager = new SearchViewManager(this, icicle); 132 133 DocumentsToolbar toolbar = (DocumentsToolbar) findViewById(R.id.toolbar); 134 setActionBar(toolbar); 135 mNavigator = new NavigationView( 136 mDrawer, 137 toolbar, 138 (Spinner) findViewById(R.id.stack), 139 mState, 140 this); 141 142 // Base classes must update result in their onCreate. 143 setResult(Activity.RESULT_CANCELED); 144 } 145 146 @Override onCreateOptionsMenu(Menu menu)147 public boolean onCreateOptionsMenu(Menu menu) { 148 boolean showMenu = super.onCreateOptionsMenu(menu); 149 150 getMenuInflater().inflate(R.menu.activity, menu); 151 mNavigator.update(); 152 boolean fullBarSearch = getResources().getBoolean(R.bool.full_bar_search_view); 153 mSearchManager.install((DocumentsToolbar) findViewById(R.id.toolbar), fullBarSearch); 154 155 return showMenu; 156 } 157 158 @Override 159 @CallSuper onPrepareOptionsMenu(Menu menu)160 public boolean onPrepareOptionsMenu(Menu menu) { 161 super.onPrepareOptionsMenu(menu); 162 163 mSearchManager.showMenu(canSearchRoot()); 164 165 final boolean inRecents = getCurrentDirectory() == null; 166 167 final MenuItem sort = menu.findItem(R.id.menu_sort); 168 final MenuItem sortSize = menu.findItem(R.id.menu_sort_size); 169 final MenuItem grid = menu.findItem(R.id.menu_grid); 170 final MenuItem list = menu.findItem(R.id.menu_list); 171 final MenuItem advanced = menu.findItem(R.id.menu_advanced); 172 final MenuItem fileSize = menu.findItem(R.id.menu_file_size); 173 174 // Search uses backend ranking; no sorting, recents doesn't support sort. 175 sort.setEnabled(!inRecents && !mSearchManager.isSearching()); 176 sortSize.setVisible(mState.showSize); // Only sort by size when file sizes are visible 177 fileSize.setVisible(!mState.forceSize); 178 179 // grid/list is effectively a toggle. 180 grid.setVisible(mState.derivedMode != State.MODE_GRID); 181 list.setVisible(mState.derivedMode != State.MODE_LIST); 182 183 advanced.setVisible(mState.showAdvancedOption); 184 advanced.setTitle(mState.showAdvancedOption && mState.showAdvanced 185 ? R.string.menu_advanced_hide : R.string.menu_advanced_show); 186 fileSize.setTitle(LocalPreferences.getDisplayFileSize(this) 187 ? R.string.menu_file_size_hide : R.string.menu_file_size_show); 188 189 return true; 190 } 191 192 @Override onDestroy()193 protected void onDestroy() { 194 getContentResolver().unregisterContentObserver(mRootsCacheObserver); 195 super.onDestroy(); 196 } 197 getState(@ullable Bundle icicle)198 private State getState(@Nullable Bundle icicle) { 199 if (icicle != null) { 200 State state = icicle.<State>getParcelable(Shared.EXTRA_STATE); 201 if (DEBUG) Log.d(mTag, "Recovered existing state object: " + state); 202 return state; 203 } 204 205 State state = new State(); 206 207 final Intent intent = getIntent(); 208 209 state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false); 210 state.forceSize = intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_FILESIZE, false); 211 state.showSize = state.forceSize || LocalPreferences.getDisplayFileSize(this); 212 state.initAcceptMimes(intent); 213 state.excludedAuthorities = getExcludedAuthorities(); 214 215 includeState(state); 216 217 // Advanced roots are shown by default without menu option if forced by config or intent. 218 boolean forceAdvanced = Shared.shouldShowDeviceRoot(this, intent); 219 boolean chosenAdvanced = LocalPreferences.getShowDeviceRoot(this, state.action); 220 state.showAdvanced = forceAdvanced || chosenAdvanced; 221 222 // Menu option is shown for whitelisted intents if advanced roots are not shown by default. 223 state.showAdvancedOption = !forceAdvanced && ( 224 Shared.shouldShowFancyFeatures(this) 225 || state.action == ACTION_OPEN 226 || state.action == ACTION_CREATE 227 || state.action == ACTION_OPEN_TREE 228 || state.action == ACTION_PICK_COPY_DESTINATION 229 || state.action == ACTION_GET_CONTENT); 230 231 if (DEBUG) Log.d(mTag, "Created new state object: " + state); 232 233 return state; 234 } 235 setRootsDrawerOpen(boolean open)236 public void setRootsDrawerOpen(boolean open) { 237 mNavigator.revealRootsDrawer(open); 238 } 239 onRootPicked(RootInfo root)240 void onRootPicked(RootInfo root) { 241 // Clicking on the current root removes search 242 mSearchManager.cancelSearch(); 243 244 // Skip refreshing if root nor directory didn't change 245 if (root.equals(getCurrentRoot()) && mState.stack.size() == 1) { 246 return; 247 } 248 249 mState.derivedMode = LocalPreferences.getViewMode(this, root, MODE_GRID); 250 251 // Clear entire backstack and start in new root 252 mState.onRootChanged(root); 253 254 // Recents is always in memory, so we just load it directly. 255 // Otherwise we delegate loading data from disk to a task 256 // to ensure a responsive ui. 257 if (mRoots.isRecentsRoot(root)) { 258 refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 259 } else { 260 new PickRootTask(this, root).executeOnExecutor(getExecutorForCurrentDirectory()); 261 } 262 } 263 264 @Override onOptionsItemSelected(MenuItem item)265 public boolean onOptionsItemSelected(MenuItem item) { 266 267 switch (item.getItemId()) { 268 case android.R.id.home: 269 onBackPressed(); 270 return true; 271 272 case R.id.menu_create_dir: 273 showCreateDirectoryDialog(); 274 return true; 275 276 case R.id.menu_search: 277 // SearchViewManager listens for this directly. 278 return false; 279 280 case R.id.menu_sort_name: 281 setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME); 282 return true; 283 284 case R.id.menu_sort_date: 285 setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED); 286 return true; 287 288 case R.id.menu_sort_size: 289 setUserSortOrder(State.SORT_ORDER_SIZE); 290 return true; 291 292 case R.id.menu_grid: 293 setViewMode(State.MODE_GRID); 294 return true; 295 296 case R.id.menu_list: 297 setViewMode(State.MODE_LIST); 298 return true; 299 300 case R.id.menu_paste_from_clipboard: 301 DirectoryFragment dir = getDirectoryFragment(); 302 if (dir != null) { 303 dir.pasteFromClipboard(); 304 } 305 return true; 306 307 case R.id.menu_advanced: 308 setDisplayAdvancedDevices(!mState.showAdvanced); 309 return true; 310 311 case R.id.menu_file_size: 312 setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this)); 313 return true; 314 315 case R.id.menu_settings: 316 Metrics.logUserAction(this, Metrics.USER_ACTION_SETTINGS); 317 318 final RootInfo root = getCurrentRoot(); 319 final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS); 320 intent.setDataAndType(root.getUri(), DocumentsContract.Root.MIME_TYPE_ITEM); 321 startActivity(intent); 322 return true; 323 324 default: 325 return super.onOptionsItemSelected(item); 326 } 327 } 328 getDirectoryFragment()329 final @Nullable DirectoryFragment getDirectoryFragment() { 330 return DirectoryFragment.get(getFragmentManager()); 331 } 332 showCreateDirectoryDialog()333 void showCreateDirectoryDialog() { 334 Metrics.logUserAction(this, Metrics.USER_ACTION_CREATE_DIR); 335 336 CreateDirectoryFragment.show(getFragmentManager()); 337 } 338 onDirectoryCreated(DocumentInfo doc)339 void onDirectoryCreated(DocumentInfo doc) { 340 // By default we do nothing, just let the new directory appear. 341 // DocumentsActivity auto-opens directories after creating them 342 // As that is more attuned to the "picker" use cases it supports. 343 } 344 345 /** 346 * Returns true if a directory can be created in the current location. 347 * @return 348 */ canCreateDirectory()349 boolean canCreateDirectory() { 350 final RootInfo root = getCurrentRoot(); 351 final DocumentInfo cwd = getCurrentDirectory(); 352 return cwd != null 353 && cwd.isCreateSupported() 354 && !mSearchManager.isSearching() 355 && !root.isRecents() 356 && !root.isDownloads(); 357 } 358 openContainerDocument(DocumentInfo doc)359 void openContainerDocument(DocumentInfo doc) { 360 assert(doc.isContainer()); 361 362 notifyDirectoryNavigated(doc.derivedUri); 363 364 mState.pushDocument(doc); 365 // Show an opening animation only if pressing "back" would get us back to the 366 // previous directory. Especially after opening a root document, pressing 367 // back, wouldn't go to the previous root, but close the activity. 368 final int anim = (mState.hasLocationChanged() && mState.stack.size() > 1) 369 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE; 370 refreshCurrentRootAndDirectory(anim); 371 } 372 373 /** 374 * Refreshes the content of the director and the menu/action bar. 375 * The current directory name and selection will get updated. 376 * @param anim 377 */ 378 @Override refreshCurrentRootAndDirectory(int anim)379 public final void refreshCurrentRootAndDirectory(int anim) { 380 mSearchManager.cancelSearch(); 381 382 refreshDirectory(anim); 383 384 final RootsFragment roots = RootsFragment.get(getFragmentManager()); 385 if (roots != null) { 386 roots.onCurrentRootChanged(); 387 } 388 389 mNavigator.update(); 390 // Causes talkback to announce the activity's new title 391 if (mState.stack.isRecents()) { 392 setTitle(mRoots.getRecentsRoot().title); 393 } else { 394 setTitle(mState.stack.getTitle()); 395 } 396 invalidateOptionsMenu(); 397 } 398 loadRoot(final Uri uri)399 final void loadRoot(final Uri uri) { 400 new LoadRootTask(this, uri).executeOnExecutor( 401 ProviderExecutor.forAuthority(uri.getAuthority())); 402 } 403 404 /** 405 * Called when search results changed. 406 * Refreshes the content of the directory. It doesn't refresh elements on the action bar. 407 * e.g. The current directory name displayed on the action bar won't get updated. 408 */ 409 @Override onSearchChanged(@ullable String query)410 public void onSearchChanged(@Nullable String query) { 411 // We should not get here if root is not searchable 412 assert(canSearchRoot()); 413 reloadSearch(query); 414 } 415 416 @Override onSearchFinished()417 public void onSearchFinished() { 418 // Restores menu icons state 419 invalidateOptionsMenu(); 420 } 421 reloadSearch(String query)422 private void reloadSearch(String query) { 423 FragmentManager fm = getFragmentManager(); 424 RootInfo root = getCurrentRoot(); 425 DocumentInfo cwd = getCurrentDirectory(); 426 427 DirectoryFragment.reloadSearch(fm, root, cwd, query); 428 } 429 getExcludedAuthorities()430 final List<String> getExcludedAuthorities() { 431 List<String> authorities = new ArrayList<>(); 432 if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) { 433 // Exclude roots provided by the calling package. 434 String packageName = getCallingPackageMaybeExtra(); 435 try { 436 PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName, 437 PackageManager.GET_PROVIDERS); 438 for (ProviderInfo provider: pkgInfo.providers) { 439 authorities.add(provider.authority); 440 } 441 } catch (PackageManager.NameNotFoundException e) { 442 Log.e(mTag, "Calling package name does not resolve: " + packageName); 443 } 444 } 445 return authorities; 446 } 447 canSearchRoot()448 boolean canSearchRoot() { 449 final RootInfo root = getCurrentRoot(); 450 return (root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0; 451 } 452 getCallingPackageMaybeExtra()453 final String getCallingPackageMaybeExtra() { 454 String callingPackage = getCallingPackage(); 455 // System apps can set the calling package name using an extra. 456 try { 457 ApplicationInfo info = getPackageManager().getApplicationInfo(callingPackage, 0); 458 if (info.isSystemApp() || info.isUpdatedSystemApp()) { 459 final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME); 460 if (extra != null) { 461 callingPackage = extra; 462 } 463 } 464 } finally { 465 return callingPackage; 466 } 467 } 468 get(Fragment fragment)469 public static BaseActivity get(Fragment fragment) { 470 return (BaseActivity) fragment.getActivity(); 471 } 472 getDisplayState()473 public State getDisplayState() { 474 return mState; 475 } 476 477 /* 478 * Get the default directory to be presented after starting the activity. 479 * Method can be overridden if the change of the behavior of the the child activity is needed. 480 */ getDefaultRoot()481 public Uri getDefaultRoot() { 482 return Shared.shouldShowDocumentsRoot(this, getIntent()) 483 ? DocumentsContract.buildHomeUri() 484 : DocumentsContract.buildRootUri( 485 "com.android.providers.downloads.documents", "downloads"); 486 } 487 488 /** 489 * Set internal storage visible based on explicit user action. 490 */ setDisplayAdvancedDevices(boolean display)491 void setDisplayAdvancedDevices(boolean display) { 492 Metrics.logUserAction(this, 493 display ? Metrics.USER_ACTION_SHOW_ADVANCED : Metrics.USER_ACTION_HIDE_ADVANCED); 494 495 LocalPreferences.setShowDeviceRoot(this, mState.action, display); 496 mState.showAdvanced = display; 497 RootsFragment.get(getFragmentManager()).onDisplayStateChanged(); 498 invalidateOptionsMenu(); 499 } 500 501 /** 502 * Set file size visible based on explicit user action. 503 */ setDisplayFileSize(boolean display)504 void setDisplayFileSize(boolean display) { 505 Metrics.logUserAction(this, 506 display ? Metrics.USER_ACTION_SHOW_SIZE : Metrics.USER_ACTION_HIDE_SIZE); 507 508 LocalPreferences.setDisplayFileSize(this, display); 509 mState.showSize = display; 510 DirectoryFragment dir = getDirectoryFragment(); 511 if (dir != null) { 512 dir.onDisplayStateChanged(); 513 } 514 invalidateOptionsMenu(); 515 } 516 517 /** 518 * Set state sort order based on explicit user action. 519 */ setUserSortOrder(int sortOrder)520 void setUserSortOrder(int sortOrder) { 521 switch(sortOrder) { 522 case State.SORT_ORDER_DISPLAY_NAME: 523 Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_NAME); 524 break; 525 case State.SORT_ORDER_LAST_MODIFIED: 526 Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_DATE); 527 break; 528 case State.SORT_ORDER_SIZE: 529 Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_SIZE); 530 break; 531 } 532 533 mState.userSortOrder = sortOrder; 534 DirectoryFragment dir = getDirectoryFragment(); 535 if (dir != null) { 536 dir.onSortOrderChanged(); 537 } 538 } 539 540 /** 541 * Set mode based on explicit user action. 542 */ setViewMode(@iewMode int mode)543 void setViewMode(@ViewMode int mode) { 544 if (mode == State.MODE_GRID) { 545 Metrics.logUserAction(this, Metrics.USER_ACTION_GRID); 546 } else if (mode == State.MODE_LIST) { 547 Metrics.logUserAction(this, Metrics.USER_ACTION_LIST); 548 } 549 550 LocalPreferences.setViewMode(this, getCurrentRoot(), mode); 551 mState.derivedMode = mode; 552 553 // view icon needs to be updated, but we *could* do it 554 // in onOptionsItemSelected, and not do the full invalidation 555 // But! That's a larger refactoring we'll save for another day. 556 invalidateOptionsMenu(); 557 DirectoryFragment dir = getDirectoryFragment(); 558 if (dir != null) { 559 dir.onViewModeChanged(); 560 } 561 } 562 setPending(boolean pending)563 public void setPending(boolean pending) { 564 final SaveFragment save = SaveFragment.get(getFragmentManager()); 565 if (save != null) { 566 save.setPending(pending); 567 } 568 } 569 570 @Override onSaveInstanceState(Bundle state)571 protected void onSaveInstanceState(Bundle state) { 572 super.onSaveInstanceState(state); 573 state.putParcelable(Shared.EXTRA_STATE, mState); 574 mSearchManager.onSaveInstanceState(state); 575 } 576 577 @Override onRestoreInstanceState(Bundle state)578 protected void onRestoreInstanceState(Bundle state) { 579 super.onRestoreInstanceState(state); 580 } 581 582 @Override isSearchExpanded()583 public boolean isSearchExpanded() { 584 return mSearchManager.isExpanded(); 585 } 586 587 @Override getCurrentRoot()588 public RootInfo getCurrentRoot() { 589 if (mState.stack.root != null) { 590 return mState.stack.root; 591 } else { 592 return mRoots.getRecentsRoot(); 593 } 594 } 595 getCurrentDirectory()596 public DocumentInfo getCurrentDirectory() { 597 return mState.stack.peek(); 598 } 599 getExecutorForCurrentDirectory()600 public Executor getExecutorForCurrentDirectory() { 601 final DocumentInfo cwd = getCurrentDirectory(); 602 if (cwd != null && cwd.authority != null) { 603 return ProviderExecutor.forAuthority(cwd.authority); 604 } else { 605 return AsyncTask.THREAD_POOL_EXECUTOR; 606 } 607 } 608 609 @Override onBackPressed()610 public void onBackPressed() { 611 // While action bar is expanded, the state stack UI is hidden. 612 if (mSearchManager.cancelSearch()) { 613 return; 614 } 615 616 DirectoryFragment dir = getDirectoryFragment(); 617 if (dir != null && dir.onBackPressed()) { 618 return; 619 } 620 621 if (!mState.hasLocationChanged()) { 622 super.onBackPressed(); 623 return; 624 } 625 626 if (onBeforePopDir() || popDir()) { 627 return; 628 } 629 630 super.onBackPressed(); 631 } 632 onBeforePopDir()633 boolean onBeforePopDir() { 634 // Files app overrides this with some fancy logic. 635 return false; 636 } 637 onStackPicked(DocumentStack stack)638 public void onStackPicked(DocumentStack stack) { 639 try { 640 // Update the restored stack to ensure we have freshest data 641 stack.updateDocuments(getContentResolver()); 642 mState.setStack(stack); 643 refreshCurrentRootAndDirectory(AnimationView.ANIM_SIDE); 644 645 } catch (FileNotFoundException e) { 646 Log.w(mTag, "Failed to restore stack: " + e); 647 } 648 } 649 650 /** 651 * Declare a global key handler to route key events when there isn't a specific focus view. This 652 * covers the scenario where a user opens DocumentsUI and just starts typing. 653 * 654 * @param keyCode 655 * @param event 656 * @return 657 */ 658 @CallSuper 659 @Override onKeyDown(int keyCode, KeyEvent event)660 public boolean onKeyDown(int keyCode, KeyEvent event) { 661 if (Events.isNavigationKeyCode(keyCode)) { 662 // Forward all unclaimed navigation keystrokes to the DirectoryFragment. This causes any 663 // stray navigation keystrokes focus the content pane, which is probably what the user 664 // is trying to do. 665 DirectoryFragment df = DirectoryFragment.get(getFragmentManager()); 666 if (df != null) { 667 df.requestFocus(); 668 return true; 669 } 670 } else if (keyCode == KeyEvent.KEYCODE_TAB) { 671 // Tab toggles focus on the navigation drawer. 672 toggleNavDrawerFocus(); 673 return true; 674 } else if (keyCode == KeyEvent.KEYCODE_DEL) { 675 popDir(); 676 return true; 677 } 678 return super.onKeyDown(keyCode, event); 679 } 680 addEventListener(EventListener listener)681 public void addEventListener(EventListener listener) { 682 mEventListeners.add(listener); 683 } 684 removeEventListener(EventListener listener)685 public void removeEventListener(EventListener listener) { 686 mEventListeners.remove(listener); 687 } 688 notifyDirectoryLoaded(Uri uri)689 public void notifyDirectoryLoaded(Uri uri) { 690 for (EventListener listener : mEventListeners) { 691 listener.onDirectoryLoaded(uri); 692 } 693 } 694 notifyDirectoryNavigated(Uri uri)695 void notifyDirectoryNavigated(Uri uri) { 696 for (EventListener listener : mEventListeners) { 697 listener.onDirectoryNavigated(uri); 698 } 699 } 700 701 /** 702 * Toggles focus between the navigation drawer and the directory listing. If the drawer isn't 703 * locked, open/close it as appropriate. 704 */ toggleNavDrawerFocus()705 void toggleNavDrawerFocus() { 706 if (mNavDrawerHasFocus) { 707 mDrawer.setOpen(false); 708 DirectoryFragment df = DirectoryFragment.get(getFragmentManager()); 709 if (df != null) { 710 df.requestFocus(); 711 } 712 } else { 713 mDrawer.setOpen(true); 714 RootsFragment rf = RootsFragment.get(getFragmentManager()); 715 if (rf != null) { 716 rf.requestFocus(); 717 } 718 } 719 mNavDrawerHasFocus = !mNavDrawerHasFocus; 720 } 721 getRootDocumentBlocking(RootInfo root)722 DocumentInfo getRootDocumentBlocking(RootInfo root) { 723 try { 724 final Uri uri = DocumentsContract.buildDocumentUri( 725 root.authority, root.documentId); 726 return DocumentInfo.fromUri(getContentResolver(), uri); 727 } catch (FileNotFoundException e) { 728 Log.w(mTag, "Failed to find root", e); 729 return null; 730 } 731 } 732 733 /** 734 * Pops the top entry off the directory stack, and returns the user to the previous directory. 735 * If the directory stack only contains one item, this method does nothing. 736 * 737 * @return Whether the stack was popped. 738 */ popDir()739 private boolean popDir() { 740 if (mState.stack.size() > 1) { 741 mState.stack.pop(); 742 refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE); 743 return true; 744 } 745 return false; 746 } 747 748 /** 749 * Closes the activity when it's idle. 750 */ addListenerForLaunchCompletion()751 private void addListenerForLaunchCompletion() { 752 addEventListener(new EventListener() { 753 @Override 754 public void onDirectoryNavigated(Uri uri) { 755 } 756 757 @Override 758 public void onDirectoryLoaded(Uri uri) { 759 removeEventListener(this); 760 getMainLooper().getQueue().addIdleHandler(new IdleHandler() { 761 @Override 762 public boolean queueIdle() { 763 // If startup benchmark is requested by a whitelisted testing package, then 764 // close the activity once idle, and notify the testing activity. 765 if (getIntent().getBooleanExtra(EXTRA_BENCHMARK, false) && 766 BENCHMARK_TESTING_PACKAGE.equals(getCallingPackage())) { 767 setResult(RESULT_OK); 768 finish(); 769 } 770 771 Metrics.logStartupMs( 772 BaseActivity.this, (int) (new Date().getTime() - mStartTime)); 773 774 // Remove the idle handler. 775 return false; 776 } 777 }); 778 new Handler().post(new Runnable() { 779 @Override public void run() { 780 } 781 }); 782 } 783 }); 784 } 785 786 private static final class PickRootTask extends PairedTask<BaseActivity, Void, DocumentInfo> { 787 private RootInfo mRoot; 788 PickRootTask(BaseActivity activity, RootInfo root)789 public PickRootTask(BaseActivity activity, RootInfo root) { 790 super(activity); 791 mRoot = root; 792 } 793 794 @Override run(Void... params)795 protected DocumentInfo run(Void... params) { 796 return mOwner.getRootDocumentBlocking(mRoot); 797 } 798 799 @Override finish(DocumentInfo result)800 protected void finish(DocumentInfo result) { 801 if (result != null) { 802 mOwner.openContainerDocument(result); 803 } 804 } 805 } 806 807 private static final class HandleRootsChangedTask 808 extends PairedTask<BaseActivity, RootInfo, RootInfo> { 809 RootInfo mCurrentRoot; 810 DocumentInfo mDefaultRootDocument; 811 HandleRootsChangedTask(BaseActivity activity)812 public HandleRootsChangedTask(BaseActivity activity) { 813 super(activity); 814 } 815 816 @Override run(RootInfo... roots)817 protected RootInfo run(RootInfo... roots) { 818 assert(roots.length == 1); 819 mCurrentRoot = roots[0]; 820 final Collection<RootInfo> cachedRoots = mOwner.mRoots.getRootsBlocking(); 821 for (final RootInfo root : cachedRoots) { 822 if (root.getUri().equals(mCurrentRoot.getUri())) { 823 // We don't need to change the current root as the current root was not removed. 824 return null; 825 } 826 } 827 828 // Choose the default root. 829 final RootInfo defaultRoot = mOwner.mRoots.getDefaultRootBlocking(mOwner.mState); 830 assert(defaultRoot != null); 831 if (!defaultRoot.isRecents()) { 832 mDefaultRootDocument = mOwner.getRootDocumentBlocking(defaultRoot); 833 } 834 return defaultRoot; 835 } 836 837 @Override finish(RootInfo defaultRoot)838 protected void finish(RootInfo defaultRoot) { 839 if (defaultRoot == null) { 840 return; 841 } 842 843 // If the activity has been launched for the specific root and it is removed, finish the 844 // activity. 845 final Uri uri = mOwner.getIntent().getData(); 846 if (uri != null && uri.equals(mCurrentRoot.getUri())) { 847 mOwner.finish(); 848 return; 849 } 850 851 // Clear entire backstack and start in new root. 852 mOwner.mState.onRootChanged(defaultRoot); 853 mOwner.mSearchManager.update(defaultRoot); 854 855 if (defaultRoot.isRecents()) { 856 mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 857 } else { 858 mOwner.openContainerDocument(mDefaultRootDocument); 859 } 860 } 861 } 862 } 863