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