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