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 invalidateOptionsMenu(); 391 } 392 loadRoot(final Uri uri)393 final void loadRoot(final Uri uri) { 394 new LoadRootTask(this, uri).executeOnExecutor( 395 ProviderExecutor.forAuthority(uri.getAuthority())); 396 } 397 398 /** 399 * Called when search results changed. 400 * Refreshes the content of the directory. It doesn't refresh elements on the action bar. 401 * e.g. The current directory name displayed on the action bar won't get updated. 402 */ 403 @Override onSearchChanged(@ullable String query)404 public void onSearchChanged(@Nullable String query) { 405 // We should not get here if root is not searchable 406 assert(canSearchRoot()); 407 reloadSearch(query); 408 } 409 410 @Override onSearchFinished()411 public void onSearchFinished() { 412 // Restores menu icons state 413 invalidateOptionsMenu(); 414 } 415 reloadSearch(String query)416 private void reloadSearch(String query) { 417 FragmentManager fm = getFragmentManager(); 418 RootInfo root = getCurrentRoot(); 419 DocumentInfo cwd = getCurrentDirectory(); 420 421 DirectoryFragment.reloadSearch(fm, root, cwd, query); 422 } 423 getExcludedAuthorities()424 final List<String> getExcludedAuthorities() { 425 List<String> authorities = new ArrayList<>(); 426 if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) { 427 // Exclude roots provided by the calling package. 428 String packageName = getCallingPackageMaybeExtra(); 429 try { 430 PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName, 431 PackageManager.GET_PROVIDERS); 432 for (ProviderInfo provider: pkgInfo.providers) { 433 authorities.add(provider.authority); 434 } 435 } catch (PackageManager.NameNotFoundException e) { 436 Log.e(mTag, "Calling package name does not resolve: " + packageName); 437 } 438 } 439 return authorities; 440 } 441 canSearchRoot()442 boolean canSearchRoot() { 443 final RootInfo root = getCurrentRoot(); 444 return (root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0; 445 } 446 getCallingPackageMaybeExtra()447 final String getCallingPackageMaybeExtra() { 448 String callingPackage = getCallingPackage(); 449 // System apps can set the calling package name using an extra. 450 try { 451 ApplicationInfo info = getPackageManager().getApplicationInfo(callingPackage, 0); 452 if (info.isSystemApp() || info.isUpdatedSystemApp()) { 453 final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME); 454 if (extra != null) { 455 callingPackage = extra; 456 } 457 } 458 } finally { 459 return callingPackage; 460 } 461 } 462 get(Fragment fragment)463 public static BaseActivity get(Fragment fragment) { 464 return (BaseActivity) fragment.getActivity(); 465 } 466 getDisplayState()467 public State getDisplayState() { 468 return mState; 469 } 470 471 /* 472 * Get the default directory to be presented after starting the activity. 473 * Method can be overridden if the change of the behavior of the the child activity is needed. 474 */ getDefaultRoot()475 public Uri getDefaultRoot() { 476 return Shared.shouldShowDocumentsRoot(this, getIntent()) 477 ? DocumentsContract.buildHomeUri() 478 : DocumentsContract.buildRootUri( 479 "com.android.providers.downloads.documents", "downloads"); 480 } 481 482 /** 483 * Set internal storage visible based on explicit user action. 484 */ setDisplayAdvancedDevices(boolean display)485 void setDisplayAdvancedDevices(boolean display) { 486 Metrics.logUserAction(this, 487 display ? Metrics.USER_ACTION_SHOW_ADVANCED : Metrics.USER_ACTION_HIDE_ADVANCED); 488 489 LocalPreferences.setShowDeviceRoot(this, mState.action, display); 490 mState.showAdvanced = display; 491 RootsFragment.get(getFragmentManager()).onDisplayStateChanged(); 492 invalidateOptionsMenu(); 493 } 494 495 /** 496 * Set file size visible based on explicit user action. 497 */ setDisplayFileSize(boolean display)498 void setDisplayFileSize(boolean display) { 499 Metrics.logUserAction(this, 500 display ? Metrics.USER_ACTION_SHOW_SIZE : Metrics.USER_ACTION_HIDE_SIZE); 501 502 LocalPreferences.setDisplayFileSize(this, display); 503 mState.showSize = display; 504 DirectoryFragment dir = getDirectoryFragment(); 505 if (dir != null) { 506 dir.onDisplayStateChanged(); 507 } 508 invalidateOptionsMenu(); 509 } 510 511 /** 512 * Set state sort order based on explicit user action. 513 */ setUserSortOrder(int sortOrder)514 void setUserSortOrder(int sortOrder) { 515 switch(sortOrder) { 516 case State.SORT_ORDER_DISPLAY_NAME: 517 Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_NAME); 518 break; 519 case State.SORT_ORDER_LAST_MODIFIED: 520 Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_DATE); 521 break; 522 case State.SORT_ORDER_SIZE: 523 Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_SIZE); 524 break; 525 } 526 527 mState.userSortOrder = sortOrder; 528 DirectoryFragment dir = getDirectoryFragment(); 529 if (dir != null) { 530 dir.onSortOrderChanged(); 531 } 532 } 533 534 /** 535 * Set mode based on explicit user action. 536 */ setViewMode(@iewMode int mode)537 void setViewMode(@ViewMode int mode) { 538 if (mode == State.MODE_GRID) { 539 Metrics.logUserAction(this, Metrics.USER_ACTION_GRID); 540 } else if (mode == State.MODE_LIST) { 541 Metrics.logUserAction(this, Metrics.USER_ACTION_LIST); 542 } 543 544 LocalPreferences.setViewMode(this, getCurrentRoot(), mode); 545 mState.derivedMode = mode; 546 547 // view icon needs to be updated, but we *could* do it 548 // in onOptionsItemSelected, and not do the full invalidation 549 // But! That's a larger refactoring we'll save for another day. 550 invalidateOptionsMenu(); 551 DirectoryFragment dir = getDirectoryFragment(); 552 if (dir != null) { 553 dir.onViewModeChanged(); 554 } 555 } 556 setPending(boolean pending)557 public void setPending(boolean pending) { 558 final SaveFragment save = SaveFragment.get(getFragmentManager()); 559 if (save != null) { 560 save.setPending(pending); 561 } 562 } 563 564 @Override onSaveInstanceState(Bundle state)565 protected void onSaveInstanceState(Bundle state) { 566 super.onSaveInstanceState(state); 567 state.putParcelable(Shared.EXTRA_STATE, mState); 568 mSearchManager.onSaveInstanceState(state); 569 } 570 571 @Override onRestoreInstanceState(Bundle state)572 protected void onRestoreInstanceState(Bundle state) { 573 super.onRestoreInstanceState(state); 574 } 575 576 @Override isSearchExpanded()577 public boolean isSearchExpanded() { 578 return mSearchManager.isExpanded(); 579 } 580 581 @Override getCurrentRoot()582 public RootInfo getCurrentRoot() { 583 if (mState.stack.root != null) { 584 return mState.stack.root; 585 } else { 586 return mRoots.getRecentsRoot(); 587 } 588 } 589 getCurrentDirectory()590 public DocumentInfo getCurrentDirectory() { 591 return mState.stack.peek(); 592 } 593 getExecutorForCurrentDirectory()594 public Executor getExecutorForCurrentDirectory() { 595 final DocumentInfo cwd = getCurrentDirectory(); 596 if (cwd != null && cwd.authority != null) { 597 return ProviderExecutor.forAuthority(cwd.authority); 598 } else { 599 return AsyncTask.THREAD_POOL_EXECUTOR; 600 } 601 } 602 603 @Override onBackPressed()604 public void onBackPressed() { 605 // While action bar is expanded, the state stack UI is hidden. 606 if (mSearchManager.cancelSearch()) { 607 return; 608 } 609 610 DirectoryFragment dir = getDirectoryFragment(); 611 if (dir != null && dir.onBackPressed()) { 612 return; 613 } 614 615 if (!mState.hasLocationChanged()) { 616 super.onBackPressed(); 617 return; 618 } 619 620 if (onBeforePopDir() || popDir()) { 621 return; 622 } 623 624 super.onBackPressed(); 625 } 626 onBeforePopDir()627 boolean onBeforePopDir() { 628 // Files app overrides this with some fancy logic. 629 return false; 630 } 631 onStackPicked(DocumentStack stack)632 public void onStackPicked(DocumentStack stack) { 633 try { 634 // Update the restored stack to ensure we have freshest data 635 stack.updateDocuments(getContentResolver()); 636 mState.setStack(stack); 637 refreshCurrentRootAndDirectory(AnimationView.ANIM_SIDE); 638 639 } catch (FileNotFoundException e) { 640 Log.w(mTag, "Failed to restore stack: " + e); 641 } 642 } 643 644 /** 645 * Declare a global key handler to route key events when there isn't a specific focus view. This 646 * covers the scenario where a user opens DocumentsUI and just starts typing. 647 * 648 * @param keyCode 649 * @param event 650 * @return 651 */ 652 @CallSuper 653 @Override onKeyDown(int keyCode, KeyEvent event)654 public boolean onKeyDown(int keyCode, KeyEvent event) { 655 if (Events.isNavigationKeyCode(keyCode)) { 656 // Forward all unclaimed navigation keystrokes to the DirectoryFragment. This causes any 657 // stray navigation keystrokes focus the content pane, which is probably what the user 658 // is trying to do. 659 DirectoryFragment df = DirectoryFragment.get(getFragmentManager()); 660 if (df != null) { 661 df.requestFocus(); 662 return true; 663 } 664 } else if (keyCode == KeyEvent.KEYCODE_TAB) { 665 // Tab toggles focus on the navigation drawer. 666 toggleNavDrawerFocus(); 667 return true; 668 } else if (keyCode == KeyEvent.KEYCODE_DEL) { 669 popDir(); 670 return true; 671 } 672 return super.onKeyDown(keyCode, event); 673 } 674 addEventListener(EventListener listener)675 public void addEventListener(EventListener listener) { 676 mEventListeners.add(listener); 677 } 678 removeEventListener(EventListener listener)679 public void removeEventListener(EventListener listener) { 680 mEventListeners.remove(listener); 681 } 682 notifyDirectoryLoaded(Uri uri)683 public void notifyDirectoryLoaded(Uri uri) { 684 for (EventListener listener : mEventListeners) { 685 listener.onDirectoryLoaded(uri); 686 } 687 } 688 notifyDirectoryNavigated(Uri uri)689 void notifyDirectoryNavigated(Uri uri) { 690 for (EventListener listener : mEventListeners) { 691 listener.onDirectoryNavigated(uri); 692 } 693 } 694 695 /** 696 * Toggles focus between the navigation drawer and the directory listing. If the drawer isn't 697 * locked, open/close it as appropriate. 698 */ toggleNavDrawerFocus()699 void toggleNavDrawerFocus() { 700 if (mNavDrawerHasFocus) { 701 mDrawer.setOpen(false); 702 DirectoryFragment df = DirectoryFragment.get(getFragmentManager()); 703 if (df != null) { 704 df.requestFocus(); 705 } 706 } else { 707 mDrawer.setOpen(true); 708 RootsFragment rf = RootsFragment.get(getFragmentManager()); 709 if (rf != null) { 710 rf.requestFocus(); 711 } 712 } 713 mNavDrawerHasFocus = !mNavDrawerHasFocus; 714 } 715 getRootDocumentBlocking(RootInfo root)716 DocumentInfo getRootDocumentBlocking(RootInfo root) { 717 try { 718 final Uri uri = DocumentsContract.buildDocumentUri( 719 root.authority, root.documentId); 720 return DocumentInfo.fromUri(getContentResolver(), uri); 721 } catch (FileNotFoundException e) { 722 Log.w(mTag, "Failed to find root", e); 723 return null; 724 } 725 } 726 727 /** 728 * Pops the top entry off the directory stack, and returns the user to the previous directory. 729 * If the directory stack only contains one item, this method does nothing. 730 * 731 * @return Whether the stack was popped. 732 */ popDir()733 private boolean popDir() { 734 if (mState.stack.size() > 1) { 735 mState.stack.pop(); 736 refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE); 737 return true; 738 } 739 return false; 740 } 741 742 /** 743 * Closes the activity when it's idle. 744 */ addListenerForLaunchCompletion()745 private void addListenerForLaunchCompletion() { 746 addEventListener(new EventListener() { 747 @Override 748 public void onDirectoryNavigated(Uri uri) { 749 } 750 751 @Override 752 public void onDirectoryLoaded(Uri uri) { 753 removeEventListener(this); 754 getMainLooper().getQueue().addIdleHandler(new IdleHandler() { 755 @Override 756 public boolean queueIdle() { 757 // If startup benchmark is requested by a whitelisted testing package, then 758 // close the activity once idle, and notify the testing activity. 759 if (getIntent().getBooleanExtra(EXTRA_BENCHMARK, false) && 760 BENCHMARK_TESTING_PACKAGE.equals(getCallingPackage())) { 761 setResult(RESULT_OK); 762 finish(); 763 } 764 765 Metrics.logStartupMs( 766 BaseActivity.this, (int) (new Date().getTime() - mStartTime)); 767 768 // Remove the idle handler. 769 return false; 770 } 771 }); 772 new Handler().post(new Runnable() { 773 @Override public void run() { 774 } 775 }); 776 } 777 }); 778 } 779 780 private static final class PickRootTask extends PairedTask<BaseActivity, Void, DocumentInfo> { 781 private RootInfo mRoot; 782 PickRootTask(BaseActivity activity, RootInfo root)783 public PickRootTask(BaseActivity activity, RootInfo root) { 784 super(activity); 785 mRoot = root; 786 } 787 788 @Override run(Void... params)789 protected DocumentInfo run(Void... params) { 790 return mOwner.getRootDocumentBlocking(mRoot); 791 } 792 793 @Override finish(DocumentInfo result)794 protected void finish(DocumentInfo result) { 795 if (result != null) { 796 mOwner.openContainerDocument(result); 797 } 798 } 799 } 800 801 private static final class HandleRootsChangedTask 802 extends PairedTask<BaseActivity, RootInfo, RootInfo> { 803 RootInfo mCurrentRoot; 804 DocumentInfo mDefaultRootDocument; 805 HandleRootsChangedTask(BaseActivity activity)806 public HandleRootsChangedTask(BaseActivity activity) { 807 super(activity); 808 } 809 810 @Override run(RootInfo... roots)811 protected RootInfo run(RootInfo... roots) { 812 assert(roots.length == 1); 813 mCurrentRoot = roots[0]; 814 final Collection<RootInfo> cachedRoots = mOwner.mRoots.getRootsBlocking(); 815 for (final RootInfo root : cachedRoots) { 816 if (root.getUri().equals(mCurrentRoot.getUri())) { 817 // We don't need to change the current root as the current root was not removed. 818 return null; 819 } 820 } 821 822 // Choose the default root. 823 final RootInfo defaultRoot = mOwner.mRoots.getDefaultRootBlocking(mOwner.mState); 824 assert(defaultRoot != null); 825 if (!defaultRoot.isRecents()) { 826 mDefaultRootDocument = mOwner.getRootDocumentBlocking(defaultRoot); 827 } 828 return defaultRoot; 829 } 830 831 @Override finish(RootInfo defaultRoot)832 protected void finish(RootInfo defaultRoot) { 833 if (defaultRoot == null) { 834 return; 835 } 836 837 // If the activity has been launched for the specific root and it is removed, finish the 838 // activity. 839 final Uri uri = mOwner.getIntent().getData(); 840 if (uri != null && uri.equals(mCurrentRoot.getUri())) { 841 mOwner.finish(); 842 return; 843 } 844 845 // Clear entire backstack and start in new root. 846 mOwner.mState.onRootChanged(defaultRoot); 847 mOwner.mSearchManager.update(defaultRoot); 848 849 if (defaultRoot.isRecents()) { 850 mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 851 } else { 852 mOwner.openContainerDocument(mDefaultRootDocument); 853 } 854 } 855 } 856 } 857