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