DocumentsActivity.java revision 4ec973925fc2cd18f9ec0d0ca5af588564fded27
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.DocumentsActivity.State.ACTION_CREATE;
20import static com.android.documentsui.DocumentsActivity.State.ACTION_GET_CONTENT;
21import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE;
22import static com.android.documentsui.DocumentsActivity.State.ACTION_OPEN;
23import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
24import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
25import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED;
26
27import android.app.ActionBar;
28import android.app.ActionBar.OnNavigationListener;
29import android.app.Activity;
30import android.app.Fragment;
31import android.app.FragmentManager;
32import android.content.ActivityNotFoundException;
33import android.content.ClipData;
34import android.content.ComponentName;
35import android.content.ContentResolver;
36import android.content.ContentValues;
37import android.content.Intent;
38import android.content.pm.ResolveInfo;
39import android.database.Cursor;
40import android.graphics.drawable.ColorDrawable;
41import android.net.Uri;
42import android.os.Bundle;
43import android.os.Parcel;
44import android.provider.DocumentsContract;
45import android.support.v4.app.ActionBarDrawerToggle;
46import android.support.v4.view.GravityCompat;
47import android.support.v4.widget.DrawerLayout;
48import android.support.v4.widget.DrawerLayout.DrawerListener;
49import android.util.Log;
50import android.view.LayoutInflater;
51import android.view.Menu;
52import android.view.MenuItem;
53import android.view.MenuItem.OnActionExpandListener;
54import android.view.View;
55import android.view.ViewGroup;
56import android.widget.BaseAdapter;
57import android.widget.ImageView;
58import android.widget.SearchView;
59import android.widget.SearchView.OnCloseListener;
60import android.widget.SearchView.OnQueryTextListener;
61import android.widget.TextView;
62import android.widget.Toast;
63
64import com.android.documentsui.RecentsProvider.RecentColumns;
65import com.android.documentsui.RecentsProvider.ResumeColumns;
66import com.android.documentsui.RecentsProvider.StateColumns;
67import com.android.documentsui.model.DocumentInfo;
68import com.android.documentsui.model.DocumentStack;
69import com.android.documentsui.model.DurableUtils;
70import com.android.documentsui.model.RootInfo;
71
72import libcore.io.IoUtils;
73
74import java.io.FileNotFoundException;
75import java.io.IOException;
76import java.util.Arrays;
77import java.util.List;
78
79public class DocumentsActivity extends Activity {
80    public static final String TAG = "Documents";
81
82    private SearchView mSearchView;
83
84    private View mRootsContainer;
85    private DrawerLayout mDrawerLayout;
86    private ActionBarDrawerToggle mDrawerToggle;
87
88    private static final String EXTRA_STATE = "state";
89
90    private boolean mIgnoreNextNavigation;
91    private boolean mIgnoreNextCollapse;
92
93    private RootsCache mRoots;
94    private State mState;
95
96    @Override
97    public void onCreate(Bundle icicle) {
98        super.onCreate(icicle);
99
100        mRoots = DocumentsApplication.getRootsCache(this);
101
102        setResult(Activity.RESULT_CANCELED);
103        setContentView(R.layout.activity);
104
105        mRootsContainer = findViewById(R.id.container_roots);
106
107        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
108
109        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
110                R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close);
111
112        mDrawerLayout.setDrawerListener(mDrawerListener);
113        mDrawerLayout.setDrawerShadow(R.drawable.ic_drawer_shadow, GravityCompat.START);
114
115        if (icicle != null) {
116            mState = icicle.getParcelable(EXTRA_STATE);
117        } else {
118            buildDefaultState();
119        }
120
121        if (mState.action == ACTION_MANAGE) {
122            mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
123        }
124
125        if (mState.action == ACTION_CREATE) {
126            final String mimeType = getIntent().getType();
127            final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
128            SaveFragment.show(getFragmentManager(), mimeType, title);
129        }
130
131        if (mState.action == ACTION_GET_CONTENT) {
132            final Intent moreApps = new Intent(getIntent());
133            moreApps.setComponent(null);
134            moreApps.setPackage(null);
135            RootsFragment.show(getFragmentManager(), moreApps);
136        } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE) {
137            RootsFragment.show(getFragmentManager(), null);
138        }
139
140        onCurrentDirectoryChanged();
141    }
142
143    private void buildDefaultState() {
144        mState = new State();
145
146        final Intent intent = getIntent();
147        final String action = intent.getAction();
148        if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
149            mState.action = ACTION_OPEN;
150        } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
151            mState.action = ACTION_CREATE;
152        } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
153            mState.action = ACTION_GET_CONTENT;
154        } else if (DocumentsContract.ACTION_MANAGE_ROOT.equals(action)) {
155            mState.action = ACTION_MANAGE;
156        }
157
158        if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
159            mState.allowMultiple = intent.getBooleanExtra(
160                    Intent.EXTRA_ALLOW_MULTIPLE, false);
161        }
162
163        if (mState.action == ACTION_MANAGE) {
164            mState.acceptMimes = new String[] { "*/*" };
165            mState.allowMultiple = true;
166        } else if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
167            mState.acceptMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES);
168        } else {
169            mState.acceptMimes = new String[] { intent.getType() };
170        }
171
172        mState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
173        mState.showAdvanced = SettingsActivity.getDisplayAdvancedDevices(this);
174
175        if (mState.action == ACTION_MANAGE) {
176            final Uri uri = intent.getData();
177            final String rootId = DocumentsContract.getRootId(uri);
178            final RootInfo root = mRoots.getRoot(uri.getAuthority(), rootId);
179            if (root != null) {
180                onRootPicked(root, true);
181            } else {
182                Log.w(TAG, "Failed to find root: " + uri);
183                finish();
184            }
185
186        } else {
187            // Restore last stack for calling package
188            // TODO: move into async loader
189            final String packageName = getCallingPackage();
190            final Cursor cursor = getContentResolver()
191                    .query(RecentsProvider.buildResume(packageName), null, null, null, null);
192            try {
193                if (cursor.moveToFirst()) {
194                    final byte[] rawStack = cursor.getBlob(
195                            cursor.getColumnIndex(ResumeColumns.STACK));
196                    DurableUtils.readFromArray(rawStack, mState.stack);
197                }
198            } catch (IOException e) {
199                Log.w(TAG, "Failed to resume", e);
200            } finally {
201                IoUtils.closeQuietly(cursor);
202            }
203
204            // If restored root isn't valid, fall back to recents
205            final RootInfo root = getCurrentRoot();
206            final List<RootInfo> matchingRoots = mRoots.getMatchingRoots(mState);
207            if (!matchingRoots.contains(root)) {
208                mState.stack.reset();
209            }
210
211            // Only open drawer when showing recents
212            if (mState.stack.isRecents()) {
213                mDrawerLayout.openDrawer(mRootsContainer);
214            }
215        }
216    }
217
218    @Override
219    public void onStart() {
220        super.onStart();
221
222        if (mState.action == ACTION_MANAGE) {
223            mState.showSize = true;
224        } else {
225            mState.showSize = SettingsActivity.getDisplayFileSize(this);
226        }
227    }
228
229    private DrawerListener mDrawerListener = new DrawerListener() {
230        @Override
231        public void onDrawerSlide(View drawerView, float slideOffset) {
232            mDrawerToggle.onDrawerSlide(drawerView, slideOffset);
233        }
234
235        @Override
236        public void onDrawerOpened(View drawerView) {
237            mDrawerToggle.onDrawerOpened(drawerView);
238            updateActionBar();
239            invalidateOptionsMenu();
240        }
241
242        @Override
243        public void onDrawerClosed(View drawerView) {
244            mDrawerToggle.onDrawerClosed(drawerView);
245            updateActionBar();
246            invalidateOptionsMenu();
247        }
248
249        @Override
250        public void onDrawerStateChanged(int newState) {
251            mDrawerToggle.onDrawerStateChanged(newState);
252        }
253    };
254
255    @Override
256    protected void onPostCreate(Bundle savedInstanceState) {
257        super.onPostCreate(savedInstanceState);
258        mDrawerToggle.syncState();
259    }
260
261    public void updateActionBar() {
262        final ActionBar actionBar = getActionBar();
263
264        actionBar.setDisplayShowHomeEnabled(true);
265
266        if (mState.action == ACTION_MANAGE) {
267            actionBar.setDisplayHomeAsUpEnabled(false);
268            mDrawerToggle.setDrawerIndicatorEnabled(false);
269        } else {
270            actionBar.setDisplayHomeAsUpEnabled(true);
271            mDrawerToggle.setDrawerIndicatorEnabled(true);
272        }
273
274        if (mDrawerLayout.isDrawerOpen(mRootsContainer)) {
275            actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
276            actionBar.setIcon(new ColorDrawable());
277
278            if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
279                actionBar.setTitle(R.string.title_open);
280            } else if (mState.action == ACTION_CREATE) {
281                actionBar.setTitle(R.string.title_save);
282            }
283        } else {
284            final RootInfo root = getCurrentRoot();
285            actionBar.setIcon(root != null ? root.loadIcon(this) : null);
286
287            if (mState.stack.size() <= 1) {
288                actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
289                actionBar.setTitle(root.title);
290            } else {
291                mIgnoreNextNavigation = true;
292                actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
293                actionBar.setTitle(null);
294                actionBar.setListNavigationCallbacks(mStackAdapter, mStackListener);
295                actionBar.setSelectedNavigationItem(mStackAdapter.getCount() - 1);
296            }
297        }
298    }
299
300    @Override
301    public boolean onCreateOptionsMenu(Menu menu) {
302        super.onCreateOptionsMenu(menu);
303        getMenuInflater().inflate(R.menu.activity, menu);
304
305        final MenuItem searchMenu = menu.findItem(R.id.menu_search);
306        mSearchView = (SearchView) searchMenu.getActionView();
307        mSearchView.setOnQueryTextListener(new OnQueryTextListener() {
308            @Override
309            public boolean onQueryTextSubmit(String query) {
310                mState.currentSearch = query;
311                onCurrentDirectoryChanged();
312                return true;
313            }
314
315            @Override
316            public boolean onQueryTextChange(String newText) {
317                return false;
318            }
319        });
320
321        searchMenu.setOnActionExpandListener(new OnActionExpandListener() {
322            @Override
323            public boolean onMenuItemActionExpand(MenuItem item) {
324                return true;
325            }
326
327            @Override
328            public boolean onMenuItemActionCollapse(MenuItem item) {
329                if (mIgnoreNextCollapse) {
330                    mIgnoreNextCollapse = false;
331                    return true;
332                }
333
334                mState.currentSearch = null;
335                onCurrentDirectoryChanged();
336                return true;
337            }
338        });
339
340        return true;
341    }
342
343    @Override
344    public boolean onPrepareOptionsMenu(Menu menu) {
345        super.onPrepareOptionsMenu(menu);
346
347        final FragmentManager fm = getFragmentManager();
348        final DocumentInfo cwd = getCurrentDirectory();
349
350        final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
351        final MenuItem search = menu.findItem(R.id.menu_search);
352        final MenuItem sort = menu.findItem(R.id.menu_sort);
353        final MenuItem sortSize = menu.findItem(R.id.menu_sort_size);
354        final MenuItem grid = menu.findItem(R.id.menu_grid);
355        final MenuItem list = menu.findItem(R.id.menu_list);
356        final MenuItem settings = menu.findItem(R.id.menu_settings);
357
358        // Open drawer means we hide most actions
359        if (mDrawerLayout.isDrawerOpen(mRootsContainer)) {
360            createDir.setVisible(false);
361            search.setVisible(false);
362            sort.setVisible(false);
363            grid.setVisible(false);
364            list.setVisible(false);
365            mIgnoreNextCollapse = true;
366            search.collapseActionView();
367            return true;
368        }
369
370        if (cwd != null) {
371            sort.setVisible(true);
372            grid.setVisible(mState.derivedMode != MODE_GRID);
373            list.setVisible(mState.derivedMode != MODE_LIST);
374        } else {
375            sort.setVisible(false);
376            grid.setVisible(false);
377            list.setVisible(false);
378        }
379
380        if (mState.currentSearch != null) {
381            // Search uses backend ranking; no sorting
382            sort.setVisible(false);
383
384            search.expandActionView();
385            mSearchView.setQuery(mState.currentSearch, false);
386        } else {
387            mIgnoreNextCollapse = true;
388            search.collapseActionView();
389        }
390
391        // Only sort by size when visible
392        sortSize.setVisible(mState.showSize);
393
394        final boolean searchVisible;
395        if (mState.action == ACTION_CREATE) {
396            createDir.setVisible(cwd != null && cwd.isCreateSupported());
397            searchVisible = false;
398
399            // No display options in recent directories
400            if (cwd == null) {
401                grid.setVisible(false);
402                list.setVisible(false);
403            }
404
405            SaveFragment.get(fm).setSaveEnabled(cwd != null && cwd.isCreateSupported());
406        } else {
407            createDir.setVisible(false);
408            searchVisible = cwd != null && cwd.isSearchSupported();
409        }
410
411        // TODO: close any search in-progress when hiding
412        search.setVisible(searchVisible);
413
414        settings.setVisible(mState.action != ACTION_MANAGE);
415
416        return true;
417    }
418
419    @Override
420    public boolean onOptionsItemSelected(MenuItem item) {
421        if (mDrawerToggle.onOptionsItemSelected(item)) {
422            return true;
423        }
424
425        final int id = item.getItemId();
426        if (id == android.R.id.home) {
427            onBackPressed();
428            return true;
429        } else if (id == R.id.menu_create_dir) {
430            CreateDirectoryFragment.show(getFragmentManager());
431            return true;
432        } else if (id == R.id.menu_search) {
433            return false;
434        } else if (id == R.id.menu_sort_name) {
435            setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME);
436            return true;
437        } else if (id == R.id.menu_sort_date) {
438            setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED);
439            return true;
440        } else if (id == R.id.menu_sort_size) {
441            setUserSortOrder(State.SORT_ORDER_SIZE);
442            return true;
443        } else if (id == R.id.menu_grid) {
444            setUserMode(State.MODE_GRID);
445            return true;
446        } else if (id == R.id.menu_list) {
447            setUserMode(State.MODE_LIST);
448            return true;
449        } else if (id == R.id.menu_settings) {
450            startActivity(new Intent(this, SettingsActivity.class));
451            return true;
452        } else {
453            return super.onOptionsItemSelected(item);
454        }
455    }
456
457    /**
458     * Update UI to reflect internal state changes not from user.
459     */
460    public void onStateChanged() {
461        invalidateOptionsMenu();
462    }
463
464    /**
465     * Set state sort order based on explicit user action.
466     */
467    private void setUserSortOrder(int sortOrder) {
468        mState.userSortOrder = sortOrder;
469        DirectoryFragment.get(getFragmentManager()).onUserSortOrderChanged();
470    }
471
472    /**
473     * Set state mode based on explicit user action.
474     */
475    private void setUserMode(int mode) {
476        mState.userMode = mode;
477        DirectoryFragment.get(getFragmentManager()).onUserModeChanged();
478    }
479
480    @Override
481    public void onBackPressed() {
482        if (!mState.stackTouched) {
483            super.onBackPressed();
484            return;
485        }
486
487        final int size = mState.stack.size();
488        if (size > 1) {
489            mState.stack.pop();
490            onCurrentDirectoryChanged();
491        } else if (size == 1 && !mDrawerLayout.isDrawerOpen(mRootsContainer)) {
492            // TODO: open root drawer once we can capture back key
493            super.onBackPressed();
494        } else {
495            super.onBackPressed();
496        }
497    }
498
499    @Override
500    protected void onSaveInstanceState(Bundle state) {
501        super.onSaveInstanceState(state);
502        state.putParcelable(EXTRA_STATE, mState);
503    }
504
505    @Override
506    protected void onRestoreInstanceState(Bundle state) {
507        super.onRestoreInstanceState(state);
508        updateActionBar();
509    }
510
511    private BaseAdapter mStackAdapter = new BaseAdapter() {
512        @Override
513        public int getCount() {
514            return mState.stack.size();
515        }
516
517        @Override
518        public DocumentInfo getItem(int position) {
519            return mState.stack.get(mState.stack.size() - position - 1);
520        }
521
522        @Override
523        public long getItemId(int position) {
524            return position;
525        }
526
527        @Override
528        public View getView(int position, View convertView, ViewGroup parent) {
529            if (convertView == null) {
530                convertView = LayoutInflater.from(parent.getContext())
531                        .inflate(R.layout.item_title, parent, false);
532            }
533
534            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
535            final DocumentInfo doc = getItem(position);
536
537            if (position == 0) {
538                final RootInfo root = getCurrentRoot();
539                title.setText(root.title);
540            } else {
541                title.setText(doc.displayName);
542            }
543
544            // No padding when shown in actionbar
545            convertView.setPadding(0, 0, 0, 0);
546            return convertView;
547        }
548
549        @Override
550        public View getDropDownView(int position, View convertView, ViewGroup parent) {
551            if (convertView == null) {
552                convertView = LayoutInflater.from(parent.getContext())
553                        .inflate(R.layout.item_title, parent, false);
554            }
555
556            final ImageView subdir = (ImageView) convertView.findViewById(R.id.subdir);
557            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
558            final DocumentInfo doc = getItem(position);
559
560            if (position == 0) {
561                final RootInfo root = getCurrentRoot();
562                title.setText(root.title);
563                subdir.setVisibility(View.GONE);
564            } else {
565                title.setText(doc.displayName);
566                subdir.setVisibility(View.VISIBLE);
567            }
568
569            return convertView;
570        }
571    };
572
573    private OnNavigationListener mStackListener = new OnNavigationListener() {
574        @Override
575        public boolean onNavigationItemSelected(int itemPosition, long itemId) {
576            if (mIgnoreNextNavigation) {
577                mIgnoreNextNavigation = false;
578                return false;
579            }
580
581            while (mState.stack.size() > itemPosition + 1) {
582                mState.stackTouched = true;
583                mState.stack.pop();
584            }
585            onCurrentDirectoryChanged();
586            return true;
587        }
588    };
589
590    public RootInfo getCurrentRoot() {
591        if (mState.stack.root != null) {
592            return mState.stack.root;
593        } else {
594            return mRoots.getRecentsRoot();
595        }
596    }
597
598    public DocumentInfo getCurrentDirectory() {
599        return mState.stack.peek();
600    }
601
602    public State getDisplayState() {
603        return mState;
604    }
605
606    private void onCurrentDirectoryChanged() {
607        final FragmentManager fm = getFragmentManager();
608        final RootInfo root = getCurrentRoot();
609        final DocumentInfo cwd = getCurrentDirectory();
610
611        if (cwd == null) {
612            // No directory means recents
613            if (mState.action == ACTION_CREATE) {
614                RecentsCreateFragment.show(fm);
615            } else {
616                DirectoryFragment.showRecentsOpen(fm);
617            }
618        } else {
619            if (mState.currentSearch != null) {
620                // Ongoing search
621                DirectoryFragment.showSearch(fm, root, cwd, mState.currentSearch);
622            } else {
623                // Normal boring directory
624                DirectoryFragment.showNormal(fm, root, cwd);
625            }
626        }
627
628        // Forget any replacement target
629        if (mState.action == ACTION_CREATE) {
630            final SaveFragment save = SaveFragment.get(fm);
631            if (save != null) {
632                save.setReplaceTarget(null);
633            }
634        }
635
636        final RootsFragment roots = RootsFragment.get(fm);
637        if (roots != null) {
638            roots.onCurrentRootChanged();
639        }
640
641        updateActionBar();
642        invalidateOptionsMenu();
643        dumpStack();
644    }
645
646    public void onStackPicked(DocumentStack stack) {
647        mState.stack = stack;
648        mState.stackTouched = true;
649        onCurrentDirectoryChanged();
650    }
651
652    public void onRootPicked(RootInfo root, boolean closeDrawer) {
653        // Clear entire backstack and start in new root
654        mState.stack.root = root;
655        mState.stack.clear();
656        mState.stackTouched = true;
657
658        if (!mRoots.isRecentsRoot(root)) {
659            try {
660                final Uri uri = DocumentsContract.buildDocumentUri(root.authority, root.documentId);
661                onDocumentPicked(DocumentInfo.fromUri(getContentResolver(), uri));
662            } catch (FileNotFoundException e) {
663            }
664        } else {
665            onCurrentDirectoryChanged();
666        }
667
668        if (closeDrawer) {
669            mDrawerLayout.closeDrawers();
670        }
671    }
672
673    public void onAppPicked(ResolveInfo info) {
674        final Intent intent = new Intent(getIntent());
675        intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
676        intent.setComponent(new ComponentName(
677                info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
678        startActivity(intent);
679        finish();
680    }
681
682    public void onDocumentPicked(DocumentInfo doc) {
683        final FragmentManager fm = getFragmentManager();
684        if (doc.isDirectory()) {
685            mState.stack.push(doc);
686            mState.stackTouched = true;
687            onCurrentDirectoryChanged();
688        } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
689            // Explicit file picked, return
690            onFinished(doc.derivedUri);
691        } else if (mState.action == ACTION_CREATE) {
692            // Replace selected file
693            SaveFragment.get(fm).setReplaceTarget(doc);
694        } else if (mState.action == ACTION_MANAGE) {
695            // First try managing the document; we expect manager to filter
696            // based on authority, so we don't grant.
697            final Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
698            manage.setData(doc.derivedUri);
699
700            try {
701                startActivity(manage);
702            } catch (ActivityNotFoundException ex) {
703                // Fall back to viewing
704                final Intent view = new Intent(Intent.ACTION_VIEW);
705                view.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
706                view.setData(doc.derivedUri);
707
708                try {
709                    startActivity(view);
710                } catch (ActivityNotFoundException ex2) {
711                    Toast.makeText(this, R.string.toast_no_application, Toast.LENGTH_SHORT).show();
712                }
713            }
714        }
715    }
716
717    public void onDocumentsPicked(List<DocumentInfo> docs) {
718        if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
719            final int size = docs.size();
720            final Uri[] uris = new Uri[size];
721            for (int i = 0; i < size; i++) {
722                uris[i] = docs.get(i).derivedUri;
723            }
724            onFinished(uris);
725        }
726    }
727
728    public void onSaveRequested(DocumentInfo replaceTarget) {
729        onFinished(replaceTarget.derivedUri);
730    }
731
732    public void onSaveRequested(String mimeType, String displayName) {
733        final DocumentInfo cwd = getCurrentDirectory();
734
735        final Uri childUri = DocumentsContract.createDocument(
736                getContentResolver(), cwd.derivedUri, mimeType, displayName);
737        if (childUri != null) {
738            onFinished(childUri);
739        } else {
740            Toast.makeText(this, R.string.save_error, Toast.LENGTH_SHORT).show();
741        }
742    }
743
744    private void onFinished(Uri... uris) {
745        Log.d(TAG, "onFinished() " + Arrays.toString(uris));
746
747        final ContentResolver resolver = getContentResolver();
748        final ContentValues values = new ContentValues();
749
750        final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
751        if (mState.action == ACTION_CREATE) {
752            // Remember stack for last create
753            values.clear();
754            values.put(RecentColumns.STACK, rawStack);
755            resolver.insert(RecentsProvider.buildRecent(), values);
756        }
757
758        // Remember location for next app launch
759        final String packageName = getCallingPackage();
760        values.clear();
761        values.put(ResumeColumns.STACK, rawStack);
762        resolver.insert(RecentsProvider.buildResume(packageName), values);
763
764        final Intent intent = new Intent();
765        if (uris.length == 1) {
766            intent.setData(uris[0]);
767        } else if (uris.length > 1) {
768            final ClipData clipData = new ClipData(
769                    null, mState.acceptMimes, new ClipData.Item(uris[0]));
770            for (int i = 1; i < uris.length; i++) {
771                clipData.addItem(new ClipData.Item(uris[i]));
772            }
773            intent.setClipData(clipData);
774        }
775
776        if (mState.action == ACTION_GET_CONTENT) {
777            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
778        } else {
779            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
780                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
781                    | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);
782        }
783
784        setResult(Activity.RESULT_OK, intent);
785        finish();
786    }
787
788    public static class State implements android.os.Parcelable {
789        public int action;
790        public String[] acceptMimes;
791
792        /** Explicit user choice */
793        public int userMode = MODE_UNKNOWN;
794        /** Derived after loader */
795        public int derivedMode = MODE_LIST;
796
797        /** Explicit user choice */
798        public int userSortOrder = SORT_ORDER_UNKNOWN;
799        /** Derived after loader */
800        public int derivedSortOrder = SORT_ORDER_DISPLAY_NAME;
801
802        public boolean allowMultiple = false;
803        public boolean showSize = false;
804        public boolean localOnly = false;
805        public boolean showAdvanced = false;
806        public boolean stackTouched = false;
807
808        /** Current user navigation stack; empty implies recents. */
809        public DocumentStack stack = new DocumentStack();
810        /** Currently active search, overriding any stack. */
811        public String currentSearch;
812
813        public static final int ACTION_OPEN = 1;
814        public static final int ACTION_CREATE = 2;
815        public static final int ACTION_GET_CONTENT = 3;
816        public static final int ACTION_MANAGE = 4;
817
818        public static final int MODE_UNKNOWN = 0;
819        public static final int MODE_LIST = 1;
820        public static final int MODE_GRID = 2;
821
822        public static final int SORT_ORDER_UNKNOWN = 0;
823        public static final int SORT_ORDER_DISPLAY_NAME = 1;
824        public static final int SORT_ORDER_LAST_MODIFIED = 2;
825        public static final int SORT_ORDER_SIZE = 3;
826
827        @Override
828        public int describeContents() {
829            return 0;
830        }
831
832        @Override
833        public void writeToParcel(Parcel out, int flags) {
834            out.writeInt(action);
835            out.writeInt(userMode);
836            out.writeStringArray(acceptMimes);
837            out.writeInt(userSortOrder);
838            out.writeInt(allowMultiple ? 1 : 0);
839            out.writeInt(showSize ? 1 : 0);
840            out.writeInt(localOnly ? 1 : 0);
841            out.writeInt(showAdvanced ? 1 : 0);
842            out.writeInt(stackTouched ? 1 : 0);
843            DurableUtils.writeToParcel(out, stack);
844            out.writeString(currentSearch);
845        }
846
847        public static final Creator<State> CREATOR = new Creator<State>() {
848            @Override
849            public State createFromParcel(Parcel in) {
850                final State state = new State();
851                state.action = in.readInt();
852                state.userMode = in.readInt();
853                state.acceptMimes = in.readStringArray();
854                state.userSortOrder = in.readInt();
855                state.allowMultiple = in.readInt() != 0;
856                state.showSize = in.readInt() != 0;
857                state.localOnly = in.readInt() != 0;
858                state.showAdvanced = in.readInt() != 0;
859                state.stackTouched = in.readInt() != 0;
860                DurableUtils.readFromParcel(in, state.stack);
861                state.currentSearch = in.readString();
862                return state;
863            }
864
865            @Override
866            public State[] newArray(int size) {
867                return new State[size];
868            }
869        };
870    }
871
872    private void dumpStack() {
873        Log.d(TAG, "Current stack: ");
874        Log.d(TAG, " * " + mState.stack.root);
875        for (DocumentInfo doc : mState.stack) {
876            Log.d(TAG, " +-- " + doc);
877        }
878    }
879
880    public static DocumentsActivity get(Fragment fragment) {
881        return (DocumentsActivity) fragment.getActivity();
882    }
883}
884