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.base.Shared.EXTRA_BENCHMARK; 20 import static com.android.documentsui.base.SharedMinimal.DEBUG; 21 import static com.android.documentsui.base.State.MODE_GRID; 22 23 import android.content.Intent; 24 import android.content.pm.PackageInfo; 25 import android.content.pm.PackageManager; 26 import android.content.pm.ProviderInfo; 27 import android.graphics.Color; 28 import android.net.Uri; 29 import android.os.Build; 30 import android.os.Bundle; 31 import android.os.MessageQueue.IdleHandler; 32 import android.preference.PreferenceManager; 33 import android.provider.DocumentsContract; 34 import android.text.TextUtils; 35 import android.util.Log; 36 import android.view.KeyEvent; 37 import android.view.Menu; 38 import android.view.MenuItem; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.widget.Checkable; 42 import android.widget.TextView; 43 44 import androidx.annotation.CallSuper; 45 import androidx.annotation.LayoutRes; 46 import androidx.annotation.VisibleForTesting; 47 import androidx.appcompat.app.AppCompatActivity; 48 import androidx.appcompat.widget.ActionMenuView; 49 import androidx.appcompat.widget.Toolbar; 50 import androidx.fragment.app.Fragment; 51 52 import com.android.documentsui.AbstractActionHandler.CommonAddons; 53 import com.android.documentsui.Injector.Injected; 54 import com.android.documentsui.NavigationViewManager.Breadcrumb; 55 import com.android.documentsui.R; 56 import com.android.documentsui.base.DocumentInfo; 57 import com.android.documentsui.base.EventHandler; 58 import com.android.documentsui.base.RootInfo; 59 import com.android.documentsui.base.Shared; 60 import com.android.documentsui.base.State; 61 import com.android.documentsui.base.State.ViewMode; 62 import com.android.documentsui.dirlist.AnimationView; 63 import com.android.documentsui.dirlist.AppsRowManager; 64 import com.android.documentsui.dirlist.DirectoryFragment; 65 import com.android.documentsui.prefs.LocalPreferences; 66 import com.android.documentsui.prefs.Preferences; 67 import com.android.documentsui.prefs.PreferencesMonitor; 68 import com.android.documentsui.prefs.ScopedPreferences; 69 import com.android.documentsui.queries.CommandInterceptor; 70 import com.android.documentsui.queries.SearchChipData; 71 import com.android.documentsui.queries.SearchFragment; 72 import com.android.documentsui.queries.SearchViewManager; 73 import com.android.documentsui.queries.SearchViewManager.SearchManagerListener; 74 import com.android.documentsui.roots.ProvidersCache; 75 import com.android.documentsui.sidebar.RootsFragment; 76 import com.android.documentsui.sorting.SortController; 77 import com.android.documentsui.sorting.SortModel; 78 79 import com.google.android.material.appbar.AppBarLayout; 80 81 import java.util.ArrayList; 82 import java.util.Date; 83 import java.util.List; 84 85 import javax.annotation.Nullable; 86 87 public abstract class BaseActivity 88 extends AppCompatActivity implements CommonAddons, NavigationViewManager.Environment { 89 90 private static final String BENCHMARK_TESTING_PACKAGE = "com.android.documentsui.appperftests"; 91 92 protected SearchViewManager mSearchManager; 93 protected AppsRowManager mAppsRowManager; 94 protected State mState; 95 96 @Injected 97 protected Injector<?> mInjector; 98 99 protected ProvidersCache mProviders; 100 protected DocumentsAccess mDocs; 101 protected DrawerController mDrawer; 102 103 protected NavigationViewManager mNavigator; 104 protected SortController mSortController; 105 106 private final List<EventListener> mEventListeners = new ArrayList<>(); 107 private final String mTag; 108 109 @LayoutRes 110 private int mLayoutId; 111 112 private RootsMonitor<BaseActivity> mRootsMonitor; 113 114 private long mStartTime; 115 private boolean mHasQueryContentFromIntent; 116 117 private PreferencesMonitor mPreferencesMonitor; 118 BaseActivity(@ayoutRes int layoutId, String tag)119 public BaseActivity(@LayoutRes int layoutId, String tag) { 120 mLayoutId = layoutId; 121 mTag = tag; 122 } 123 refreshDirectory(int anim)124 protected abstract void refreshDirectory(int anim); 125 /** Allows sub-classes to include information in a newly created State instance. */ includeState(State initialState)126 protected abstract void includeState(State initialState); onDirectoryCreated(DocumentInfo doc)127 protected abstract void onDirectoryCreated(DocumentInfo doc); 128 getInjector()129 public abstract Injector<?> getInjector(); 130 131 @CallSuper 132 @Override onCreate(Bundle icicle)133 public void onCreate(Bundle icicle) { 134 // Record the time when onCreate is invoked for metric. 135 mStartTime = new Date().getTime(); 136 137 // ToDo Create tool to check resource version before applyStyle for the theme 138 // If version code is not match, we should reset overlay package to default, 139 // in case Activity continueusly encounter resource not found exception 140 getTheme().applyStyle(R.style.DocumentsDefaultTheme, false); 141 142 super.onCreate(icicle); 143 144 final Intent intent = getIntent(); 145 146 addListenerForLaunchCompletion(); 147 148 setContentView(mLayoutId); 149 150 setContainer(); 151 152 mInjector = getInjector(); 153 mState = getState(icicle); 154 mDrawer = DrawerController.create(this, mInjector.config); 155 Metrics.logActivityLaunch(mState, intent); 156 157 mProviders = DocumentsApplication.getProvidersCache(this); 158 mDocs = DocumentsAccess.create(this); 159 160 Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 161 setSupportActionBar(toolbar); 162 163 Breadcrumb breadcrumb = 164 Shared.findView(this, R.id.dropdown_breadcrumb, R.id.horizontal_breadcrumb); 165 assert(breadcrumb != null); 166 167 mNavigator = new NavigationViewManager(this, mDrawer, mState, this, breadcrumb); 168 SearchManagerListener searchListener = new SearchManagerListener() { 169 /** 170 * Called when search results changed. Refreshes the content of the directory. It 171 * doesn't refresh elements on the action bar. e.g. The current directory name displayed 172 * on the action bar won't get updated. 173 */ 174 @Override 175 public void onSearchChanged(@Nullable String query) { 176 if (mSearchManager.isSearching()) { 177 Metrics.logSearchMode(query != null, mSearchManager.hasCheckedChip()); 178 if (mInjector.pickResult != null) { 179 mInjector.pickResult.increaseActionCount(); 180 } 181 } 182 183 mInjector.actions.loadDocumentsForCurrentStack(); 184 185 expandAppBar(); 186 DirectoryFragment dir = getDirectoryFragment(); 187 if (dir != null) { 188 dir.scrollToTop(); 189 } 190 } 191 192 @Override 193 public void onSearchFinished() { 194 // Restores menu icons state 195 invalidateOptionsMenu(); 196 } 197 198 @Override 199 public void onSearchViewChanged(boolean opened) { 200 mNavigator.update(); 201 } 202 203 @Override 204 public void onSearchChipStateChanged(View v) { 205 final Checkable chip = (Checkable) v; 206 if (chip.isChecked()) { 207 final SearchChipData item = (SearchChipData) v.getTag(); 208 Metrics.logUserAction(MetricConsts.USER_ACTION_SEARCH_CHIP); 209 Metrics.logSearchType(item.getChipType()); 210 } 211 } 212 213 @Override 214 public void onSearchViewFocusChanged(boolean hasFocus) { 215 final boolean isInitailSearch 216 = !TextUtils.isEmpty(mSearchManager.getCurrentSearch()) 217 && TextUtils.isEmpty(mSearchManager.getSearchViewText()); 218 if (hasFocus && (SearchFragment.get(getSupportFragmentManager()) == null) 219 && !isInitailSearch) { 220 SearchFragment.showFragment(getSupportFragmentManager(), 221 mSearchManager.getSearchViewText()); 222 } 223 } 224 225 @Override 226 public void onSearchViewClearClicked() { 227 if (SearchFragment.get(getSupportFragmentManager()) == null) { 228 SearchFragment.showFragment(getSupportFragmentManager(), 229 mSearchManager.getSearchViewText()); 230 } 231 } 232 }; 233 234 // "Commands" are meta input for controlling system behavior. 235 // We piggy back on search input as it is the only text input 236 // area in the app. But the functionality is independent 237 // of "regular" search query processing. 238 final CommandInterceptor cmdInterceptor = new CommandInterceptor(mInjector.features); 239 cmdInterceptor.add(new CommandInterceptor.DumpRootsCacheHandler(this)); 240 241 // A tiny decorator that adds support for enabling CommandInterceptor 242 // based on query input. It's sorta like CommandInterceptor, but its metaaahhh. 243 EventHandler<String> queryInterceptor = 244 CommandInterceptor.createDebugModeFlipper( 245 mInjector.features, 246 mInjector.debugHelper::toggleDebugMode, 247 cmdInterceptor); 248 249 ViewGroup chipGroup = findViewById(R.id.search_chip_group); 250 mSearchManager = new SearchViewManager(searchListener, queryInterceptor, 251 chipGroup, icicle); 252 // initialize the chip sets by accept mime types 253 mSearchManager.initChipSets(mState.acceptMimes); 254 // update the chip items by the mime types of the root 255 mSearchManager.updateChips(getCurrentRoot().derivedMimeTypes); 256 // parse the query content from intent when launch the 257 // activity at the first time 258 if (icicle == null) { 259 mHasQueryContentFromIntent = mSearchManager.parseQueryContentFromIntent(getIntent(), 260 mState.action); 261 } 262 263 mNavigator.setSearchBarClickListener(v -> { 264 mSearchManager.onSearchBarClicked(); 265 mNavigator.update(); 266 }); 267 268 mSortController = SortController.create(this, mState.derivedMode, mState.sortModel); 269 270 mPreferencesMonitor = new PreferencesMonitor( 271 getApplicationContext().getPackageName(), 272 PreferenceManager.getDefaultSharedPreferences(this), 273 this::onPreferenceChanged); 274 mPreferencesMonitor.start(); 275 276 // Base classes must update result in their onCreate. 277 setResult(AppCompatActivity.RESULT_CANCELED); 278 } 279 onPreferenceChanged(String pref)280 public void onPreferenceChanged(String pref) { 281 // For now, we only work with prefs that we backup. This 282 // just limits the scope of what we expect to come flowing 283 // through here until we know we want more and fancier options. 284 assert(Preferences.shouldBackup(pref)); 285 286 switch (pref) { 287 case ScopedPreferences.INCLUDE_DEVICE_ROOT: 288 updateDisplayAdvancedDevices(mInjector.prefs.getShowDeviceRoot()); 289 } 290 } 291 292 @Override onPostCreate(Bundle savedInstanceState)293 protected void onPostCreate(Bundle savedInstanceState) { 294 super.onPostCreate(savedInstanceState); 295 296 mRootsMonitor = new RootsMonitor<>( 297 this, 298 mInjector.actions, 299 mProviders, 300 mDocs, 301 mState, 302 mSearchManager, 303 mInjector.actionModeController::finishActionMode); 304 mRootsMonitor.start(); 305 } 306 307 @Override onCreateOptionsMenu(Menu menu)308 public boolean onCreateOptionsMenu(Menu menu) { 309 boolean showMenu = super.onCreateOptionsMenu(menu); 310 311 getMenuInflater().inflate(R.menu.activity, menu); 312 mNavigator.update(); 313 boolean fullBarSearch = getResources().getBoolean(R.bool.full_bar_search_view); 314 boolean showSearchBar = getResources().getBoolean(R.bool.show_search_bar); 315 mSearchManager.install(menu, fullBarSearch, showSearchBar); 316 317 final ActionMenuView subMenuView = findViewById(R.id.sub_menu); 318 // If size is 0, it means the menu has not inflated and it should only do once. 319 if (subMenuView.getMenu().size() == 0) { 320 subMenuView.setOnMenuItemClickListener(this::onOptionsItemSelected); 321 getMenuInflater().inflate(R.menu.sub_menu, subMenuView.getMenu()); 322 } 323 324 return showMenu; 325 } 326 327 @Override 328 @CallSuper onPrepareOptionsMenu(Menu menu)329 public boolean onPrepareOptionsMenu(Menu menu) { 330 super.onPrepareOptionsMenu(menu); 331 mSearchManager.showMenu(mState.stack); 332 final ActionMenuView subMenuView = findViewById(R.id.sub_menu); 333 mInjector.menuManager.updateSubMenu(subMenuView.getMenu()); 334 return true; 335 } 336 337 @Override onDestroy()338 protected void onDestroy() { 339 mRootsMonitor.stop(); 340 mPreferencesMonitor.stop(); 341 mSortController.destroy(); 342 super.onDestroy(); 343 } 344 getState(@ullable Bundle icicle)345 private State getState(@Nullable Bundle icicle) { 346 if (icicle != null) { 347 State state = icicle.<State>getParcelable(Shared.EXTRA_STATE); 348 if (DEBUG) { 349 Log.d(mTag, "Recovered existing state object: " + state); 350 } 351 return state; 352 } 353 354 State state = new State(); 355 356 final Intent intent = getIntent(); 357 358 state.sortModel = SortModel.createModel(); 359 state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false); 360 state.excludedAuthorities = getExcludedAuthorities(); 361 362 includeState(state); 363 364 state.showAdvanced = Shared.mustShowDeviceRoot(intent) 365 || mInjector.prefs.getShowDeviceRoot(); 366 367 // Only show the toggle if advanced isn't forced enabled. 368 state.showDeviceStorageOption = !Shared.mustShowDeviceRoot(intent); 369 370 if (DEBUG) { 371 Log.d(mTag, "Created new state object: " + state); 372 } 373 374 return state; 375 } 376 setContainer()377 private void setContainer() { 378 View root = findViewById(R.id.coordinator_layout); 379 root.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 380 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 381 root.setOnApplyWindowInsetsListener((v, insets) -> { 382 root.setPadding(insets.getSystemWindowInsetLeft(), 383 insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0); 384 385 View saveContainer = findViewById(R.id.container_save); 386 saveContainer.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom()); 387 388 View rootsContainer = findViewById(R.id.container_roots); 389 rootsContainer.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom()); 390 391 return insets.consumeSystemWindowInsets(); 392 }); 393 394 getWindow().setNavigationBarDividerColor(Color.TRANSPARENT); 395 if (Build.VERSION.SDK_INT >= 29) { 396 getWindow().setNavigationBarColor(Color.TRANSPARENT); 397 getWindow().setNavigationBarContrastEnforced(true); 398 } else { 399 getWindow().setNavigationBarColor(getColor(R.color.nav_bar_translucent)); 400 } 401 } 402 403 @Override setRootsDrawerOpen(boolean open)404 public void setRootsDrawerOpen(boolean open) { 405 mNavigator.revealRootsDrawer(open); 406 } 407 408 @Override onRootPicked(RootInfo root)409 public void onRootPicked(RootInfo root) { 410 // Clicking on the current root removes search 411 mSearchManager.cancelSearch(); 412 413 // Skip refreshing if root nor directory didn't change 414 if (root.equals(getCurrentRoot()) && mState.stack.size() == 1) { 415 return; 416 } 417 418 mInjector.actionModeController.finishActionMode(); 419 mSortController.onViewModeChanged(mState.derivedMode); 420 421 // Set summary header's visibility. Only recents and downloads root may have summary in 422 // their docs. 423 mState.sortModel.setDimensionVisibility( 424 SortModel.SORT_DIMENSION_ID_SUMMARY, 425 root.isRecents() || root.isDownloads() ? View.VISIBLE : View.INVISIBLE); 426 427 // Clear entire backstack and start in new root 428 mState.stack.changeRoot(root); 429 430 // Recents is always in memory, so we just load it directly. 431 // Otherwise we delegate loading data from disk to a task 432 // to ensure a responsive ui. 433 if (mProviders.isRecentsRoot(root)) { 434 refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 435 } else { 436 mInjector.actions.getRootDocument( 437 root, 438 TimeoutTask.DEFAULT_TIMEOUT, 439 doc -> mInjector.actions.openRootDocument(doc)); 440 } 441 442 expandAppBar(); 443 updateHeaderTitle(); 444 } 445 446 @Override onOptionsItemSelected(MenuItem item)447 public boolean onOptionsItemSelected(MenuItem item) { 448 449 switch (item.getItemId()) { 450 case android.R.id.home: 451 onBackPressed(); 452 return true; 453 454 case R.id.option_menu_create_dir: 455 getInjector().actions.showCreateDirectoryDialog(); 456 return true; 457 458 case R.id.option_menu_search: 459 // SearchViewManager listens for this directly. 460 return false; 461 462 case R.id.option_menu_advanced: 463 onDisplayAdvancedDevices(); 464 return true; 465 466 case R.id.option_menu_select_all: 467 getInjector().actions.selectAllFiles(); 468 return true; 469 470 case R.id.option_menu_debug: 471 getInjector().actions.showDebugMessage(); 472 return true; 473 474 case R.id.option_menu_sort: 475 getInjector().actions.showSortDialog(); 476 return true; 477 478 case R.id.sub_menu_grid: 479 setViewMode(State.MODE_GRID); 480 return true; 481 482 case R.id.sub_menu_list: 483 setViewMode(State.MODE_LIST); 484 return true; 485 486 default: 487 return super.onOptionsItemSelected(item); 488 } 489 } 490 getDirectoryFragment()491 protected final @Nullable DirectoryFragment getDirectoryFragment() { 492 return DirectoryFragment.get(getSupportFragmentManager()); 493 } 494 495 /** 496 * Returns true if a directory can be created in the current location. 497 * @return 498 */ canCreateDirectory()499 protected boolean canCreateDirectory() { 500 final RootInfo root = getCurrentRoot(); 501 final DocumentInfo cwd = getCurrentDirectory(); 502 return cwd != null 503 && cwd.isCreateSupported() 504 && !mSearchManager.isSearching() 505 && !root.isRecents(); 506 } 507 508 /** 509 * Returns true if a directory can be inspected. 510 */ canInspectDirectory()511 protected boolean canInspectDirectory() { 512 return false; 513 } 514 515 // TODO: make navigator listen to state 516 @Override updateNavigator()517 public final void updateNavigator() { 518 mNavigator.update(); 519 } 520 521 @Override restoreRootAndDirectory()522 public void restoreRootAndDirectory() { 523 // We're trying to restore stuff in document stack from saved instance. If we didn't have a 524 // chance to spawn a fragment before we need to do it now. However if we spawned a fragment 525 // already, system will automatically restore the fragment for us so we don't need to do 526 // that manually this time. 527 if (DirectoryFragment.get(getSupportFragmentManager()) == null) { 528 refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 529 } 530 } 531 532 /** 533 * Refreshes the content of the director and the menu/action bar. 534 * The current directory name and selection will get updated. 535 * @param anim 536 */ 537 @Override refreshCurrentRootAndDirectory(int anim)538 public final void refreshCurrentRootAndDirectory(int anim) { 539 // The following call will crash if it's called before onCreateOptionMenu() is called in 540 // which we install menu item to search view manager, and there is a search query we need to 541 // restore. This happens when we're still initializing our UI so we shouldn't cancel the 542 // search which will be restored later in onCreateOptionMenu(). Try finding a way to guard 543 // refreshCurrentRootAndDirectory() from being called while we're restoring the state of UI 544 // from the saved state passed in onCreate(). 545 mSearchManager.cancelSearch(); 546 547 // only set the query content in the first launch 548 if (mHasQueryContentFromIntent) { 549 mHasQueryContentFromIntent = false; 550 mSearchManager.setCurrentSearch(mSearchManager.getQueryContentFromIntent()); 551 } 552 553 mState.derivedMode = LocalPreferences.getViewMode(this, mState.stack.getRoot(), MODE_GRID); 554 555 mNavigator.update(); 556 557 refreshDirectory(anim); 558 559 final RootsFragment roots = RootsFragment.get(getSupportFragmentManager()); 560 if (roots != null) { 561 roots.onCurrentRootChanged(); 562 } 563 564 // Causes talkback to announce the activity's new title 565 setTitle(mState.stack.getTitle()); 566 567 invalidateOptionsMenu(); 568 mSortController.onViewModeChanged(mState.derivedMode); 569 mSearchManager.updateChips(getCurrentRoot().derivedMimeTypes); 570 mAppsRowManager.updateView(this); 571 } 572 getExcludedAuthorities()573 private final List<String> getExcludedAuthorities() { 574 List<String> authorities = new ArrayList<>(); 575 if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) { 576 // Exclude roots provided by the calling package. 577 String packageName = Shared.getCallingPackageName(this); 578 try { 579 PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName, 580 PackageManager.GET_PROVIDERS); 581 for (ProviderInfo provider: pkgInfo.providers) { 582 authorities.add(provider.authority); 583 } 584 } catch (PackageManager.NameNotFoundException e) { 585 Log.e(mTag, "Calling package name does not resolve: " + packageName); 586 } 587 } 588 return authorities; 589 } 590 get(Fragment fragment)591 public static BaseActivity get(Fragment fragment) { 592 return (BaseActivity) fragment.getActivity(); 593 } 594 getDisplayState()595 public State getDisplayState() { 596 return mState; 597 } 598 599 /** 600 * Set internal storage visible based on explicit user action. 601 */ onDisplayAdvancedDevices()602 private void onDisplayAdvancedDevices() { 603 boolean display = !mState.showAdvanced; 604 Metrics.logUserAction(display 605 ? MetricConsts.USER_ACTION_SHOW_ADVANCED : MetricConsts.USER_ACTION_HIDE_ADVANCED); 606 607 mInjector.prefs.setShowDeviceRoot(display); 608 updateDisplayAdvancedDevices(display); 609 } 610 updateDisplayAdvancedDevices(boolean display)611 private void updateDisplayAdvancedDevices(boolean display) { 612 mState.showAdvanced = display; 613 @Nullable RootsFragment fragment = RootsFragment.get(getSupportFragmentManager()); 614 if (fragment != null) { 615 // This also takes care of updating launcher shortcuts (which are roots :) 616 fragment.onDisplayStateChanged(); 617 } 618 invalidateOptionsMenu(); 619 } 620 621 /** 622 * Set mode based on explicit user action. 623 */ setViewMode(@iewMode int mode)624 void setViewMode(@ViewMode int mode) { 625 if (mode == State.MODE_GRID) { 626 Metrics.logUserAction(MetricConsts.USER_ACTION_GRID); 627 } else if (mode == State.MODE_LIST) { 628 Metrics.logUserAction(MetricConsts.USER_ACTION_LIST); 629 } 630 631 LocalPreferences.setViewMode(this, getCurrentRoot(), mode); 632 mState.derivedMode = mode; 633 634 final ActionMenuView subMenuView = findViewById(R.id.sub_menu); 635 mInjector.menuManager.updateSubMenu(subMenuView.getMenu()); 636 637 DirectoryFragment dir = getDirectoryFragment(); 638 if (dir != null) { 639 dir.onViewModeChanged(); 640 } 641 642 mSortController.onViewModeChanged(mode); 643 } 644 setPending(boolean pending)645 public void setPending(boolean pending) { 646 // TODO: Isolate this behavior to PickActivity. 647 } 648 expandAppBar()649 public void expandAppBar() { 650 final AppBarLayout appBarLayout = findViewById(R.id.app_bar); 651 if (appBarLayout != null) { 652 appBarLayout.setExpanded(true); 653 } 654 } 655 updateHeaderTitle()656 public void updateHeaderTitle() { 657 if (!mState.stack.isInitialized()) { 658 //stack has not initialized, the header will update after the stack finishes loading 659 return; 660 } 661 662 final RootInfo root = mState.stack.getRoot(); 663 final String rootTitle = root.title; 664 String result; 665 666 switch (root.derivedType) { 667 case RootInfo.TYPE_RECENTS: 668 result = getHeaderRecentTitle(); 669 break; 670 case RootInfo.TYPE_IMAGES: 671 case RootInfo.TYPE_VIDEO: 672 case RootInfo.TYPE_AUDIO: 673 result = getString(R.string.root_info_header_media, rootTitle); 674 break; 675 case RootInfo.TYPE_DOWNLOADS: 676 case RootInfo.TYPE_LOCAL: 677 case RootInfo.TYPE_MTP: 678 case RootInfo.TYPE_SD: 679 case RootInfo.TYPE_USB: 680 result = getHeaderStorageTitle(rootTitle); 681 break; 682 default: 683 final String summary = root.summary; 684 result = getHeaderDefaultTitle(rootTitle, summary); 685 break; 686 } 687 688 TextView headerTitle = findViewById(R.id.header_title); 689 headerTitle.setText(result); 690 } 691 getHeaderRecentTitle()692 private String getHeaderRecentTitle() { 693 // If stack size larger than 1, it means user global search than enter a folder, but search 694 // is not expanded on that time. 695 boolean isGlobalSearch = mSearchManager.isSearching() || mState.stack.size() > 1; 696 if (mState.isPhotoPicking()) { 697 final int resId = isGlobalSearch 698 ? R.string.root_info_header_image_global_search 699 : R.string.root_info_header_image_recent; 700 return getString(resId); 701 } else { 702 final int resId = isGlobalSearch 703 ? R.string.root_info_header_global_search 704 : R.string.root_info_header_recent; 705 return getString(resId); 706 } 707 } 708 getHeaderStorageTitle(String rootTitle)709 private String getHeaderStorageTitle(String rootTitle) { 710 final int resId = mState.isPhotoPicking() 711 ? R.string.root_info_header_image_storage : R.string.root_info_header_storage; 712 return getString(resId, rootTitle); 713 } 714 getHeaderDefaultTitle(String rootTitle, String summary)715 private String getHeaderDefaultTitle(String rootTitle, String summary) { 716 if (TextUtils.isEmpty(summary)) { 717 final int resId = mState.isPhotoPicking() 718 ? R.string.root_info_header_image_app : R.string.root_info_header_app; 719 return getString(resId, rootTitle); 720 } else { 721 final int resId = mState.isPhotoPicking() 722 ? R.string.root_info_header_image_app_with_summary 723 : R.string.root_info_header_app_with_summary; 724 return getString(resId, rootTitle, summary); 725 } 726 } 727 728 /** 729 * Get title string equal to the string action bar displayed. 730 * @return current directory title name 731 */ getCurrentTitle()732 public String getCurrentTitle() { 733 if (!mState.stack.isInitialized()) { 734 return null; 735 } 736 737 if (mState.stack.size() > 1) { 738 return getCurrentDirectory().displayName; 739 } else { 740 return getCurrentRoot().title; 741 } 742 } 743 744 @Override onSaveInstanceState(Bundle state)745 protected void onSaveInstanceState(Bundle state) { 746 super.onSaveInstanceState(state); 747 state.putParcelable(Shared.EXTRA_STATE, mState); 748 mSearchManager.onSaveInstanceState(state); 749 } 750 751 @Override isSearchExpanded()752 public boolean isSearchExpanded() { 753 return mSearchManager.isExpanded(); 754 } 755 756 @Override getCurrentRoot()757 public RootInfo getCurrentRoot() { 758 RootInfo root = mState.stack.getRoot(); 759 if (root != null) { 760 return root; 761 } else { 762 return mProviders.getRecentsRoot(); 763 } 764 } 765 766 @Override getCurrentDirectory()767 public DocumentInfo getCurrentDirectory() { 768 return mState.stack.peek(); 769 } 770 771 @Override isInRecents()772 public boolean isInRecents() { 773 return mState.stack.isRecents(); 774 } 775 776 @VisibleForTesting addEventListener(EventListener listener)777 public void addEventListener(EventListener listener) { 778 mEventListeners.add(listener); 779 } 780 781 @VisibleForTesting removeEventListener(EventListener listener)782 public void removeEventListener(EventListener listener) { 783 mEventListeners.remove(listener); 784 } 785 786 @VisibleForTesting notifyDirectoryLoaded(Uri uri)787 public void notifyDirectoryLoaded(Uri uri) { 788 for (EventListener listener : mEventListeners) { 789 listener.onDirectoryLoaded(uri); 790 } 791 } 792 793 @VisibleForTesting 794 @Override notifyDirectoryNavigated(Uri uri)795 public void notifyDirectoryNavigated(Uri uri) { 796 for (EventListener listener : mEventListeners) { 797 listener.onDirectoryNavigated(uri); 798 } 799 } 800 801 @Override dispatchKeyEvent(KeyEvent event)802 public boolean dispatchKeyEvent(KeyEvent event) { 803 if (event.getAction() == KeyEvent.ACTION_DOWN) { 804 mInjector.debugHelper.debugCheck(event.getDownTime(), event.getKeyCode()); 805 } 806 807 DocumentsApplication.getDragAndDropManager(this).onKeyEvent(event); 808 809 return super.dispatchKeyEvent(event); 810 } 811 812 @Override onActivityResult(int requestCode, int resultCode, Intent data)813 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 814 super.onActivityResult(requestCode, resultCode, data); 815 mInjector.actions.onActivityResult(requestCode, resultCode, data); 816 } 817 818 /** 819 * Pops the top entry off the directory stack, and returns the user to the previous directory. 820 * If the directory stack only contains one item, this method does nothing. 821 * 822 * @return Whether the stack was popped. 823 */ popDir()824 protected boolean popDir() { 825 if (mState.stack.size() > 1) { 826 final DirectoryFragment fragment = getDirectoryFragment(); 827 if (fragment != null) { 828 fragment.stopScroll(); 829 } 830 831 mState.stack.pop(); 832 refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE); 833 return true; 834 } 835 return false; 836 } 837 focusSidebar()838 protected boolean focusSidebar() { 839 RootsFragment rf = RootsFragment.get(getSupportFragmentManager()); 840 assert (rf != null); 841 return rf.requestFocus(); 842 } 843 844 /** 845 * Closes the activity when it's idle. 846 */ addListenerForLaunchCompletion()847 private void addListenerForLaunchCompletion() { 848 addEventListener(new EventListener() { 849 @Override 850 public void onDirectoryNavigated(Uri uri) { 851 } 852 853 @Override 854 public void onDirectoryLoaded(Uri uri) { 855 removeEventListener(this); 856 getMainLooper().getQueue().addIdleHandler(new IdleHandler() { 857 @Override 858 public boolean queueIdle() { 859 // If startup benchmark is requested by a whitelisted testing package, then 860 // close the activity once idle, and notify the testing activity. 861 if (getIntent().getBooleanExtra(EXTRA_BENCHMARK, false) && 862 BENCHMARK_TESTING_PACKAGE.equals(getCallingPackage())) { 863 setResult(RESULT_OK); 864 finish(); 865 } 866 867 Metrics.logStartupMs((int) (new Date().getTime() - mStartTime)); 868 869 // Remove the idle handler. 870 return false; 871 } 872 }); 873 } 874 }); 875 } 876 877 @VisibleForTesting 878 protected interface EventListener { 879 /** 880 * @param uri Uri navigated to. If recents, then null. 881 */ onDirectoryNavigated(@ullable Uri uri)882 void onDirectoryNavigated(@Nullable Uri uri); 883 884 /** 885 * @param uri Uri of the loaded directory. If recents, then null. 886 */ onDirectoryLoaded(@ullable Uri uri)887 void onDirectoryLoaded(@Nullable Uri uri); 888 } 889 } 890