DocumentsActivity.java revision e66c1778f80f4b18e29e018eca3a338f125f23b9
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            try {
858                final Uri uri = DocumentsContract.buildDocumentUri(root.authority, root.documentId);
859                final DocumentInfo doc = DocumentInfo.fromUri(getContentResolver(), uri);
860                mState.stack.push(doc);
861                mState.stackTouched = true;
862                onCurrentDirectoryChanged(ANIM_SIDE);
863            } catch (FileNotFoundException e) {
864            }
865        } else {
866            onCurrentDirectoryChanged(ANIM_SIDE);
867        }
868
869        if (closeDrawer) {
870            setRootsDrawerOpen(false);
871        }
872    }
873
874    public void onAppPicked(ResolveInfo info) {
875        final Intent intent = new Intent(getIntent());
876        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
877        intent.setComponent(new ComponentName(
878                info.activityInfo.applicationInfo.packageName, info.activityInfo.name));
879        startActivityForResult(intent, CODE_FORWARD);
880    }
881
882    @Override
883    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
884        Log.d(TAG, "onActivityResult() code=" + resultCode);
885
886        // Only relay back results when not canceled; otherwise stick around to
887        // let the user pick another app/backend.
888        if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) {
889
890            // Remember that we last picked via external app
891            final String packageName = getCallingPackage();
892            final ContentValues values = new ContentValues();
893            values.put(ResumeColumns.EXTERNAL, 1);
894            getContentResolver().insert(RecentsProvider.buildResume(packageName), values);
895
896            // Pass back result to original caller
897            setResult(resultCode, data);
898            finish();
899        } else {
900            super.onActivityResult(requestCode, resultCode, data);
901        }
902    }
903
904    public void onDocumentPicked(DocumentInfo doc) {
905        final FragmentManager fm = getFragmentManager();
906        if (doc.isDirectory()) {
907            mState.stack.push(doc);
908            mState.stackTouched = true;
909            onCurrentDirectoryChanged(ANIM_DOWN);
910        } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
911            // Explicit file picked, return
912            onFinished(doc.derivedUri);
913        } else if (mState.action == ACTION_CREATE) {
914            // Replace selected file
915            SaveFragment.get(fm).setReplaceTarget(doc);
916        } else if (mState.action == ACTION_MANAGE) {
917            // First try managing the document; we expect manager to filter
918            // based on authority, so we don't grant.
919            final Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
920            manage.setData(doc.derivedUri);
921
922            try {
923                startActivity(manage);
924            } catch (ActivityNotFoundException ex) {
925                // Fall back to viewing
926                final Intent view = new Intent(Intent.ACTION_VIEW);
927                view.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
928                view.setData(doc.derivedUri);
929
930                try {
931                    startActivity(view);
932                } catch (ActivityNotFoundException ex2) {
933                    Toast.makeText(this, R.string.toast_no_application, Toast.LENGTH_SHORT).show();
934                }
935            }
936        }
937    }
938
939    public void onDocumentsPicked(List<DocumentInfo> docs) {
940        if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
941            final int size = docs.size();
942            final Uri[] uris = new Uri[size];
943            for (int i = 0; i < size; i++) {
944                uris[i] = docs.get(i).derivedUri;
945            }
946            onFinished(uris);
947        }
948    }
949
950    public void onSaveRequested(DocumentInfo replaceTarget) {
951        onFinished(replaceTarget.derivedUri);
952    }
953
954    public void onSaveRequested(String mimeType, String displayName) {
955        final DocumentInfo cwd = getCurrentDirectory();
956
957        final Uri childUri = DocumentsContract.createDocument(
958                getContentResolver(), cwd.derivedUri, mimeType, displayName);
959        if (childUri != null) {
960            onFinished(childUri);
961        } else {
962            Toast.makeText(this, R.string.save_error, Toast.LENGTH_SHORT).show();
963        }
964    }
965
966    private void onFinished(Uri... uris) {
967        Log.d(TAG, "onFinished() " + Arrays.toString(uris));
968
969        final ContentResolver resolver = getContentResolver();
970        final ContentValues values = new ContentValues();
971
972        final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
973        if (mState.action == ACTION_CREATE) {
974            // Remember stack for last create
975            values.clear();
976            values.put(RecentColumns.STACK, rawStack);
977            resolver.insert(RecentsProvider.buildRecent(), values);
978        }
979
980        // Remember location for next app launch
981        final String packageName = getCallingPackage();
982        values.clear();
983        values.put(ResumeColumns.STACK, rawStack);
984        values.put(ResumeColumns.EXTERNAL, 0);
985        resolver.insert(RecentsProvider.buildResume(packageName), values);
986
987        final Intent intent = new Intent();
988        if (uris.length == 1) {
989            intent.setData(uris[0]);
990        } else if (uris.length > 1) {
991            final ClipData clipData = new ClipData(
992                    null, mState.acceptMimes, new ClipData.Item(uris[0]));
993            for (int i = 1; i < uris.length; i++) {
994                clipData.addItem(new ClipData.Item(uris[i]));
995            }
996            intent.setClipData(clipData);
997        }
998
999        if (mState.action == ACTION_GET_CONTENT) {
1000            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
1001        } else {
1002            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
1003                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
1004                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
1005        }
1006
1007        setResult(Activity.RESULT_OK, intent);
1008        finish();
1009    }
1010
1011    public static class State implements android.os.Parcelable {
1012        public int action;
1013        public String[] acceptMimes;
1014
1015        /** Explicit user choice */
1016        public int userMode = MODE_UNKNOWN;
1017        /** Derived after loader */
1018        public int derivedMode = MODE_LIST;
1019
1020        /** Explicit user choice */
1021        public int userSortOrder = SORT_ORDER_UNKNOWN;
1022        /** Derived after loader */
1023        public int derivedSortOrder = SORT_ORDER_DISPLAY_NAME;
1024
1025        public boolean allowMultiple = false;
1026        public boolean showSize = false;
1027        public boolean localOnly = false;
1028        public boolean showAdvanced = false;
1029        public boolean stackTouched = false;
1030        public boolean restored = false;
1031
1032        /** Current user navigation stack; empty implies recents. */
1033        public DocumentStack stack = new DocumentStack();
1034        /** Currently active search, overriding any stack. */
1035        public String currentSearch;
1036
1037        /** Instance state for every shown directory */
1038        public HashMap<String, SparseArray<Parcelable>> dirState = Maps.newHashMap();
1039
1040        public static final int ACTION_OPEN = 1;
1041        public static final int ACTION_CREATE = 2;
1042        public static final int ACTION_GET_CONTENT = 3;
1043        public static final int ACTION_MANAGE = 4;
1044
1045        public static final int MODE_UNKNOWN = 0;
1046        public static final int MODE_LIST = 1;
1047        public static final int MODE_GRID = 2;
1048
1049        public static final int SORT_ORDER_UNKNOWN = 0;
1050        public static final int SORT_ORDER_DISPLAY_NAME = 1;
1051        public static final int SORT_ORDER_LAST_MODIFIED = 2;
1052        public static final int SORT_ORDER_SIZE = 3;
1053
1054        @Override
1055        public int describeContents() {
1056            return 0;
1057        }
1058
1059        @Override
1060        public void writeToParcel(Parcel out, int flags) {
1061            out.writeInt(action);
1062            out.writeInt(userMode);
1063            out.writeStringArray(acceptMimes);
1064            out.writeInt(userSortOrder);
1065            out.writeInt(allowMultiple ? 1 : 0);
1066            out.writeInt(showSize ? 1 : 0);
1067            out.writeInt(localOnly ? 1 : 0);
1068            out.writeInt(showAdvanced ? 1 : 0);
1069            out.writeInt(stackTouched ? 1 : 0);
1070            out.writeInt(restored ? 1 : 0);
1071            DurableUtils.writeToParcel(out, stack);
1072            out.writeString(currentSearch);
1073            out.writeMap(dirState);
1074        }
1075
1076        public static final Creator<State> CREATOR = new Creator<State>() {
1077            @Override
1078            public State createFromParcel(Parcel in) {
1079                final State state = new State();
1080                state.action = in.readInt();
1081                state.userMode = in.readInt();
1082                state.acceptMimes = in.readStringArray();
1083                state.userSortOrder = in.readInt();
1084                state.allowMultiple = in.readInt() != 0;
1085                state.showSize = in.readInt() != 0;
1086                state.localOnly = in.readInt() != 0;
1087                state.showAdvanced = in.readInt() != 0;
1088                state.stackTouched = in.readInt() != 0;
1089                state.restored = in.readInt() != 0;
1090                DurableUtils.readFromParcel(in, state.stack);
1091                state.currentSearch = in.readString();
1092                in.readMap(state.dirState, null);
1093                return state;
1094            }
1095
1096            @Override
1097            public State[] newArray(int size) {
1098                return new State[size];
1099            }
1100        };
1101    }
1102
1103    private void dumpStack() {
1104        Log.d(TAG, "Current stack: ");
1105        Log.d(TAG, " * " + mState.stack.root);
1106        for (DocumentInfo doc : mState.stack) {
1107            Log.d(TAG, " +-- " + doc);
1108        }
1109    }
1110
1111    public static DocumentsActivity get(Fragment fragment) {
1112        return (DocumentsActivity) fragment.getActivity();
1113    }
1114}
1115