• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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