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.Intent;
24 import android.content.pm.PackageInfo;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ProviderInfo;
27 import android.graphics.Color;
28 import android.net.Uri;
29 import android.os.Build;
30 import android.os.Bundle;
31 import android.os.MessageQueue.IdleHandler;
32 import android.preference.PreferenceManager;
33 import android.provider.DocumentsContract;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.view.KeyEvent;
37 import android.view.Menu;
38 import android.view.MenuItem;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.widget.Checkable;
42 import android.widget.TextView;
43 
44 import androidx.annotation.CallSuper;
45 import androidx.annotation.LayoutRes;
46 import androidx.annotation.VisibleForTesting;
47 import androidx.appcompat.app.AppCompatActivity;
48 import androidx.appcompat.widget.ActionMenuView;
49 import androidx.appcompat.widget.Toolbar;
50 import androidx.fragment.app.Fragment;
51 
52 import com.android.documentsui.AbstractActionHandler.CommonAddons;
53 import com.android.documentsui.Injector.Injected;
54 import com.android.documentsui.NavigationViewManager.Breadcrumb;
55 import com.android.documentsui.R;
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.dirlist.AnimationView;
63 import com.android.documentsui.dirlist.AppsRowManager;
64 import com.android.documentsui.dirlist.DirectoryFragment;
65 import com.android.documentsui.prefs.LocalPreferences;
66 import com.android.documentsui.prefs.Preferences;
67 import com.android.documentsui.prefs.PreferencesMonitor;
68 import com.android.documentsui.prefs.ScopedPreferences;
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 
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 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 icicle)133     public void onCreate(Bundle icicle) {
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(icicle);
143 
144         final Intent intent = getIntent();
145 
146         addListenerForLaunchCompletion();
147 
148         setContentView(mLayoutId);
149 
150         setContainer();
151 
152         mInjector = getInjector();
153         mState = getState(icicle);
154         mDrawer = DrawerController.create(this, mInjector.config);
155         Metrics.logActivityLaunch(mState, intent);
156 
157         mProviders = DocumentsApplication.getProvidersCache(this);
158         mDocs = DocumentsAccess.create(this);
159 
160         Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
161         setSupportActionBar(toolbar);
162 
163         Breadcrumb breadcrumb =
164                 Shared.findView(this, R.id.dropdown_breadcrumb, R.id.horizontal_breadcrumb);
165         assert(breadcrumb != null);
166 
167         mNavigator = new NavigationViewManager(this, mDrawer, mState, this, breadcrumb);
168         SearchManagerListener searchListener = new SearchManagerListener() {
169             /**
170              * Called when search results changed. Refreshes the content of the directory. It
171              * doesn't refresh elements on the action bar. e.g. The current directory name displayed
172              * on the action bar won't get updated.
173              */
174             @Override
175             public void onSearchChanged(@Nullable String query) {
176                 if (mSearchManager.isSearching()) {
177                     Metrics.logSearchMode(query != null, mSearchManager.hasCheckedChip());
178                     if (mInjector.pickResult != null) {
179                         mInjector.pickResult.increaseActionCount();
180                     }
181                 }
182 
183                 mInjector.actions.loadDocumentsForCurrentStack();
184 
185                 expandAppBar();
186                 DirectoryFragment dir = getDirectoryFragment();
187                 if (dir != null) {
188                     dir.scrollToTop();
189                 }
190             }
191 
192             @Override
193             public void onSearchFinished() {
194                 // Restores menu icons state
195                 invalidateOptionsMenu();
196             }
197 
198             @Override
199             public void onSearchViewChanged(boolean opened) {
200                 mNavigator.update();
201             }
202 
203             @Override
204             public void onSearchChipStateChanged(View v) {
205                 final Checkable chip = (Checkable) v;
206                 if (chip.isChecked()) {
207                     final SearchChipData item = (SearchChipData) v.getTag();
208                     Metrics.logUserAction(MetricConsts.USER_ACTION_SEARCH_CHIP);
209                     Metrics.logSearchType(item.getChipType());
210                 }
211             }
212 
213             @Override
214             public void onSearchViewFocusChanged(boolean hasFocus) {
215                 final boolean isInitailSearch
216                         = !TextUtils.isEmpty(mSearchManager.getCurrentSearch())
217                         && TextUtils.isEmpty(mSearchManager.getSearchViewText());
218                 if (hasFocus && (SearchFragment.get(getSupportFragmentManager()) == null)
219                         && !isInitailSearch) {
220                     SearchFragment.showFragment(getSupportFragmentManager(),
221                             mSearchManager.getSearchViewText());
222                 }
223             }
224 
225             @Override
226             public void onSearchViewClearClicked() {
227                 if (SearchFragment.get(getSupportFragmentManager()) == null) {
228                     SearchFragment.showFragment(getSupportFragmentManager(),
229                             mSearchManager.getSearchViewText());
230                 }
231             }
232         };
233 
234         // "Commands" are meta input for controlling system behavior.
235         // We piggy back on search input as it is the only text input
236         // area in the app. But the functionality is independent
237         // of "regular" search query processing.
238         final CommandInterceptor cmdInterceptor = new CommandInterceptor(mInjector.features);
239         cmdInterceptor.add(new CommandInterceptor.DumpRootsCacheHandler(this));
240 
241         // A tiny decorator that adds support for enabling CommandInterceptor
242         // based on query input. It's sorta like CommandInterceptor, but its metaaahhh.
243         EventHandler<String> queryInterceptor =
244                 CommandInterceptor.createDebugModeFlipper(
245                         mInjector.features,
246                         mInjector.debugHelper::toggleDebugMode,
247                         cmdInterceptor);
248 
249         ViewGroup chipGroup = findViewById(R.id.search_chip_group);
250         mSearchManager = new SearchViewManager(searchListener, queryInterceptor,
251                 chipGroup, icicle);
252         // initialize the chip sets by accept mime types
253         mSearchManager.initChipSets(mState.acceptMimes);
254         // update the chip items by the mime types of the root
255         mSearchManager.updateChips(getCurrentRoot().derivedMimeTypes);
256         // parse the query content from intent when launch the
257         // activity at the first time
258         if (icicle == null) {
259             mHasQueryContentFromIntent = mSearchManager.parseQueryContentFromIntent(getIntent(),
260                     mState.action);
261         }
262 
263         mNavigator.setSearchBarClickListener(v -> {
264             mSearchManager.onSearchBarClicked();
265             mNavigator.update();
266         });
267 
268         mSortController = SortController.create(this, mState.derivedMode, mState.sortModel);
269 
270         mPreferencesMonitor = new PreferencesMonitor(
271                 getApplicationContext().getPackageName(),
272                 PreferenceManager.getDefaultSharedPreferences(this),
273                 this::onPreferenceChanged);
274         mPreferencesMonitor.start();
275 
276         // Base classes must update result in their onCreate.
277         setResult(AppCompatActivity.RESULT_CANCELED);
278     }
279 
onPreferenceChanged(String pref)280     public void onPreferenceChanged(String pref) {
281         // For now, we only work with prefs that we backup. This
282         // just limits the scope of what we expect to come flowing
283         // through here until we know we want more and fancier options.
284         assert(Preferences.shouldBackup(pref));
285 
286         switch (pref) {
287             case ScopedPreferences.INCLUDE_DEVICE_ROOT:
288                 updateDisplayAdvancedDevices(mInjector.prefs.getShowDeviceRoot());
289         }
290     }
291 
292     @Override
onPostCreate(Bundle savedInstanceState)293     protected void onPostCreate(Bundle savedInstanceState) {
294         super.onPostCreate(savedInstanceState);
295 
296         mRootsMonitor = new RootsMonitor<>(
297                 this,
298                 mInjector.actions,
299                 mProviders,
300                 mDocs,
301                 mState,
302                 mSearchManager,
303                 mInjector.actionModeController::finishActionMode);
304         mRootsMonitor.start();
305     }
306 
307     @Override
onCreateOptionsMenu(Menu menu)308     public boolean onCreateOptionsMenu(Menu menu) {
309         boolean showMenu = super.onCreateOptionsMenu(menu);
310 
311         getMenuInflater().inflate(R.menu.activity, menu);
312         mNavigator.update();
313         boolean fullBarSearch = getResources().getBoolean(R.bool.full_bar_search_view);
314         boolean showSearchBar = getResources().getBoolean(R.bool.show_search_bar);
315         mSearchManager.install(menu, fullBarSearch, showSearchBar);
316 
317         final ActionMenuView subMenuView = findViewById(R.id.sub_menu);
318         // If size is 0, it means the menu has not inflated and it should only do once.
319         if (subMenuView.getMenu().size() == 0) {
320             subMenuView.setOnMenuItemClickListener(this::onOptionsItemSelected);
321             getMenuInflater().inflate(R.menu.sub_menu, subMenuView.getMenu());
322         }
323 
324         return showMenu;
325     }
326 
327     @Override
328     @CallSuper
onPrepareOptionsMenu(Menu menu)329     public boolean onPrepareOptionsMenu(Menu menu) {
330         super.onPrepareOptionsMenu(menu);
331         mSearchManager.showMenu(mState.stack);
332         final ActionMenuView subMenuView = findViewById(R.id.sub_menu);
333         mInjector.menuManager.updateSubMenu(subMenuView.getMenu());
334         return true;
335     }
336 
337     @Override
onDestroy()338     protected void onDestroy() {
339         mRootsMonitor.stop();
340         mPreferencesMonitor.stop();
341         mSortController.destroy();
342         super.onDestroy();
343     }
344 
getState(@ullable Bundle icicle)345     private State getState(@Nullable Bundle icicle) {
346         if (icicle != null) {
347             State state = icicle.<State>getParcelable(Shared.EXTRA_STATE);
348             if (DEBUG) {
349                 Log.d(mTag, "Recovered existing state object: " + state);
350             }
351             return state;
352         }
353 
354         State state = new State();
355 
356         final Intent intent = getIntent();
357 
358         state.sortModel = SortModel.createModel();
359         state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
360         state.excludedAuthorities = getExcludedAuthorities();
361 
362         includeState(state);
363 
364         state.showAdvanced = Shared.mustShowDeviceRoot(intent)
365                 || mInjector.prefs.getShowDeviceRoot();
366 
367         // Only show the toggle if advanced isn't forced enabled.
368         state.showDeviceStorageOption = !Shared.mustShowDeviceRoot(intent);
369 
370         if (DEBUG) {
371             Log.d(mTag, "Created new state object: " + state);
372         }
373 
374         return state;
375     }
376 
setContainer()377     private void setContainer() {
378         View root = findViewById(R.id.coordinator_layout);
379         root.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
380                 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
381         root.setOnApplyWindowInsetsListener((v, insets) -> {
382             root.setPadding(insets.getSystemWindowInsetLeft(),
383                     insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
384 
385             View saveContainer = findViewById(R.id.container_save);
386             saveContainer.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
387 
388             View rootsContainer = findViewById(R.id.container_roots);
389             rootsContainer.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
390 
391             return insets.consumeSystemWindowInsets();
392         });
393 
394         getWindow().setNavigationBarDividerColor(Color.TRANSPARENT);
395         if (Build.VERSION.SDK_INT >= 29) {
396             getWindow().setNavigationBarColor(Color.TRANSPARENT);
397             getWindow().setNavigationBarContrastEnforced(true);
398         } else {
399             getWindow().setNavigationBarColor(getColor(R.color.nav_bar_translucent));
400         }
401     }
402 
403     @Override
setRootsDrawerOpen(boolean open)404     public void setRootsDrawerOpen(boolean open) {
405         mNavigator.revealRootsDrawer(open);
406     }
407 
408     @Override
onRootPicked(RootInfo root)409     public void onRootPicked(RootInfo root) {
410         // Clicking on the current root removes search
411         mSearchManager.cancelSearch();
412 
413         // Skip refreshing if root nor directory didn't change
414         if (root.equals(getCurrentRoot()) && mState.stack.size() == 1) {
415             return;
416         }
417 
418         mInjector.actionModeController.finishActionMode();
419         mSortController.onViewModeChanged(mState.derivedMode);
420 
421         // Set summary header's visibility. Only recents and downloads root may have summary in
422         // their docs.
423         mState.sortModel.setDimensionVisibility(
424                 SortModel.SORT_DIMENSION_ID_SUMMARY,
425                 root.isRecents() || root.isDownloads() ? View.VISIBLE : View.INVISIBLE);
426 
427         // Clear entire backstack and start in new root
428         mState.stack.changeRoot(root);
429 
430         // Recents is always in memory, so we just load it directly.
431         // Otherwise we delegate loading data from disk to a task
432         // to ensure a responsive ui.
433         if (mProviders.isRecentsRoot(root)) {
434             refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
435         } else {
436             mInjector.actions.getRootDocument(
437                     root,
438                     TimeoutTask.DEFAULT_TIMEOUT,
439                     doc -> mInjector.actions.openRootDocument(doc));
440         }
441 
442         expandAppBar();
443         updateHeaderTitle();
444     }
445 
446     @Override
onOptionsItemSelected(MenuItem item)447     public boolean onOptionsItemSelected(MenuItem item) {
448 
449         switch (item.getItemId()) {
450             case android.R.id.home:
451                 onBackPressed();
452                 return true;
453 
454             case R.id.option_menu_create_dir:
455                 getInjector().actions.showCreateDirectoryDialog();
456                 return true;
457 
458             case R.id.option_menu_search:
459                 // SearchViewManager listens for this directly.
460                 return false;
461 
462             case R.id.option_menu_advanced:
463                 onDisplayAdvancedDevices();
464                 return true;
465 
466             case R.id.option_menu_select_all:
467                 getInjector().actions.selectAllFiles();
468                 return true;
469 
470             case R.id.option_menu_debug:
471                 getInjector().actions.showDebugMessage();
472                 return true;
473 
474             case R.id.option_menu_sort:
475                 getInjector().actions.showSortDialog();
476                 return true;
477 
478             case R.id.sub_menu_grid:
479                 setViewMode(State.MODE_GRID);
480                 return true;
481 
482             case R.id.sub_menu_list:
483                 setViewMode(State.MODE_LIST);
484                 return true;
485 
486             default:
487                 return super.onOptionsItemSelected(item);
488         }
489     }
490 
getDirectoryFragment()491     protected final @Nullable DirectoryFragment getDirectoryFragment() {
492         return DirectoryFragment.get(getSupportFragmentManager());
493     }
494 
495     /**
496      * Returns true if a directory can be created in the current location.
497      * @return
498      */
canCreateDirectory()499     protected boolean canCreateDirectory() {
500         final RootInfo root = getCurrentRoot();
501         final DocumentInfo cwd = getCurrentDirectory();
502         return cwd != null
503                 && cwd.isCreateSupported()
504                 && !mSearchManager.isSearching()
505                 && !root.isRecents();
506     }
507 
508     /**
509      * Returns true if a directory can be inspected.
510      */
canInspectDirectory()511     protected boolean canInspectDirectory() {
512         return false;
513     }
514 
515     // TODO: make navigator listen to state
516     @Override
updateNavigator()517     public final void updateNavigator() {
518         mNavigator.update();
519     }
520 
521     @Override
restoreRootAndDirectory()522     public void restoreRootAndDirectory() {
523         // We're trying to restore stuff in document stack from saved instance. If we didn't have a
524         // chance to spawn a fragment before we need to do it now. However if we spawned a fragment
525         // already, system will automatically restore the fragment for us so we don't need to do
526         // that manually this time.
527         if (DirectoryFragment.get(getSupportFragmentManager()) == null) {
528             refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
529         }
530     }
531 
532     /**
533      * Refreshes the content of the director and the menu/action bar.
534      * The current directory name and selection will get updated.
535      * @param anim
536      */
537     @Override
refreshCurrentRootAndDirectory(int anim)538     public final void refreshCurrentRootAndDirectory(int anim) {
539         // The following call will crash if it's called before onCreateOptionMenu() is called in
540         // which we install menu item to search view manager, and there is a search query we need to
541         // restore. This happens when we're still initializing our UI so we shouldn't cancel the
542         // search which will be restored later in onCreateOptionMenu(). Try finding a way to guard
543         // refreshCurrentRootAndDirectory() from being called while we're restoring the state of UI
544         // from the saved state passed in onCreate().
545         mSearchManager.cancelSearch();
546 
547         // only set the query content in the first launch
548         if (mHasQueryContentFromIntent) {
549             mHasQueryContentFromIntent = false;
550             mSearchManager.setCurrentSearch(mSearchManager.getQueryContentFromIntent());
551         }
552 
553         mState.derivedMode = LocalPreferences.getViewMode(this, mState.stack.getRoot(), MODE_GRID);
554 
555         mNavigator.update();
556 
557         refreshDirectory(anim);
558 
559         final RootsFragment roots = RootsFragment.get(getSupportFragmentManager());
560         if (roots != null) {
561             roots.onCurrentRootChanged();
562         }
563 
564         // Causes talkback to announce the activity's new title
565         setTitle(mState.stack.getTitle());
566 
567         invalidateOptionsMenu();
568         mSortController.onViewModeChanged(mState.derivedMode);
569         mSearchManager.updateChips(getCurrentRoot().derivedMimeTypes);
570         mAppsRowManager.updateView(this);
571     }
572 
getExcludedAuthorities()573     private final List<String> getExcludedAuthorities() {
574         List<String> authorities = new ArrayList<>();
575         if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) {
576             // Exclude roots provided by the calling package.
577             String packageName = Shared.getCallingPackageName(this);
578             try {
579                 PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName,
580                         PackageManager.GET_PROVIDERS);
581                 for (ProviderInfo provider: pkgInfo.providers) {
582                     authorities.add(provider.authority);
583                 }
584             } catch (PackageManager.NameNotFoundException e) {
585                 Log.e(mTag, "Calling package name does not resolve: " + packageName);
586             }
587         }
588         return authorities;
589     }
590 
get(Fragment fragment)591     public static BaseActivity get(Fragment fragment) {
592         return (BaseActivity) fragment.getActivity();
593     }
594 
getDisplayState()595     public State getDisplayState() {
596         return mState;
597     }
598 
599     /**
600      * Set internal storage visible based on explicit user action.
601      */
onDisplayAdvancedDevices()602     private void onDisplayAdvancedDevices() {
603         boolean display = !mState.showAdvanced;
604         Metrics.logUserAction(display
605                 ? MetricConsts.USER_ACTION_SHOW_ADVANCED : MetricConsts.USER_ACTION_HIDE_ADVANCED);
606 
607         mInjector.prefs.setShowDeviceRoot(display);
608         updateDisplayAdvancedDevices(display);
609     }
610 
updateDisplayAdvancedDevices(boolean display)611     private void updateDisplayAdvancedDevices(boolean display) {
612         mState.showAdvanced = display;
613         @Nullable RootsFragment fragment = RootsFragment.get(getSupportFragmentManager());
614         if (fragment != null) {
615             // This also takes care of updating launcher shortcuts (which are roots :)
616             fragment.onDisplayStateChanged();
617         }
618         invalidateOptionsMenu();
619     }
620 
621     /**
622      * Set mode based on explicit user action.
623      */
setViewMode(@iewMode int mode)624     void setViewMode(@ViewMode int mode) {
625         if (mode == State.MODE_GRID) {
626             Metrics.logUserAction(MetricConsts.USER_ACTION_GRID);
627         } else if (mode == State.MODE_LIST) {
628             Metrics.logUserAction(MetricConsts.USER_ACTION_LIST);
629         }
630 
631         LocalPreferences.setViewMode(this, getCurrentRoot(), mode);
632         mState.derivedMode = mode;
633 
634         final ActionMenuView subMenuView = findViewById(R.id.sub_menu);
635         mInjector.menuManager.updateSubMenu(subMenuView.getMenu());
636 
637         DirectoryFragment dir = getDirectoryFragment();
638         if (dir != null) {
639             dir.onViewModeChanged();
640         }
641 
642         mSortController.onViewModeChanged(mode);
643     }
644 
setPending(boolean pending)645     public void setPending(boolean pending) {
646         // TODO: Isolate this behavior to PickActivity.
647     }
648 
expandAppBar()649     public void expandAppBar() {
650         final AppBarLayout appBarLayout = findViewById(R.id.app_bar);
651         if (appBarLayout != null) {
652             appBarLayout.setExpanded(true);
653         }
654     }
655 
updateHeaderTitle()656     public void updateHeaderTitle() {
657         if (!mState.stack.isInitialized()) {
658             //stack has not initialized, the header will update after the stack finishes loading
659             return;
660         }
661 
662         final RootInfo root = mState.stack.getRoot();
663         final String rootTitle = root.title;
664         String result;
665 
666         switch (root.derivedType) {
667             case RootInfo.TYPE_RECENTS:
668                 result = getHeaderRecentTitle();
669                 break;
670             case RootInfo.TYPE_IMAGES:
671             case RootInfo.TYPE_VIDEO:
672             case RootInfo.TYPE_AUDIO:
673                 result = getString(R.string.root_info_header_media, rootTitle);
674                 break;
675             case RootInfo.TYPE_DOWNLOADS:
676             case RootInfo.TYPE_LOCAL:
677             case RootInfo.TYPE_MTP:
678             case RootInfo.TYPE_SD:
679             case RootInfo.TYPE_USB:
680                 result = getHeaderStorageTitle(rootTitle);
681                 break;
682             default:
683                 final String summary = root.summary;
684                 result = getHeaderDefaultTitle(rootTitle, summary);
685                 break;
686         }
687 
688         TextView headerTitle = findViewById(R.id.header_title);
689         headerTitle.setText(result);
690     }
691 
getHeaderRecentTitle()692     private String getHeaderRecentTitle() {
693         // If stack size larger than 1, it means user global search than enter a folder, but search
694         // is not expanded on that time.
695         boolean isGlobalSearch = mSearchManager.isSearching() || mState.stack.size() > 1;
696         if (mState.isPhotoPicking()) {
697             final int resId = isGlobalSearch
698                     ? R.string.root_info_header_image_global_search
699                     : R.string.root_info_header_image_recent;
700             return getString(resId);
701         } else {
702             final int resId = isGlobalSearch
703                     ? R.string.root_info_header_global_search
704                     : R.string.root_info_header_recent;
705             return getString(resId);
706         }
707     }
708 
getHeaderStorageTitle(String rootTitle)709     private String getHeaderStorageTitle(String rootTitle) {
710         final int resId = mState.isPhotoPicking()
711                 ? R.string.root_info_header_image_storage : R.string.root_info_header_storage;
712         return getString(resId, rootTitle);
713     }
714 
getHeaderDefaultTitle(String rootTitle, String summary)715     private String getHeaderDefaultTitle(String rootTitle, String summary) {
716         if (TextUtils.isEmpty(summary)) {
717             final int resId = mState.isPhotoPicking()
718                     ? R.string.root_info_header_image_app : R.string.root_info_header_app;
719             return getString(resId, rootTitle);
720         } else {
721             final int resId = mState.isPhotoPicking()
722                     ? R.string.root_info_header_image_app_with_summary
723                     : R.string.root_info_header_app_with_summary;
724             return getString(resId, rootTitle, summary);
725         }
726     }
727 
728     /**
729      * Get title string equal to the string action bar displayed.
730      * @return current directory title name
731      */
getCurrentTitle()732     public String getCurrentTitle() {
733         if (!mState.stack.isInitialized()) {
734             return null;
735         }
736 
737         if (mState.stack.size() > 1) {
738             return getCurrentDirectory().displayName;
739         } else {
740             return getCurrentRoot().title;
741         }
742     }
743 
744     @Override
onSaveInstanceState(Bundle state)745     protected void onSaveInstanceState(Bundle state) {
746         super.onSaveInstanceState(state);
747         state.putParcelable(Shared.EXTRA_STATE, mState);
748         mSearchManager.onSaveInstanceState(state);
749     }
750 
751     @Override
isSearchExpanded()752     public boolean isSearchExpanded() {
753         return mSearchManager.isExpanded();
754     }
755 
756     @Override
getCurrentRoot()757     public RootInfo getCurrentRoot() {
758         RootInfo root = mState.stack.getRoot();
759         if (root != null) {
760             return root;
761         } else {
762             return mProviders.getRecentsRoot();
763         }
764     }
765 
766     @Override
getCurrentDirectory()767     public DocumentInfo getCurrentDirectory() {
768         return mState.stack.peek();
769     }
770 
771     @Override
isInRecents()772     public boolean isInRecents() {
773         return mState.stack.isRecents();
774     }
775 
776     @VisibleForTesting
addEventListener(EventListener listener)777     public void addEventListener(EventListener listener) {
778         mEventListeners.add(listener);
779     }
780 
781     @VisibleForTesting
removeEventListener(EventListener listener)782     public void removeEventListener(EventListener listener) {
783         mEventListeners.remove(listener);
784     }
785 
786     @VisibleForTesting
notifyDirectoryLoaded(Uri uri)787     public void notifyDirectoryLoaded(Uri uri) {
788         for (EventListener listener : mEventListeners) {
789             listener.onDirectoryLoaded(uri);
790         }
791     }
792 
793     @VisibleForTesting
794     @Override
notifyDirectoryNavigated(Uri uri)795     public void notifyDirectoryNavigated(Uri uri) {
796         for (EventListener listener : mEventListeners) {
797             listener.onDirectoryNavigated(uri);
798         }
799     }
800 
801     @Override
dispatchKeyEvent(KeyEvent event)802     public boolean dispatchKeyEvent(KeyEvent event) {
803         if (event.getAction() == KeyEvent.ACTION_DOWN) {
804             mInjector.debugHelper.debugCheck(event.getDownTime(), event.getKeyCode());
805         }
806 
807         DocumentsApplication.getDragAndDropManager(this).onKeyEvent(event);
808 
809         return super.dispatchKeyEvent(event);
810     }
811 
812     @Override
onActivityResult(int requestCode, int resultCode, Intent data)813     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
814         super.onActivityResult(requestCode, resultCode, data);
815         mInjector.actions.onActivityResult(requestCode, resultCode, data);
816     }
817 
818     /**
819      * Pops the top entry off the directory stack, and returns the user to the previous directory.
820      * If the directory stack only contains one item, this method does nothing.
821      *
822      * @return Whether the stack was popped.
823      */
popDir()824     protected boolean popDir() {
825         if (mState.stack.size() > 1) {
826             final DirectoryFragment fragment = getDirectoryFragment();
827             if (fragment != null) {
828                 fragment.stopScroll();
829             }
830 
831             mState.stack.pop();
832             refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE);
833             return true;
834         }
835         return false;
836     }
837 
focusSidebar()838     protected boolean focusSidebar() {
839         RootsFragment rf = RootsFragment.get(getSupportFragmentManager());
840         assert (rf != null);
841         return rf.requestFocus();
842     }
843 
844     /**
845      * Closes the activity when it's idle.
846      */
addListenerForLaunchCompletion()847     private void addListenerForLaunchCompletion() {
848         addEventListener(new EventListener() {
849             @Override
850             public void onDirectoryNavigated(Uri uri) {
851             }
852 
853             @Override
854             public void onDirectoryLoaded(Uri uri) {
855                 removeEventListener(this);
856                 getMainLooper().getQueue().addIdleHandler(new IdleHandler() {
857                     @Override
858                     public boolean queueIdle() {
859                         // If startup benchmark is requested by a whitelisted testing package, then
860                         // close the activity once idle, and notify the testing activity.
861                         if (getIntent().getBooleanExtra(EXTRA_BENCHMARK, false) &&
862                                 BENCHMARK_TESTING_PACKAGE.equals(getCallingPackage())) {
863                             setResult(RESULT_OK);
864                             finish();
865                         }
866 
867                         Metrics.logStartupMs((int) (new Date().getTime() - mStartTime));
868 
869                         // Remove the idle handler.
870                         return false;
871                     }
872                 });
873             }
874         });
875     }
876 
877     @VisibleForTesting
878     protected interface EventListener {
879         /**
880          * @param uri Uri navigated to. If recents, then null.
881          */
onDirectoryNavigated(@ullable Uri uri)882         void onDirectoryNavigated(@Nullable Uri uri);
883 
884         /**
885          * @param uri Uri of the loaded directory. If recents, then null.
886          */
onDirectoryLoaded(@ullable Uri uri)887         void onDirectoryLoaded(@Nullable Uri uri);
888     }
889 }
890