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