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