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