• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.documentsui;
18 
19 import static com.android.documentsui.Shared.DEBUG;
20 import static com.android.documentsui.Shared.EXTRA_BENCHMARK;
21 import static com.android.documentsui.State.ACTION_CREATE;
22 import static com.android.documentsui.State.ACTION_GET_CONTENT;
23 import static com.android.documentsui.State.ACTION_OPEN;
24 import static com.android.documentsui.State.ACTION_OPEN_TREE;
25 import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION;
26 import static com.android.documentsui.State.MODE_GRID;
27 
28 import android.app.Activity;
29 import android.app.Fragment;
30 import android.app.FragmentManager;
31 import android.content.Intent;
32 import android.content.pm.ApplicationInfo;
33 import android.content.pm.PackageInfo;
34 import android.content.pm.PackageManager;
35 import android.content.pm.ProviderInfo;
36 import android.database.ContentObserver;
37 import android.net.Uri;
38 import android.os.AsyncTask;
39 import android.os.Bundle;
40 import android.os.Handler;
41 import android.os.MessageQueue.IdleHandler;
42 import android.provider.DocumentsContract;
43 import android.provider.DocumentsContract.Root;
44 import android.support.annotation.CallSuper;
45 import android.support.annotation.LayoutRes;
46 import android.support.annotation.Nullable;
47 import android.util.Log;
48 import android.view.KeyEvent;
49 import android.view.Menu;
50 import android.view.MenuItem;
51 import android.widget.Spinner;
52 
53 import com.android.documentsui.SearchViewManager.SearchManagerListener;
54 import com.android.documentsui.State.ViewMode;
55 import com.android.documentsui.dirlist.AnimationView;
56 import com.android.documentsui.dirlist.DirectoryFragment;
57 import com.android.documentsui.dirlist.Model;
58 import com.android.documentsui.model.DocumentInfo;
59 import com.android.documentsui.model.DocumentStack;
60 import com.android.documentsui.model.RootInfo;
61 
62 import java.io.FileNotFoundException;
63 import java.util.ArrayList;
64 import java.util.Collection;
65 import java.util.Date;
66 import java.util.List;
67 import java.util.concurrent.Executor;
68 
69 public abstract class BaseActivity extends Activity
70         implements SearchManagerListener, NavigationView.Environment {
71 
72     private static final String BENCHMARK_TESTING_PACKAGE = "com.android.documentsui.appperftests";
73 
74     State mState;
75     RootsCache mRoots;
76     SearchViewManager mSearchManager;
77     DrawerController mDrawer;
78     NavigationView mNavigator;
79     List<EventListener> mEventListeners = new ArrayList<>();
80 
81     private final String mTag;
82     private final ContentObserver mRootsCacheObserver = new ContentObserver(new Handler()) {
83         @Override
84         public void onChange(boolean selfChange) {
85             new HandleRootsChangedTask(BaseActivity.this).execute(getCurrentRoot());
86         }
87     };
88 
89     @LayoutRes
90     private int mLayoutId;
91 
92     private boolean mNavDrawerHasFocus;
93     private long mStartTime;
94 
onDocumentPicked(DocumentInfo doc, Model model)95     public abstract void onDocumentPicked(DocumentInfo doc, Model model);
onDocumentsPicked(List<DocumentInfo> docs)96     public abstract void onDocumentsPicked(List<DocumentInfo> docs);
97 
onTaskFinished(Uri... uris)98     abstract void onTaskFinished(Uri... uris);
refreshDirectory(int anim)99     abstract void refreshDirectory(int anim);
100     /** Allows sub-classes to include information in a newly created State instance. */
includeState(State initialState)101     abstract void includeState(State initialState);
102 
BaseActivity(@ayoutRes int layoutId, String tag)103     public BaseActivity(@LayoutRes int layoutId, String tag) {
104         mLayoutId = layoutId;
105         mTag = tag;
106     }
107 
108     @CallSuper
109     @Override
onCreate(Bundle icicle)110     public void onCreate(Bundle icicle) {
111         // Record the time when onCreate is invoked for metric.
112         mStartTime = new Date().getTime();
113 
114         super.onCreate(icicle);
115 
116         final Intent intent = getIntent();
117 
118         addListenerForLaunchCompletion();
119 
120         setContentView(mLayoutId);
121 
122         mDrawer = DrawerController.create(this);
123         mState = getState(icicle);
124         Metrics.logActivityLaunch(this, mState, intent);
125 
126         mRoots = DocumentsApplication.getRootsCache(this);
127 
128         getContentResolver().registerContentObserver(
129                 RootsCache.sNotificationUri, false, mRootsCacheObserver);
130 
131         mSearchManager = new SearchViewManager(this, icicle);
132 
133         DocumentsToolbar toolbar = (DocumentsToolbar) findViewById(R.id.toolbar);
134         setActionBar(toolbar);
135         mNavigator = new NavigationView(
136                 mDrawer,
137                 toolbar,
138                 (Spinner) findViewById(R.id.stack),
139                 mState,
140                 this);
141 
142         // Base classes must update result in their onCreate.
143         setResult(Activity.RESULT_CANCELED);
144     }
145 
146     @Override
onCreateOptionsMenu(Menu menu)147     public boolean onCreateOptionsMenu(Menu menu) {
148         boolean showMenu = super.onCreateOptionsMenu(menu);
149 
150         getMenuInflater().inflate(R.menu.activity, menu);
151         mNavigator.update();
152         boolean fullBarSearch = getResources().getBoolean(R.bool.full_bar_search_view);
153         mSearchManager.install((DocumentsToolbar) findViewById(R.id.toolbar), fullBarSearch);
154 
155         return showMenu;
156     }
157 
158     @Override
159     @CallSuper
onPrepareOptionsMenu(Menu menu)160     public boolean onPrepareOptionsMenu(Menu menu) {
161         super.onPrepareOptionsMenu(menu);
162 
163         mSearchManager.showMenu(canSearchRoot());
164 
165         final boolean inRecents = getCurrentDirectory() == null;
166 
167         final MenuItem sort = menu.findItem(R.id.menu_sort);
168         final MenuItem sortSize = menu.findItem(R.id.menu_sort_size);
169         final MenuItem grid = menu.findItem(R.id.menu_grid);
170         final MenuItem list = menu.findItem(R.id.menu_list);
171         final MenuItem advanced = menu.findItem(R.id.menu_advanced);
172         final MenuItem fileSize = menu.findItem(R.id.menu_file_size);
173 
174         // Search uses backend ranking; no sorting, recents doesn't support sort.
175         sort.setEnabled(!inRecents && !mSearchManager.isSearching());
176         sortSize.setVisible(mState.showSize); // Only sort by size when file sizes are visible
177         fileSize.setVisible(!mState.forceSize);
178 
179         // grid/list is effectively a toggle.
180         grid.setVisible(mState.derivedMode != State.MODE_GRID);
181         list.setVisible(mState.derivedMode != State.MODE_LIST);
182 
183         advanced.setVisible(mState.showAdvancedOption);
184         advanced.setTitle(mState.showAdvancedOption && mState.showAdvanced
185                 ? R.string.menu_advanced_hide : R.string.menu_advanced_show);
186         fileSize.setTitle(LocalPreferences.getDisplayFileSize(this)
187                 ? R.string.menu_file_size_hide : R.string.menu_file_size_show);
188 
189         return true;
190     }
191 
192     @Override
onDestroy()193     protected void onDestroy() {
194         getContentResolver().unregisterContentObserver(mRootsCacheObserver);
195         super.onDestroy();
196     }
197 
getState(@ullable Bundle icicle)198     private State getState(@Nullable Bundle icicle) {
199         if (icicle != null) {
200             State state = icicle.<State>getParcelable(Shared.EXTRA_STATE);
201             if (DEBUG) Log.d(mTag, "Recovered existing state object: " + state);
202             return state;
203         }
204 
205         State state = new State();
206 
207         final Intent intent = getIntent();
208 
209         state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
210         state.forceSize = intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_FILESIZE, false);
211         state.showSize = state.forceSize || LocalPreferences.getDisplayFileSize(this);
212         state.initAcceptMimes(intent);
213         state.excludedAuthorities = getExcludedAuthorities();
214 
215         includeState(state);
216 
217         // Advanced roots are shown by default without menu option if forced by config or intent.
218         boolean forceAdvanced = Shared.shouldShowDeviceRoot(this, intent);
219         boolean chosenAdvanced = LocalPreferences.getShowDeviceRoot(this, state.action);
220         state.showAdvanced = forceAdvanced || chosenAdvanced;
221 
222         // Menu option is shown for whitelisted intents if advanced roots are not shown by default.
223         state.showAdvancedOption = !forceAdvanced && (
224                 Shared.shouldShowFancyFeatures(this)
225                 || state.action == ACTION_OPEN
226                 || state.action == ACTION_CREATE
227                 || state.action == ACTION_OPEN_TREE
228                 || state.action == ACTION_PICK_COPY_DESTINATION
229                 || state.action == ACTION_GET_CONTENT);
230 
231         if (DEBUG) Log.d(mTag, "Created new state object: " + state);
232 
233         return state;
234     }
235 
setRootsDrawerOpen(boolean open)236     public void setRootsDrawerOpen(boolean open) {
237         mNavigator.revealRootsDrawer(open);
238     }
239 
onRootPicked(RootInfo root)240     void onRootPicked(RootInfo root) {
241         // Clicking on the current root removes search
242         mSearchManager.cancelSearch();
243 
244         // Skip refreshing if root nor directory didn't change
245         if (root.equals(getCurrentRoot()) && mState.stack.size() == 1) {
246             return;
247         }
248 
249         mState.derivedMode = LocalPreferences.getViewMode(this, root, MODE_GRID);
250 
251         // Clear entire backstack and start in new root
252         mState.onRootChanged(root);
253 
254         // Recents is always in memory, so we just load it directly.
255         // Otherwise we delegate loading data from disk to a task
256         // to ensure a responsive ui.
257         if (mRoots.isRecentsRoot(root)) {
258             refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
259         } else {
260             new PickRootTask(this, root).executeOnExecutor(getExecutorForCurrentDirectory());
261         }
262     }
263 
264     @Override
onOptionsItemSelected(MenuItem item)265     public boolean onOptionsItemSelected(MenuItem item) {
266 
267         switch (item.getItemId()) {
268             case android.R.id.home:
269                 onBackPressed();
270                 return true;
271 
272             case R.id.menu_create_dir:
273                 showCreateDirectoryDialog();
274                 return true;
275 
276             case R.id.menu_search:
277                 // SearchViewManager listens for this directly.
278                 return false;
279 
280             case R.id.menu_sort_name:
281                 setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME);
282                 return true;
283 
284             case R.id.menu_sort_date:
285                 setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED);
286                 return true;
287 
288             case R.id.menu_sort_size:
289                 setUserSortOrder(State.SORT_ORDER_SIZE);
290                 return true;
291 
292             case R.id.menu_grid:
293                 setViewMode(State.MODE_GRID);
294                 return true;
295 
296             case R.id.menu_list:
297                 setViewMode(State.MODE_LIST);
298                 return true;
299 
300             case R.id.menu_paste_from_clipboard:
301                 DirectoryFragment dir = getDirectoryFragment();
302                 if (dir != null) {
303                     dir.pasteFromClipboard();
304                 }
305                 return true;
306 
307             case R.id.menu_advanced:
308                 setDisplayAdvancedDevices(!mState.showAdvanced);
309                 return true;
310 
311             case R.id.menu_file_size:
312                 setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this));
313                 return true;
314 
315             case R.id.menu_settings:
316                 Metrics.logUserAction(this, Metrics.USER_ACTION_SETTINGS);
317 
318                 final RootInfo root = getCurrentRoot();
319                 final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS);
320                 intent.setDataAndType(root.getUri(), DocumentsContract.Root.MIME_TYPE_ITEM);
321                 startActivity(intent);
322                 return true;
323 
324             default:
325                 return super.onOptionsItemSelected(item);
326         }
327     }
328 
getDirectoryFragment()329     final @Nullable DirectoryFragment getDirectoryFragment() {
330         return DirectoryFragment.get(getFragmentManager());
331     }
332 
showCreateDirectoryDialog()333     void showCreateDirectoryDialog() {
334         Metrics.logUserAction(this, Metrics.USER_ACTION_CREATE_DIR);
335 
336         CreateDirectoryFragment.show(getFragmentManager());
337     }
338 
onDirectoryCreated(DocumentInfo doc)339     void onDirectoryCreated(DocumentInfo doc) {
340         // By default we do nothing, just let the new directory appear.
341         // DocumentsActivity auto-opens directories after creating them
342         // As that is more attuned to the "picker" use cases it supports.
343     }
344 
345     /**
346      * Returns true if a directory can be created in the current location.
347      * @return
348      */
canCreateDirectory()349     boolean canCreateDirectory() {
350         final RootInfo root = getCurrentRoot();
351         final DocumentInfo cwd = getCurrentDirectory();
352         return cwd != null
353                 && cwd.isCreateSupported()
354                 && !mSearchManager.isSearching()
355                 && !root.isRecents()
356                 && !root.isDownloads();
357     }
358 
openContainerDocument(DocumentInfo doc)359     void openContainerDocument(DocumentInfo doc) {
360         assert(doc.isContainer());
361 
362         notifyDirectoryNavigated(doc.derivedUri);
363 
364         mState.pushDocument(doc);
365         // Show an opening animation only if pressing "back" would get us back to the
366         // previous directory. Especially after opening a root document, pressing
367         // back, wouldn't go to the previous root, but close the activity.
368         final int anim = (mState.hasLocationChanged() && mState.stack.size() > 1)
369                 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
370         refreshCurrentRootAndDirectory(anim);
371     }
372 
373     /**
374      * Refreshes the content of the director and the menu/action bar.
375      * The current directory name and selection will get updated.
376      * @param anim
377      */
378     @Override
refreshCurrentRootAndDirectory(int anim)379     public final void refreshCurrentRootAndDirectory(int anim) {
380         mSearchManager.cancelSearch();
381 
382         refreshDirectory(anim);
383 
384         final RootsFragment roots = RootsFragment.get(getFragmentManager());
385         if (roots != null) {
386             roots.onCurrentRootChanged();
387         }
388 
389         mNavigator.update();
390         // Causes talkback to announce the activity's new title
391         if (mState.stack.isRecents()) {
392             setTitle(mRoots.getRecentsRoot().title);
393         } else {
394             setTitle(mState.stack.getTitle());
395         }
396         invalidateOptionsMenu();
397     }
398 
loadRoot(final Uri uri)399     final void loadRoot(final Uri uri) {
400         new LoadRootTask(this, uri).executeOnExecutor(
401                 ProviderExecutor.forAuthority(uri.getAuthority()));
402     }
403 
404     /**
405      * Called when search results changed.
406      * Refreshes the content of the directory. It doesn't refresh elements on the action bar.
407      * e.g. The current directory name displayed on the action bar won't get updated.
408      */
409     @Override
onSearchChanged(@ullable String query)410     public void onSearchChanged(@Nullable String query) {
411         // We should not get here if root is not searchable
412         assert(canSearchRoot());
413         reloadSearch(query);
414     }
415 
416     @Override
onSearchFinished()417     public void onSearchFinished() {
418         // Restores menu icons state
419         invalidateOptionsMenu();
420     }
421 
reloadSearch(String query)422     private void reloadSearch(String query) {
423         FragmentManager fm = getFragmentManager();
424         RootInfo root = getCurrentRoot();
425         DocumentInfo cwd = getCurrentDirectory();
426 
427         DirectoryFragment.reloadSearch(fm, root, cwd, query);
428     }
429 
getExcludedAuthorities()430     final List<String> getExcludedAuthorities() {
431         List<String> authorities = new ArrayList<>();
432         if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) {
433             // Exclude roots provided by the calling package.
434             String packageName = getCallingPackageMaybeExtra();
435             try {
436                 PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName,
437                         PackageManager.GET_PROVIDERS);
438                 for (ProviderInfo provider: pkgInfo.providers) {
439                     authorities.add(provider.authority);
440                 }
441             } catch (PackageManager.NameNotFoundException e) {
442                 Log.e(mTag, "Calling package name does not resolve: " + packageName);
443             }
444         }
445         return authorities;
446     }
447 
canSearchRoot()448     boolean canSearchRoot() {
449         final RootInfo root = getCurrentRoot();
450         return (root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0;
451     }
452 
getCallingPackageMaybeExtra()453     final String getCallingPackageMaybeExtra() {
454         String callingPackage = getCallingPackage();
455         // System apps can set the calling package name using an extra.
456         try {
457             ApplicationInfo info = getPackageManager().getApplicationInfo(callingPackage, 0);
458             if (info.isSystemApp() || info.isUpdatedSystemApp()) {
459                 final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME);
460                 if (extra != null) {
461                     callingPackage = extra;
462                 }
463             }
464         } finally {
465             return callingPackage;
466         }
467     }
468 
get(Fragment fragment)469     public static BaseActivity get(Fragment fragment) {
470         return (BaseActivity) fragment.getActivity();
471     }
472 
getDisplayState()473     public State getDisplayState() {
474         return mState;
475     }
476 
477     /*
478      * Get the default directory to be presented after starting the activity.
479      * Method can be overridden if the change of the behavior of the the child activity is needed.
480      */
getDefaultRoot()481     public Uri getDefaultRoot() {
482         return Shared.shouldShowDocumentsRoot(this, getIntent())
483                 ? DocumentsContract.buildHomeUri()
484                 : DocumentsContract.buildRootUri(
485                         "com.android.providers.downloads.documents", "downloads");
486     }
487 
488     /**
489      * Set internal storage visible based on explicit user action.
490      */
setDisplayAdvancedDevices(boolean display)491     void setDisplayAdvancedDevices(boolean display) {
492         Metrics.logUserAction(this,
493                 display ? Metrics.USER_ACTION_SHOW_ADVANCED : Metrics.USER_ACTION_HIDE_ADVANCED);
494 
495         LocalPreferences.setShowDeviceRoot(this, mState.action, display);
496         mState.showAdvanced = display;
497         RootsFragment.get(getFragmentManager()).onDisplayStateChanged();
498         invalidateOptionsMenu();
499     }
500 
501     /**
502      * Set file size visible based on explicit user action.
503      */
setDisplayFileSize(boolean display)504     void setDisplayFileSize(boolean display) {
505         Metrics.logUserAction(this,
506                 display ? Metrics.USER_ACTION_SHOW_SIZE : Metrics.USER_ACTION_HIDE_SIZE);
507 
508         LocalPreferences.setDisplayFileSize(this, display);
509         mState.showSize = display;
510         DirectoryFragment dir = getDirectoryFragment();
511         if (dir != null) {
512             dir.onDisplayStateChanged();
513         }
514         invalidateOptionsMenu();
515     }
516 
517     /**
518      * Set state sort order based on explicit user action.
519      */
setUserSortOrder(int sortOrder)520     void setUserSortOrder(int sortOrder) {
521         switch(sortOrder) {
522             case State.SORT_ORDER_DISPLAY_NAME:
523                 Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_NAME);
524                 break;
525             case State.SORT_ORDER_LAST_MODIFIED:
526                 Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_DATE);
527                 break;
528             case State.SORT_ORDER_SIZE:
529                 Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_SIZE);
530                 break;
531         }
532 
533         mState.userSortOrder = sortOrder;
534         DirectoryFragment dir = getDirectoryFragment();
535         if (dir != null) {
536             dir.onSortOrderChanged();
537         }
538     }
539 
540     /**
541      * Set mode based on explicit user action.
542      */
setViewMode(@iewMode int mode)543     void setViewMode(@ViewMode int mode) {
544         if (mode == State.MODE_GRID) {
545             Metrics.logUserAction(this, Metrics.USER_ACTION_GRID);
546         } else if (mode == State.MODE_LIST) {
547             Metrics.logUserAction(this, Metrics.USER_ACTION_LIST);
548         }
549 
550         LocalPreferences.setViewMode(this, getCurrentRoot(), mode);
551         mState.derivedMode = mode;
552 
553         // view icon needs to be updated, but we *could* do it
554         // in onOptionsItemSelected, and not do the full invalidation
555         // But! That's a larger refactoring we'll save for another day.
556         invalidateOptionsMenu();
557         DirectoryFragment dir = getDirectoryFragment();
558         if (dir != null) {
559             dir.onViewModeChanged();
560         }
561     }
562 
setPending(boolean pending)563     public void setPending(boolean pending) {
564         final SaveFragment save = SaveFragment.get(getFragmentManager());
565         if (save != null) {
566             save.setPending(pending);
567         }
568     }
569 
570     @Override
onSaveInstanceState(Bundle state)571     protected void onSaveInstanceState(Bundle state) {
572         super.onSaveInstanceState(state);
573         state.putParcelable(Shared.EXTRA_STATE, mState);
574         mSearchManager.onSaveInstanceState(state);
575     }
576 
577     @Override
onRestoreInstanceState(Bundle state)578     protected void onRestoreInstanceState(Bundle state) {
579         super.onRestoreInstanceState(state);
580     }
581 
582     @Override
isSearchExpanded()583     public boolean isSearchExpanded() {
584         return mSearchManager.isExpanded();
585     }
586 
587     @Override
getCurrentRoot()588     public RootInfo getCurrentRoot() {
589         if (mState.stack.root != null) {
590             return mState.stack.root;
591         } else {
592             return mRoots.getRecentsRoot();
593         }
594     }
595 
getCurrentDirectory()596     public DocumentInfo getCurrentDirectory() {
597         return mState.stack.peek();
598     }
599 
getExecutorForCurrentDirectory()600     public Executor getExecutorForCurrentDirectory() {
601         final DocumentInfo cwd = getCurrentDirectory();
602         if (cwd != null && cwd.authority != null) {
603             return ProviderExecutor.forAuthority(cwd.authority);
604         } else {
605             return AsyncTask.THREAD_POOL_EXECUTOR;
606         }
607     }
608 
609     @Override
onBackPressed()610     public void onBackPressed() {
611         // While action bar is expanded, the state stack UI is hidden.
612         if (mSearchManager.cancelSearch()) {
613             return;
614         }
615 
616         DirectoryFragment dir = getDirectoryFragment();
617         if (dir != null && dir.onBackPressed()) {
618             return;
619         }
620 
621         if (!mState.hasLocationChanged()) {
622             super.onBackPressed();
623             return;
624         }
625 
626         if (onBeforePopDir() || popDir()) {
627             return;
628         }
629 
630         super.onBackPressed();
631     }
632 
onBeforePopDir()633     boolean onBeforePopDir() {
634         // Files app overrides this with some fancy logic.
635         return false;
636     }
637 
onStackPicked(DocumentStack stack)638     public void onStackPicked(DocumentStack stack) {
639         try {
640             // Update the restored stack to ensure we have freshest data
641             stack.updateDocuments(getContentResolver());
642             mState.setStack(stack);
643             refreshCurrentRootAndDirectory(AnimationView.ANIM_SIDE);
644 
645         } catch (FileNotFoundException e) {
646             Log.w(mTag, "Failed to restore stack: " + e);
647         }
648     }
649 
650     /**
651      * Declare a global key handler to route key events when there isn't a specific focus view. This
652      * covers the scenario where a user opens DocumentsUI and just starts typing.
653      *
654      * @param keyCode
655      * @param event
656      * @return
657      */
658     @CallSuper
659     @Override
onKeyDown(int keyCode, KeyEvent event)660     public boolean onKeyDown(int keyCode, KeyEvent event) {
661         if (Events.isNavigationKeyCode(keyCode)) {
662             // Forward all unclaimed navigation keystrokes to the DirectoryFragment. This causes any
663             // stray navigation keystrokes focus the content pane, which is probably what the user
664             // is trying to do.
665             DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
666             if (df != null) {
667                 df.requestFocus();
668                 return true;
669             }
670         } else if (keyCode == KeyEvent.KEYCODE_TAB) {
671             // Tab toggles focus on the navigation drawer.
672             toggleNavDrawerFocus();
673             return true;
674         } else if (keyCode == KeyEvent.KEYCODE_DEL) {
675             popDir();
676             return true;
677         }
678         return super.onKeyDown(keyCode, event);
679     }
680 
addEventListener(EventListener listener)681     public void addEventListener(EventListener listener) {
682         mEventListeners.add(listener);
683     }
684 
removeEventListener(EventListener listener)685     public void removeEventListener(EventListener listener) {
686         mEventListeners.remove(listener);
687     }
688 
notifyDirectoryLoaded(Uri uri)689     public void notifyDirectoryLoaded(Uri uri) {
690         for (EventListener listener : mEventListeners) {
691             listener.onDirectoryLoaded(uri);
692         }
693     }
694 
notifyDirectoryNavigated(Uri uri)695     void notifyDirectoryNavigated(Uri uri) {
696         for (EventListener listener : mEventListeners) {
697             listener.onDirectoryNavigated(uri);
698         }
699     }
700 
701     /**
702      * Toggles focus between the navigation drawer and the directory listing. If the drawer isn't
703      * locked, open/close it as appropriate.
704      */
toggleNavDrawerFocus()705     void toggleNavDrawerFocus() {
706         if (mNavDrawerHasFocus) {
707             mDrawer.setOpen(false);
708             DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
709             if (df != null) {
710                 df.requestFocus();
711             }
712         } else {
713             mDrawer.setOpen(true);
714             RootsFragment rf = RootsFragment.get(getFragmentManager());
715             if (rf != null) {
716                 rf.requestFocus();
717             }
718         }
719         mNavDrawerHasFocus = !mNavDrawerHasFocus;
720     }
721 
getRootDocumentBlocking(RootInfo root)722     DocumentInfo getRootDocumentBlocking(RootInfo root) {
723         try {
724             final Uri uri = DocumentsContract.buildDocumentUri(
725                     root.authority, root.documentId);
726             return DocumentInfo.fromUri(getContentResolver(), uri);
727         } catch (FileNotFoundException e) {
728             Log.w(mTag, "Failed to find root", e);
729             return null;
730         }
731     }
732 
733     /**
734      * Pops the top entry off the directory stack, and returns the user to the previous directory.
735      * If the directory stack only contains one item, this method does nothing.
736      *
737      * @return Whether the stack was popped.
738      */
popDir()739     private boolean popDir() {
740         if (mState.stack.size() > 1) {
741             mState.stack.pop();
742             refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE);
743             return true;
744         }
745         return false;
746     }
747 
748     /**
749      * Closes the activity when it's idle.
750      */
addListenerForLaunchCompletion()751     private void addListenerForLaunchCompletion() {
752         addEventListener(new EventListener() {
753             @Override
754             public void onDirectoryNavigated(Uri uri) {
755             }
756 
757             @Override
758             public void onDirectoryLoaded(Uri uri) {
759                 removeEventListener(this);
760                 getMainLooper().getQueue().addIdleHandler(new IdleHandler() {
761                     @Override
762                     public boolean queueIdle() {
763                         // If startup benchmark is requested by a whitelisted testing package, then
764                         // close the activity once idle, and notify the testing activity.
765                         if (getIntent().getBooleanExtra(EXTRA_BENCHMARK, false) &&
766                                 BENCHMARK_TESTING_PACKAGE.equals(getCallingPackage())) {
767                             setResult(RESULT_OK);
768                             finish();
769                         }
770 
771                         Metrics.logStartupMs(
772                                 BaseActivity.this, (int) (new Date().getTime() - mStartTime));
773 
774                         // Remove the idle handler.
775                         return false;
776                     }
777                 });
778                 new Handler().post(new Runnable() {
779                     @Override public void run() {
780                     }
781                 });
782             }
783         });
784     }
785 
786     private static final class PickRootTask extends PairedTask<BaseActivity, Void, DocumentInfo> {
787         private RootInfo mRoot;
788 
PickRootTask(BaseActivity activity, RootInfo root)789         public PickRootTask(BaseActivity activity, RootInfo root) {
790             super(activity);
791             mRoot = root;
792         }
793 
794         @Override
run(Void... params)795         protected DocumentInfo run(Void... params) {
796             return mOwner.getRootDocumentBlocking(mRoot);
797         }
798 
799         @Override
finish(DocumentInfo result)800         protected void finish(DocumentInfo result) {
801             if (result != null) {
802                 mOwner.openContainerDocument(result);
803             }
804         }
805     }
806 
807     private static final class HandleRootsChangedTask
808             extends PairedTask<BaseActivity, RootInfo, RootInfo> {
809         RootInfo mCurrentRoot;
810         DocumentInfo mDefaultRootDocument;
811 
HandleRootsChangedTask(BaseActivity activity)812         public HandleRootsChangedTask(BaseActivity activity) {
813             super(activity);
814         }
815 
816         @Override
run(RootInfo... roots)817         protected RootInfo run(RootInfo... roots) {
818             assert(roots.length == 1);
819             mCurrentRoot = roots[0];
820             final Collection<RootInfo> cachedRoots = mOwner.mRoots.getRootsBlocking();
821             for (final RootInfo root : cachedRoots) {
822                 if (root.getUri().equals(mCurrentRoot.getUri())) {
823                     // We don't need to change the current root as the current root was not removed.
824                     return null;
825                 }
826             }
827 
828             // Choose the default root.
829             final RootInfo defaultRoot = mOwner.mRoots.getDefaultRootBlocking(mOwner.mState);
830             assert(defaultRoot != null);
831             if (!defaultRoot.isRecents()) {
832                 mDefaultRootDocument = mOwner.getRootDocumentBlocking(defaultRoot);
833             }
834             return defaultRoot;
835         }
836 
837         @Override
finish(RootInfo defaultRoot)838         protected void finish(RootInfo defaultRoot) {
839             if (defaultRoot == null) {
840                 return;
841             }
842 
843             // If the activity has been launched for the specific root and it is removed, finish the
844             // activity.
845             final Uri uri = mOwner.getIntent().getData();
846             if (uri != null && uri.equals(mCurrentRoot.getUri())) {
847                 mOwner.finish();
848                 return;
849             }
850 
851             // Clear entire backstack and start in new root.
852             mOwner.mState.onRootChanged(defaultRoot);
853             mOwner.mSearchManager.update(defaultRoot);
854 
855             if (defaultRoot.isRecents()) {
856                 mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
857             } else {
858                 mOwner.openContainerDocument(mDefaultRootDocument);
859             }
860         }
861     }
862 }
863