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
17package com.android.documentsui;
18
19import static com.android.documentsui.DirectoryFragment.ANIM_DOWN;
20import static com.android.documentsui.DirectoryFragment.ANIM_NONE;
21import static com.android.documentsui.DirectoryFragment.ANIM_SIDE;
22import static com.android.documentsui.DirectoryFragment.ANIM_UP;
23import static com.android.documentsui.DocumentsActivity.State.ACTION_CREATE;
24import static com.android.documentsui.DocumentsActivity.State.ACTION_GET_CONTENT;
25import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE;
26import static com.android.documentsui.DocumentsActivity.State.ACTION_OPEN;
27import static com.android.documentsui.DocumentsActivity.State.ACTION_OPEN_TREE;
28import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
29import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
30
31import android.app.Activity;
32import android.app.Fragment;
33import android.app.FragmentManager;
34import android.content.ActivityNotFoundException;
35import android.content.ClipData;
36import android.content.ComponentName;
37import android.content.ContentProviderClient;
38import android.content.ContentResolver;
39import android.content.ContentValues;
40import android.content.Context;
41import android.content.Intent;
42import android.content.pm.ResolveInfo;
43import android.content.res.Resources;
44import android.database.Cursor;
45import android.graphics.Point;
46import android.net.Uri;
47import android.os.AsyncTask;
48import android.os.Bundle;
49import android.os.Parcel;
50import android.os.Parcelable;
51import android.provider.DocumentsContract;
52import android.provider.DocumentsContract.Root;
53import android.support.v4.app.ActionBarDrawerToggle;
54import android.support.v4.widget.DrawerLayout;
55import android.support.v4.widget.DrawerLayout.DrawerListener;
56import android.util.Log;
57import android.util.SparseArray;
58import android.view.LayoutInflater;
59import android.view.Menu;
60import android.view.MenuItem;
61import android.view.MenuItem.OnActionExpandListener;
62import android.view.View;
63import android.view.ViewGroup;
64import android.view.WindowManager;
65import android.widget.AdapterView;
66import android.widget.AdapterView.OnItemSelectedListener;
67import android.widget.BaseAdapter;
68import android.widget.ImageView;
69import android.widget.SearchView;
70import android.widget.SearchView.OnQueryTextListener;
71import android.widget.Spinner;
72import android.widget.TextView;
73import android.widget.Toast;
74import android.widget.Toolbar;
75
76import com.android.documentsui.RecentsProvider.RecentColumns;
77import com.android.documentsui.RecentsProvider.ResumeColumns;
78import com.android.documentsui.model.DocumentInfo;
79import com.android.documentsui.model.DocumentStack;
80import com.android.documentsui.model.DurableUtils;
81import com.android.documentsui.model.RootInfo;
82import com.google.common.collect.Maps;
83
84import libcore.io.IoUtils;
85
86import java.io.FileNotFoundException;
87import java.io.IOException;
88import java.util.Arrays;
89import java.util.Collection;
90import java.util.HashMap;
91import java.util.List;
92import java.util.concurrent.Executor;
93
94public 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
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
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
269        public RestoreRootTask(Uri rootUri) {
270            mRootUri = rootUri;
271        }
272
273        @Override
274        protected RootInfo doInBackground(Void... params) {
275            final String rootId = DocumentsContract.getRootId(mRootUri);
276            return mRoots.getRootOneshot(mRootUri.getAuthority(), rootId);
277        }
278
279        @Override
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
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
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
384    protected void onPostCreate(Bundle savedInstanceState) {
385        super.onPostCreate(savedInstanceState);
386        if (mDrawerToggle != null) {
387            mDrawerToggle.syncState();
388        }
389        updateActionBar();
390    }
391
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
402    private boolean isRootsDrawerOpen() {
403        if (mShowAsDialog) {
404            return false;
405        } else {
406            return mDrawerLayout.isDrawerOpen(mRootsDrawer);
407        }
408    }
409
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
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
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
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
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
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     */
673    public void onStateChanged() {
674        invalidateOptionsMenu();
675    }
676
677    /**
678     * Set state sort order based on explicit user action.
679     */
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     */
688    private void setUserMode(int mode) {
689        mState.userMode = mode;
690        DirectoryFragment.get(getFragmentManager()).onUserModeChanged();
691    }
692
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
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
720    protected void onSaveInstanceState(Bundle state) {
721        super.onSaveInstanceState(state);
722        state.putParcelable(EXTRA_STATE, mState);
723    }
724
725    @Override
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
811    public RootInfo getCurrentRoot() {
812        if (mState.stack.root != null) {
813            return mState.stack.root;
814        } else {
815            return mRoots.getRecentsRoot();
816        }
817    }
818
819    public DocumentInfo getCurrentDirectory() {
820        return mState.stack.peek();
821    }
822
823    private String getCallingPackageMaybeExtra() {
824        final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME);
825        return (extra != null) ? extra : getCallingPackage();
826    }
827
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
837    public State getDisplayState() {
838        return mState;
839    }
840
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
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
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
932        public PickRootTask(RootInfo root) {
933            mRoot = root;
934        }
935
936        @Override
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
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
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
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
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
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
1034    public void onSaveRequested(DocumentInfo replaceTarget) {
1035        new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getCurrentExecutor());
1036    }
1037
1038    public void onSaveRequested(String mimeType, String displayName) {
1039        new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor());
1040    }
1041
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
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
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
1105        public CreateFinishTask(String mimeType, String displayName) {
1106            mMimeType = mimeType;
1107            mDisplayName = displayName;
1108        }
1109
1110        @Override
1111        protected void onPreExecute() {
1112            setPending(true);
1113        }
1114
1115        @Override
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
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
1156        public ExistingFinishTask(Uri... uris) {
1157            mUris = uris;
1158        }
1159
1160        @Override
1161        protected Void doInBackground(Void... params) {
1162            saveStackBlocking();
1163            return null;
1164        }
1165
1166        @Override
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
1175        public PickFinishTask(Uri uri) {
1176            mUri = uri;
1177        }
1178
1179        @Override
1180        protected Void doInBackground(Void... params) {
1181            saveStackBlocking();
1182            return null;
1183        }
1184
1185        @Override
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
1237        public int describeContents() {
1238            return 0;
1239        }
1240
1241        @Override
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
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
1295    public static DocumentsActivity get(Fragment fragment) {
1296        return (DocumentsActivity) fragment.getActivity();
1297    }
1298}
1299