1/*
2 * Copyright (C) 2013 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.BaseActivity.State.ACTION_BROWSE;
20import static com.android.documentsui.BaseActivity.State.ACTION_CREATE;
21import static com.android.documentsui.BaseActivity.State.ACTION_GET_CONTENT;
22import static com.android.documentsui.BaseActivity.State.ACTION_MANAGE;
23import static com.android.documentsui.BaseActivity.State.ACTION_OPEN;
24import static com.android.documentsui.BaseActivity.State.ACTION_OPEN_COPY_DESTINATION;
25import static com.android.documentsui.BaseActivity.State.ACTION_OPEN_TREE;
26import static com.android.documentsui.DirectoryFragment.ANIM_DOWN;
27import static com.android.documentsui.DirectoryFragment.ANIM_NONE;
28import static com.android.documentsui.DirectoryFragment.ANIM_UP;
29
30import java.util.ArrayList;
31import java.util.Arrays;
32import java.util.List;
33
34import android.app.ActionBar;
35import android.app.Activity;
36import android.app.Fragment;
37import android.app.FragmentManager;
38import android.content.ActivityNotFoundException;
39import android.content.ClipData;
40import android.content.ComponentName;
41import android.content.ContentProviderClient;
42import android.content.ContentResolver;
43import android.content.ContentValues;
44import android.content.Context;
45import android.content.Intent;
46import android.content.pm.ResolveInfo;
47import android.content.res.Resources;
48import android.graphics.Point;
49import android.net.Uri;
50import android.os.AsyncTask;
51import android.os.Bundle;
52import android.os.Parcelable;
53import android.provider.DocumentsContract;
54import android.provider.DocumentsContract.Root;
55import android.support.v4.app.ActionBarDrawerToggle;
56import android.support.v4.widget.DrawerLayout;
57import android.support.v4.widget.DrawerLayout.DrawerListener;
58import android.util.Log;
59import android.view.Menu;
60import android.view.MenuItem;
61import android.view.View;
62import android.view.WindowManager;
63import android.widget.BaseAdapter;
64import android.widget.Spinner;
65import android.widget.Toast;
66import android.widget.Toolbar;
67
68import com.android.documentsui.RecentsProvider.RecentColumns;
69import com.android.documentsui.RecentsProvider.ResumeColumns;
70import com.android.documentsui.model.DocumentInfo;
71import com.android.documentsui.model.DocumentStack;
72import com.android.documentsui.model.DurableUtils;
73import com.android.documentsui.model.RootInfo;
74
75public class DocumentsActivity extends BaseActivity {
76    private static final int CODE_FORWARD = 42;
77    public static final String TAG = "Documents";
78
79    private boolean mShowAsDialog;
80
81    private Toolbar mToolbar;
82    private Spinner mToolbarStack;
83
84    private Toolbar mRootsToolbar;
85
86    private DrawerLayout mDrawerLayout;
87    private ActionBarDrawerToggle mDrawerToggle;
88    private View mRootsDrawer;
89
90    private DirectoryContainerView mDirectoryContainer;
91
92    private State mState;
93
94    private ItemSelectedListener mStackListener;
95    private BaseAdapter mStackAdapter;
96
97    public DocumentsActivity() {
98        super(TAG);
99    }
100
101    @Override
102    public void onCreate(Bundle icicle) {
103        super.onCreate(icicle);
104
105        setResult(Activity.RESULT_CANCELED);
106        setContentView(R.layout.activity);
107
108        final Context context = this;
109        final Resources res = getResources();
110        mShowAsDialog = res.getBoolean(R.bool.show_as_dialog);
111
112        if (mShowAsDialog) {
113            // Strongly define our horizontal dimension; we leave vertical as
114            // WRAP_CONTENT so that system resizes us when IME is showing.
115            final WindowManager.LayoutParams a = getWindow().getAttributes();
116
117            final Point size = new Point();
118            getWindowManager().getDefaultDisplay().getSize(size);
119            a.width = (int) res.getFraction(R.dimen.dialog_width, size.x, size.x);
120
121            getWindow().setAttributes(a);
122
123        } else {
124            // Non-dialog means we have a drawer
125            mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
126
127            mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
128                    R.drawable.ic_hamburger, R.string.drawer_open, R.string.drawer_close);
129
130            mDrawerLayout.setDrawerListener(mDrawerListener);
131
132            mRootsDrawer = findViewById(R.id.drawer_roots);
133        }
134
135        mDirectoryContainer = (DirectoryContainerView) findViewById(R.id.container_directory);
136
137        mState = (icicle != null)
138                ? icicle.<State>getParcelable(EXTRA_STATE)
139                : buildDefaultState();
140
141        mToolbar = (Toolbar) findViewById(R.id.toolbar);
142        mToolbar.setTitleTextAppearance(context,
143                android.R.style.TextAppearance_DeviceDefault_Widget_ActionBar_Title);
144
145        mStackAdapter = new StackAdapter();
146        mStackListener = new ItemSelectedListener();
147        mToolbarStack = (Spinner) findViewById(R.id.stack);
148        mToolbarStack.setOnItemSelectedListener(mStackListener);
149
150        mRootsToolbar = (Toolbar) findViewById(R.id.roots_toolbar);
151        if (mRootsToolbar != null) {
152            mRootsToolbar.setTitleTextAppearance(context,
153                    android.R.style.TextAppearance_DeviceDefault_Widget_ActionBar_Title);
154        }
155
156        setActionBar(mToolbar);
157
158        // Hide roots when we're managing a specific root
159        if (mState.action == ACTION_MANAGE || mState.action == ACTION_BROWSE) {
160            if (mShowAsDialog) {
161                findViewById(R.id.container_roots).setVisibility(View.GONE);
162            } else {
163                mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
164            }
165        }
166
167        if (mState.action == ACTION_CREATE) {
168            final String mimeType = getIntent().getType();
169            final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
170            SaveFragment.show(getFragmentManager(), mimeType, title);
171        } else if (mState.action == ACTION_OPEN_TREE ||
172                   mState.action == ACTION_OPEN_COPY_DESTINATION) {
173            PickFragment.show(getFragmentManager());
174        }
175
176        if (mState.action == ACTION_GET_CONTENT) {
177            final Intent moreApps = new Intent(getIntent());
178            moreApps.setComponent(null);
179            moreApps.setPackage(null);
180            RootsFragment.show(getFragmentManager(), moreApps);
181        } else if (mState.action == ACTION_OPEN ||
182                   mState.action == ACTION_CREATE ||
183                   mState.action == ACTION_OPEN_TREE ||
184                   mState.action == ACTION_OPEN_COPY_DESTINATION) {
185            RootsFragment.show(getFragmentManager(), null);
186        }
187
188        if (!mState.restored) {
189            // In this case, we set the activity title in AsyncTask.onPostExecute().  To prevent
190            // talkback from reading aloud the default title, we clear it here.
191            setTitle("");
192            if (mState.action == ACTION_MANAGE || mState.action == ACTION_BROWSE) {
193                final Uri rootUri = getIntent().getData();
194                new RestoreRootTask(rootUri).executeOnExecutor(getCurrentExecutor());
195            } else {
196                new RestoreStackTask().execute();
197            }
198
199            // Show a failure dialog if there was a failed operation.
200            final Intent intent = getIntent();
201            final DocumentStack dstStack = intent.getParcelableExtra(CopyService.EXTRA_STACK);
202            final int failure = intent.getIntExtra(CopyService.EXTRA_FAILURE, 0);
203            if (failure != 0) {
204                final ArrayList<DocumentInfo> failedSrcList =
205                        intent.getParcelableArrayListExtra(CopyService.EXTRA_SRC_LIST);
206                FailureDialogFragment.show(getFragmentManager(), failure, failedSrcList, dstStack);
207            }
208        } else {
209            onCurrentDirectoryChanged(ANIM_NONE);
210        }
211    }
212
213    private State buildDefaultState() {
214        State state = new State();
215
216        final Intent intent = getIntent();
217        final String action = intent.getAction();
218        if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
219            state.action = ACTION_OPEN;
220        } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
221            state.action = ACTION_CREATE;
222        } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
223            state.action = ACTION_GET_CONTENT;
224        } else if (Intent.ACTION_OPEN_DOCUMENT_TREE.equals(action)) {
225            state.action = ACTION_OPEN_TREE;
226        } else if (DocumentsContract.ACTION_MANAGE_ROOT.equals(action)) {
227            state.action = ACTION_MANAGE;
228        } else if (DocumentsContract.ACTION_BROWSE_DOCUMENT_ROOT.equals(action)) {
229            state.action = ACTION_BROWSE;
230        } else if (DocumentsIntent.ACTION_OPEN_COPY_DESTINATION.equals(action)) {
231            state.action = ACTION_OPEN_COPY_DESTINATION;
232        }
233
234        if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT) {
235            state.allowMultiple = intent.getBooleanExtra(
236                    Intent.EXTRA_ALLOW_MULTIPLE, false);
237        }
238
239        if (state.action == ACTION_MANAGE || state.action == ACTION_BROWSE) {
240            state.acceptMimes = new String[] { "*/*" };
241            state.allowMultiple = true;
242        } else if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
243            state.acceptMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES);
244        } else {
245            state.acceptMimes = new String[] { intent.getType() };
246        }
247
248        state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
249        state.forceAdvanced = intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, false);
250        state.showAdvanced = state.forceAdvanced
251                | LocalPreferences.getDisplayAdvancedDevices(this);
252
253        if (state.action == ACTION_MANAGE || state.action == ACTION_BROWSE) {
254            state.showSize = true;
255        } else {
256            state.showSize = LocalPreferences.getDisplayFileSize(this);
257        }
258        if (state.action == ACTION_OPEN_COPY_DESTINATION) {
259            state.directoryCopy = intent.getBooleanExtra(
260                    BaseActivity.DocumentsIntent.EXTRA_DIRECTORY_COPY, false);
261        }
262
263        state.excludedAuthorities = getExcludedAuthorities();
264
265        return state;
266    }
267
268    private class RestoreRootTask extends AsyncTask<Void, Void, RootInfo> {
269        private Uri mRootUri;
270
271        public RestoreRootTask(Uri rootUri) {
272            mRootUri = rootUri;
273        }
274
275        @Override
276        protected RootInfo doInBackground(Void... params) {
277            final String rootId = DocumentsContract.getRootId(mRootUri);
278            return mRoots.getRootOneshot(mRootUri.getAuthority(), rootId);
279        }
280
281        @Override
282        protected void onPostExecute(RootInfo root) {
283            if (isDestroyed()) return;
284            mState.restored = true;
285
286            if (root != null) {
287                onRootPicked(root);
288            } else {
289                Log.w(TAG, "Failed to find root: " + mRootUri);
290                finish();
291            }
292        }
293    }
294
295    @Override
296    void onStackRestored(boolean restored, boolean external) {
297        // Show drawer when no stack restored, but only when requesting
298        // non-visual content. However, if we last used an external app,
299        // drawer is always shown.
300
301        boolean showDrawer = false;
302        if (!restored) {
303            showDrawer = true;
304        }
305        if (MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
306            showDrawer = false;
307        }
308        if (external && mState.action == ACTION_GET_CONTENT) {
309            showDrawer = true;
310        }
311
312        if (showDrawer) {
313            setRootsDrawerOpen(true);
314        }
315    }
316
317    public void onAppPicked(ResolveInfo info) {
318        final Intent intent = new Intent(getIntent());
319        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
320        intent.setComponent(new ComponentName(
321                info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
322        startActivityForResult(intent, CODE_FORWARD);
323    }
324
325    @Override
326    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
327        Log.d(TAG, "onActivityResult() code=" + resultCode);
328
329        // Only relay back results when not canceled; otherwise stick around to
330        // let the user pick another app/backend.
331        if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) {
332
333            // Remember that we last picked via external app
334            final String packageName = getCallingPackageMaybeExtra();
335            final ContentValues values = new ContentValues();
336            values.put(ResumeColumns.EXTERNAL, 1);
337            getContentResolver().insert(RecentsProvider.buildResume(packageName), values);
338
339            // Pass back result to original caller
340            setResult(resultCode, data);
341            finish();
342        } else {
343            super.onActivityResult(requestCode, resultCode, data);
344        }
345    }
346
347    private DrawerListener mDrawerListener = new DrawerListener() {
348        @Override
349        public void onDrawerSlide(View drawerView, float slideOffset) {
350            mDrawerToggle.onDrawerSlide(drawerView, slideOffset);
351        }
352
353        @Override
354        public void onDrawerOpened(View drawerView) {
355            mDrawerToggle.onDrawerOpened(drawerView);
356        }
357
358        @Override
359        public void onDrawerClosed(View drawerView) {
360            mDrawerToggle.onDrawerClosed(drawerView);
361        }
362
363        @Override
364        public void onDrawerStateChanged(int newState) {
365            mDrawerToggle.onDrawerStateChanged(newState);
366        }
367    };
368
369    @Override
370    protected void onPostCreate(Bundle savedInstanceState) {
371        super.onPostCreate(savedInstanceState);
372        if (mDrawerToggle != null) {
373            mDrawerToggle.syncState();
374        }
375        updateActionBar();
376    }
377
378    public void setRootsDrawerOpen(boolean open) {
379        if (!mShowAsDialog) {
380            if (open) {
381                mDrawerLayout.openDrawer(mRootsDrawer);
382            } else {
383                mDrawerLayout.closeDrawer(mRootsDrawer);
384            }
385        }
386    }
387
388    private boolean isRootsDrawerOpen() {
389        if (mShowAsDialog) {
390            return false;
391        } else {
392            return mDrawerLayout.isDrawerOpen(mRootsDrawer);
393        }
394    }
395
396    @Override
397    public void updateActionBar() {
398        if (mRootsToolbar != null) {
399            final String prompt = getIntent().getStringExtra(DocumentsContract.EXTRA_PROMPT);
400            if (prompt != null) {
401                mRootsToolbar.setTitle(prompt);
402            } else {
403                if (mState.action == ACTION_OPEN ||
404                    mState.action == ACTION_GET_CONTENT ||
405                    mState.action == ACTION_OPEN_TREE) {
406                    mRootsToolbar.setTitle(R.string.title_open);
407                } else if (mState.action == ACTION_CREATE ||
408                           mState.action == ACTION_OPEN_COPY_DESTINATION) {
409                    mRootsToolbar.setTitle(R.string.title_save);
410                }
411            }
412        }
413
414        if (!mShowAsDialog && mDrawerLayout.getDrawerLockMode(mRootsDrawer) ==
415                DrawerLayout.LOCK_MODE_UNLOCKED) {
416            mToolbar.setNavigationIcon(R.drawable.ic_hamburger);
417            mToolbar.setNavigationContentDescription(R.string.drawer_open);
418            mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
419                @Override
420                public void onClick(View v) {
421                    setRootsDrawerOpen(true);
422                }
423            });
424        } else {
425            mToolbar.setNavigationIcon(null);
426            mToolbar.setNavigationContentDescription(R.string.drawer_open);
427            mToolbar.setNavigationOnClickListener(null);
428        }
429
430        if (mSearchManager.isExpanded()) {
431            mToolbar.setTitle(null);
432            mToolbarStack.setVisibility(View.GONE);
433            mToolbarStack.setAdapter(null);
434        } else {
435            if (mState.stack.size() <= 1) {
436                mToolbar.setTitle(getCurrentRoot().title);
437                mToolbarStack.setVisibility(View.GONE);
438                mToolbarStack.setAdapter(null);
439            } else {
440                mToolbar.setTitle(null);
441                mToolbarStack.setVisibility(View.VISIBLE);
442                mToolbarStack.setAdapter(mStackAdapter);
443
444                mStackListener.mIgnoreNextNavigation = true;
445                mToolbarStack.setSelection(mStackAdapter.getCount() - 1);
446            }
447        }
448    }
449
450    @Override
451    public boolean onCreateOptionsMenu(Menu menu) {
452        boolean showMenu = super.onCreateOptionsMenu(menu);
453
454        // Most actions are visible when showing as dialog
455        if (mShowAsDialog) {
456            expandMenus(menu);
457        }
458        return showMenu;
459    }
460
461    @Override
462    public boolean onPrepareOptionsMenu(Menu menu) {
463        super.onPrepareOptionsMenu(menu);
464
465        final RootInfo root = getCurrentRoot();
466        final DocumentInfo cwd = getCurrentDirectory();
467
468        final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
469        final MenuItem grid = menu.findItem(R.id.menu_grid);
470        final MenuItem list = menu.findItem(R.id.menu_list);
471        final MenuItem advanced = menu.findItem(R.id.menu_advanced);
472        final MenuItem fileSize = menu.findItem(R.id.menu_file_size);
473        final MenuItem settings = menu.findItem(R.id.menu_settings);
474
475        boolean fileSizeVisible = !(mState.action == ACTION_MANAGE
476                || mState.action == ACTION_BROWSE);
477        if (mState.action == ACTION_CREATE
478                || mState.action == ACTION_OPEN_TREE
479                || mState.action == ACTION_OPEN_COPY_DESTINATION) {
480            createDir.setVisible(cwd != null && cwd.isCreateSupported());
481            mSearchManager.showMenu(false);
482
483            // No display options in recent directories
484            if (cwd == null) {
485                grid.setVisible(false);
486                list.setVisible(false);
487                fileSizeVisible = false;
488            }
489
490            if (mState.action == ACTION_CREATE) {
491                final FragmentManager fm = getFragmentManager();
492                SaveFragment.get(fm).setSaveEnabled(cwd != null && cwd.isCreateSupported());
493            }
494        } else {
495            createDir.setVisible(false);
496        }
497
498        advanced.setVisible(!(mState.action == ACTION_MANAGE || mState.action == ACTION_BROWSE));
499        fileSize.setVisible(fileSizeVisible);
500
501        settings.setVisible((mState.action == ACTION_MANAGE || mState.action == ACTION_BROWSE)
502                && (root.flags & Root.FLAG_HAS_SETTINGS) != 0);
503
504        return true;
505    }
506
507    @Override
508    public boolean onOptionsItemSelected(MenuItem item) {
509        if (mDrawerToggle != null && mDrawerToggle.onOptionsItemSelected(item)) {
510            return true;
511        }
512        return super.onOptionsItemSelected(item);
513    }
514
515    @Override
516    public void onBackPressed() {
517        // While action bar is expanded, the state stack UI is hidden.
518        if (mSearchManager.cancelSearch()) {
519            return;
520        }
521
522        if (!mState.stackTouched) {
523            super.onBackPressed();
524            return;
525        }
526
527        final int size = mState.stack.size();
528        if (size > 1) {
529            mState.stack.pop();
530            onCurrentDirectoryChanged(ANIM_UP);
531        } else if (size == 1 && !isRootsDrawerOpen()) {
532            // TODO: open root drawer once we can capture back key
533            super.onBackPressed();
534        } else {
535            super.onBackPressed();
536        }
537    }
538
539    @Override
540    public State getDisplayState() {
541        return mState;
542    }
543
544    @Override
545    void onDirectoryChanged(int anim) {
546        final FragmentManager fm = getFragmentManager();
547        final RootInfo root = getCurrentRoot();
548        final DocumentInfo cwd = getCurrentDirectory();
549
550        mDirectoryContainer.setDrawDisappearingFirst(anim == ANIM_DOWN);
551
552        if (cwd == null) {
553            // No directory means recents
554            if (mState.action == ACTION_CREATE ||
555                mState.action == ACTION_OPEN_TREE ||
556                mState.action == ACTION_OPEN_COPY_DESTINATION) {
557                RecentsCreateFragment.show(fm);
558            } else {
559                DirectoryFragment.showRecentsOpen(fm, anim);
560
561                // Start recents in grid when requesting visual things
562                final boolean visualMimes = MimePredicate.mimeMatches(
563                        MimePredicate.VISUAL_MIMES, mState.acceptMimes);
564                mState.userMode = visualMimes ? State.MODE_GRID : State.MODE_LIST;
565                mState.derivedMode = mState.userMode;
566            }
567        } else {
568            if (mState.currentSearch != null) {
569                // Ongoing search
570                DirectoryFragment.showSearch(fm, root, mState.currentSearch, anim);
571            } else {
572                // Normal boring directory
573                DirectoryFragment.showNormal(fm, root, cwd, anim);
574            }
575        }
576
577        // Forget any replacement target
578        if (mState.action == ACTION_CREATE) {
579            final SaveFragment save = SaveFragment.get(fm);
580            if (save != null) {
581                save.setReplaceTarget(null);
582            }
583        }
584
585        if (mState.action == ACTION_OPEN_TREE ||
586            mState.action == ACTION_OPEN_COPY_DESTINATION) {
587            final PickFragment pick = PickFragment.get(fm);
588            if (pick != null) {
589                pick.setPickTarget(mState.action, cwd);
590            }
591        }
592    }
593
594    void onSaveRequested(DocumentInfo replaceTarget) {
595        new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getCurrentExecutor());
596    }
597
598    void onSaveRequested(String mimeType, String displayName) {
599        new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor());
600    }
601
602    @Override
603    void onRootPicked(RootInfo root) {
604        super.onRootPicked(root);
605        setRootsDrawerOpen(false);
606    }
607
608    @Override
609    public void onDocumentPicked(DocumentInfo doc) {
610        final FragmentManager fm = getFragmentManager();
611        if (doc.isDirectory()) {
612            mState.stack.push(doc);
613            mState.stackTouched = true;
614            onCurrentDirectoryChanged(ANIM_DOWN);
615        } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
616            // Explicit file picked, return
617            new ExistingFinishTask(doc.derivedUri).executeOnExecutor(getCurrentExecutor());
618        } else if (mState.action == ACTION_CREATE) {
619            // Replace selected file
620            SaveFragment.get(fm).setReplaceTarget(doc);
621        } else if (mState.action == ACTION_MANAGE) {
622            // First try managing the document; we expect manager to filter
623            // based on authority, so we don't grant.
624            final Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
625            manage.setData(doc.derivedUri);
626
627            try {
628                startActivity(manage);
629            } catch (ActivityNotFoundException ex) {
630                // Fall back to viewing
631                final Intent view = new Intent(Intent.ACTION_VIEW);
632                view.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
633                view.setData(doc.derivedUri);
634
635                try {
636                    startActivity(view);
637                } catch (ActivityNotFoundException ex2) {
638                    Toast.makeText(this, R.string.toast_no_application, Toast.LENGTH_SHORT).show();
639                }
640            }
641        } else if (mState.action == ACTION_BROWSE) {
642            // Go straight to viewing
643            final Intent view = new Intent(Intent.ACTION_VIEW);
644            view.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
645            view.setData(doc.derivedUri);
646
647            try {
648                startActivity(view);
649            } catch (ActivityNotFoundException ex) {
650                Toast.makeText(this, R.string.toast_no_application, Toast.LENGTH_SHORT).show();
651            }
652        }
653    }
654
655    @Override
656    public void onDocumentsPicked(List<DocumentInfo> docs) {
657        if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
658            final int size = docs.size();
659            final Uri[] uris = new Uri[size];
660            for (int i = 0; i < size; i++) {
661                uris[i] = docs.get(i).derivedUri;
662            }
663            new ExistingFinishTask(uris).executeOnExecutor(getCurrentExecutor());
664        }
665    }
666
667    public void onPickRequested(DocumentInfo pickTarget) {
668        Uri result;
669        if (mState.action == ACTION_OPEN_TREE) {
670            result = DocumentsContract.buildTreeDocumentUri(
671                    pickTarget.authority, pickTarget.documentId);
672        } else if (mState.action == ACTION_OPEN_COPY_DESTINATION) {
673            result = pickTarget.derivedUri;
674        } else {
675            // Should not be reached.
676            throw new IllegalStateException("Invalid mState.action.");
677        }
678        new PickFinishTask(result).executeOnExecutor(getCurrentExecutor());
679    }
680
681    @Override
682    void saveStackBlocking() {
683        final ContentResolver resolver = getContentResolver();
684        final ContentValues values = new ContentValues();
685
686        final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
687        if (mState.action == ACTION_CREATE ||
688            mState.action == ACTION_OPEN_TREE ||
689            mState.action == ACTION_OPEN_COPY_DESTINATION) {
690            // Remember stack for last create
691            values.clear();
692            values.put(RecentColumns.KEY, mState.stack.buildKey());
693            values.put(RecentColumns.STACK, rawStack);
694            resolver.insert(RecentsProvider.buildRecent(), values);
695        }
696
697        // Remember location for next app launch
698        final String packageName = getCallingPackageMaybeExtra();
699        values.clear();
700        values.put(ResumeColumns.STACK, rawStack);
701        values.put(ResumeColumns.EXTERNAL, 0);
702        resolver.insert(RecentsProvider.buildResume(packageName), values);
703    }
704
705    @Override
706    void onTaskFinished(Uri... uris) {
707        Log.d(TAG, "onFinished() " + Arrays.toString(uris));
708
709        final Intent intent = new Intent();
710        if (uris.length == 1) {
711            intent.setData(uris[0]);
712        } else if (uris.length > 1) {
713            final ClipData clipData = new ClipData(
714                    null, mState.acceptMimes, new ClipData.Item(uris[0]));
715            for (int i = 1; i < uris.length; i++) {
716                clipData.addItem(new ClipData.Item(uris[i]));
717            }
718            intent.setClipData(clipData);
719        }
720
721        if (mState.action == ACTION_GET_CONTENT) {
722            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
723        } else if (mState.action == ACTION_OPEN_TREE) {
724            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
725                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
726                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
727                    | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
728        } else if (mState.action == ACTION_OPEN_COPY_DESTINATION) {
729            // Picking a copy destination is only used internally by us, so we
730            // don't need to extend permissions to the caller.
731            intent.putExtra(CopyService.EXTRA_STACK, (Parcelable) mState.stack);
732        } else {
733            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
734                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
735                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
736        }
737
738        setResult(Activity.RESULT_OK, intent);
739        finish();
740    }
741
742    public static DocumentsActivity get(Fragment fragment) {
743        return (DocumentsActivity) fragment.getActivity();
744    }
745
746    private final class PickFinishTask extends AsyncTask<Void, Void, Void> {
747        private final Uri mUri;
748
749        public PickFinishTask(Uri uri) {
750            mUri = uri;
751        }
752
753        @Override
754        protected Void doInBackground(Void... params) {
755            saveStackBlocking();
756            return null;
757        }
758
759        @Override
760        protected void onPostExecute(Void result) {
761            onTaskFinished(mUri);
762        }
763    }
764
765    final class ExistingFinishTask extends AsyncTask<Void, Void, Void> {
766        private final Uri[] mUris;
767
768        public ExistingFinishTask(Uri... uris) {
769            mUris = uris;
770        }
771
772        @Override
773        protected Void doInBackground(Void... params) {
774            saveStackBlocking();
775            return null;
776        }
777
778        @Override
779        protected void onPostExecute(Void result) {
780            onTaskFinished(mUris);
781        }
782    }
783
784    /**
785     * Task that creates a new document in the background.
786     */
787    final class CreateFinishTask extends AsyncTask<Void, Void, Uri> {
788        private final String mMimeType;
789        private final String mDisplayName;
790
791        public CreateFinishTask(String mimeType, String displayName) {
792            mMimeType = mimeType;
793            mDisplayName = displayName;
794        }
795
796        @Override
797        protected void onPreExecute() {
798            setPending(true);
799        }
800
801        @Override
802        protected Uri doInBackground(Void... params) {
803            final ContentResolver resolver = getContentResolver();
804            final DocumentInfo cwd = getCurrentDirectory();
805
806            ContentProviderClient client = null;
807            Uri childUri = null;
808            try {
809                client = DocumentsApplication.acquireUnstableProviderOrThrow(
810                        resolver, cwd.derivedUri.getAuthority());
811                childUri = DocumentsContract.createDocument(
812                        client, cwd.derivedUri, mMimeType, mDisplayName);
813            } catch (Exception e) {
814                Log.w(TAG, "Failed to create document", e);
815            } finally {
816                ContentProviderClient.releaseQuietly(client);
817            }
818
819            if (childUri != null) {
820                saveStackBlocking();
821            }
822
823            return childUri;
824        }
825
826        @Override
827        protected void onPostExecute(Uri result) {
828            if (result != null) {
829                onTaskFinished(result);
830            } else {
831                Toast.makeText(DocumentsActivity.this, R.string.save_error, Toast.LENGTH_SHORT)
832                        .show();
833            }
834
835            setPending(false);
836        }
837    }
838}
839