DocumentsActivity.java revision a8f03a2d81394002242d1909488eaa4a0467ea05
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.State.ACTION_CREATE;
20import static com.android.documentsui.State.ACTION_GET_CONTENT;
21import static com.android.documentsui.State.ACTION_OPEN;
22import static com.android.documentsui.State.ACTION_OPEN_TREE;
23import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION;
24import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_NONE;
25
26import android.app.Activity;
27import android.app.Fragment;
28import android.app.FragmentManager;
29import android.content.ClipData;
30import android.content.ComponentName;
31import android.content.ContentProviderClient;
32import android.content.ContentResolver;
33import android.content.ContentValues;
34import android.content.Intent;
35import android.content.pm.ResolveInfo;
36import android.content.res.Resources;
37import android.net.Uri;
38import android.os.AsyncTask;
39import android.os.Bundle;
40import android.os.Parcelable;
41import android.provider.DocumentsContract;
42import android.support.design.widget.Snackbar;
43import android.util.Log;
44import android.view.Menu;
45import android.view.MenuItem;
46import android.view.View;
47import android.widget.BaseAdapter;
48import android.widget.Spinner;
49import android.widget.Toolbar;
50
51import com.android.documentsui.RecentsProvider.RecentColumns;
52import com.android.documentsui.RecentsProvider.ResumeColumns;
53import com.android.documentsui.dirlist.DirectoryFragment;
54import com.android.documentsui.model.DocumentInfo;
55import com.android.documentsui.model.DurableUtils;
56import com.android.documentsui.model.RootInfo;
57
58import java.util.Arrays;
59import java.util.List;
60
61public class DocumentsActivity extends BaseActivity {
62    private static final int CODE_FORWARD = 42;
63    private static final String TAG = "DocumentsActivity";
64
65    private Toolbar mToolbar;
66    private Spinner mToolbarStack;
67
68    private Toolbar mRootsToolbar;
69
70    private ItemSelectedListener mStackListener;
71    private BaseAdapter mStackAdapter;
72
73    public DocumentsActivity() {
74        super(R.layout.docs_activity, TAG);
75    }
76
77    @Override
78    public void onCreate(Bundle icicle) {
79        super.onCreate(icicle);
80
81        final Resources res = getResources();
82
83        mDrawer = DrawerController.create(this);
84        mToolbar = (Toolbar) findViewById(R.id.toolbar);
85
86        mStackAdapter = new StackAdapter();
87        mStackListener = new ItemSelectedListener();
88        mToolbarStack = (Spinner) findViewById(R.id.stack);
89        mToolbarStack.setOnItemSelectedListener(mStackListener);
90
91        mRootsToolbar = (Toolbar) findViewById(R.id.roots_toolbar);
92
93        setActionBar(mToolbar);
94
95        if (mState.action == ACTION_CREATE) {
96            final String mimeType = getIntent().getType();
97            final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
98            SaveFragment.show(getFragmentManager(), mimeType, title);
99        } else if (mState.action == ACTION_OPEN_TREE ||
100                   mState.action == ACTION_PICK_COPY_DESTINATION) {
101            PickFragment.show(getFragmentManager());
102        }
103
104        if (mState.action == ACTION_GET_CONTENT) {
105            final Intent moreApps = new Intent(getIntent());
106            moreApps.setComponent(null);
107            moreApps.setPackage(null);
108            RootsFragment.show(getFragmentManager(), moreApps);
109        } else if (mState.action == ACTION_OPEN ||
110                   mState.action == ACTION_CREATE ||
111                   mState.action == ACTION_OPEN_TREE ||
112                   mState.action == ACTION_PICK_COPY_DESTINATION) {
113            RootsFragment.show(getFragmentManager(), null);
114        }
115
116        if (!mState.restored) {
117            // In this case, we set the activity title in AsyncTask.onPostExecute().  To prevent
118            // talkback from reading aloud the default title, we clear it here.
119            setTitle("");
120            new RestoreStackTask().execute();
121        } else {
122            onCurrentDirectoryChanged(ANIM_NONE);
123        }
124    }
125
126    @Override
127    State buildState() {
128        State state = buildDefaultState();
129
130        final Intent intent = getIntent();
131        final String action = intent.getAction();
132        if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
133            state.action = ACTION_OPEN;
134        } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
135            state.action = ACTION_CREATE;
136        } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
137            state.action = ACTION_GET_CONTENT;
138        } else if (Intent.ACTION_OPEN_DOCUMENT_TREE.equals(action)) {
139            state.action = ACTION_OPEN_TREE;
140        } else if (Shared.ACTION_PICK_COPY_DESTINATION.equals(action)) {
141            state.action = ACTION_PICK_COPY_DESTINATION;
142        }
143
144        if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT) {
145            state.allowMultiple = intent.getBooleanExtra(
146                    Intent.EXTRA_ALLOW_MULTIPLE, false);
147        }
148
149        if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT
150                || state.action == ACTION_CREATE) {
151            state.openableOnly = intent.hasCategory(Intent.CATEGORY_OPENABLE);
152        }
153
154        if (state.action == ACTION_PICK_COPY_DESTINATION) {
155            state.directoryCopy = intent.getBooleanExtra(
156                    Shared.EXTRA_DIRECTORY_COPY, false);
157            state.transferMode = intent.getIntExtra(CopyService.EXTRA_TRANSFER_MODE,
158                    CopyService.TRANSFER_MODE_COPY);
159        }
160
161        return state;
162    }
163
164    @Override
165    void onStackRestored(boolean restored, boolean external) {
166        // Show drawer when no stack restored, but only when requesting
167        // non-visual content. However, if we last used an external app,
168        // drawer is always shown.
169
170        boolean showDrawer = false;
171        if (!restored) {
172            showDrawer = true;
173        }
174        if (MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
175            showDrawer = false;
176        }
177        if (external && mState.action == ACTION_GET_CONTENT) {
178            showDrawer = true;
179        }
180
181        if (showDrawer) {
182            setRootsDrawerOpen(true);
183        }
184    }
185
186    public void onAppPicked(ResolveInfo info) {
187        final Intent intent = new Intent(getIntent());
188        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
189        intent.setComponent(new ComponentName(
190                info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
191        startActivityForResult(intent, CODE_FORWARD);
192    }
193
194    @Override
195    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
196        Log.d(TAG, "onActivityResult() code=" + resultCode);
197
198        // Only relay back results when not canceled; otherwise stick around to
199        // let the user pick another app/backend.
200        if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) {
201
202            // Remember that we last picked via external app
203            final String packageName = getCallingPackageMaybeExtra();
204            final ContentValues values = new ContentValues();
205            values.put(ResumeColumns.EXTERNAL, 1);
206            getContentResolver().insert(RecentsProvider.buildResume(packageName), values);
207
208            // Pass back result to original caller
209            setResult(resultCode, data);
210            finish();
211        } else {
212            super.onActivityResult(requestCode, resultCode, data);
213        }
214    }
215
216    @Override
217    protected void onPostCreate(Bundle savedInstanceState) {
218        super.onPostCreate(savedInstanceState);
219        mDrawer.syncState();
220        updateActionBar();
221    }
222
223    public void setRootsDrawerOpen(boolean open) {
224        mDrawer.setOpen(open);
225    }
226
227    @Override
228    public void updateActionBar() {
229        if (mRootsToolbar != null) {
230            final String prompt = getIntent().getStringExtra(DocumentsContract.EXTRA_PROMPT);
231            if (prompt != null) {
232                mRootsToolbar.setTitle(prompt);
233            } else {
234                if (mState.action == ACTION_OPEN ||
235                    mState.action == ACTION_GET_CONTENT ||
236                    mState.action == ACTION_OPEN_TREE) {
237                    mRootsToolbar.setTitle(R.string.title_open);
238                } else if (mState.action == ACTION_CREATE ||
239                           mState.action == ACTION_PICK_COPY_DESTINATION) {
240                    mRootsToolbar.setTitle(R.string.title_save);
241                }
242            }
243        }
244
245        if (mDrawer.isUnlocked()) {
246            mToolbar.setNavigationIcon(R.drawable.ic_hamburger);
247            mToolbar.setNavigationContentDescription(R.string.drawer_open);
248            mToolbar.setNavigationOnClickListener(
249                    new View.OnClickListener() {
250                        @Override
251                        public void onClick(View v) {
252                            setRootsDrawerOpen(true);
253                        }
254                    });
255        } else {
256            mToolbar.setNavigationIcon(null);
257            mToolbar.setNavigationContentDescription(R.string.drawer_open);
258            mToolbar.setNavigationOnClickListener(null);
259        }
260
261        if (mSearchManager.isExpanded()) {
262            mToolbar.setTitle(null);
263            mToolbarStack.setVisibility(View.GONE);
264            mToolbarStack.setAdapter(null);
265        } else {
266            if (mState.stack.size() <= 1) {
267                mToolbar.setTitle(getCurrentRoot().title);
268                mToolbarStack.setVisibility(View.GONE);
269                mToolbarStack.setAdapter(null);
270            } else {
271                mToolbar.setTitle(null);
272                mToolbarStack.setVisibility(View.VISIBLE);
273                mToolbarStack.setAdapter(mStackAdapter);
274
275                mStackListener.mIgnoreNextNavigation = true;
276                mToolbarStack.setSelection(mStackAdapter.getCount() - 1);
277            }
278        }
279    }
280
281    @Override
282    public boolean onCreateOptionsMenu(Menu menu) {
283        boolean showMenu = super.onCreateOptionsMenu(menu);
284
285        expandMenus(menu);
286        return showMenu;
287    }
288
289    @Override
290    public boolean onPrepareOptionsMenu(Menu menu) {
291        super.onPrepareOptionsMenu(menu);
292
293        final DocumentInfo cwd = getCurrentDirectory();
294
295        final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
296        final MenuItem grid = menu.findItem(R.id.menu_grid);
297        final MenuItem list = menu.findItem(R.id.menu_list);
298        final MenuItem fileSize = menu.findItem(R.id.menu_file_size);
299        final MenuItem settings = menu.findItem(R.id.menu_settings);
300
301        boolean recents = cwd == null;
302        boolean picking = mState.action == ACTION_CREATE
303                || mState.action == ACTION_OPEN_TREE
304                || mState.action == ACTION_PICK_COPY_DESTINATION;
305
306        createDir.setVisible(picking && !recents && cwd.isCreateSupported());
307        mSearchManager.showMenu(!picking);
308
309        // No display options in recent directories
310        grid.setVisible(!(picking && recents));
311        list.setVisible(!(picking && recents));
312
313        fileSize.setVisible(fileSize.isVisible() && !picking);
314        settings.setVisible(false);
315
316        if (mState.action == ACTION_CREATE) {
317            final FragmentManager fm = getFragmentManager();
318            SaveFragment.get(fm).prepareForDirectory(cwd);
319        }
320
321        Menus.disableHiddenItems(menu);
322
323        return true;
324    }
325
326    @Override
327    public boolean onOptionsItemSelected(MenuItem item) {
328        return mDrawer.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
329    }
330
331    @Override
332    void onDirectoryChanged(int anim) {
333        final FragmentManager fm = getFragmentManager();
334        final RootInfo root = getCurrentRoot();
335        final DocumentInfo cwd = getCurrentDirectory();
336
337        if (cwd == null) {
338            // No directory means recents
339            if (mState.action == ACTION_CREATE ||
340                mState.action == ACTION_OPEN_TREE ||
341                mState.action == ACTION_PICK_COPY_DESTINATION) {
342                RecentsCreateFragment.show(fm);
343            } else {
344                DirectoryFragment.showRecentsOpen(fm, anim);
345
346                // Start recents in grid when requesting visual things
347                final boolean visualMimes = MimePredicate.mimeMatches(
348                        MimePredicate.VISUAL_MIMES, mState.acceptMimes);
349                mState.userMode = visualMimes ? State.MODE_GRID : State.MODE_LIST;
350                mState.derivedMode = mState.userMode;
351            }
352        } else {
353            if (mState.currentSearch != null) {
354                // Ongoing search
355                DirectoryFragment.showSearch(fm, root, mState.currentSearch, anim);
356            } else {
357                // Normal boring directory
358                DirectoryFragment.showNormal(fm, root, cwd, anim);
359            }
360        }
361
362        // Forget any replacement target
363        if (mState.action == ACTION_CREATE) {
364            final SaveFragment save = SaveFragment.get(fm);
365            if (save != null) {
366                save.setReplaceTarget(null);
367            }
368        }
369
370        if (mState.action == ACTION_OPEN_TREE ||
371            mState.action == ACTION_PICK_COPY_DESTINATION) {
372            final PickFragment pick = PickFragment.get(fm);
373            if (pick != null) {
374                pick.setPickTarget(mState.action, mState.transferMode, cwd);
375            }
376        }
377    }
378
379    void onSaveRequested(DocumentInfo replaceTarget) {
380        new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getExecutorForCurrentDirectory());
381    }
382
383    void onSaveRequested(String mimeType, String displayName) {
384        new CreateFinishTask(mimeType, displayName).executeOnExecutor(getExecutorForCurrentDirectory());
385    }
386
387    @Override
388    void onRootPicked(RootInfo root) {
389        super.onRootPicked(root);
390        setRootsDrawerOpen(false);
391    }
392
393    @Override
394    public void onDocumentPicked(DocumentInfo doc, DocumentContext context) {
395        final FragmentManager fm = getFragmentManager();
396        if (doc.isContainer()) {
397            openContainerDocument(doc);
398        } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
399            // Explicit file picked, return
400            new ExistingFinishTask(doc.derivedUri).executeOnExecutor(getExecutorForCurrentDirectory());
401        } else if (mState.action == ACTION_CREATE) {
402            // Replace selected file
403            SaveFragment.get(fm).setReplaceTarget(doc);
404        }
405    }
406
407    @Override
408    public void onDocumentsPicked(List<DocumentInfo> docs) {
409        if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
410            final int size = docs.size();
411            final Uri[] uris = new Uri[size];
412            for (int i = 0; i < size; i++) {
413                uris[i] = docs.get(i).derivedUri;
414            }
415            new ExistingFinishTask(uris).executeOnExecutor(getExecutorForCurrentDirectory());
416        }
417    }
418
419    public void onPickRequested(DocumentInfo pickTarget) {
420        Uri result;
421        if (mState.action == ACTION_OPEN_TREE) {
422            result = DocumentsContract.buildTreeDocumentUri(
423                    pickTarget.authority, pickTarget.documentId);
424        } else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
425            result = pickTarget.derivedUri;
426        } else {
427            // Should not be reached.
428            throw new IllegalStateException("Invalid mState.action.");
429        }
430        new PickFinishTask(result).executeOnExecutor(getExecutorForCurrentDirectory());
431    }
432
433    @Override
434    void saveStackBlocking() {
435        final ContentResolver resolver = getContentResolver();
436        final ContentValues values = new ContentValues();
437
438        final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
439        if (mState.action == ACTION_CREATE ||
440            mState.action == ACTION_OPEN_TREE ||
441            mState.action == ACTION_PICK_COPY_DESTINATION) {
442            // Remember stack for last create
443            values.clear();
444            values.put(RecentColumns.KEY, mState.stack.buildKey());
445            values.put(RecentColumns.STACK, rawStack);
446            resolver.insert(RecentsProvider.buildRecent(), values);
447        }
448
449        // Remember location for next app launch
450        final String packageName = getCallingPackageMaybeExtra();
451        values.clear();
452        values.put(ResumeColumns.STACK, rawStack);
453        values.put(ResumeColumns.EXTERNAL, 0);
454        resolver.insert(RecentsProvider.buildResume(packageName), values);
455    }
456
457    @Override
458    void onTaskFinished(Uri... uris) {
459        Log.d(TAG, "onFinished() " + Arrays.toString(uris));
460
461        final Intent intent = new Intent();
462        if (uris.length == 1) {
463            intent.setData(uris[0]);
464        } else if (uris.length > 1) {
465            final ClipData clipData = new ClipData(
466                    null, mState.acceptMimes, new ClipData.Item(uris[0]));
467            for (int i = 1; i < uris.length; i++) {
468                clipData.addItem(new ClipData.Item(uris[i]));
469            }
470            intent.setClipData(clipData);
471        }
472
473        if (mState.action == ACTION_GET_CONTENT) {
474            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
475        } else if (mState.action == ACTION_OPEN_TREE) {
476            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
477                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
478                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
479                    | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
480        } else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
481            // Picking a copy destination is only used internally by us, so we
482            // don't need to extend permissions to the caller.
483            intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
484            intent.putExtra(CopyService.EXTRA_TRANSFER_MODE, mState.transferMode);
485        } else {
486            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
487                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
488                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
489        }
490
491        setResult(Activity.RESULT_OK, intent);
492        finish();
493    }
494
495    public static DocumentsActivity get(Fragment fragment) {
496        return (DocumentsActivity) fragment.getActivity();
497    }
498
499    private final class PickFinishTask extends AsyncTask<Void, Void, Void> {
500        private final Uri mUri;
501
502        public PickFinishTask(Uri uri) {
503            mUri = uri;
504        }
505
506        @Override
507        protected Void doInBackground(Void... params) {
508            saveStackBlocking();
509            return null;
510        }
511
512        @Override
513        protected void onPostExecute(Void result) {
514            onTaskFinished(mUri);
515        }
516    }
517
518    final class ExistingFinishTask extends AsyncTask<Void, Void, Void> {
519        private final Uri[] mUris;
520
521        public ExistingFinishTask(Uri... uris) {
522            mUris = uris;
523        }
524
525        @Override
526        protected Void doInBackground(Void... params) {
527            saveStackBlocking();
528            return null;
529        }
530
531        @Override
532        protected void onPostExecute(Void result) {
533            onTaskFinished(mUris);
534        }
535    }
536
537    /**
538     * Task that creates a new document in the background.
539     */
540    final class CreateFinishTask extends AsyncTask<Void, Void, Uri> {
541        private final String mMimeType;
542        private final String mDisplayName;
543
544        public CreateFinishTask(String mimeType, String displayName) {
545            mMimeType = mimeType;
546            mDisplayName = displayName;
547        }
548
549        @Override
550        protected void onPreExecute() {
551            setPending(true);
552        }
553
554        @Override
555        protected Uri doInBackground(Void... params) {
556            final ContentResolver resolver = getContentResolver();
557            final DocumentInfo cwd = getCurrentDirectory();
558
559            ContentProviderClient client = null;
560            Uri childUri = null;
561            try {
562                client = DocumentsApplication.acquireUnstableProviderOrThrow(
563                        resolver, cwd.derivedUri.getAuthority());
564                childUri = DocumentsContract.createDocument(
565                        client, cwd.derivedUri, mMimeType, mDisplayName);
566            } catch (Exception e) {
567                Log.w(TAG, "Failed to create document", e);
568            } finally {
569                ContentProviderClient.releaseQuietly(client);
570            }
571
572            if (childUri != null) {
573                saveStackBlocking();
574            }
575
576            return childUri;
577        }
578
579        @Override
580        protected void onPostExecute(Uri result) {
581            if (result != null) {
582                onTaskFinished(result);
583            } else {
584                Snackbars.makeSnackbar(
585                    DocumentsActivity.this, R.string.save_error, Snackbar.LENGTH_SHORT).show();
586            }
587
588            setPending(false);
589        }
590    }
591}
592