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         updateActionBar();
390     }
391 
setRootsDrawerOpen(boolean open)392     public void setRootsDrawerOpen(boolean open) {
393         if (!mShowAsDialog) {
394             if (open) {
395                 mDrawerLayout.openDrawer(mRootsDrawer);
396             } else {
397                 mDrawerLayout.closeDrawer(mRootsDrawer);
398             }
399         }
400     }
401 
isRootsDrawerOpen()402     private boolean isRootsDrawerOpen() {
403         if (mShowAsDialog) {
404             return false;
405         } else {
406             return mDrawerLayout.isDrawerOpen(mRootsDrawer);
407         }
408     }
409 
updateActionBar()410     public void updateActionBar() {
411         if (mRootsToolbar != null) {
412             if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT
413                     || mState.action == ACTION_OPEN_TREE) {
414                 mRootsToolbar.setTitle(R.string.title_open);
415             } else if (mState.action == ACTION_CREATE) {
416                 mRootsToolbar.setTitle(R.string.title_save);
417             }
418         }
419 
420         final RootInfo root = getCurrentRoot();
421         final boolean showRootIcon = mShowAsDialog || (mState.action == ACTION_MANAGE);
422         if (showRootIcon) {
423             mToolbar.setNavigationIcon(
424                     root != null ? root.loadToolbarIcon(mToolbar.getContext()) : null);
425             mToolbar.setNavigationContentDescription(R.string.drawer_open);
426             mToolbar.setNavigationOnClickListener(null);
427         } else {
428             mToolbar.setNavigationIcon(R.drawable.ic_hamburger);
429             mToolbar.setNavigationContentDescription(R.string.drawer_open);
430             mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
431                 @Override
432                 public void onClick(View v) {
433                     setRootsDrawerOpen(true);
434                 }
435             });
436         }
437 
438         if (mSearchExpanded) {
439             mToolbar.setTitle(null);
440             mToolbarStack.setVisibility(View.GONE);
441             mToolbarStack.setAdapter(null);
442         } else {
443             if (mState.stack.size() <= 1) {
444                 mToolbar.setTitle(root.title);
445                 mToolbarStack.setVisibility(View.GONE);
446                 mToolbarStack.setAdapter(null);
447             } else {
448                 mToolbar.setTitle(null);
449                 mToolbarStack.setVisibility(View.VISIBLE);
450                 mToolbarStack.setAdapter(mStackAdapter);
451 
452                 mIgnoreNextNavigation = true;
453                 mToolbarStack.setSelection(mStackAdapter.getCount() - 1);
454             }
455         }
456     }
457 
458     @Override
onCreateOptionsMenu(Menu menu)459     public boolean onCreateOptionsMenu(Menu menu) {
460         super.onCreateOptionsMenu(menu);
461         getMenuInflater().inflate(R.menu.activity, menu);
462 
463         // Most actions are visible when showing as dialog
464         if (mShowAsDialog) {
465             for (int i = 0; i < menu.size(); i++) {
466                 final MenuItem item = menu.getItem(i);
467                 switch (item.getItemId()) {
468                     case R.id.menu_advanced:
469                     case R.id.menu_file_size:
470                         break;
471                     default:
472                         item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
473                 }
474             }
475         }
476 
477         final MenuItem searchMenu = menu.findItem(R.id.menu_search);
478         mSearchView = (SearchView) searchMenu.getActionView();
479         mSearchView.setOnQueryTextListener(new OnQueryTextListener() {
480             @Override
481             public boolean onQueryTextSubmit(String query) {
482                 mSearchExpanded = true;
483                 mState.currentSearch = query;
484                 mSearchView.clearFocus();
485                 onCurrentDirectoryChanged(ANIM_NONE);
486                 return true;
487             }
488 
489             @Override
490             public boolean onQueryTextChange(String newText) {
491                 return false;
492             }
493         });
494 
495         searchMenu.setOnActionExpandListener(new OnActionExpandListener() {
496             @Override
497             public boolean onMenuItemActionExpand(MenuItem item) {
498                 mSearchExpanded = true;
499                 updateActionBar();
500                 return true;
501             }
502 
503             @Override
504             public boolean onMenuItemActionCollapse(MenuItem item) {
505                 mSearchExpanded = false;
506                 if (mIgnoreNextCollapse) {
507                     mIgnoreNextCollapse = false;
508                     return true;
509                 }
510 
511                 mState.currentSearch = null;
512                 onCurrentDirectoryChanged(ANIM_NONE);
513                 return true;
514             }
515         });
516 
517         mSearchView.setOnCloseListener(new SearchView.OnCloseListener() {
518             @Override
519             public boolean onClose() {
520                 mSearchExpanded = false;
521                 if (mIgnoreNextClose) {
522                     mIgnoreNextClose = false;
523                     return false;
524                 }
525 
526                 mState.currentSearch = null;
527                 onCurrentDirectoryChanged(ANIM_NONE);
528                 return false;
529             }
530         });
531 
532         return true;
533     }
534 
535     @Override
onPrepareOptionsMenu(Menu menu)536     public boolean onPrepareOptionsMenu(Menu menu) {
537         super.onPrepareOptionsMenu(menu);
538 
539         final FragmentManager fm = getFragmentManager();
540 
541         final RootInfo root = getCurrentRoot();
542         final DocumentInfo cwd = getCurrentDirectory();
543 
544         final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
545         final MenuItem search = menu.findItem(R.id.menu_search);
546         final MenuItem sort = menu.findItem(R.id.menu_sort);
547         final MenuItem sortSize = menu.findItem(R.id.menu_sort_size);
548         final MenuItem grid = menu.findItem(R.id.menu_grid);
549         final MenuItem list = menu.findItem(R.id.menu_list);
550         final MenuItem advanced = menu.findItem(R.id.menu_advanced);
551         final MenuItem fileSize = menu.findItem(R.id.menu_file_size);
552 
553         sort.setVisible(cwd != null);
554         grid.setVisible(mState.derivedMode != MODE_GRID);
555         list.setVisible(mState.derivedMode != MODE_LIST);
556 
557         if (mState.currentSearch != null) {
558             // Search uses backend ranking; no sorting
559             sort.setVisible(false);
560 
561             search.expandActionView();
562 
563             mSearchView.setIconified(false);
564             mSearchView.clearFocus();
565             mSearchView.setQuery(mState.currentSearch, false);
566         } else {
567             mIgnoreNextClose = true;
568             mSearchView.setIconified(true);
569             mSearchView.clearFocus();
570 
571             mIgnoreNextCollapse = true;
572             search.collapseActionView();
573         }
574 
575         // Only sort by size when visible
576         sortSize.setVisible(mState.showSize);
577 
578         boolean searchVisible;
579         boolean fileSizeVisible = mState.action != ACTION_MANAGE;
580         if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) {
581             createDir.setVisible(cwd != null && cwd.isCreateSupported());
582             searchVisible = false;
583 
584             // No display options in recent directories
585             if (cwd == null) {
586                 grid.setVisible(false);
587                 list.setVisible(false);
588                 fileSizeVisible = false;
589             }
590 
591             if (mState.action == ACTION_CREATE) {
592                 SaveFragment.get(fm).setSaveEnabled(cwd != null && cwd.isCreateSupported());
593             }
594         } else {
595             createDir.setVisible(false);
596 
597             searchVisible = root != null
598                     && ((root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0);
599         }
600 
601         // TODO: close any search in-progress when hiding
602         search.setVisible(searchVisible);
603 
604         advanced.setTitle(LocalPreferences.getDisplayAdvancedDevices(this)
605                 ? R.string.menu_advanced_hide : R.string.menu_advanced_show);
606         fileSize.setTitle(LocalPreferences.getDisplayFileSize(this)
607                 ? R.string.menu_file_size_hide : R.string.menu_file_size_show);
608 
609         advanced.setVisible(mState.action != ACTION_MANAGE);
610         fileSize.setVisible(fileSizeVisible);
611 
612         return true;
613     }
614 
615     @Override
onOptionsItemSelected(MenuItem item)616     public boolean onOptionsItemSelected(MenuItem item) {
617         if (mDrawerToggle != null && mDrawerToggle.onOptionsItemSelected(item)) {
618             return true;
619         }
620 
621         final int id = item.getItemId();
622         if (id == android.R.id.home) {
623             onBackPressed();
624             return true;
625         } else if (id == R.id.menu_create_dir) {
626             CreateDirectoryFragment.show(getFragmentManager());
627             return true;
628         } else if (id == R.id.menu_search) {
629             return false;
630         } else if (id == R.id.menu_sort_name) {
631             setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME);
632             return true;
633         } else if (id == R.id.menu_sort_date) {
634             setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED);
635             return true;
636         } else if (id == R.id.menu_sort_size) {
637             setUserSortOrder(State.SORT_ORDER_SIZE);
638             return true;
639         } else if (id == R.id.menu_grid) {
640             setUserMode(State.MODE_GRID);
641             return true;
642         } else if (id == R.id.menu_list) {
643             setUserMode(State.MODE_LIST);
644             return true;
645         } else if (id == R.id.menu_advanced) {
646             setDisplayAdvancedDevices(!LocalPreferences.getDisplayAdvancedDevices(this));
647             return true;
648         } else if (id == R.id.menu_file_size) {
649             setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this));
650             return true;
651         } else {
652             return super.onOptionsItemSelected(item);
653         }
654     }
655 
setDisplayAdvancedDevices(boolean display)656     private void setDisplayAdvancedDevices(boolean display) {
657         LocalPreferences.setDisplayAdvancedDevices(this, display);
658         mState.showAdvanced = mState.forceAdvanced | display;
659         RootsFragment.get(getFragmentManager()).onDisplayStateChanged();
660         invalidateOptionsMenu();
661     }
662 
setDisplayFileSize(boolean display)663     private void setDisplayFileSize(boolean display) {
664         LocalPreferences.setDisplayFileSize(this, display);
665         mState.showSize = display;
666         DirectoryFragment.get(getFragmentManager()).onDisplayStateChanged();
667         invalidateOptionsMenu();
668     }
669 
670     /**
671      * Update UI to reflect internal state changes not from user.
672      */
onStateChanged()673     public void onStateChanged() {
674         invalidateOptionsMenu();
675     }
676 
677     /**
678      * Set state sort order based on explicit user action.
679      */
setUserSortOrder(int sortOrder)680     private void setUserSortOrder(int sortOrder) {
681         mState.userSortOrder = sortOrder;
682         DirectoryFragment.get(getFragmentManager()).onUserSortOrderChanged();
683     }
684 
685     /**
686      * Set state mode based on explicit user action.
687      */
setUserMode(int mode)688     private void setUserMode(int mode) {
689         mState.userMode = mode;
690         DirectoryFragment.get(getFragmentManager()).onUserModeChanged();
691     }
692 
setPending(boolean pending)693     public void setPending(boolean pending) {
694         final SaveFragment save = SaveFragment.get(getFragmentManager());
695         if (save != null) {
696             save.setPending(pending);
697         }
698     }
699 
700     @Override
onBackPressed()701     public void onBackPressed() {
702         if (!mState.stackTouched) {
703             super.onBackPressed();
704             return;
705         }
706 
707         final int size = mState.stack.size();
708         if (size > 1) {
709             mState.stack.pop();
710             onCurrentDirectoryChanged(ANIM_UP);
711         } else if (size == 1 && !isRootsDrawerOpen()) {
712             // TODO: open root drawer once we can capture back key
713             super.onBackPressed();
714         } else {
715             super.onBackPressed();
716         }
717     }
718 
719     @Override
onSaveInstanceState(Bundle state)720     protected void onSaveInstanceState(Bundle state) {
721         super.onSaveInstanceState(state);
722         state.putParcelable(EXTRA_STATE, mState);
723     }
724 
725     @Override
onRestoreInstanceState(Bundle state)726     protected void onRestoreInstanceState(Bundle state) {
727         super.onRestoreInstanceState(state);
728     }
729 
730     private BaseAdapter mStackAdapter = new BaseAdapter() {
731         @Override
732         public int getCount() {
733             return mState.stack.size();
734         }
735 
736         @Override
737         public DocumentInfo getItem(int position) {
738             return mState.stack.get(mState.stack.size() - position - 1);
739         }
740 
741         @Override
742         public long getItemId(int position) {
743             return position;
744         }
745 
746         @Override
747         public View getView(int position, View convertView, ViewGroup parent) {
748             if (convertView == null) {
749                 convertView = LayoutInflater.from(parent.getContext())
750                         .inflate(R.layout.item_subdir_title, parent, false);
751             }
752 
753             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
754             final DocumentInfo doc = getItem(position);
755 
756             if (position == 0) {
757                 final RootInfo root = getCurrentRoot();
758                 title.setText(root.title);
759             } else {
760                 title.setText(doc.displayName);
761             }
762 
763             return convertView;
764         }
765 
766         @Override
767         public View getDropDownView(int position, View convertView, ViewGroup parent) {
768             if (convertView == null) {
769                 convertView = LayoutInflater.from(parent.getContext())
770                         .inflate(R.layout.item_subdir, parent, false);
771             }
772 
773             final ImageView subdir = (ImageView) convertView.findViewById(R.id.subdir);
774             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
775             final DocumentInfo doc = getItem(position);
776 
777             if (position == 0) {
778                 final RootInfo root = getCurrentRoot();
779                 title.setText(root.title);
780                 subdir.setVisibility(View.GONE);
781             } else {
782                 title.setText(doc.displayName);
783                 subdir.setVisibility(View.VISIBLE);
784             }
785 
786             return convertView;
787         }
788     };
789 
790     private OnItemSelectedListener mStackListener = new OnItemSelectedListener() {
791         @Override
792         public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
793             if (mIgnoreNextNavigation) {
794                 mIgnoreNextNavigation = false;
795                 return;
796             }
797 
798             while (mState.stack.size() > position + 1) {
799                 mState.stackTouched = true;
800                 mState.stack.pop();
801             }
802             onCurrentDirectoryChanged(ANIM_UP);
803         }
804 
805         @Override
806         public void onNothingSelected(AdapterView<?> parent) {
807             // Ignored
808         }
809     };
810 
getCurrentRoot()811     public RootInfo getCurrentRoot() {
812         if (mState.stack.root != null) {
813             return mState.stack.root;
814         } else {
815             return mRoots.getRecentsRoot();
816         }
817     }
818 
getCurrentDirectory()819     public DocumentInfo getCurrentDirectory() {
820         return mState.stack.peek();
821     }
822 
getCallingPackageMaybeExtra()823     private String getCallingPackageMaybeExtra() {
824         final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME);
825         return (extra != null) ? extra : getCallingPackage();
826     }
827 
getCurrentExecutor()828     public Executor getCurrentExecutor() {
829         final DocumentInfo cwd = getCurrentDirectory();
830         if (cwd != null && cwd.authority != null) {
831             return ProviderExecutor.forAuthority(cwd.authority);
832         } else {
833             return AsyncTask.THREAD_POOL_EXECUTOR;
834         }
835     }
836 
getDisplayState()837     public State getDisplayState() {
838         return mState;
839     }
840 
onCurrentDirectoryChanged(int anim)841     private void onCurrentDirectoryChanged(int anim) {
842         final FragmentManager fm = getFragmentManager();
843         final RootInfo root = getCurrentRoot();
844         final DocumentInfo cwd = getCurrentDirectory();
845 
846         mDirectoryContainer.setDrawDisappearingFirst(anim == ANIM_DOWN);
847 
848         if (cwd == null) {
849             // No directory means recents
850             if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) {
851                 RecentsCreateFragment.show(fm);
852             } else {
853                 DirectoryFragment.showRecentsOpen(fm, anim);
854 
855                 // Start recents in grid when requesting visual things
856                 final boolean visualMimes = MimePredicate.mimeMatches(
857                         MimePredicate.VISUAL_MIMES, mState.acceptMimes);
858                 mState.userMode = visualMimes ? MODE_GRID : MODE_LIST;
859                 mState.derivedMode = mState.userMode;
860             }
861         } else {
862             if (mState.currentSearch != null) {
863                 // Ongoing search
864                 DirectoryFragment.showSearch(fm, root, mState.currentSearch, anim);
865             } else {
866                 // Normal boring directory
867                 DirectoryFragment.showNormal(fm, root, cwd, anim);
868             }
869         }
870 
871         // Forget any replacement target
872         if (mState.action == ACTION_CREATE) {
873             final SaveFragment save = SaveFragment.get(fm);
874             if (save != null) {
875                 save.setReplaceTarget(null);
876             }
877         }
878 
879         if (mState.action == ACTION_OPEN_TREE) {
880             final PickFragment pick = PickFragment.get(fm);
881             if (pick != null) {
882                 final CharSequence displayName = (mState.stack.size() <= 1) ? root.title
883                         : cwd.displayName;
884                 pick.setPickTarget(cwd, displayName);
885             }
886         }
887 
888         final RootsFragment roots = RootsFragment.get(fm);
889         if (roots != null) {
890             roots.onCurrentRootChanged();
891         }
892 
893         updateActionBar();
894         invalidateOptionsMenu();
895         dumpStack();
896     }
897 
onStackPicked(DocumentStack stack)898     public void onStackPicked(DocumentStack stack) {
899         try {
900             // Update the restored stack to ensure we have freshest data
901             stack.updateDocuments(getContentResolver());
902 
903             mState.stack = stack;
904             mState.stackTouched = true;
905             onCurrentDirectoryChanged(ANIM_SIDE);
906 
907         } catch (FileNotFoundException e) {
908             Log.w(TAG, "Failed to restore stack: " + e);
909         }
910     }
911 
onRootPicked(RootInfo root, boolean closeDrawer)912     public void onRootPicked(RootInfo root, boolean closeDrawer) {
913         // Clear entire backstack and start in new root
914         mState.stack.root = root;
915         mState.stack.clear();
916         mState.stackTouched = true;
917 
918         if (!mRoots.isRecentsRoot(root)) {
919             new PickRootTask(root).executeOnExecutor(getCurrentExecutor());
920         } else {
921             onCurrentDirectoryChanged(ANIM_SIDE);
922         }
923 
924         if (closeDrawer) {
925             setRootsDrawerOpen(false);
926         }
927     }
928 
929     private class PickRootTask extends AsyncTask<Void, Void, DocumentInfo> {
930         private RootInfo mRoot;
931 
PickRootTask(RootInfo root)932         public PickRootTask(RootInfo root) {
933             mRoot = root;
934         }
935 
936         @Override
doInBackground(Void... params)937         protected DocumentInfo doInBackground(Void... params) {
938             try {
939                 final Uri uri = DocumentsContract.buildDocumentUri(
940                         mRoot.authority, mRoot.documentId);
941                 return DocumentInfo.fromUri(getContentResolver(), uri);
942             } catch (FileNotFoundException e) {
943                 Log.w(TAG, "Failed to find root", e);
944                 return null;
945             }
946         }
947 
948         @Override
onPostExecute(DocumentInfo result)949         protected void onPostExecute(DocumentInfo result) {
950             if (result != null) {
951                 mState.stack.push(result);
952                 mState.stackTouched = true;
953                 onCurrentDirectoryChanged(ANIM_SIDE);
954             }
955         }
956     }
957 
onAppPicked(ResolveInfo info)958     public void onAppPicked(ResolveInfo info) {
959         final Intent intent = new Intent(getIntent());
960         intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
961         intent.setComponent(new ComponentName(
962                 info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
963         startActivityForResult(intent, CODE_FORWARD);
964     }
965 
966     @Override
onActivityResult(int requestCode, int resultCode, Intent data)967     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
968         Log.d(TAG, "onActivityResult() code=" + resultCode);
969 
970         // Only relay back results when not canceled; otherwise stick around to
971         // let the user pick another app/backend.
972         if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) {
973 
974             // Remember that we last picked via external app
975             final String packageName = getCallingPackageMaybeExtra();
976             final ContentValues values = new ContentValues();
977             values.put(ResumeColumns.EXTERNAL, 1);
978             getContentResolver().insert(RecentsProvider.buildResume(packageName), values);
979 
980             // Pass back result to original caller
981             setResult(resultCode, data);
982             finish();
983         } else {
984             super.onActivityResult(requestCode, resultCode, data);
985         }
986     }
987 
onDocumentPicked(DocumentInfo doc)988     public void onDocumentPicked(DocumentInfo doc) {
989         final FragmentManager fm = getFragmentManager();
990         if (doc.isDirectory()) {
991             mState.stack.push(doc);
992             mState.stackTouched = true;
993             onCurrentDirectoryChanged(ANIM_DOWN);
994         } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
995             // Explicit file picked, return
996             new ExistingFinishTask(doc.derivedUri).executeOnExecutor(getCurrentExecutor());
997         } else if (mState.action == ACTION_CREATE) {
998             // Replace selected file
999             SaveFragment.get(fm).setReplaceTarget(doc);
1000         } else if (mState.action == ACTION_MANAGE) {
1001             // First try managing the document; we expect manager to filter
1002             // based on authority, so we don't grant.
1003             final Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
1004             manage.setData(doc.derivedUri);
1005 
1006             try {
1007                 startActivity(manage);
1008             } catch (ActivityNotFoundException ex) {
1009                 // Fall back to viewing
1010                 final Intent view = new Intent(Intent.ACTION_VIEW);
1011                 view.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1012                 view.setData(doc.derivedUri);
1013 
1014                 try {
1015                     startActivity(view);
1016                 } catch (ActivityNotFoundException ex2) {
1017                     Toast.makeText(this, R.string.toast_no_application, Toast.LENGTH_SHORT).show();
1018                 }
1019             }
1020         }
1021     }
1022 
onDocumentsPicked(List<DocumentInfo> docs)1023     public void onDocumentsPicked(List<DocumentInfo> docs) {
1024         if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
1025             final int size = docs.size();
1026             final Uri[] uris = new Uri[size];
1027             for (int i = 0; i < size; i++) {
1028                 uris[i] = docs.get(i).derivedUri;
1029             }
1030             new ExistingFinishTask(uris).executeOnExecutor(getCurrentExecutor());
1031         }
1032     }
1033 
onSaveRequested(DocumentInfo replaceTarget)1034     public void onSaveRequested(DocumentInfo replaceTarget) {
1035         new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getCurrentExecutor());
1036     }
1037 
onSaveRequested(String mimeType, String displayName)1038     public void onSaveRequested(String mimeType, String displayName) {
1039         new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor());
1040     }
1041 
onPickRequested(DocumentInfo pickTarget)1042     public void onPickRequested(DocumentInfo pickTarget) {
1043         final Uri viaUri = DocumentsContract.buildTreeDocumentUri(pickTarget.authority,
1044                 pickTarget.documentId);
1045         new PickFinishTask(viaUri).executeOnExecutor(getCurrentExecutor());
1046     }
1047 
saveStackBlocking()1048     private void saveStackBlocking() {
1049         final ContentResolver resolver = getContentResolver();
1050         final ContentValues values = new ContentValues();
1051 
1052         final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
1053         if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) {
1054             // Remember stack for last create
1055             values.clear();
1056             values.put(RecentColumns.KEY, mState.stack.buildKey());
1057             values.put(RecentColumns.STACK, rawStack);
1058             resolver.insert(RecentsProvider.buildRecent(), values);
1059         }
1060 
1061         // Remember location for next app launch
1062         final String packageName = getCallingPackageMaybeExtra();
1063         values.clear();
1064         values.put(ResumeColumns.STACK, rawStack);
1065         values.put(ResumeColumns.EXTERNAL, 0);
1066         resolver.insert(RecentsProvider.buildResume(packageName), values);
1067     }
1068 
onFinished(Uri... uris)1069     private void onFinished(Uri... uris) {
1070         Log.d(TAG, "onFinished() " + Arrays.toString(uris));
1071 
1072         final Intent intent = new Intent();
1073         if (uris.length == 1) {
1074             intent.setData(uris[0]);
1075         } else if (uris.length > 1) {
1076             final ClipData clipData = new ClipData(
1077                     null, mState.acceptMimes, new ClipData.Item(uris[0]));
1078             for (int i = 1; i < uris.length; i++) {
1079                 clipData.addItem(new ClipData.Item(uris[i]));
1080             }
1081             intent.setClipData(clipData);
1082         }
1083 
1084         if (mState.action == ACTION_GET_CONTENT) {
1085             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1086         } else if (mState.action == ACTION_OPEN_TREE) {
1087             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
1088                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
1089                     | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
1090                     | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
1091         } else {
1092             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
1093                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
1094                     | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
1095         }
1096 
1097         setResult(Activity.RESULT_OK, intent);
1098         finish();
1099     }
1100 
1101     private class CreateFinishTask extends AsyncTask<Void, Void, Uri> {
1102         private final String mMimeType;
1103         private final String mDisplayName;
1104 
CreateFinishTask(String mimeType, String displayName)1105         public CreateFinishTask(String mimeType, String displayName) {
1106             mMimeType = mimeType;
1107             mDisplayName = displayName;
1108         }
1109 
1110         @Override
onPreExecute()1111         protected void onPreExecute() {
1112             setPending(true);
1113         }
1114 
1115         @Override
doInBackground(Void... params)1116         protected Uri doInBackground(Void... params) {
1117             final ContentResolver resolver = getContentResolver();
1118             final DocumentInfo cwd = getCurrentDirectory();
1119 
1120             ContentProviderClient client = null;
1121             Uri childUri = null;
1122             try {
1123                 client = DocumentsApplication.acquireUnstableProviderOrThrow(
1124                         resolver, cwd.derivedUri.getAuthority());
1125                 childUri = DocumentsContract.createDocument(
1126                         client, cwd.derivedUri, mMimeType, mDisplayName);
1127             } catch (Exception e) {
1128                 Log.w(TAG, "Failed to create document", e);
1129             } finally {
1130                 ContentProviderClient.releaseQuietly(client);
1131             }
1132 
1133             if (childUri != null) {
1134                 saveStackBlocking();
1135             }
1136 
1137             return childUri;
1138         }
1139 
1140         @Override
onPostExecute(Uri result)1141         protected void onPostExecute(Uri result) {
1142             if (result != null) {
1143                 onFinished(result);
1144             } else {
1145                 Toast.makeText(DocumentsActivity.this, R.string.save_error, Toast.LENGTH_SHORT)
1146                         .show();
1147             }
1148 
1149             setPending(false);
1150         }
1151     }
1152 
1153     private class ExistingFinishTask extends AsyncTask<Void, Void, Void> {
1154         private final Uri[] mUris;
1155 
ExistingFinishTask(Uri... uris)1156         public ExistingFinishTask(Uri... uris) {
1157             mUris = uris;
1158         }
1159 
1160         @Override
doInBackground(Void... params)1161         protected Void doInBackground(Void... params) {
1162             saveStackBlocking();
1163             return null;
1164         }
1165 
1166         @Override
onPostExecute(Void result)1167         protected void onPostExecute(Void result) {
1168             onFinished(mUris);
1169         }
1170     }
1171 
1172     private class PickFinishTask extends AsyncTask<Void, Void, Void> {
1173         private final Uri mUri;
1174 
PickFinishTask(Uri uri)1175         public PickFinishTask(Uri uri) {
1176             mUri = uri;
1177         }
1178 
1179         @Override
doInBackground(Void... params)1180         protected Void doInBackground(Void... params) {
1181             saveStackBlocking();
1182             return null;
1183         }
1184 
1185         @Override
onPostExecute(Void result)1186         protected void onPostExecute(Void result) {
1187             onFinished(mUri);
1188         }
1189     }
1190 
1191     public static class State implements android.os.Parcelable {
1192         public int action;
1193         public String[] acceptMimes;
1194 
1195         /** Explicit user choice */
1196         public int userMode = MODE_UNKNOWN;
1197         /** Derived after loader */
1198         public int derivedMode = MODE_LIST;
1199 
1200         /** Explicit user choice */
1201         public int userSortOrder = SORT_ORDER_UNKNOWN;
1202         /** Derived after loader */
1203         public int derivedSortOrder = SORT_ORDER_DISPLAY_NAME;
1204 
1205         public boolean allowMultiple = false;
1206         public boolean showSize = false;
1207         public boolean localOnly = false;
1208         public boolean forceAdvanced = false;
1209         public boolean showAdvanced = false;
1210         public boolean stackTouched = false;
1211         public boolean restored = false;
1212 
1213         /** Current user navigation stack; empty implies recents. */
1214         public DocumentStack stack = new DocumentStack();
1215         /** Currently active search, overriding any stack. */
1216         public String currentSearch;
1217 
1218         /** Instance state for every shown directory */
1219         public HashMap<String, SparseArray<Parcelable>> dirState = Maps.newHashMap();
1220 
1221         public static final int ACTION_OPEN = 1;
1222         public static final int ACTION_CREATE = 2;
1223         public static final int ACTION_GET_CONTENT = 3;
1224         public static final int ACTION_OPEN_TREE = 4;
1225         public static final int ACTION_MANAGE = 5;
1226 
1227         public static final int MODE_UNKNOWN = 0;
1228         public static final int MODE_LIST = 1;
1229         public static final int MODE_GRID = 2;
1230 
1231         public static final int SORT_ORDER_UNKNOWN = 0;
1232         public static final int SORT_ORDER_DISPLAY_NAME = 1;
1233         public static final int SORT_ORDER_LAST_MODIFIED = 2;
1234         public static final int SORT_ORDER_SIZE = 3;
1235 
1236         @Override
describeContents()1237         public int describeContents() {
1238             return 0;
1239         }
1240 
1241         @Override
writeToParcel(Parcel out, int flags)1242         public void writeToParcel(Parcel out, int flags) {
1243             out.writeInt(action);
1244             out.writeInt(userMode);
1245             out.writeStringArray(acceptMimes);
1246             out.writeInt(userSortOrder);
1247             out.writeInt(allowMultiple ? 1 : 0);
1248             out.writeInt(showSize ? 1 : 0);
1249             out.writeInt(localOnly ? 1 : 0);
1250             out.writeInt(forceAdvanced ? 1 : 0);
1251             out.writeInt(showAdvanced ? 1 : 0);
1252             out.writeInt(stackTouched ? 1 : 0);
1253             out.writeInt(restored ? 1 : 0);
1254             DurableUtils.writeToParcel(out, stack);
1255             out.writeString(currentSearch);
1256             out.writeMap(dirState);
1257         }
1258 
1259         public static final Creator<State> CREATOR = new Creator<State>() {
1260             @Override
1261             public State createFromParcel(Parcel in) {
1262                 final State state = new State();
1263                 state.action = in.readInt();
1264                 state.userMode = in.readInt();
1265                 state.acceptMimes = in.readStringArray();
1266                 state.userSortOrder = in.readInt();
1267                 state.allowMultiple = in.readInt() != 0;
1268                 state.showSize = in.readInt() != 0;
1269                 state.localOnly = in.readInt() != 0;
1270                 state.forceAdvanced = in.readInt() != 0;
1271                 state.showAdvanced = in.readInt() != 0;
1272                 state.stackTouched = in.readInt() != 0;
1273                 state.restored = in.readInt() != 0;
1274                 DurableUtils.readFromParcel(in, state.stack);
1275                 state.currentSearch = in.readString();
1276                 in.readMap(state.dirState, null);
1277                 return state;
1278             }
1279 
1280             @Override
1281             public State[] newArray(int size) {
1282                 return new State[size];
1283             }
1284         };
1285     }
1286 
dumpStack()1287     private void dumpStack() {
1288         Log.d(TAG, "Current stack: ");
1289         Log.d(TAG, " * " + mState.stack.root);
1290         for (DocumentInfo doc : mState.stack) {
1291             Log.d(TAG, " +-- " + doc);
1292         }
1293     }
1294 
get(Fragment fragment)1295     public static DocumentsActivity get(Fragment fragment) {
1296         return (DocumentsActivity) fragment.getActivity();
1297     }
1298 }
1299