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