• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.documentsui;
18 
19 import static com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_UNKNOWN;
20 import static com.android.documentsui.Shared.DEBUG;
21 
22 import android.app.Activity;
23 import android.app.FragmentManager;
24 import android.content.ActivityNotFoundException;
25 import android.content.ClipData;
26 import android.content.ContentResolver;
27 import android.content.ContentValues;
28 import android.content.Intent;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.Parcelable;
32 import android.provider.DocumentsContract;
33 import android.support.design.widget.Snackbar;
34 import android.util.Log;
35 import android.view.KeyEvent;
36 import android.view.Menu;
37 import android.view.MenuItem;
38 
39 import com.android.documentsui.OperationDialogFragment.DialogType;
40 import com.android.documentsui.RecentsProvider.ResumeColumns;
41 import com.android.documentsui.dirlist.AnimationView;
42 import com.android.documentsui.dirlist.DirectoryFragment;
43 import com.android.documentsui.dirlist.Model;
44 import com.android.documentsui.model.DocumentInfo;
45 import com.android.documentsui.model.DocumentStack;
46 import com.android.documentsui.model.DurableUtils;
47 import com.android.documentsui.model.RootInfo;
48 import com.android.documentsui.services.FileOperationService;
49 
50 import java.io.FileNotFoundException;
51 import java.util.ArrayList;
52 import java.util.Arrays;
53 import java.util.Collection;
54 import java.util.List;
55 
56 /**
57  * Standalone file management activity.
58  */
59 public 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 
FilesActivity()71     public FilesActivity() {
72         super(R.layout.files_activity, TAG);
73     }
74 
75     @Override
onCreate(Bundle icicle)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
includeState(State state)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
onPostCreate(Bundle savedInstanceState)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
onResume()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
getDrawerTitle()191     public String getDrawerTitle() {
192         Intent intent = getIntent();
193         return (intent != null && intent.hasExtra(Intent.EXTRA_TITLE))
194                 ? intent.getStringExtra(Intent.EXTRA_TITLE)
195                 : getTitle().toString();
196     }
197 
198     @Override
onPrepareOptionsMenu(Menu menu)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
onOptionsItemSelected(MenuItem item)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 
createNewWindow()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
refreshDirectory(int anim)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
onRootPicked(RootInfo root)276     void onRootPicked(RootInfo root) {
277         super.onRootPicked(root);
278         mDrawer.setOpen(false);
279     }
280 
281     @Override
onDocumentsPicked(List<DocumentInfo> docs)282     public void onDocumentsPicked(List<DocumentInfo> docs) {
283         throw new UnsupportedOperationException();
284     }
285 
286     @Override
onDocumentPicked(DocumentInfo doc, Model model)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      */
openDocument(DocumentInfo doc, Model model)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.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
345         intent.setData(doc.derivedUri);
346 
347         if (DEBUG && intent.getClipData() != null) {
348             Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData());
349         }
350 
351         try {
352             startActivity(intent);
353         } catch (ActivityNotFoundException e) {
354             Snackbars.makeSnackbar(
355                     this, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
356         }
357     }
358 
359     @Override
onKeyShortcut(int keyCode, KeyEvent event)360     public boolean onKeyShortcut(int keyCode, KeyEvent event) {
361         DirectoryFragment dir;
362         // TODO: All key events should be statically bound using alphabeticShortcut.
363         // But not working.
364         switch (keyCode) {
365             case KeyEvent.KEYCODE_A:
366                 dir = getDirectoryFragment();
367                 if (dir != null) {
368                     dir.selectAllFiles();
369                 }
370                 return true;
371             case KeyEvent.KEYCODE_C:
372                 dir = getDirectoryFragment();
373                 if (dir != null) {
374                     dir.copySelectedToClipboard();
375                 }
376                 return true;
377             case KeyEvent.KEYCODE_V:
378                 dir = getDirectoryFragment();
379                 if (dir != null) {
380                     dir.pasteFromClipboard();
381                 }
382                 return true;
383             default:
384                 return super.onKeyShortcut(keyCode, event);
385         }
386     }
387 
388     // Do some "do what a I want" drawer fiddling, but don't
389     // do it if user already hit back recently and we recently
390     // did some fiddling.
391     @Override
onBeforePopDir()392     boolean onBeforePopDir() {
393         int size = mState.stack.size();
394 
395         if (mDrawer.isPresent()
396                 && (System.currentTimeMillis() - mDrawerLastFiddled) > DRAWER_NO_FIDDLE_DELAY) {
397             // Close drawer if it is open.
398             if (mDrawer.isOpen()) {
399                 mDrawer.setOpen(false);
400                 mDrawerLastFiddled = System.currentTimeMillis();
401                 return true;
402             }
403 
404             // Open the Close drawer if it is closed and we're at the top of a root.
405             if (size <= 1) {
406                 mDrawer.setOpen(true);
407                 // Remember so we don't just close it again if back is pressed again.
408                 mDrawerLastFiddled = System.currentTimeMillis();
409                 return true;
410             }
411         }
412 
413         return false;
414     }
415 
416     // Turns out only DocumentsActivity was ever calling saveStackBlocking.
417     // There may be a  case where we want to contribute entries from
418     // Behavior here in FilesActivity, but it isn't yet obvious.
419     // TODO: Contribute to recents, or remove this.
writeStackToRecentsBlocking()420     void writeStackToRecentsBlocking() {
421         final ContentResolver resolver = getContentResolver();
422         final ContentValues values = new ContentValues();
423 
424         final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
425 
426         // Remember location for next app launch
427         final String packageName = getCallingPackageMaybeExtra();
428         values.clear();
429         values.put(ResumeColumns.STACK, rawStack);
430         values.put(ResumeColumns.EXTERNAL, 0);
431         resolver.insert(RecentsProvider.buildResume(packageName), values);
432     }
433 
434     @Override
onTaskFinished(Uri... uris)435     void onTaskFinished(Uri... uris) {
436         if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris));
437 
438         final Intent intent = new Intent();
439         if (uris.length == 1) {
440             intent.setData(uris[0]);
441         } else if (uris.length > 1) {
442             final ClipData clipData = new ClipData(
443                     null, mState.acceptMimes, new ClipData.Item(uris[0]));
444             for (int i = 1; i < uris.length; i++) {
445                 clipData.addItem(new ClipData.Item(uris[i]));
446             }
447             intent.setClipData(clipData);
448         }
449 
450         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
451                 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
452                 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
453 
454         setResult(Activity.RESULT_OK, intent);
455         finish();
456     }
457 
458     /**
459      * Builds a stack for the specific Uris. Multi roots are not supported, as it's impossible
460      * to know which root to select. Also, the stack doesn't contain intermediate directories.
461      * It's primarly used for opening ZIP archives from Downloads app.
462      */
463     private static final class OpenUriForViewTask extends PairedTask<FilesActivity, Uri, Void> {
464 
465         private final State mState;
OpenUriForViewTask(FilesActivity activity)466         public OpenUriForViewTask(FilesActivity activity) {
467             super(activity);
468             mState = activity.mState;
469         }
470 
471         @Override
run(Uri... params)472         protected Void run(Uri... params) {
473             final Uri uri = params[0];
474 
475             final RootsCache rootsCache = DocumentsApplication.getRootsCache(mOwner);
476             final String authority = uri.getAuthority();
477 
478             final Collection<RootInfo> roots =
479                     rootsCache.getRootsForAuthorityBlocking(authority);
480             if (roots.isEmpty()) {
481                 Log.e(TAG, "Failed to find root for the requested Uri: " + uri);
482                 return null;
483             }
484 
485             final RootInfo root = roots.iterator().next();
486             mState.stack.root = root;
487             try {
488                 mState.stack.add(DocumentInfo.fromUri(mOwner.getContentResolver(), uri));
489             } catch (FileNotFoundException e) {
490                 Log.e(TAG, "Failed to resolve DocumentInfo from Uri: " + uri);
491             }
492             mState.stack.add(mOwner.getRootDocumentBlocking(root));
493             return null;
494         }
495 
496         @Override
finish(Void result)497         protected void finish(Void result) {
498             mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
499         }
500     }
501 }
502