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         invalidateOptionsMenu();
391     }
392 
loadRoot(final Uri uri)393     final void loadRoot(final Uri uri) {
394         new LoadRootTask(this, uri).executeOnExecutor(
395                 ProviderExecutor.forAuthority(uri.getAuthority()));
396     }
397 
398     /**
399      * Called when search results changed.
400      * Refreshes the content of the directory. It doesn't refresh elements on the action bar.
401      * e.g. The current directory name displayed on the action bar won't get updated.
402      */
403     @Override
onSearchChanged(@ullable String query)404     public void onSearchChanged(@Nullable String query) {
405         // We should not get here if root is not searchable
406         assert(canSearchRoot());
407         reloadSearch(query);
408     }
409 
410     @Override
onSearchFinished()411     public void onSearchFinished() {
412         // Restores menu icons state
413         invalidateOptionsMenu();
414     }
415 
reloadSearch(String query)416     private void reloadSearch(String query) {
417         FragmentManager fm = getFragmentManager();
418         RootInfo root = getCurrentRoot();
419         DocumentInfo cwd = getCurrentDirectory();
420 
421         DirectoryFragment.reloadSearch(fm, root, cwd, query);
422     }
423 
getExcludedAuthorities()424     final List<String> getExcludedAuthorities() {
425         List<String> authorities = new ArrayList<>();
426         if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) {
427             // Exclude roots provided by the calling package.
428             String packageName = getCallingPackageMaybeExtra();
429             try {
430                 PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName,
431                         PackageManager.GET_PROVIDERS);
432                 for (ProviderInfo provider: pkgInfo.providers) {
433                     authorities.add(provider.authority);
434                 }
435             } catch (PackageManager.NameNotFoundException e) {
436                 Log.e(mTag, "Calling package name does not resolve: " + packageName);
437             }
438         }
439         return authorities;
440     }
441 
canSearchRoot()442     boolean canSearchRoot() {
443         final RootInfo root = getCurrentRoot();
444         return (root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0;
445     }
446 
getCallingPackageMaybeExtra()447     final String getCallingPackageMaybeExtra() {
448         String callingPackage = getCallingPackage();
449         // System apps can set the calling package name using an extra.
450         try {
451             ApplicationInfo info = getPackageManager().getApplicationInfo(callingPackage, 0);
452             if (info.isSystemApp() || info.isUpdatedSystemApp()) {
453                 final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME);
454                 if (extra != null) {
455                     callingPackage = extra;
456                 }
457             }
458         } finally {
459             return callingPackage;
460         }
461     }
462 
get(Fragment fragment)463     public static BaseActivity get(Fragment fragment) {
464         return (BaseActivity) fragment.getActivity();
465     }
466 
getDisplayState()467     public State getDisplayState() {
468         return mState;
469     }
470 
471     /*
472      * Get the default directory to be presented after starting the activity.
473      * Method can be overridden if the change of the behavior of the the child activity is needed.
474      */
getDefaultRoot()475     public Uri getDefaultRoot() {
476         return Shared.shouldShowDocumentsRoot(this, getIntent())
477                 ? DocumentsContract.buildHomeUri()
478                 : DocumentsContract.buildRootUri(
479                         "com.android.providers.downloads.documents", "downloads");
480     }
481 
482     /**
483      * Set internal storage visible based on explicit user action.
484      */
setDisplayAdvancedDevices(boolean display)485     void setDisplayAdvancedDevices(boolean display) {
486         Metrics.logUserAction(this,
487                 display ? Metrics.USER_ACTION_SHOW_ADVANCED : Metrics.USER_ACTION_HIDE_ADVANCED);
488 
489         LocalPreferences.setShowDeviceRoot(this, mState.action, display);
490         mState.showAdvanced = display;
491         RootsFragment.get(getFragmentManager()).onDisplayStateChanged();
492         invalidateOptionsMenu();
493     }
494 
495     /**
496      * Set file size visible based on explicit user action.
497      */
setDisplayFileSize(boolean display)498     void setDisplayFileSize(boolean display) {
499         Metrics.logUserAction(this,
500                 display ? Metrics.USER_ACTION_SHOW_SIZE : Metrics.USER_ACTION_HIDE_SIZE);
501 
502         LocalPreferences.setDisplayFileSize(this, display);
503         mState.showSize = display;
504         DirectoryFragment dir = getDirectoryFragment();
505         if (dir != null) {
506             dir.onDisplayStateChanged();
507         }
508         invalidateOptionsMenu();
509     }
510 
511     /**
512      * Set state sort order based on explicit user action.
513      */
setUserSortOrder(int sortOrder)514     void setUserSortOrder(int sortOrder) {
515         switch(sortOrder) {
516             case State.SORT_ORDER_DISPLAY_NAME:
517                 Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_NAME);
518                 break;
519             case State.SORT_ORDER_LAST_MODIFIED:
520                 Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_DATE);
521                 break;
522             case State.SORT_ORDER_SIZE:
523                 Metrics.logUserAction(this, Metrics.USER_ACTION_SORT_SIZE);
524                 break;
525         }
526 
527         mState.userSortOrder = sortOrder;
528         DirectoryFragment dir = getDirectoryFragment();
529         if (dir != null) {
530             dir.onSortOrderChanged();
531         }
532     }
533 
534     /**
535      * Set mode based on explicit user action.
536      */
setViewMode(@iewMode int mode)537     void setViewMode(@ViewMode int mode) {
538         if (mode == State.MODE_GRID) {
539             Metrics.logUserAction(this, Metrics.USER_ACTION_GRID);
540         } else if (mode == State.MODE_LIST) {
541             Metrics.logUserAction(this, Metrics.USER_ACTION_LIST);
542         }
543 
544         LocalPreferences.setViewMode(this, getCurrentRoot(), mode);
545         mState.derivedMode = mode;
546 
547         // view icon needs to be updated, but we *could* do it
548         // in onOptionsItemSelected, and not do the full invalidation
549         // But! That's a larger refactoring we'll save for another day.
550         invalidateOptionsMenu();
551         DirectoryFragment dir = getDirectoryFragment();
552         if (dir != null) {
553             dir.onViewModeChanged();
554         }
555     }
556 
setPending(boolean pending)557     public void setPending(boolean pending) {
558         final SaveFragment save = SaveFragment.get(getFragmentManager());
559         if (save != null) {
560             save.setPending(pending);
561         }
562     }
563 
564     @Override
onSaveInstanceState(Bundle state)565     protected void onSaveInstanceState(Bundle state) {
566         super.onSaveInstanceState(state);
567         state.putParcelable(Shared.EXTRA_STATE, mState);
568         mSearchManager.onSaveInstanceState(state);
569     }
570 
571     @Override
onRestoreInstanceState(Bundle state)572     protected void onRestoreInstanceState(Bundle state) {
573         super.onRestoreInstanceState(state);
574     }
575 
576     @Override
isSearchExpanded()577     public boolean isSearchExpanded() {
578         return mSearchManager.isExpanded();
579     }
580 
581     @Override
getCurrentRoot()582     public RootInfo getCurrentRoot() {
583         if (mState.stack.root != null) {
584             return mState.stack.root;
585         } else {
586             return mRoots.getRecentsRoot();
587         }
588     }
589 
getCurrentDirectory()590     public DocumentInfo getCurrentDirectory() {
591         return mState.stack.peek();
592     }
593 
getExecutorForCurrentDirectory()594     public Executor getExecutorForCurrentDirectory() {
595         final DocumentInfo cwd = getCurrentDirectory();
596         if (cwd != null && cwd.authority != null) {
597             return ProviderExecutor.forAuthority(cwd.authority);
598         } else {
599             return AsyncTask.THREAD_POOL_EXECUTOR;
600         }
601     }
602 
603     @Override
onBackPressed()604     public void onBackPressed() {
605         // While action bar is expanded, the state stack UI is hidden.
606         if (mSearchManager.cancelSearch()) {
607             return;
608         }
609 
610         DirectoryFragment dir = getDirectoryFragment();
611         if (dir != null && dir.onBackPressed()) {
612             return;
613         }
614 
615         if (!mState.hasLocationChanged()) {
616             super.onBackPressed();
617             return;
618         }
619 
620         if (onBeforePopDir() || popDir()) {
621             return;
622         }
623 
624         super.onBackPressed();
625     }
626 
onBeforePopDir()627     boolean onBeforePopDir() {
628         // Files app overrides this with some fancy logic.
629         return false;
630     }
631 
onStackPicked(DocumentStack stack)632     public void onStackPicked(DocumentStack stack) {
633         try {
634             // Update the restored stack to ensure we have freshest data
635             stack.updateDocuments(getContentResolver());
636             mState.setStack(stack);
637             refreshCurrentRootAndDirectory(AnimationView.ANIM_SIDE);
638 
639         } catch (FileNotFoundException e) {
640             Log.w(mTag, "Failed to restore stack: " + e);
641         }
642     }
643 
644     /**
645      * Declare a global key handler to route key events when there isn't a specific focus view. This
646      * covers the scenario where a user opens DocumentsUI and just starts typing.
647      *
648      * @param keyCode
649      * @param event
650      * @return
651      */
652     @CallSuper
653     @Override
onKeyDown(int keyCode, KeyEvent event)654     public boolean onKeyDown(int keyCode, KeyEvent event) {
655         if (Events.isNavigationKeyCode(keyCode)) {
656             // Forward all unclaimed navigation keystrokes to the DirectoryFragment. This causes any
657             // stray navigation keystrokes focus the content pane, which is probably what the user
658             // is trying to do.
659             DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
660             if (df != null) {
661                 df.requestFocus();
662                 return true;
663             }
664         } else if (keyCode == KeyEvent.KEYCODE_TAB) {
665             // Tab toggles focus on the navigation drawer.
666             toggleNavDrawerFocus();
667             return true;
668         } else if (keyCode == KeyEvent.KEYCODE_DEL) {
669             popDir();
670             return true;
671         }
672         return super.onKeyDown(keyCode, event);
673     }
674 
addEventListener(EventListener listener)675     public void addEventListener(EventListener listener) {
676         mEventListeners.add(listener);
677     }
678 
removeEventListener(EventListener listener)679     public void removeEventListener(EventListener listener) {
680         mEventListeners.remove(listener);
681     }
682 
notifyDirectoryLoaded(Uri uri)683     public void notifyDirectoryLoaded(Uri uri) {
684         for (EventListener listener : mEventListeners) {
685             listener.onDirectoryLoaded(uri);
686         }
687     }
688 
notifyDirectoryNavigated(Uri uri)689     void notifyDirectoryNavigated(Uri uri) {
690         for (EventListener listener : mEventListeners) {
691             listener.onDirectoryNavigated(uri);
692         }
693     }
694 
695     /**
696      * Toggles focus between the navigation drawer and the directory listing. If the drawer isn't
697      * locked, open/close it as appropriate.
698      */
toggleNavDrawerFocus()699     void toggleNavDrawerFocus() {
700         if (mNavDrawerHasFocus) {
701             mDrawer.setOpen(false);
702             DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
703             if (df != null) {
704                 df.requestFocus();
705             }
706         } else {
707             mDrawer.setOpen(true);
708             RootsFragment rf = RootsFragment.get(getFragmentManager());
709             if (rf != null) {
710                 rf.requestFocus();
711             }
712         }
713         mNavDrawerHasFocus = !mNavDrawerHasFocus;
714     }
715 
getRootDocumentBlocking(RootInfo root)716     DocumentInfo getRootDocumentBlocking(RootInfo root) {
717         try {
718             final Uri uri = DocumentsContract.buildDocumentUri(
719                     root.authority, root.documentId);
720             return DocumentInfo.fromUri(getContentResolver(), uri);
721         } catch (FileNotFoundException e) {
722             Log.w(mTag, "Failed to find root", e);
723             return null;
724         }
725     }
726 
727     /**
728      * Pops the top entry off the directory stack, and returns the user to the previous directory.
729      * If the directory stack only contains one item, this method does nothing.
730      *
731      * @return Whether the stack was popped.
732      */
popDir()733     private boolean popDir() {
734         if (mState.stack.size() > 1) {
735             mState.stack.pop();
736             refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE);
737             return true;
738         }
739         return false;
740     }
741 
742     /**
743      * Closes the activity when it's idle.
744      */
addListenerForLaunchCompletion()745     private void addListenerForLaunchCompletion() {
746         addEventListener(new EventListener() {
747             @Override
748             public void onDirectoryNavigated(Uri uri) {
749             }
750 
751             @Override
752             public void onDirectoryLoaded(Uri uri) {
753                 removeEventListener(this);
754                 getMainLooper().getQueue().addIdleHandler(new IdleHandler() {
755                     @Override
756                     public boolean queueIdle() {
757                         // If startup benchmark is requested by a whitelisted testing package, then
758                         // close the activity once idle, and notify the testing activity.
759                         if (getIntent().getBooleanExtra(EXTRA_BENCHMARK, false) &&
760                                 BENCHMARK_TESTING_PACKAGE.equals(getCallingPackage())) {
761                             setResult(RESULT_OK);
762                             finish();
763                         }
764 
765                         Metrics.logStartupMs(
766                                 BaseActivity.this, (int) (new Date().getTime() - mStartTime));
767 
768                         // Remove the idle handler.
769                         return false;
770                     }
771                 });
772                 new Handler().post(new Runnable() {
773                     @Override public void run() {
774                     }
775                 });
776             }
777         });
778     }
779 
780     private static final class PickRootTask extends PairedTask<BaseActivity, Void, DocumentInfo> {
781         private RootInfo mRoot;
782 
PickRootTask(BaseActivity activity, RootInfo root)783         public PickRootTask(BaseActivity activity, RootInfo root) {
784             super(activity);
785             mRoot = root;
786         }
787 
788         @Override
run(Void... params)789         protected DocumentInfo run(Void... params) {
790             return mOwner.getRootDocumentBlocking(mRoot);
791         }
792 
793         @Override
finish(DocumentInfo result)794         protected void finish(DocumentInfo result) {
795             if (result != null) {
796                 mOwner.openContainerDocument(result);
797             }
798         }
799     }
800 
801     private static final class HandleRootsChangedTask
802             extends PairedTask<BaseActivity, RootInfo, RootInfo> {
803         RootInfo mCurrentRoot;
804         DocumentInfo mDefaultRootDocument;
805 
HandleRootsChangedTask(BaseActivity activity)806         public HandleRootsChangedTask(BaseActivity activity) {
807             super(activity);
808         }
809 
810         @Override
run(RootInfo... roots)811         protected RootInfo run(RootInfo... roots) {
812             assert(roots.length == 1);
813             mCurrentRoot = roots[0];
814             final Collection<RootInfo> cachedRoots = mOwner.mRoots.getRootsBlocking();
815             for (final RootInfo root : cachedRoots) {
816                 if (root.getUri().equals(mCurrentRoot.getUri())) {
817                     // We don't need to change the current root as the current root was not removed.
818                     return null;
819                 }
820             }
821 
822             // Choose the default root.
823             final RootInfo defaultRoot = mOwner.mRoots.getDefaultRootBlocking(mOwner.mState);
824             assert(defaultRoot != null);
825             if (!defaultRoot.isRecents()) {
826                 mDefaultRootDocument = mOwner.getRootDocumentBlocking(defaultRoot);
827             }
828             return defaultRoot;
829         }
830 
831         @Override
finish(RootInfo defaultRoot)832         protected void finish(RootInfo defaultRoot) {
833             if (defaultRoot == null) {
834                 return;
835             }
836 
837             // If the activity has been launched for the specific root and it is removed, finish the
838             // activity.
839             final Uri uri = mOwner.getIntent().getData();
840             if (uri != null && uri.equals(mCurrentRoot.getUri())) {
841                 mOwner.finish();
842                 return;
843             }
844 
845             // Clear entire backstack and start in new root.
846             mOwner.mState.onRootChanged(defaultRoot);
847             mOwner.mSearchManager.update(defaultRoot);
848 
849             if (defaultRoot.isRecents()) {
850                 mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
851             } else {
852                 mOwner.openContainerDocument(mDefaultRootDocument);
853             }
854         }
855     }
856 }
857