[go: nahoru, domu]

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
17package com.android.documentsui;
18
19import static com.android.documentsui.Shared.DEBUG;
20import static com.android.documentsui.Shared.EXTRA_BENCHMARK;
21import static com.android.documentsui.State.ACTION_CREATE;
22import static com.android.documentsui.State.ACTION_GET_CONTENT;
23import static com.android.documentsui.State.ACTION_OPEN;
24import static com.android.documentsui.State.ACTION_OPEN_TREE;
25import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION;
26import static com.android.documentsui.State.MODE_GRID;
27
28import android.app.Activity;
29import android.app.Fragment;
30import android.app.FragmentManager;
31import android.content.Intent;
32import android.content.pm.ApplicationInfo;
33import android.content.pm.PackageInfo;
34import android.content.pm.PackageManager;
35import android.content.pm.ProviderInfo;
36import android.database.ContentObserver;
37import android.net.Uri;
38import android.os.AsyncTask;
39import android.os.Bundle;
40import android.os.Handler;
41import android.os.MessageQueue.IdleHandler;
42import android.provider.DocumentsContract;
43import android.provider.DocumentsContract.Root;
44import android.support.annotation.CallSuper;
45import android.support.annotation.LayoutRes;
46import android.support.annotation.Nullable;
47import android.util.Log;
48import android.view.KeyEvent;
49import android.view.Menu;
50import android.view.MenuItem;
51import android.widget.Spinner;
52
53import com.android.documentsui.SearchViewManager.SearchManagerListener;
54import com.android.documentsui.State.ViewMode;
55import com.android.documentsui.dirlist.AnimationView;
56import com.android.documentsui.dirlist.DirectoryFragment;
57import com.android.documentsui.dirlist.Model;
58import com.android.documentsui.model.DocumentInfo;
59import com.android.documentsui.model.DocumentStack;
60import com.android.documentsui.model.RootInfo;
61
62import java.io.FileNotFoundException;
63import java.util.ArrayList;
64import java.util.Collection;
65import java.util.Date;
66import java.util.List;
67import java.util.concurrent.Executor;
68
69public 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