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