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.OperationDialogFragment.DIALOG_TYPE_UNKNOWN;
20import static com.android.documentsui.Shared.DEBUG;
21
22import android.app.Activity;
23import android.app.FragmentManager;
24import android.content.ActivityNotFoundException;
25import android.content.ClipData;
26import android.content.ContentResolver;
27import android.content.ContentValues;
28import android.content.Intent;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.Parcelable;
32import android.provider.DocumentsContract;
33import android.support.design.widget.Snackbar;
34import android.util.Log;
35import android.view.KeyEvent;
36import android.view.Menu;
37import android.view.MenuItem;
38
39import com.android.documentsui.OperationDialogFragment.DialogType;
40import com.android.documentsui.RecentsProvider.ResumeColumns;
41import com.android.documentsui.dirlist.AnimationView;
42import com.android.documentsui.dirlist.DirectoryFragment;
43import com.android.documentsui.dirlist.Model;
44import com.android.documentsui.model.DocumentInfo;
45import com.android.documentsui.model.DocumentStack;
46import com.android.documentsui.model.DurableUtils;
47import com.android.documentsui.model.RootInfo;
48import com.android.documentsui.services.FileOperationService;
49
50import java.io.FileNotFoundException;
51import java.util.ArrayList;
52import java.util.Arrays;
53import java.util.Collection;
54import java.util.List;
55
56/**
57 * Standalone file management activity.
58 */
59public class FilesActivity extends BaseActivity {
60
61    public static final String TAG = "FilesActivity";
62
63    // See comments where this const is referenced for details.
64    private static final int DRAWER_NO_FIDDLE_DELAY = 1500;
65
66    // Track the time we opened the drawer in response to back being pressed.
67    // We use the time gap to figure out whether to close app or reopen the drawer.
68    private long mDrawerLastFiddled;
69    private DocumentClipper mClipper;
70
71    public FilesActivity() {
72        super(R.layout.files_activity, TAG);
73    }
74
75    @Override
76    public void onCreate(Bundle icicle) {
77        super.onCreate(icicle);
78
79        mClipper = new DocumentClipper(this);
80
81        RootsFragment.show(getFragmentManager(), null);
82
83        final Intent intent = getIntent();
84        final Uri uri = intent.getData();
85
86        if (mState.restored) {
87            if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData());
88        } else if (!mState.stack.isEmpty()) {
89            // If a non-empty stack is present in our state, it was read (presumably)
90            // from EXTRA_STACK intent extra. In this case, we'll skip other means of
91            // loading or restoring the stack (like URI).
92            //
93            // When restoring from a stack, if a URI is present, it should only ever be:
94            // -- a launch URI: Launch URIs support sensible activity management,
95            //    but don't specify a real content target)
96            // -- a fake Uri from notifications. These URIs have no authority (TODO: details).
97            //
98            // Any other URI is *sorta* unexpected...except when browsing an archive
99            // in downloads.
100            if(uri != null
101                    && uri.getAuthority() != null
102                    && !uri.equals(mState.stack.peek())
103                    && !LauncherActivity.isLaunchUri(uri)) {
104                if (DEBUG) Log.w(TAG,
105                        "Launching with non-empty stack. Ignoring unexpected uri: " + uri);
106            } else {
107                if (DEBUG) Log.d(TAG, "Launching with non-empty stack.");
108            }
109            refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
110        } else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
111            assert(uri != null);
112            new OpenUriForViewTask(this).executeOnExecutor(
113                    ProviderExecutor.forAuthority(uri.getAuthority()), uri);
114        } else if (DocumentsContract.isRootUri(this, uri)) {
115            if (DEBUG) Log.d(TAG, "Launching with root URI.");
116            // If we've got a specific root to display, restore that root using a dedicated
117            // authority. That way a misbehaving provider won't result in an ANR.
118            loadRoot(uri);
119        } else {
120            if (DEBUG) Log.d(TAG, "All other means skipped. Launching into default directory.");
121            loadRoot(getDefaultRoot());
122        }
123
124        final @DialogType int dialogType = intent.getIntExtra(
125                FileOperationService.EXTRA_DIALOG_TYPE, DIALOG_TYPE_UNKNOWN);
126        // DialogFragment takes care of restoring the dialog on configuration change.
127        // Only show it manually for the first time (icicle is null).
128        if (icicle == null && dialogType != DIALOG_TYPE_UNKNOWN) {
129            final int opType = intent.getIntExtra(
130                    FileOperationService.EXTRA_OPERATION,
131                    FileOperationService.OPERATION_COPY);
132            final ArrayList<DocumentInfo> srcList =
133                    intent.getParcelableArrayListExtra(FileOperationService.EXTRA_SRC_LIST);
134            OperationDialogFragment.show(
135                    getFragmentManager(),
136                    dialogType,
137                    srcList,
138                    mState.stack,
139                    opType);
140        }
141    }
142
143    @Override
144    void includeState(State state) {
145        final Intent intent = getIntent();
146
147        state.action = State.ACTION_BROWSE;
148        state.allowMultiple = true;
149
150        // Options specific to the DocumentsActivity.
151        assert(!intent.hasExtra(Intent.EXTRA_LOCAL_ONLY));
152
153        final DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
154        if (stack != null) {
155            state.stack = stack;
156        }
157    }
158
159    @Override
160    protected void onPostCreate(Bundle savedInstanceState) {
161        super.onPostCreate(savedInstanceState);
162        // This check avoids a flicker from "Recents" to "Home".
163        // Only update action bar at this point if there is an active
164        // serach. Why? Because this avoid an early (undesired) load of
165        // the recents root...which is the default root in other activities.
166        // In Files app "Home" is the default, but it is loaded async.
167        // update will be called once Home root is loaded.
168        // Except while searching we need this call to ensure the
169        // search bits get layed out correctly.
170        if (mSearchManager.isSearching()) {
171            mNavigator.update();
172        }
173    }
174
175    @Override
176    public void onResume() {
177        super.onResume();
178
179        final RootInfo root = getCurrentRoot();
180
181        // If we're browsing a specific root, and that root went away, then we
182        // have no reason to hang around.
183        // TODO: Rather than just disappearing, maybe we should inform
184        // the user what has happened, let them close us. Less surprising.
185        if (mRoots.getRootBlocking(root.authority, root.rootId) == null) {
186            finish();
187        }
188    }
189
190    @Override
191    public String getDrawerTitle() {
192        Intent intent = getIntent();
193        return (intent != null && intent.hasExtra(Intent.EXTRA_TITLE))
194                ? intent.getStringExtra(Intent.EXTRA_TITLE)
195                : getString(R.string.downloads_label);
196    }
197
198    @Override
199    public boolean onPrepareOptionsMenu(Menu menu) {
200        super.onPrepareOptionsMenu(menu);
201
202        final RootInfo root = getCurrentRoot();
203
204        final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
205        final MenuItem pasteFromCb = menu.findItem(R.id.menu_paste_from_clipboard);
206        final MenuItem settings = menu.findItem(R.id.menu_settings);
207        final MenuItem newWindow = menu.findItem(R.id.menu_new_window);
208
209        createDir.setVisible(true);
210        createDir.setEnabled(canCreateDirectory());
211        pasteFromCb.setEnabled(mClipper.hasItemsToPaste());
212        settings.setVisible(root.hasSettings());
213        newWindow.setVisible(Shared.shouldShowFancyFeatures(this));
214
215        Menus.disableHiddenItems(menu, pasteFromCb);
216        // It hides icon if searching in progress
217        mSearchManager.updateMenu();
218        return true;
219    }
220
221    @Override
222    public boolean onOptionsItemSelected(MenuItem item) {
223        switch (item.getItemId()) {
224            case R.id.menu_create_dir:
225                assert(canCreateDirectory());
226                showCreateDirectoryDialog();
227                break;
228            case R.id.menu_new_window:
229                createNewWindow();
230                break;
231            case R.id.menu_paste_from_clipboard:
232                DirectoryFragment dir = getDirectoryFragment();
233                if (dir != null) {
234                    dir.pasteFromClipboard();
235                }
236                break;
237            default:
238                return super.onOptionsItemSelected(item);
239        }
240        return true;
241    }
242
243    private void createNewWindow() {
244        Metrics.logUserAction(this, Metrics.USER_ACTION_NEW_WINDOW);
245
246        Intent intent = LauncherActivity.createLaunchIntent(this);
247        intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
248
249        // With new multi-window mode we have to pick how we are launched.
250        // By default we'd be launched in-place above the existing app.
251        // By setting launch-to-side ActivityManager will open us to side.
252        if (isInMultiWindowMode()) {
253            intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
254        }
255
256        startActivity(intent);
257    }
258
259    @Override
260    void refreshDirectory(int anim) {
261        final FragmentManager fm = getFragmentManager();
262        final RootInfo root = getCurrentRoot();
263        final DocumentInfo cwd = getCurrentDirectory();
264
265        assert(!mSearchManager.isSearching());
266
267        if (cwd == null) {
268            DirectoryFragment.showRecentsOpen(fm, anim);
269        } else {
270            // Normal boring directory
271            DirectoryFragment.showDirectory(fm, root, cwd, anim);
272        }
273    }
274
275    @Override
276    void onRootPicked(RootInfo root) {
277        super.onRootPicked(root);
278        mDrawer.setOpen(false);
279    }
280
281    @Override
282    public void onDocumentsPicked(List<DocumentInfo> docs) {
283        throw new UnsupportedOperationException();
284    }
285
286    @Override
287    public void onDocumentPicked(DocumentInfo doc, Model model) {
288        // Anything on downloads goes through the back through downloads manager
289        // (that's the MANAGE_DOCUMENT bit).
290        // This is done for two reasons:
291        // 1) The file in question might be a failed/queued or otherwise have some
292        //    specialized download handling.
293        // 2) For APKs, the download manager will add on some important security stuff
294        //    like origin URL.
295        // All other files not on downloads, event APKs, would get no benefit from this
296        // treatment, thusly the "isDownloads" check.
297
298        // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for
299        // files in archives. Also, if the activity is already browsing a ZIP from downloads,
300        // then skip MANAGE_DOCUMENTS.
301        final boolean isViewing = Intent.ACTION_VIEW.equals(getIntent().getAction());
302        final boolean isInArchive = mState.stack.size() > 1;
303        if (getCurrentRoot().isDownloads() && !isInArchive && !isViewing) {
304            // First try managing the document; we expect manager to filter
305            // based on authority, so we don't grant.
306            final Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
307            manage.setData(doc.derivedUri);
308
309            try {
310                startActivity(manage);
311                return;
312            } catch (ActivityNotFoundException ex) {
313                // fall back to regular handling below.
314            }
315        }
316
317        if (doc.isContainer()) {
318            openContainerDocument(doc);
319        } else {
320            openDocument(doc, model);
321        }
322    }
323
324    /**
325     * Launches an intent to view the specified document.
326     */
327    private void openDocument(DocumentInfo doc, Model model) {
328        Intent intent = new QuickViewIntentBuilder(
329                getPackageManager(), getResources(), doc, model).build();
330
331        if (intent != null) {
332            // TODO: un-work around issue b/24963914. Should be fixed soon.
333            try {
334                startActivity(intent);
335                return;
336            } catch (SecurityException e) {
337                // Carry on to regular view mode.
338                Log.e(TAG, "Caught security error: " + e.getLocalizedMessage());
339            }
340        }
341
342        // Fall back to traditional VIEW action...
343        intent = new Intent(Intent.ACTION_VIEW);
344        intent.setDataAndType(doc.derivedUri, doc.mimeType);
345
346        // Downloads has traditionally added the WRITE permission
347        // in the TrampolineActivity. Since this behavior is long
348        // established, we set the same permission for non-managed files
349        // This ensures consistent behavior between the Downloads root
350        // and other roots.
351        int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
352        if (doc.isWriteSupported()) {
353            flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
354        }
355        intent.setFlags(flags);
356
357        if (DEBUG && intent.getClipData() != null) {
358            Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData());
359        }
360
361        try {
362            startActivity(intent);
363        } catch (ActivityNotFoundException e) {
364            Snackbars.makeSnackbar(
365                    this, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
366        }
367    }
368
369    @Override
370    public boolean onKeyShortcut(int keyCode, KeyEvent event) {
371        DirectoryFragment dir;
372        // TODO: All key events should be statically bound using alphabeticShortcut.
373        // But not working.
374        switch (keyCode) {
375            case KeyEvent.KEYCODE_A:
376                dir = getDirectoryFragment();
377                if (dir != null) {
378                    dir.selectAllFiles();
379                }
380                return true;
381            case KeyEvent.KEYCODE_C:
382                dir = getDirectoryFragment();
383                if (dir != null) {
384                    dir.copySelectedToClipboard();
385                }
386                return true;
387            case KeyEvent.KEYCODE_V:
388                dir = getDirectoryFragment();
389                if (dir != null) {
390                    dir.pasteFromClipboard();
391                }
392                return true;
393            default:
394                return super.onKeyShortcut(keyCode, event);
395        }
396    }
397
398    // Do some "do what a I want" drawer fiddling, but don't
399    // do it if user already hit back recently and we recently
400    // did some fiddling.
401    @Override
402    boolean onBeforePopDir() {
403        int size = mState.stack.size();
404
405        if (mDrawer.isPresent()
406                && (System.currentTimeMillis() - mDrawerLastFiddled) > DRAWER_NO_FIDDLE_DELAY) {
407            // Close drawer if it is open.
408            if (mDrawer.isOpen()) {
409                mDrawer.setOpen(false);
410                mDrawerLastFiddled = System.currentTimeMillis();
411                return true;
412            }
413
414            // Open the Close drawer if it is closed and we're at the top of a root.
415            if (size <= 1) {
416                mDrawer.setOpen(true);
417                // Remember so we don't just close it again if back is pressed again.
418                mDrawerLastFiddled = System.currentTimeMillis();
419                return true;
420            }
421        }
422
423        return false;
424    }
425
426    // Turns out only DocumentsActivity was ever calling saveStackBlocking.
427    // There may be a  case where we want to contribute entries from
428    // Behavior here in FilesActivity, but it isn't yet obvious.
429    // TODO: Contribute to recents, or remove this.
430    void writeStackToRecentsBlocking() {
431        final ContentResolver resolver = getContentResolver();
432        final ContentValues values = new ContentValues();
433
434        final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
435
436        // Remember location for next app launch
437        final String packageName = getCallingPackageMaybeExtra();
438        values.clear();
439        values.put(ResumeColumns.STACK, rawStack);
440        values.put(ResumeColumns.EXTERNAL, 0);
441        resolver.insert(RecentsProvider.buildResume(packageName), values);
442    }
443
444    @Override
445    void onTaskFinished(Uri... uris) {
446        if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris));
447
448        final Intent intent = new Intent();
449        if (uris.length == 1) {
450            intent.setData(uris[0]);
451        } else if (uris.length > 1) {
452            final ClipData clipData = new ClipData(
453                    null, mState.acceptMimes, new ClipData.Item(uris[0]));
454            for (int i = 1; i < uris.length; i++) {
455                clipData.addItem(new ClipData.Item(uris[i]));
456            }
457            intent.setClipData(clipData);
458        }
459
460        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
461                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
462                | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
463
464        setResult(Activity.RESULT_OK, intent);
465        finish();
466    }
467
468    /**
469     * Builds a stack for the specific Uris. Multi roots are not supported, as it's impossible
470     * to know which root to select. Also, the stack doesn't contain intermediate directories.
471     * It's primarly used for opening ZIP archives from Downloads app.
472     */
473    private static final class OpenUriForViewTask extends PairedTask<FilesActivity, Uri, Void> {
474
475        private final State mState;
476        public OpenUriForViewTask(FilesActivity activity) {
477            super(activity);
478            mState = activity.mState;
479        }
480
481        @Override
482        protected Void run(Uri... params) {
483            final Uri uri = params[0];
484
485            final RootsCache rootsCache = DocumentsApplication.getRootsCache(mOwner);
486            final String authority = uri.getAuthority();
487
488            final Collection<RootInfo> roots =
489                    rootsCache.getRootsForAuthorityBlocking(authority);
490            if (roots.isEmpty()) {
491                Log.e(TAG, "Failed to find root for the requested Uri: " + uri);
492                return null;
493            }
494
495            final RootInfo root = roots.iterator().next();
496            mState.stack.root = root;
497            try {
498                mState.stack.add(DocumentInfo.fromUri(mOwner.getContentResolver(), uri));
499            } catch (FileNotFoundException e) {
500                Log.e(TAG, "Failed to resolve DocumentInfo from Uri: " + uri);
501            }
502            mState.stack.add(mOwner.getRootDocumentBlocking(root));
503            return null;
504        }
505
506        @Override
507        protected void finish(Void result) {
508            mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
509        }
510    }
511}
512