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.Shared.DEBUG;
20import static com.android.documentsui.State.ACTION_CREATE;
21import static com.android.documentsui.State.ACTION_GET_CONTENT;
22import static com.android.documentsui.State.ACTION_OPEN;
23import static com.android.documentsui.State.ACTION_OPEN_TREE;
24import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION;
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.database.Cursor;
37import android.net.Uri;
38import android.os.Bundle;
39import android.os.Parcelable;
40import android.provider.DocumentsContract;
41import android.support.design.widget.Snackbar;
42import android.util.Log;
43import android.view.Menu;
44import android.view.MenuItem;
45
46import com.android.documentsui.RecentsProvider.RecentColumns;
47import com.android.documentsui.RecentsProvider.ResumeColumns;
48import com.android.documentsui.dirlist.AnimationView;
49import com.android.documentsui.dirlist.DirectoryFragment;
50import com.android.documentsui.dirlist.Model;
51import com.android.documentsui.model.DocumentInfo;
52import com.android.documentsui.model.DurableUtils;
53import com.android.documentsui.model.RootInfo;
54import com.android.documentsui.services.FileOperationService;
55
56import libcore.io.IoUtils;
57
58import java.io.FileNotFoundException;
59import java.io.IOException;
60import java.util.Arrays;
61import java.util.Collection;
62import java.util.List;
63
64public class DocumentsActivity extends BaseActivity {
65    private static final int CODE_FORWARD = 42;
66    private static final String TAG = "DocumentsActivity";
67
68    public DocumentsActivity() {
69        super(R.layout.documents_activity, TAG);
70    }
71
72    @Override
73    public void onCreate(Bundle icicle) {
74        super.onCreate(icicle);
75
76        if (mState.action == ACTION_CREATE) {
77            final String mimeType = getIntent().getType();
78            final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
79            SaveFragment.show(getFragmentManager(), mimeType, title);
80        } else if (mState.action == ACTION_OPEN_TREE ||
81                   mState.action == ACTION_PICK_COPY_DESTINATION) {
82            PickFragment.show(getFragmentManager());
83        }
84
85        if (mState.action == ACTION_GET_CONTENT) {
86            final Intent moreApps = new Intent(getIntent());
87            moreApps.setComponent(null);
88            moreApps.setPackage(null);
89            RootsFragment.show(getFragmentManager(), moreApps);
90        } else if (mState.action == ACTION_OPEN ||
91                   mState.action == ACTION_CREATE ||
92                   mState.action == ACTION_OPEN_TREE ||
93                   mState.action == ACTION_PICK_COPY_DESTINATION) {
94            RootsFragment.show(getFragmentManager(), null);
95        }
96
97        if (mState.restored) {
98            if (DEBUG) Log.d(TAG, "Stack already resolved");
99        } else {
100            // We set the activity title in AsyncTask.onPostExecute().
101            // To prevent talkback from reading aloud the default title, we clear it here.
102            setTitle("");
103
104            // As a matter of policy we don't load the last used stack for the copy
105            // destination picker (user is already in Files app).
106            // Concensus was that the experice was too confusing.
107            // In all other cases, where the user is visiting us from another app
108            // we restore the stack as last used from that app.
109            if (mState.action == ACTION_PICK_COPY_DESTINATION) {
110                if (DEBUG) Log.d(TAG, "Launching directly into Home directory.");
111                loadRoot(getDefaultRoot());
112            } else {
113                if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package.");
114                new LoadLastUsedStackTask(this).execute();
115            }
116        }
117    }
118
119    @Override
120    void includeState(State state) {
121        final Intent intent = getIntent();
122        final String action = intent.getAction();
123        if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
124            state.action = ACTION_OPEN;
125        } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
126            state.action = ACTION_CREATE;
127        } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
128            state.action = ACTION_GET_CONTENT;
129        } else if (Intent.ACTION_OPEN_DOCUMENT_TREE.equals(action)) {
130            state.action = ACTION_OPEN_TREE;
131        } else if (Shared.ACTION_PICK_COPY_DESTINATION.equals(action)) {
132            state.action = ACTION_PICK_COPY_DESTINATION;
133        }
134
135        if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT) {
136            state.allowMultiple = intent.getBooleanExtra(
137                    Intent.EXTRA_ALLOW_MULTIPLE, false);
138        }
139
140        if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT
141                || state.action == ACTION_CREATE) {
142            state.openableOnly = intent.hasCategory(Intent.CATEGORY_OPENABLE);
143        }
144
145        if (state.action == ACTION_PICK_COPY_DESTINATION) {
146            // Indicates that a copy operation (or move) includes a directory.
147            // Why? Directory creation isn't supported by some roots (like Downloads).
148            // This allows us to restrict available roots to just those with support.
149            state.directoryCopy = intent.getBooleanExtra(
150                    Shared.EXTRA_DIRECTORY_COPY, false);
151            state.copyOperationSubType = intent.getIntExtra(
152                    FileOperationService.EXTRA_OPERATION,
153                    FileOperationService.OPERATION_COPY);
154        }
155    }
156
157    public void onAppPicked(ResolveInfo info) {
158        final Intent intent = new Intent(getIntent());
159        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
160        intent.setComponent(new ComponentName(
161                info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
162        startActivityForResult(intent, CODE_FORWARD);
163    }
164
165    @Override
166    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
167        if (DEBUG) Log.d(TAG, "onActivityResult() code=" + resultCode);
168
169        // Only relay back results when not canceled; otherwise stick around to
170        // let the user pick another app/backend.
171        if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) {
172
173            // Remember that we last picked via external app
174            final String packageName = getCallingPackageMaybeExtra();
175            final ContentValues values = new ContentValues();
176            values.put(ResumeColumns.EXTERNAL, 1);
177            getContentResolver().insert(RecentsProvider.buildResume(packageName), values);
178
179            // Pass back result to original caller
180            setResult(resultCode, data);
181            finish();
182        } else {
183            super.onActivityResult(requestCode, resultCode, data);
184        }
185    }
186
187    @Override
188    protected void onPostCreate(Bundle savedInstanceState) {
189        super.onPostCreate(savedInstanceState);
190        mDrawer.update();
191        mNavigator.update();
192    }
193
194    @Override
195    public String getDrawerTitle() {
196        String title = getIntent().getStringExtra(DocumentsContract.EXTRA_PROMPT);
197        if (title == null) {
198            if (mState.action == ACTION_OPEN ||
199                mState.action == ACTION_GET_CONTENT ||
200                mState.action == ACTION_OPEN_TREE) {
201                title = getResources().getString(R.string.title_open);
202            } else if (mState.action == ACTION_CREATE ||
203                       mState.action == ACTION_PICK_COPY_DESTINATION) {
204                title = getResources().getString(R.string.title_save);
205            } else {
206                // If all else fails, just call it "Documents".
207                title = getResources().getString(R.string.app_label);
208            }
209        }
210
211        return title;
212    }
213
214    @Override
215    public boolean onPrepareOptionsMenu(Menu menu) {
216        super.onPrepareOptionsMenu(menu);
217
218        final DocumentInfo cwd = getCurrentDirectory();
219
220        boolean picking = mState.action == ACTION_CREATE
221                || mState.action == ACTION_OPEN_TREE
222                || mState.action == ACTION_PICK_COPY_DESTINATION;
223
224        if (picking) {
225            // May already be hidden because the root
226            // doesn't support search.
227            mSearchManager.showMenu(false);
228        }
229
230        final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
231        final MenuItem grid = menu.findItem(R.id.menu_grid);
232        final MenuItem list = menu.findItem(R.id.menu_list);
233        final MenuItem fileSize = menu.findItem(R.id.menu_file_size);
234
235
236        createDir.setVisible(picking);
237        createDir.setEnabled(canCreateDirectory());
238
239        // No display options in recent directories
240        boolean inRecents = cwd == null;
241        if (picking && inRecents) {
242            grid.setVisible(false);
243            list.setVisible(false);
244        }
245
246        fileSize.setVisible(fileSize.isVisible() && !picking);
247
248        if (mState.action == ACTION_CREATE) {
249            final FragmentManager fm = getFragmentManager();
250            SaveFragment.get(fm).prepareForDirectory(cwd);
251        }
252
253        Menus.disableHiddenItems(menu);
254
255        return true;
256    }
257
258    @Override
259    void refreshDirectory(int anim) {
260        final FragmentManager fm = getFragmentManager();
261        final RootInfo root = getCurrentRoot();
262        final DocumentInfo cwd = getCurrentDirectory();
263
264        if (cwd == null) {
265            // No directory means recents
266            if (mState.action == ACTION_CREATE ||
267                mState.action == ACTION_OPEN_TREE ||
268                mState.action == ACTION_PICK_COPY_DESTINATION) {
269                RecentsCreateFragment.show(fm);
270            } else {
271                DirectoryFragment.showRecentsOpen(fm, anim);
272
273                // In recents we pick layout mode based on the mimetype,
274                // picking GRID for visual types. We intentionally don't
275                // consult a user's saved preferences here since they are
276                // set per root (not per root and per mimetype).
277                boolean visualMimes = MimePredicate.mimeMatches(
278                        MimePredicate.VISUAL_MIMES, mState.acceptMimes);
279                mState.derivedMode = visualMimes ? State.MODE_GRID : State.MODE_LIST;
280            }
281        } else {
282                // Normal boring directory
283                DirectoryFragment.showDirectory(fm, root, cwd, anim);
284        }
285
286        // Forget any replacement target
287        if (mState.action == ACTION_CREATE) {
288            final SaveFragment save = SaveFragment.get(fm);
289            if (save != null) {
290                save.setReplaceTarget(null);
291            }
292        }
293
294        if (mState.action == ACTION_OPEN_TREE ||
295            mState.action == ACTION_PICK_COPY_DESTINATION) {
296            final PickFragment pick = PickFragment.get(fm);
297            if (pick != null) {
298                pick.setPickTarget(mState.action, mState.copyOperationSubType, cwd);
299            }
300        }
301    }
302
303    void onSaveRequested(DocumentInfo replaceTarget) {
304        new ExistingFinishTask(this, replaceTarget.derivedUri)
305                .executeOnExecutor(getExecutorForCurrentDirectory());
306    }
307
308    @Override
309    void onDirectoryCreated(DocumentInfo doc) {
310        assert(doc.isDirectory());
311        openContainerDocument(doc);
312    }
313
314    void onSaveRequested(String mimeType, String displayName) {
315        new CreateFinishTask(this, mimeType, displayName)
316                .executeOnExecutor(getExecutorForCurrentDirectory());
317    }
318
319    @Override
320    void onRootPicked(RootInfo root) {
321        super.onRootPicked(root);
322        mNavigator.revealRootsDrawer(false);
323    }
324
325    @Override
326    public void onDocumentPicked(DocumentInfo doc, Model model) {
327        final FragmentManager fm = getFragmentManager();
328        if (doc.isContainer()) {
329            openContainerDocument(doc);
330        } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
331            // Explicit file picked, return
332            new ExistingFinishTask(this, doc.derivedUri)
333                    .executeOnExecutor(getExecutorForCurrentDirectory());
334        } else if (mState.action == ACTION_CREATE) {
335            // Replace selected file
336            SaveFragment.get(fm).setReplaceTarget(doc);
337        }
338    }
339
340    @Override
341    public void onDocumentsPicked(List<DocumentInfo> docs) {
342        if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
343            final int size = docs.size();
344            final Uri[] uris = new Uri[size];
345            for (int i = 0; i < size; i++) {
346                uris[i] = docs.get(i).derivedUri;
347            }
348            new ExistingFinishTask(this, uris)
349                    .executeOnExecutor(getExecutorForCurrentDirectory());
350        }
351    }
352
353    public void onPickRequested(DocumentInfo pickTarget) {
354        Uri result;
355        if (mState.action == ACTION_OPEN_TREE) {
356            result = DocumentsContract.buildTreeDocumentUri(
357                    pickTarget.authority, pickTarget.documentId);
358        } else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
359            result = pickTarget.derivedUri;
360        } else {
361            // Should not be reached.
362            throw new IllegalStateException("Invalid mState.action.");
363        }
364        new PickFinishTask(this, result).executeOnExecutor(getExecutorForCurrentDirectory());
365    }
366
367    void writeStackToRecentsBlocking() {
368        final ContentResolver resolver = getContentResolver();
369        final ContentValues values = new ContentValues();
370
371        final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
372        if (mState.action == ACTION_CREATE ||
373            mState.action == ACTION_OPEN_TREE ||
374            mState.action == ACTION_PICK_COPY_DESTINATION) {
375            // Remember stack for last create
376            values.clear();
377            values.put(RecentColumns.KEY, mState.stack.buildKey());
378            values.put(RecentColumns.STACK, rawStack);
379            resolver.insert(RecentsProvider.buildRecent(), values);
380        }
381
382        // Remember location for next app launch
383        final String packageName = getCallingPackageMaybeExtra();
384        values.clear();
385        values.put(ResumeColumns.STACK, rawStack);
386        values.put(ResumeColumns.EXTERNAL, 0);
387        resolver.insert(RecentsProvider.buildResume(packageName), values);
388    }
389
390    @Override
391    void onTaskFinished(Uri... uris) {
392        if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris));
393
394        final Intent intent = new Intent();
395        if (uris.length == 1) {
396            intent.setData(uris[0]);
397        } else if (uris.length > 1) {
398            final ClipData clipData = new ClipData(
399                    null, mState.acceptMimes, new ClipData.Item(uris[0]));
400            for (int i = 1; i < uris.length; i++) {
401                clipData.addItem(new ClipData.Item(uris[i]));
402            }
403            intent.setClipData(clipData);
404        }
405
406        if (mState.action == ACTION_GET_CONTENT) {
407            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
408        } else if (mState.action == ACTION_OPEN_TREE) {
409            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
410                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
411                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
412                    | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
413        } else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
414            // Picking a copy destination is only used internally by us, so we
415            // don't need to extend permissions to the caller.
416            intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
417            intent.putExtra(FileOperationService.EXTRA_OPERATION, mState.copyOperationSubType);
418        } else {
419            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
420                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
421                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
422        }
423
424        setResult(Activity.RESULT_OK, intent);
425        finish();
426    }
427
428
429    public static DocumentsActivity get(Fragment fragment) {
430        return (DocumentsActivity) fragment.getActivity();
431    }
432
433    /**
434     * Loads the last used path (stack) from Recents (history).
435     * The path selected is based on the calling package name. So the last
436     * path for an app like Gmail can be different than the last path
437     * for an app like DropBox.
438     */
439    private static final class LoadLastUsedStackTask
440            extends PairedTask<DocumentsActivity, Void, Void> {
441
442        private volatile boolean mRestoredStack;
443        private volatile boolean mExternal;
444        private State mState;
445
446        public LoadLastUsedStackTask(DocumentsActivity activity) {
447            super(activity);
448            mState = activity.mState;
449        }
450
451        @Override
452        protected Void run(Void... params) {
453            if (DEBUG && !mState.stack.isEmpty()) {
454                Log.w(TAG, "Overwriting existing stack.");
455            }
456            RootsCache roots = DocumentsApplication.getRootsCache(mOwner);
457
458            String packageName = mOwner.getCallingPackageMaybeExtra();
459            Uri resumeUri = RecentsProvider.buildResume(packageName);
460            Cursor cursor = mOwner.getContentResolver().query(resumeUri, null, null, null, null);
461            try {
462                if (cursor.moveToFirst()) {
463                    mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0;
464                    final byte[] rawStack = cursor.getBlob(
465                            cursor.getColumnIndex(ResumeColumns.STACK));
466                    DurableUtils.readFromArray(rawStack, mState.stack);
467                    mRestoredStack = true;
468                }
469            } catch (IOException e) {
470                Log.w(TAG, "Failed to resume: " + e);
471            } finally {
472                IoUtils.closeQuietly(cursor);
473            }
474
475            if (mRestoredStack) {
476                // Update the restored stack to ensure we have freshest data
477                final Collection<RootInfo> matchingRoots = roots.getMatchingRootsBlocking(mState);
478                try {
479                    mState.stack.updateRoot(matchingRoots);
480                    mState.stack.updateDocuments(mOwner.getContentResolver());
481                } catch (FileNotFoundException e) {
482                    Log.w(TAG, "Failed to restore stack for package: " + packageName
483                            + " because of error: "+ e);
484                    mState.stack.reset();
485                    mRestoredStack = false;
486                }
487            }
488
489            return null;
490        }
491
492        @Override
493        protected void finish(Void result) {
494            mState.restored = true;
495            mState.external = mExternal;
496            mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
497        }
498    }
499
500    private static final class PickFinishTask extends PairedTask<DocumentsActivity, Void, Void> {
501        private final Uri mUri;
502
503        public PickFinishTask(DocumentsActivity activity, Uri uri) {
504            super(activity);
505            mUri = uri;
506        }
507
508        @Override
509        protected Void run(Void... params) {
510            mOwner.writeStackToRecentsBlocking();
511            return null;
512        }
513
514        @Override
515        protected void finish(Void result) {
516            mOwner.onTaskFinished(mUri);
517        }
518    }
519
520    private static final class ExistingFinishTask extends PairedTask<DocumentsActivity, Void, Void> {
521        private final Uri[] mUris;
522
523        public ExistingFinishTask(DocumentsActivity activity, Uri... uris) {
524            super(activity);
525            mUris = uris;
526        }
527
528        @Override
529        protected Void run(Void... params) {
530            mOwner.writeStackToRecentsBlocking();
531            return null;
532        }
533
534        @Override
535        protected void finish(Void result) {
536            mOwner.onTaskFinished(mUris);
537        }
538    }
539
540    /**
541     * Task that creates a new document in the background.
542     */
543    private static final class CreateFinishTask extends PairedTask<DocumentsActivity, Void, Uri> {
544        private final String mMimeType;
545        private final String mDisplayName;
546
547        public CreateFinishTask(DocumentsActivity activity, String mimeType, String displayName) {
548            super(activity);
549            mMimeType = mimeType;
550            mDisplayName = displayName;
551        }
552
553        @Override
554        protected void prepare() {
555            mOwner.setPending(true);
556        }
557
558        @Override
559        protected Uri run(Void... params) {
560            final ContentResolver resolver = mOwner.getContentResolver();
561            final DocumentInfo cwd = mOwner.getCurrentDirectory();
562
563            ContentProviderClient client = null;
564            Uri childUri = null;
565            try {
566                client = DocumentsApplication.acquireUnstableProviderOrThrow(
567                        resolver, cwd.derivedUri.getAuthority());
568                childUri = DocumentsContract.createDocument(
569                        client, cwd.derivedUri, mMimeType, mDisplayName);
570            } catch (Exception e) {
571                Log.w(TAG, "Failed to create document", e);
572            } finally {
573                ContentProviderClient.releaseQuietly(client);
574            }
575
576            if (childUri != null) {
577                mOwner.writeStackToRecentsBlocking();
578            }
579
580            return childUri;
581        }
582
583        @Override
584        protected void finish(Uri result) {
585            if (result != null) {
586                mOwner.onTaskFinished(result);
587            } else {
588                Snackbars.makeSnackbar(
589                        mOwner, R.string.save_error, Snackbar.LENGTH_SHORT).show();
590            }
591
592            mOwner.setPending(false);
593        }
594    }
595}
596