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