Home | History | Annotate | Download | only in documentsui
      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 
     95     public abstract void onDocumentPicked(DocumentInfo doc, Model model);
     96     public abstract void onDocumentsPicked(List<DocumentInfo> docs);
     97 
     98     abstract void onTaskFinished(Uri... uris);
     99     abstract void refreshDirectory(int anim);
    100     /** Allows sub-classes to include information in a newly created State instance. */
    101     abstract void includeState(State initialState);
    102 
    103     public BaseActivity(@LayoutRes int layoutId, String tag) {
    104         mLayoutId = layoutId;
    105         mTag = tag;
    106     }
    107 
    108     @CallSuper
    109     @Override
    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
    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
    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
    193     protected void onDestroy() {
    194         getContentResolver().unregisterContentObserver(mRootsCacheObserver);
    195         super.onDestroy();
    196     }
    197 
    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 
    236     public void setRootsDrawerOpen(boolean open) {
    237         mNavigator.revealRootsDrawer(open);
    238     }
    239 
    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
    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 
    329     final @Nullable DirectoryFragment getDirectoryFragment() {
    330         return DirectoryFragment.get(getFragmentManager());
    331     }
    332 
    333     void showCreateDirectoryDialog() {
    334         Metrics.logUserAction(this, Metrics.USER_ACTION_CREATE_DIR);
    335 
    336         CreateDirectoryFragment.show(getFragmentManager());
    337     }
    338 
    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      */
    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 
    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
    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 
    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
    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
    411     public void onSearchFinished() {
    412         // Restores menu icons state
    413         invalidateOptionsMenu();
    414     }
    415 
    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 
    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 
    442     boolean canSearchRoot() {
    443         final RootInfo root = getCurrentRoot();
    444         return (root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0;
    445     }
    446 
    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 
    463     public static BaseActivity get(Fragment fragment) {
    464         return (BaseActivity) fragment.getActivity();
    465     }
    466 
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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
    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
    572     protected void onRestoreInstanceState(Bundle state) {
    573         super.onRestoreInstanceState(state);
    574     }
    575 
    576     @Override
    577     public boolean isSearchExpanded() {
    578         return mSearchManager.isExpanded();
    579     }
    580 
    581     @Override
    582     public RootInfo getCurrentRoot() {
    583         if (mState.stack.root != null) {
    584             return mState.stack.root;
    585         } else {
    586             return mRoots.getRecentsRoot();
    587         }
    588     }
    589 
    590     public DocumentInfo getCurrentDirectory() {
    591         return mState.stack.peek();
    592     }
    593 
    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
    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 
    627     boolean onBeforePopDir() {
    628         // Files app overrides this with some fancy logic.
    629         return false;
    630     }
    631 
    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
    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 
    675     public void addEventListener(EventListener listener) {
    676         mEventListeners.add(listener);
    677     }
    678 
    679     public void removeEventListener(EventListener listener) {
    680         mEventListeners.remove(listener);
    681     }
    682 
    683     public void notifyDirectoryLoaded(Uri uri) {
    684         for (EventListener listener : mEventListeners) {
    685             listener.onDirectoryLoaded(uri);
    686         }
    687     }
    688 
    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      */
    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 
    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      */
    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      */
    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 
    783         public PickRootTask(BaseActivity activity, RootInfo root) {
    784             super(activity);
    785             mRoot = root;
    786         }
    787 
    788         @Override
    789         protected DocumentInfo run(Void... params) {
    790             return mOwner.getRootDocumentBlocking(mRoot);
    791         }
    792 
    793         @Override
    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 
    806         public HandleRootsChangedTask(BaseActivity activity) {
    807             super(activity);
    808         }
    809 
    810         @Override
    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
    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