• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.documentsui;
18 
19 import static com.android.documentsui.DirectoryFragment.ANIM_DOWN;
20 import static com.android.documentsui.DirectoryFragment.ANIM_NONE;
21 import static com.android.documentsui.DirectoryFragment.ANIM_SIDE;
22 import static com.android.documentsui.DirectoryFragment.ANIM_UP;
23 import static com.android.documentsui.DocumentsActivity.State.ACTION_CREATE;
24 import static com.android.documentsui.DocumentsActivity.State.ACTION_GET_CONTENT;
25 import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE;
26 import static com.android.documentsui.DocumentsActivity.State.ACTION_OPEN;
27 import static com.android.documentsui.DocumentsActivity.State.ACTION_OPEN_TREE;
28 import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
29 import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
30 
31 import android.app.Activity;
32 import android.app.Fragment;
33 import android.app.FragmentManager;
34 import android.content.ActivityNotFoundException;
35 import android.content.ClipData;
36 import android.content.ComponentName;
37 import android.content.ContentProviderClient;
38 import android.content.ContentResolver;
39 import android.content.ContentValues;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.content.pm.ResolveInfo;
43 import android.content.res.Resources;
44 import android.database.Cursor;
45 import android.graphics.Point;
46 import android.net.Uri;
47 import android.os.AsyncTask;
48 import android.os.Bundle;
49 import android.os.Parcel;
50 import android.os.Parcelable;
51 import android.provider.DocumentsContract;
52 import android.provider.DocumentsContract.Root;
53 import android.support.v4.app.ActionBarDrawerToggle;
54 import android.support.v4.widget.DrawerLayout;
55 import android.support.v4.widget.DrawerLayout.DrawerListener;
56 import android.util.Log;
57 import android.util.SparseArray;
58 import android.view.LayoutInflater;
59 import android.view.Menu;
60 import android.view.MenuItem;
61 import android.view.MenuItem.OnActionExpandListener;
62 import android.view.View;
63 import android.view.ViewGroup;
64 import android.view.WindowManager;
65 import android.widget.AdapterView;
66 import android.widget.AdapterView.OnItemSelectedListener;
67 import android.widget.BaseAdapter;
68 import android.widget.ImageView;
69 import android.widget.SearchView;
70 import android.widget.SearchView.OnQueryTextListener;
71 import android.widget.Spinner;
72 import android.widget.TextView;
73 import android.widget.Toast;
74 import android.widget.Toolbar;
75 
76 import com.android.documentsui.RecentsProvider.RecentColumns;
77 import com.android.documentsui.RecentsProvider.ResumeColumns;
78 import com.android.documentsui.model.DocumentInfo;
79 import com.android.documentsui.model.DocumentStack;
80 import com.android.documentsui.model.DurableUtils;
81 import com.android.documentsui.model.RootInfo;
82 import com.google.common.collect.Maps;
83 
84 import libcore.io.IoUtils;
85 
86 import java.io.FileNotFoundException;
87 import java.io.IOException;
88 import java.util.Arrays;
89 import java.util.Collection;
90 import java.util.HashMap;
91 import java.util.List;
92 import java.util.concurrent.Executor;
93 
94 public class DocumentsActivity extends Activity {
95     public static final String TAG = "Documents";
96 
97     private static final String EXTRA_STATE = "state";
98 
99     private static final int CODE_FORWARD = 42;
100 
101     private boolean mShowAsDialog;
102 
103     private SearchView mSearchView;
104 
105     private Toolbar mToolbar;
106     private Spinner mToolbarStack;
107 
108     private Toolbar mRootsToolbar;
109 
110     private DrawerLayout mDrawerLayout;
111     private ActionBarDrawerToggle mDrawerToggle;
112     private View mRootsDrawer;
113 
114     private DirectoryContainerView mDirectoryContainer;
115 
116     private boolean mIgnoreNextNavigation;
117     private boolean mIgnoreNextClose;
118     private boolean mIgnoreNextCollapse;
119 
120     private boolean mSearchExpanded;
121 
122     private RootsCache mRoots;
123     private State mState;
124 
125     @Override
onCreate(Bundle icicle)126     public void onCreate(Bundle icicle) {
127         super.onCreate(icicle);
128 
129         mRoots = DocumentsApplication.getRootsCache(this);
130 
131         setResult(Activity.RESULT_CANCELED);
132         setContentView(R.layout.activity);
133 
134         final Context context = this;
135         final Resources res = getResources();
136         mShowAsDialog = res.getBoolean(R.bool.show_as_dialog);
137 
138         if (mShowAsDialog) {
139             // Strongly define our horizontal dimension; we leave vertical as
140             // WRAP_CONTENT so that system resizes us when IME is showing.
141             final WindowManager.LayoutParams a = getWindow().getAttributes();
142 
143             final Point size = new Point();
144             getWindowManager().getDefaultDisplay().getSize(size);
145             a.width = (int) res.getFraction(R.dimen.dialog_width, size.x, size.x);
146 
147             getWindow().setAttributes(a);
148 
149         } else {
150             // Non-dialog means we have a drawer
151             mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
152 
153             mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
154                     R.drawable.ic_hamburger, R.string.drawer_open, R.string.drawer_close);
155 
156             mDrawerLayout.setDrawerListener(mDrawerListener);
157 
158             mRootsDrawer = findViewById(R.id.drawer_roots);
159         }
160 
161         mDirectoryContainer = (DirectoryContainerView) findViewById(R.id.container_directory);
162 
163         if (icicle != null) {
164             mState = icicle.getParcelable(EXTRA_STATE);
165         } else {
166             buildDefaultState();
167         }
168 
169         mToolbar = (Toolbar) findViewById(R.id.toolbar);
170         mToolbar.setTitleTextAppearance(context,
171                 android.R.style.TextAppearance_DeviceDefault_Widget_ActionBar_Title);
172 
173         mToolbarStack = (Spinner) findViewById(R.id.stack);
174         mToolbarStack.setOnItemSelectedListener(mStackListener);
175 
176         mRootsToolbar = (Toolbar) findViewById(R.id.roots_toolbar);
177         if (mRootsToolbar != null) {
178             mRootsToolbar.setTitleTextAppearance(context,
179                     android.R.style.TextAppearance_DeviceDefault_Widget_ActionBar_Title);
180         }
181 
182         setActionBar(mToolbar);
183 
184         // Hide roots when we're managing a specific root
185         if (mState.action == ACTION_MANAGE) {
186             if (mShowAsDialog) {
187                 findViewById(R.id.container_roots).setVisibility(View.GONE);
188             } else {
189                 mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
190             }
191         }
192 
193         if (mState.action == ACTION_CREATE) {
194             final String mimeType = getIntent().getType();
195             final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
196             SaveFragment.show(getFragmentManager(), mimeType, title);
197         } else if (mState.action == ACTION_OPEN_TREE) {
198             PickFragment.show(getFragmentManager());
199         }
200 
201         if (mState.action == ACTION_GET_CONTENT) {
202             final Intent moreApps = new Intent(getIntent());
203             moreApps.setComponent(null);
204             moreApps.setPackage(null);
205             RootsFragment.show(getFragmentManager(), moreApps);
206         } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE
207                 || mState.action == ACTION_OPEN_TREE) {
208             RootsFragment.show(getFragmentManager(), null);
209         }
210 
211         if (!mState.restored) {
212             if (mState.action == ACTION_MANAGE) {
213                 final Uri rootUri = getIntent().getData();
214                 new RestoreRootTask(rootUri).executeOnExecutor(getCurrentExecutor());
215             } else {
216                 new RestoreStackTask().execute();
217             }
218         } else {
219             onCurrentDirectoryChanged(ANIM_NONE);
220         }
221     }
222 
buildDefaultState()223     private void buildDefaultState() {
224         mState = new State();
225 
226         final Intent intent = getIntent();
227         final String action = intent.getAction();
228         if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
229             mState.action = ACTION_OPEN;
230         } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
231             mState.action = ACTION_CREATE;
232         } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
233             mState.action = ACTION_GET_CONTENT;
234         } else if (Intent.ACTION_OPEN_DOCUMENT_TREE.equals(action)) {
235             mState.action = ACTION_OPEN_TREE;
236         } else if (DocumentsContract.ACTION_MANAGE_ROOT.equals(action)) {
237             mState.action = ACTION_MANAGE;
238         }
239 
240         if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
241             mState.allowMultiple = intent.getBooleanExtra(
242                     Intent.EXTRA_ALLOW_MULTIPLE, false);
243         }
244 
245         if (mState.action == ACTION_MANAGE) {
246             mState.acceptMimes = new String[] { "*/*" };
247             mState.allowMultiple = true;
248         } else if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
249             mState.acceptMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES);
250         } else {
251             mState.acceptMimes = new String[] { intent.getType() };
252         }
253 
254         mState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
255         mState.forceAdvanced = intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, false);
256         mState.showAdvanced = mState.forceAdvanced
257                 | LocalPreferences.getDisplayAdvancedDevices(this);
258 
259         if (mState.action == ACTION_MANAGE) {
260             mState.showSize = true;
261         } else {
262             mState.showSize = LocalPreferences.getDisplayFileSize(this);
263         }
264     }
265 
266     private class RestoreRootTask extends AsyncTask<Void, Void, RootInfo> {
267         private Uri mRootUri;
268 
RestoreRootTask(Uri rootUri)269         public RestoreRootTask(Uri rootUri) {
270             mRootUri = rootUri;
271         }
272 
273         @Override
doInBackground(Void... params)274         protected RootInfo doInBackground(Void... params) {
275             final String rootId = DocumentsContract.getRootId(mRootUri);
276             return mRoots.getRootOneshot(mRootUri.getAuthority(), rootId);
277         }
278 
279         @Override
onPostExecute(RootInfo root)280         protected void onPostExecute(RootInfo root) {
281             if (isDestroyed()) return;
282             mState.restored = true;
283 
284             if (root != null) {
285                 onRootPicked(root, true);
286             } else {
287                 Log.w(TAG, "Failed to find root: " + mRootUri);
288                 finish();
289             }
290         }
291     }
292 
293     private class RestoreStackTask extends AsyncTask<Void, Void, Void> {
294         private volatile boolean mRestoredStack;
295         private volatile boolean mExternal;
296 
297         @Override
doInBackground(Void... params)298         protected Void doInBackground(Void... params) {
299             // Restore last stack for calling package
300             final String packageName = getCallingPackageMaybeExtra();
301             final Cursor cursor = getContentResolver()
302                     .query(RecentsProvider.buildResume(packageName), null, null, null, null);
303             try {
304                 if (cursor.moveToFirst()) {
305                     mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0;
306                     final byte[] rawStack = cursor.getBlob(
307                             cursor.getColumnIndex(ResumeColumns.STACK));
308                     DurableUtils.readFromArray(rawStack, mState.stack);
309                     mRestoredStack = true;
310                 }
311             } catch (IOException e) {
312                 Log.w(TAG, "Failed to resume: " + e);
313             } finally {
314                 IoUtils.closeQuietly(cursor);
315             }
316 
317             if (mRestoredStack) {
318                 // Update the restored stack to ensure we have freshest data
319                 final Collection<RootInfo> matchingRoots = mRoots.getMatchingRootsBlocking(mState);
320                 try {
321                     mState.stack.updateRoot(matchingRoots);
322                     mState.stack.updateDocuments(getContentResolver());
323                 } catch (FileNotFoundException e) {
324                     Log.w(TAG, "Failed to restore stack: " + e);
325                     mState.stack.reset();
326                     mRestoredStack = false;
327                 }
328             }
329 
330             return null;
331         }
332 
333         @Override
onPostExecute(Void result)334         protected void onPostExecute(Void result) {
335             if (isDestroyed()) return;
336             mState.restored = true;
337 
338             // Show drawer when no stack restored, but only when requesting
339             // non-visual content. However, if we last used an external app,
340             // drawer is always shown.
341 
342             boolean showDrawer = false;
343             if (!mRestoredStack) {
344                 showDrawer = true;
345             }
346             if (MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
347                 showDrawer = false;
348             }
349             if (mExternal && mState.action == ACTION_GET_CONTENT) {
350                 showDrawer = true;
351             }
352 
353             if (showDrawer) {
354                 setRootsDrawerOpen(true);
355             }
356 
357             onCurrentDirectoryChanged(ANIM_NONE);
358         }
359     }
360 
361     private DrawerListener mDrawerListener = new DrawerListener() {
362         @Override
363         public void onDrawerSlide(View drawerView, float slideOffset) {
364             mDrawerToggle.onDrawerSlide(drawerView, slideOffset);
365         }
366 
367         @Override
368         public void onDrawerOpened(View drawerView) {
369             mDrawerToggle.onDrawerOpened(drawerView);
370         }
371 
372         @Override
373         public void onDrawerClosed(View drawerView) {
374             mDrawerToggle.onDrawerClosed(drawerView);
375         }
376 
377         @Override
378         public void onDrawerStateChanged(int newState) {
379             mDrawerToggle.onDrawerStateChanged(newState);
380         }
381     };
382 
383     @Override
onPostCreate(Bundle savedInstanceState)384     protected void onPostCreate(Bundle savedInstanceState) {
385         super.onPostCreate(savedInstanceState);
386         if (mDrawerToggle != null) {
387             mDrawerToggle.syncState();
388         }
389     }
390 
setRootsDrawerOpen(boolean open)391     public void setRootsDrawerOpen(boolean open) {
392         if (!mShowAsDialog) {
393             if (open) {
394                 mDrawerLayout.openDrawer(mRootsDrawer);
395             } else {
396                 mDrawerLayout.closeDrawer(mRootsDrawer);
397             }
398         }
399     }
400 
isRootsDrawerOpen()401     private boolean isRootsDrawerOpen() {
402         if (mShowAsDialog) {
403             return false;
404         } else {
405             return mDrawerLayout.isDrawerOpen(mRootsDrawer);
406         }
407     }
408 
updateActionBar()409     public void updateActionBar() {
410         if (mRootsToolbar != null) {
411             if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT
412                     || mState.action == ACTION_OPEN_TREE) {
413                 mRootsToolbar.setTitle(R.string.title_open);
414             } else if (mState.action == ACTION_CREATE) {
415                 mRootsToolbar.setTitle(R.string.title_save);
416             }
417         }
418 
419         final RootInfo root = getCurrentRoot();
420         final boolean showRootIcon = mShowAsDialog || (mState.action == ACTION_MANAGE);
421         if (showRootIcon) {
422             mToolbar.setNavigationIcon(
423                     root != null ? root.loadToolbarIcon(mToolbar.getContext()) : null);
424             mToolbar.setNavigationContentDescription(R.string.drawer_open);
425             mToolbar.setNavigationOnClickListener(null);
426         } else {
427             mToolbar.setNavigationIcon(R.drawable.ic_hamburger);
428             mToolbar.setNavigationContentDescription(R.string.drawer_open);
429             mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
430                 @Override
431                 public void onClick(View v) {
432                     setRootsDrawerOpen(true);
433                 }
434             });
435         }
436 
437         if (mSearchExpanded) {
438             mToolbar.setTitle(null);
439             mToolbarStack.setVisibility(View.GONE);
440             mToolbarStack.setAdapter(null);
441         } else {
442             if (mState.stack.size() <= 1) {
443                 mToolbar.setTitle(root.title);
444                 mToolbarStack.setVisibility(View.GONE);
445                 mToolbarStack.setAdapter(null);
446             } else {
447                 mToolbar.setTitle(null);
448                 mToolbarStack.setVisibility(View.VISIBLE);
449                 mToolbarStack.setAdapter(mStackAdapter);
450 
451                 mIgnoreNextNavigation = true;
452                 mToolbarStack.setSelection(mStackAdapter.getCount() - 1);
453             }
454         }
455     }
456 
457     @Override
onCreateOptionsMenu(Menu menu)458     public boolean onCreateOptionsMenu(Menu menu) {
459         super.onCreateOptionsMenu(menu);
460         getMenuInflater().inflate(R.menu.activity, menu);
461 
462         // Most actions are visible when showing as dialog
463         if (mShowAsDialog) {
464             for (int i = 0; i < menu.size(); i++) {
465                 final MenuItem item = menu.getItem(i);
466                 switch (item.getItemId()) {
467                     case R.id.menu_advanced:
468                     case R.id.menu_file_size:
469                         break;
470                     default:
471                         item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
472                 }
473             }
474         }
475 
476         final MenuItem searchMenu = menu.findItem(R.id.menu_search);
477         mSearchView = (SearchView) searchMenu.getActionView();
478         mSearchView.setOnQueryTextListener(new OnQueryTextListener() {
479             @Override
480             public boolean onQueryTextSubmit(String query) {
481                 mSearchExpanded = true;
482                 mState.currentSearch = query;
483                 mSearchView.clearFocus();
484                 onCurrentDirectoryChanged(ANIM_NONE);
485                 return true;
486             }
487 
488             @Override
489             public boolean onQueryTextChange(String newText) {
490                 return false;
491             }
492         });
493 
494         searchMenu.setOnActionExpandListener(new OnActionExpandListener() {
495             @Override
496             public boolean onMenuItemActionExpand(MenuItem item) {
497                 mSearchExpanded = true;
498                 updateActionBar();
499                 return true;
500             }
501 
502             @Override
503             public boolean onMenuItemActionCollapse(MenuItem item) {
504                 mSearchExpanded = false;
505                 if (mIgnoreNextCollapse) {
506                     mIgnoreNextCollapse = false;
507                     return true;
508                 }
509 
510                 mState.currentSearch = null;
511                 onCurrentDirectoryChanged(ANIM_NONE);
512                 return true;
513             }
514         });
515 
516         mSearchView.setOnCloseListener(new SearchView.OnCloseListener() {
517             @Override
518             public boolean onClose() {
519                 mSearchExpanded = false;
520                 if (mIgnoreNextClose) {
521                     mIgnoreNextClose = false;
522                     return false;
523                 }
524 
525                 mState.currentSearch = null;
526                 onCurrentDirectoryChanged(ANIM_NONE);
527                 return false;
528             }
529         });
530 
531         return true;
532     }
533 
534     @Override
onPrepareOptionsMenu(Menu menu)535     public boolean onPrepareOptionsMenu(Menu menu) {
536         super.onPrepareOptionsMenu(menu);
537 
538         final FragmentManager fm = getFragmentManager();
539 
540         final RootInfo root = getCurrentRoot();
541         final DocumentInfo cwd = getCurrentDirectory();
542 
543         final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
544         final MenuItem search = menu.findItem(R.id.menu_search);
545         final MenuItem sort = menu.findItem(R.id.menu_sort);
546         final MenuItem sortSize = menu.findItem(R.id.menu_sort_size);
547         final MenuItem grid = menu.findItem(R.id.menu_grid);
548         final MenuItem list = menu.findItem(R.id.menu_list);
549         final MenuItem advanced = menu.findItem(R.id.menu_advanced);
550         final MenuItem fileSize = menu.findItem(R.id.menu_file_size);
551 
552         sort.setVisible(cwd != null);
553         grid.setVisible(mState.derivedMode != MODE_GRID);
554         list.setVisible(mState.derivedMode != MODE_LIST);
555 
556         if (mState.currentSearch != null) {
557             // Search uses backend ranking; no sorting
558             sort.setVisible(false);
559 
560             search.expandActionView();
561 
562             mSearchView.setIconified(false);
563             mSearchView.clearFocus();
564             mSearchView.setQuery(mState.currentSearch, false);
565         } else {
566             mIgnoreNextClose = true;
567             mSearchView.setIconified(true);
568             mSearchView.clearFocus();
569 
570             mIgnoreNextCollapse = true;
571             search.collapseActionView();
572         }
573 
574         // Only sort by size when visible
575         sortSize.setVisible(mState.showSize);
576 
577         final boolean searchVisible;
578         if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) {
579             createDir.setVisible(cwd != null && cwd.isCreateSupported());
580             searchVisible = false;
581 
582             // No display options in recent directories
583             if (cwd == null) {
584                 grid.setVisible(false);
585                 list.setVisible(false);
586             }
587 
588             if (mState.action == ACTION_CREATE) {
589                 SaveFragment.get(fm).setSaveEnabled(cwd != null && cwd.isCreateSupported());
590             }
591         } else {
592             createDir.setVisible(false);
593 
594             searchVisible = root != null
595                     && ((root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0);
596         }
597 
598         // TODO: close any search in-progress when hiding
599         search.setVisible(searchVisible);
600 
601         advanced.setTitle(LocalPreferences.getDisplayAdvancedDevices(this)
602                 ? R.string.menu_advanced_hide : R.string.menu_advanced_show);
603         fileSize.setTitle(LocalPreferences.getDisplayFileSize(this)
604                 ? R.string.menu_file_size_hide : R.string.menu_file_size_show);
605 
606         advanced.setVisible(mState.action != ACTION_MANAGE);
607         fileSize.setVisible(mState.action != ACTION_MANAGE);
608 
609         return true;
610     }
611 
612     @Override
onOptionsItemSelected(MenuItem item)613     public boolean onOptionsItemSelected(MenuItem item) {
614         if (mDrawerToggle != null && mDrawerToggle.onOptionsItemSelected(item)) {
615             return true;
616         }
617 
618         final int id = item.getItemId();
619         if (id == android.R.id.home) {
620             onBackPressed();
621             return true;
622         } else if (id == R.id.menu_create_dir) {
623             CreateDirectoryFragment.show(getFragmentManager());
624             return true;
625         } else if (id == R.id.menu_search) {
626             return false;
627         } else if (id == R.id.menu_sort_name) {
628             setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME);
629             return true;
630         } else if (id == R.id.menu_sort_date) {
631             setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED);
632             return true;
633         } else if (id == R.id.menu_sort_size) {
634             setUserSortOrder(State.SORT_ORDER_SIZE);
635             return true;
636         } else if (id == R.id.menu_grid) {
637             setUserMode(State.MODE_GRID);
638             return true;
639         } else if (id == R.id.menu_list) {
640             setUserMode(State.MODE_LIST);
641             return true;
642         } else if (id == R.id.menu_advanced) {
643             setDisplayAdvancedDevices(!LocalPreferences.getDisplayAdvancedDevices(this));
644             return true;
645         } else if (id == R.id.menu_file_size) {
646             setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this));
647             return true;
648         } else {
649             return super.onOptionsItemSelected(item);
650         }
651     }
652 
setDisplayAdvancedDevices(boolean display)653     private void setDisplayAdvancedDevices(boolean display) {
654         LocalPreferences.setDisplayAdvancedDevices(this, display);
655         mState.showAdvanced = mState.forceAdvanced | display;
656         RootsFragment.get(getFragmentManager()).onDisplayStateChanged();
657         invalidateOptionsMenu();
658     }
659 
setDisplayFileSize(boolean display)660     private void setDisplayFileSize(boolean display) {
661         LocalPreferences.setDisplayFileSize(this, display);
662         mState.showSize = display;
663         DirectoryFragment.get(getFragmentManager()).onDisplayStateChanged();
664         invalidateOptionsMenu();
665     }
666 
667     /**
668      * Update UI to reflect internal state changes not from user.
669      */
onStateChanged()670     public void onStateChanged() {
671         invalidateOptionsMenu();
672     }
673 
674     /**
675      * Set state sort order based on explicit user action.
676      */
setUserSortOrder(int sortOrder)677     private void setUserSortOrder(int sortOrder) {
678         mState.userSortOrder = sortOrder;
679         DirectoryFragment.get(getFragmentManager()).onUserSortOrderChanged();
680     }
681 
682     /**
683      * Set state mode based on explicit user action.
684      */
setUserMode(int mode)685     private void setUserMode(int mode) {
686         mState.userMode = mode;
687         DirectoryFragment.get(getFragmentManager()).onUserModeChanged();
688     }
689 
setPending(boolean pending)690     public void setPending(boolean pending) {
691         final SaveFragment save = SaveFragment.get(getFragmentManager());
692         if (save != null) {
693             save.setPending(pending);
694         }
695     }
696 
697     @Override
onBackPressed()698     public void onBackPressed() {
699         if (!mState.stackTouched) {
700             super.onBackPressed();
701             return;
702         }
703 
704         final int size = mState.stack.size();
705         if (size > 1) {
706             mState.stack.pop();
707             onCurrentDirectoryChanged(ANIM_UP);
708         } else if (size == 1 && !isRootsDrawerOpen()) {
709             // TODO: open root drawer once we can capture back key
710             super.onBackPressed();
711         } else {
712             super.onBackPressed();
713         }
714     }
715 
716     @Override
onSaveInstanceState(Bundle state)717     protected void onSaveInstanceState(Bundle state) {
718         super.onSaveInstanceState(state);
719         state.putParcelable(EXTRA_STATE, mState);
720     }
721 
722     @Override
onRestoreInstanceState(Bundle state)723     protected void onRestoreInstanceState(Bundle state) {
724         super.onRestoreInstanceState(state);
725         updateActionBar();
726     }
727 
728     private BaseAdapter mStackAdapter = new BaseAdapter() {
729         @Override
730         public int getCount() {
731             return mState.stack.size();
732         }
733 
734         @Override
735         public DocumentInfo getItem(int position) {
736             return mState.stack.get(mState.stack.size() - position - 1);
737         }
738 
739         @Override
740         public long getItemId(int position) {
741             return position;
742         }
743 
744         @Override
745         public View getView(int position, View convertView, ViewGroup parent) {
746             if (convertView == null) {
747                 convertView = LayoutInflater.from(parent.getContext())
748                         .inflate(R.layout.item_subdir_title, parent, false);
749             }
750 
751             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
752             final DocumentInfo doc = getItem(position);
753 
754             if (position == 0) {
755                 final RootInfo root = getCurrentRoot();
756                 title.setText(root.title);
757             } else {
758                 title.setText(doc.displayName);
759             }
760 
761             return convertView;
762         }
763 
764         @Override
765         public View getDropDownView(int position, View convertView, ViewGroup parent) {
766             if (convertView == null) {
767                 convertView = LayoutInflater.from(parent.getContext())
768                         .inflate(R.layout.item_subdir, parent, false);
769             }
770 
771             final ImageView subdir = (ImageView) convertView.findViewById(R.id.subdir);
772             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
773             final DocumentInfo doc = getItem(position);
774 
775             if (position == 0) {
776                 final RootInfo root = getCurrentRoot();
777                 title.setText(root.title);
778                 subdir.setVisibility(View.GONE);
779             } else {
780                 title.setText(doc.displayName);
781                 subdir.setVisibility(View.VISIBLE);
782             }
783 
784             return convertView;
785         }
786     };
787 
788     private OnItemSelectedListener mStackListener = new OnItemSelectedListener() {
789         @Override
790         public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
791             if (mIgnoreNextNavigation) {
792                 mIgnoreNextNavigation = false;
793                 return;
794             }
795 
796             while (mState.stack.size() > position + 1) {
797                 mState.stackTouched = true;
798                 mState.stack.pop();
799             }
800             onCurrentDirectoryChanged(ANIM_UP);
801         }
802 
803         @Override
804         public void onNothingSelected(AdapterView<?> parent) {
805             // Ignored
806         }
807     };
808 
getCurrentRoot()809     public RootInfo getCurrentRoot() {
810         if (mState.stack.root != null) {
811             return mState.stack.root;
812         } else {
813             return mRoots.getRecentsRoot();
814         }
815     }
816 
getCurrentDirectory()817     public DocumentInfo getCurrentDirectory() {
818         return mState.stack.peek();
819     }
820 
getCallingPackageMaybeExtra()821     private String getCallingPackageMaybeExtra() {
822         final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME);
823         return (extra != null) ? extra : getCallingPackage();
824     }
825 
getCurrentExecutor()826     public Executor getCurrentExecutor() {
827         final DocumentInfo cwd = getCurrentDirectory();
828         if (cwd != null && cwd.authority != null) {
829             return ProviderExecutor.forAuthority(cwd.authority);
830         } else {
831             return AsyncTask.THREAD_POOL_EXECUTOR;
832         }
833     }
834 
getDisplayState()835     public State getDisplayState() {
836         return mState;
837     }
838 
onCurrentDirectoryChanged(int anim)839     private void onCurrentDirectoryChanged(int anim) {
840         final FragmentManager fm = getFragmentManager();
841         final RootInfo root = getCurrentRoot();
842         final DocumentInfo cwd = getCurrentDirectory();
843 
844         mDirectoryContainer.setDrawDisappearingFirst(anim == ANIM_DOWN);
845 
846         if (cwd == null) {
847             // No directory means recents
848             if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) {
849                 RecentsCreateFragment.show(fm);
850             } else {
851                 DirectoryFragment.showRecentsOpen(fm, anim);
852 
853                 // Start recents in grid when requesting visual things
854                 final boolean visualMimes = MimePredicate.mimeMatches(
855                         MimePredicate.VISUAL_MIMES, mState.acceptMimes);
856                 mState.userMode = visualMimes ? MODE_GRID : MODE_LIST;
857                 mState.derivedMode = mState.userMode;
858             }
859         } else {
860             if (mState.currentSearch != null) {
861                 // Ongoing search
862                 DirectoryFragment.showSearch(fm, root, mState.currentSearch, anim);
863             } else {
864                 // Normal boring directory
865                 DirectoryFragment.showNormal(fm, root, cwd, anim);
866             }
867         }
868 
869         // Forget any replacement target
870         if (mState.action == ACTION_CREATE) {
871             final SaveFragment save = SaveFragment.get(fm);
872             if (save != null) {
873                 save.setReplaceTarget(null);
874             }
875         }
876 
877         if (mState.action == ACTION_OPEN_TREE) {
878             final PickFragment pick = PickFragment.get(fm);
879             if (pick != null) {
880                 final CharSequence displayName = (mState.stack.size() <= 1) ? root.title
881                         : cwd.displayName;
882                 pick.setPickTarget(cwd, displayName);
883             }
884         }
885 
886         final RootsFragment roots = RootsFragment.get(fm);
887         if (roots != null) {
888             roots.onCurrentRootChanged();
889         }
890 
891         updateActionBar();
892         invalidateOptionsMenu();
893         dumpStack();
894     }
895 
onStackPicked(DocumentStack stack)896     public void onStackPicked(DocumentStack stack) {
897         try {
898             // Update the restored stack to ensure we have freshest data
899             stack.updateDocuments(getContentResolver());
900 
901             mState.stack = stack;
902             mState.stackTouched = true;
903             onCurrentDirectoryChanged(ANIM_SIDE);
904 
905         } catch (FileNotFoundException e) {
906             Log.w(TAG, "Failed to restore stack: " + e);
907         }
908     }
909 
onRootPicked(RootInfo root, boolean closeDrawer)910     public void onRootPicked(RootInfo root, boolean closeDrawer) {
911         // Clear entire backstack and start in new root
912         mState.stack.root = root;
913         mState.stack.clear();
914         mState.stackTouched = true;
915 
916         if (!mRoots.isRecentsRoot(root)) {
917             new PickRootTask(root).executeOnExecutor(getCurrentExecutor());
918         } else {
919             onCurrentDirectoryChanged(ANIM_SIDE);
920         }
921 
922         if (closeDrawer) {
923             setRootsDrawerOpen(false);
924         }
925     }
926 
927     private class PickRootTask extends AsyncTask<Void, Void, DocumentInfo> {
928         private RootInfo mRoot;
929 
PickRootTask(RootInfo root)930         public PickRootTask(RootInfo root) {
931             mRoot = root;
932         }
933 
934         @Override
doInBackground(Void... params)935         protected DocumentInfo doInBackground(Void... params) {
936             try {
937                 final Uri uri = DocumentsContract.buildDocumentUri(
938                         mRoot.authority, mRoot.documentId);
939                 return DocumentInfo.fromUri(getContentResolver(), uri);
940             } catch (FileNotFoundException e) {
941                 Log.w(TAG, "Failed to find root", e);
942                 return null;
943             }
944         }
945 
946         @Override
onPostExecute(DocumentInfo result)947         protected void onPostExecute(DocumentInfo result) {
948             if (result != null) {
949                 mState.stack.push(result);
950                 mState.stackTouched = true;
951                 onCurrentDirectoryChanged(ANIM_SIDE);
952             }
953         }
954     }
955 
onAppPicked(ResolveInfo info)956     public void onAppPicked(ResolveInfo info) {
957         final Intent intent = new Intent(getIntent());
958         intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
959         intent.setComponent(new ComponentName(
960                 info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
961         startActivityForResult(intent, CODE_FORWARD);
962     }
963 
964     @Override
onActivityResult(int requestCode, int resultCode, Intent data)965     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
966         Log.d(TAG, "onActivityResult() code=" + resultCode);
967 
968         // Only relay back results when not canceled; otherwise stick around to
969         // let the user pick another app/backend.
970         if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) {
971 
972             // Remember that we last picked via external app
973             final String packageName = getCallingPackageMaybeExtra();
974             final ContentValues values = new ContentValues();
975             values.put(ResumeColumns.EXTERNAL, 1);
976             getContentResolver().insert(RecentsProvider.buildResume(packageName), values);
977 
978             // Pass back result to original caller
979             setResult(resultCode, data);
980             finish();
981         } else {
982             super.onActivityResult(requestCode, resultCode, data);
983         }
984     }
985 
onDocumentPicked(DocumentInfo doc)986     public void onDocumentPicked(DocumentInfo doc) {
987         final FragmentManager fm = getFragmentManager();
988         if (doc.isDirectory()) {
989             mState.stack.push(doc);
990             mState.stackTouched = true;
991             onCurrentDirectoryChanged(ANIM_DOWN);
992         } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
993             // Explicit file picked, return
994             new ExistingFinishTask(doc.derivedUri).executeOnExecutor(getCurrentExecutor());
995         } else if (mState.action == ACTION_CREATE) {
996             // Replace selected file
997             SaveFragment.get(fm).setReplaceTarget(doc);
998         } else if (mState.action == ACTION_MANAGE) {
999             // First try managing the document; we expect manager to filter
1000             // based on authority, so we don't grant.
1001             final Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
1002             manage.setData(doc.derivedUri);
1003 
1004             try {
1005                 startActivity(manage);
1006             } catch (ActivityNotFoundException ex) {
1007                 // Fall back to viewing
1008                 final Intent view = new Intent(Intent.ACTION_VIEW);
1009                 view.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1010                 view.setData(doc.derivedUri);
1011 
1012                 try {
1013                     startActivity(view);
1014                 } catch (ActivityNotFoundException ex2) {
1015                     Toast.makeText(this, R.string.toast_no_application, Toast.LENGTH_SHORT).show();
1016                 }
1017             }
1018         }
1019     }
1020 
onDocumentsPicked(List<DocumentInfo> docs)1021     public void onDocumentsPicked(List<DocumentInfo> docs) {
1022         if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
1023             final int size = docs.size();
1024             final Uri[] uris = new Uri[size];
1025             for (int i = 0; i < size; i++) {
1026                 uris[i] = docs.get(i).derivedUri;
1027             }
1028             new ExistingFinishTask(uris).executeOnExecutor(getCurrentExecutor());
1029         }
1030     }
1031 
onSaveRequested(DocumentInfo replaceTarget)1032     public void onSaveRequested(DocumentInfo replaceTarget) {
1033         new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getCurrentExecutor());
1034     }
1035 
onSaveRequested(String mimeType, String displayName)1036     public void onSaveRequested(String mimeType, String displayName) {
1037         new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor());
1038     }
1039 
onPickRequested(DocumentInfo pickTarget)1040     public void onPickRequested(DocumentInfo pickTarget) {
1041         final Uri viaUri = DocumentsContract.buildTreeDocumentUri(pickTarget.authority,
1042                 pickTarget.documentId);
1043         new PickFinishTask(viaUri).executeOnExecutor(getCurrentExecutor());
1044     }
1045 
saveStackBlocking()1046     private void saveStackBlocking() {
1047         final ContentResolver resolver = getContentResolver();
1048         final ContentValues values = new ContentValues();
1049 
1050         final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
1051         if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) {
1052             // Remember stack for last create
1053             values.clear();
1054             values.put(RecentColumns.KEY, mState.stack.buildKey());
1055             values.put(RecentColumns.STACK, rawStack);
1056             resolver.insert(RecentsProvider.buildRecent(), values);
1057         }
1058 
1059         // Remember location for next app launch
1060         final String packageName = getCallingPackageMaybeExtra();
1061         values.clear();
1062         values.put(ResumeColumns.STACK, rawStack);
1063         values.put(ResumeColumns.EXTERNAL, 0);
1064         resolver.insert(RecentsProvider.buildResume(packageName), values);
1065     }
1066 
onFinished(Uri... uris)1067     private void onFinished(Uri... uris) {
1068         Log.d(TAG, "onFinished() " + Arrays.toString(uris));
1069 
1070         final Intent intent = new Intent();
1071         if (uris.length == 1) {
1072             intent.setData(uris[0]);
1073         } else if (uris.length > 1) {
1074             final ClipData clipData = new ClipData(
1075                     null, mState.acceptMimes, new ClipData.Item(uris[0]));
1076             for (int i = 1; i < uris.length; i++) {
1077                 clipData.addItem(new ClipData.Item(uris[i]));
1078             }
1079             intent.setClipData(clipData);
1080         }
1081 
1082         if (mState.action == ACTION_GET_CONTENT) {
1083             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1084         } else if (mState.action == ACTION_OPEN_TREE) {
1085             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
1086                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
1087                     | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
1088                     | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
1089         } else {
1090             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
1091                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
1092                     | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
1093         }
1094 
1095         setResult(Activity.RESULT_OK, intent);
1096         finish();
1097     }
1098 
1099     private class CreateFinishTask extends AsyncTask<Void, Void, Uri> {
1100         private final String mMimeType;
1101         private final String mDisplayName;
1102 
CreateFinishTask(String mimeType, String displayName)1103         public CreateFinishTask(String mimeType, String displayName) {
1104             mMimeType = mimeType;
1105             mDisplayName = displayName;
1106         }
1107 
1108         @Override
onPreExecute()1109         protected void onPreExecute() {
1110             setPending(true);
1111         }
1112 
1113         @Override
doInBackground(Void... params)1114         protected Uri doInBackground(Void... params) {
1115             final ContentResolver resolver = getContentResolver();
1116             final DocumentInfo cwd = getCurrentDirectory();
1117 
1118             ContentProviderClient client = null;
1119             Uri childUri = null;
1120             try {
1121                 client = DocumentsApplication.acquireUnstableProviderOrThrow(
1122                         resolver, cwd.derivedUri.getAuthority());
1123                 childUri = DocumentsContract.createDocument(
1124                         client, cwd.derivedUri, mMimeType, mDisplayName);
1125             } catch (Exception e) {
1126                 Log.w(TAG, "Failed to create document", e);
1127             } finally {
1128                 ContentProviderClient.releaseQuietly(client);
1129             }
1130 
1131             if (childUri != null) {
1132                 saveStackBlocking();
1133             }
1134 
1135             return childUri;
1136         }
1137 
1138         @Override
onPostExecute(Uri result)1139         protected void onPostExecute(Uri result) {
1140             if (result != null) {
1141                 onFinished(result);
1142             } else {
1143                 Toast.makeText(DocumentsActivity.this, R.string.save_error, Toast.LENGTH_SHORT)
1144                         .show();
1145             }
1146 
1147             setPending(false);
1148         }
1149     }
1150 
1151     private class ExistingFinishTask extends AsyncTask<Void, Void, Void> {
1152         private final Uri[] mUris;
1153 
ExistingFinishTask(Uri... uris)1154         public ExistingFinishTask(Uri... uris) {
1155             mUris = uris;
1156         }
1157 
1158         @Override
doInBackground(Void... params)1159         protected Void doInBackground(Void... params) {
1160             saveStackBlocking();
1161             return null;
1162         }
1163 
1164         @Override
onPostExecute(Void result)1165         protected void onPostExecute(Void result) {
1166             onFinished(mUris);
1167         }
1168     }
1169 
1170     private class PickFinishTask extends AsyncTask<Void, Void, Void> {
1171         private final Uri mUri;
1172 
PickFinishTask(Uri uri)1173         public PickFinishTask(Uri uri) {
1174             mUri = uri;
1175         }
1176 
1177         @Override
doInBackground(Void... params)1178         protected Void doInBackground(Void... params) {
1179             saveStackBlocking();
1180             return null;
1181         }
1182 
1183         @Override
onPostExecute(Void result)1184         protected void onPostExecute(Void result) {
1185             onFinished(mUri);
1186         }
1187     }
1188 
1189     public static class State implements android.os.Parcelable {
1190         public int action;
1191         public String[] acceptMimes;
1192 
1193         /** Explicit user choice */
1194         public int userMode = MODE_UNKNOWN;
1195         /** Derived after loader */
1196         public int derivedMode = MODE_LIST;
1197 
1198         /** Explicit user choice */
1199         public int userSortOrder = SORT_ORDER_UNKNOWN;
1200         /** Derived after loader */
1201         public int derivedSortOrder = SORT_ORDER_DISPLAY_NAME;
1202 
1203         public boolean allowMultiple = false;
1204         public boolean showSize = false;
1205         public boolean localOnly = false;
1206         public boolean forceAdvanced = false;
1207         public boolean showAdvanced = false;
1208         public boolean stackTouched = false;
1209         public boolean restored = false;
1210 
1211         /** Current user navigation stack; empty implies recents. */
1212         public DocumentStack stack = new DocumentStack();
1213         /** Currently active search, overriding any stack. */
1214         public String currentSearch;
1215 
1216         /** Instance state for every shown directory */
1217         public HashMap<String, SparseArray<Parcelable>> dirState = Maps.newHashMap();
1218 
1219         public static final int ACTION_OPEN = 1;
1220         public static final int ACTION_CREATE = 2;
1221         public static final int ACTION_GET_CONTENT = 3;
1222         public static final int ACTION_OPEN_TREE = 4;
1223         public static final int ACTION_MANAGE = 5;
1224 
1225         public static final int MODE_UNKNOWN = 0;
1226         public static final int MODE_LIST = 1;
1227         public static final int MODE_GRID = 2;
1228 
1229         public static final int SORT_ORDER_UNKNOWN = 0;
1230         public static final int SORT_ORDER_DISPLAY_NAME = 1;
1231         public static final int SORT_ORDER_LAST_MODIFIED = 2;
1232         public static final int SORT_ORDER_SIZE = 3;
1233 
1234         @Override
describeContents()1235         public int describeContents() {
1236             return 0;
1237         }
1238 
1239         @Override
writeToParcel(Parcel out, int flags)1240         public void writeToParcel(Parcel out, int flags) {
1241             out.writeInt(action);
1242             out.writeInt(userMode);
1243             out.writeStringArray(acceptMimes);
1244             out.writeInt(userSortOrder);
1245             out.writeInt(allowMultiple ? 1 : 0);
1246             out.writeInt(showSize ? 1 : 0);
1247             out.writeInt(localOnly ? 1 : 0);
1248             out.writeInt(forceAdvanced ? 1 : 0);
1249             out.writeInt(showAdvanced ? 1 : 0);
1250             out.writeInt(stackTouched ? 1 : 0);
1251             out.writeInt(restored ? 1 : 0);
1252             DurableUtils.writeToParcel(out, stack);
1253             out.writeString(currentSearch);
1254             out.writeMap(dirState);
1255         }
1256 
1257         public static final Creator<State> CREATOR = new Creator<State>() {
1258             @Override
1259             public State createFromParcel(Parcel in) {
1260                 final State state = new State();
1261                 state.action = in.readInt();
1262                 state.userMode = in.readInt();
1263                 state.acceptMimes = in.readStringArray();
1264                 state.userSortOrder = in.readInt();
1265                 state.allowMultiple = in.readInt() != 0;
1266                 state.showSize = in.readInt() != 0;
1267                 state.localOnly = in.readInt() != 0;
1268                 state.forceAdvanced = in.readInt() != 0;
1269                 state.showAdvanced = in.readInt() != 0;
1270                 state.stackTouched = in.readInt() != 0;
1271                 state.restored = in.readInt() != 0;
1272                 DurableUtils.readFromParcel(in, state.stack);
1273                 state.currentSearch = in.readString();
1274                 in.readMap(state.dirState, null);
1275                 return state;
1276             }
1277 
1278             @Override
1279             public State[] newArray(int size) {
1280                 return new State[size];
1281             }
1282         };
1283     }
1284 
dumpStack()1285     private void dumpStack() {
1286         Log.d(TAG, "Current stack: ");
1287         Log.d(TAG, " * " + mState.stack.root);
1288         for (DocumentInfo doc : mState.stack) {
1289             Log.d(TAG, " +-- " + doc);
1290         }
1291     }
1292 
get(Fragment fragment)1293     public static DocumentsActivity get(Fragment fragment) {
1294         return (DocumentsActivity) fragment.getActivity();
1295     }
1296 }
1297