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 (popDir()) {
    616             return;
    617         }
    618 
    619         super.onBackPressed();
    620     }
    621 
    622     public void onStackPicked(DocumentStack stack) {
    623         try {
    624             // Update the restored stack to ensure we have freshest data
    625             stack.updateDocuments(getContentResolver());
    626             mState.setStack(stack);
    627             refreshCurrentRootAndDirectory(AnimationView.ANIM_SIDE);
    628 
    629         } catch (FileNotFoundException e) {
    630             Log.w(mTag, "Failed to restore stack: " + e);
    631         }
    632     }
    633 
    634     /**
    635      * Declare a global key handler to route key events when there isn't a specific focus view. This
    636      * covers the scenario where a user opens DocumentsUI and just starts typing.
    637      *
    638      * @param keyCode
    639      * @param event
    640      * @return
    641      */
    642     @CallSuper
    643     @Override
    644     public boolean onKeyDown(int keyCode, KeyEvent event) {
    645         if (Events.isNavigationKeyCode(keyCode)) {
    646             // Forward all unclaimed navigation keystrokes to the DirectoryFragment. This causes any
    647             // stray navigation keystrokes focus the content pane, which is probably what the user
    648             // is trying to do.
    649             DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
    650             if (df != null) {
    651                 df.requestFocus();
    652                 return true;
    653             }
    654         } else if (keyCode == KeyEvent.KEYCODE_TAB) {
    655             // Tab toggles focus on the navigation drawer.
    656             toggleNavDrawerFocus();
    657             return true;
    658         } else if (keyCode == KeyEvent.KEYCODE_DEL) {
    659             popDir();
    660             return true;
    661         }
    662         return super.onKeyDown(keyCode, event);
    663     }
    664 
    665     public void addEventListener(EventListener listener) {
    666         mEventListeners.add(listener);
    667     }
    668 
    669     public void removeEventListener(EventListener listener) {
    670         mEventListeners.remove(listener);
    671     }
    672 
    673     public void notifyDirectoryLoaded(Uri uri) {
    674         for (EventListener listener : mEventListeners) {
    675             listener.onDirectoryLoaded(uri);
    676         }
    677     }
    678 
    679     void notifyDirectoryNavigated(Uri uri) {
    680         for (EventListener listener : mEventListeners) {
    681             listener.onDirectoryNavigated(uri);
    682         }
    683     }
    684 
    685     /**
    686      * Toggles focus between the navigation drawer and the directory listing. If the drawer isn't
    687      * locked, open/close it as appropriate.
    688      */
    689     void toggleNavDrawerFocus() {
    690         if (mNavDrawerHasFocus) {
    691             mDrawer.setOpen(false);
    692             DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
    693             if (df != null) {
    694                 df.requestFocus();
    695             }
    696         } else {
    697             mDrawer.setOpen(true);
    698             RootsFragment rf = RootsFragment.get(getFragmentManager());
    699             if (rf != null) {
    700                 rf.requestFocus();
    701             }
    702         }
    703         mNavDrawerHasFocus = !mNavDrawerHasFocus;
    704     }
    705 
    706     DocumentInfo getRootDocumentBlocking(RootInfo root) {
    707         try {
    708             final Uri uri = DocumentsContract.buildDocumentUri(
    709                     root.authority, root.documentId);
    710             return DocumentInfo.fromUri(getContentResolver(), uri);
    711         } catch (FileNotFoundException e) {
    712             Log.w(mTag, "Failed to find root", e);
    713             return null;
    714         }
    715     }
    716 
    717     /**
    718      * Pops the top entry off the directory stack, and returns the user to the previous directory.
    719      * If the directory stack only contains one item, this method does nothing.
    720      *
    721      * @return Whether the stack was popped.
    722      */
    723     private boolean popDir() {
    724         if (mState.stack.size() > 1) {
    725             mState.stack.pop();
    726             refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE);
    727             return true;
    728         }
    729         return false;
    730     }
    731 
    732     /**
    733      * Closes the activity when it's idle.
    734      */
    735     private void addListenerForLaunchCompletion() {
    736         addEventListener(new EventListener() {
    737             @Override
    738             public void onDirectoryNavigated(Uri uri) {
    739             }
    740 
    741             @Override
    742             public void onDirectoryLoaded(Uri uri) {
    743                 removeEventListener(this);
    744                 getMainLooper().getQueue().addIdleHandler(new IdleHandler() {
    745                     @Override
    746                     public boolean queueIdle() {
    747                         // If startup benchmark is requested by a whitelisted testing package, then
    748                         // close the activity once idle, and notify the testing activity.
    749                         if (getIntent().getBooleanExtra(EXTRA_BENCHMARK, false) &&
    750                                 BENCHMARK_TESTING_PACKAGE.equals(getCallingPackage())) {
    751                             setResult(RESULT_OK);
    752                             finish();
    753                         }
    754 
    755                         Metrics.logStartupMs(
    756                                 BaseActivity.this, (int) (new Date().getTime() - mStartTime));
    757 
    758                         // Remove the idle handler.
    759                         return false;
    760                     }
    761                 });
    762                 new Handler().post(new Runnable() {
    763                     @Override public void run() {
    764                     }
    765                 });
    766             }
    767         });
    768     }
    769 
    770     private static final class PickRootTask extends PairedTask<BaseActivity, Void, DocumentInfo> {
    771         private RootInfo mRoot;
    772 
    773         public PickRootTask(BaseActivity activity, RootInfo root) {
    774             super(activity);
    775             mRoot = root;
    776         }
    777 
    778         @Override
    779         protected DocumentInfo run(Void... params) {
    780             return mOwner.getRootDocumentBlocking(mRoot);
    781         }
    782 
    783         @Override
    784         protected void finish(DocumentInfo result) {
    785             if (result != null) {
    786                 mOwner.openContainerDocument(result);
    787             }
    788         }
    789     }
    790 
    791     private static final class HandleRootsChangedTask
    792             extends PairedTask<BaseActivity, RootInfo, RootInfo> {
    793         RootInfo mCurrentRoot;
    794         DocumentInfo mDefaultRootDocument;
    795 
    796         public HandleRootsChangedTask(BaseActivity activity) {
    797             super(activity);
    798         }
    799 
    800         @Override
    801         protected RootInfo run(RootInfo... roots) {
    802             assert(roots.length == 1);
    803             mCurrentRoot = roots[0];
    804             final Collection<RootInfo> cachedRoots = mOwner.mRoots.getRootsBlocking();
    805             for (final RootInfo root : cachedRoots) {
    806                 if (root.getUri().equals(mCurrentRoot.getUri())) {
    807                     // We don't need to change the current root as the current root was not removed.
    808                     return null;
    809                 }
    810             }
    811 
    812             // Choose the default root.
    813             final RootInfo defaultRoot = mOwner.mRoots.getDefaultRootBlocking(mOwner.mState);
    814             assert(defaultRoot != null);
    815             if (!defaultRoot.isRecents()) {
    816                 mDefaultRootDocument = mOwner.getRootDocumentBlocking(defaultRoot);
    817             }
    818             return defaultRoot;
    819         }
    820 
    821         @Override
    822         protected void finish(RootInfo defaultRoot) {
    823             if (defaultRoot == null) {
    824                 return;
    825             }
    826 
    827             // If the activity has been launched for the specific root and it is removed, finish the
    828             // activity.
    829             final Uri uri = mOwner.getIntent().getData();
    830             if (uri != null && uri.equals(mCurrentRoot.getUri())) {
    831                 mOwner.finish();
    832                 return;
    833             }
    834 
    835             // Clear entire backstack and start in new root.
    836             mOwner.mState.onRootChanged(defaultRoot);
    837             mOwner.mSearchManager.update(defaultRoot);
    838 
    839             if (defaultRoot.isRecents()) {
    840                 mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
    841             } else {
    842                 mOwner.openContainerDocument(mDefaultRootDocument);
    843             }
    844         }
    845     }
    846 }
    847