Home | History | Annotate | Download | only in documentsui
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.documentsui;
     18 
     19 import static com.android.documentsui.DocumentsActivity.TAG;
     20 import static com.android.documentsui.DocumentsActivity.State.ACTION_CREATE;
     21 import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE;
     22 import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
     23 import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
     24 import static com.android.documentsui.DocumentsActivity.State.MODE_UNKNOWN;
     25 import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_UNKNOWN;
     26 import static com.android.documentsui.model.DocumentInfo.getCursorInt;
     27 import static com.android.documentsui.model.DocumentInfo.getCursorLong;
     28 import static com.android.documentsui.model.DocumentInfo.getCursorString;
     29 
     30 import android.app.ActivityManager;
     31 import android.app.Fragment;
     32 import android.app.FragmentManager;
     33 import android.app.FragmentTransaction;
     34 import android.app.LoaderManager.LoaderCallbacks;
     35 import android.content.ContentProviderClient;
     36 import android.content.ContentResolver;
     37 import android.content.ContentValues;
     38 import android.content.Context;
     39 import android.content.Intent;
     40 import android.content.Loader;
     41 import android.content.res.Resources;
     42 import android.database.Cursor;
     43 import android.graphics.Bitmap;
     44 import android.graphics.Point;
     45 import android.graphics.drawable.Drawable;
     46 import android.graphics.drawable.InsetDrawable;
     47 import android.net.Uri;
     48 import android.os.AsyncTask;
     49 import android.os.Bundle;
     50 import android.os.CancellationSignal;
     51 import android.os.OperationCanceledException;
     52 import android.os.Parcelable;
     53 import android.provider.DocumentsContract;
     54 import android.provider.DocumentsContract.Document;
     55 import android.text.format.DateUtils;
     56 import android.text.format.Formatter;
     57 import android.text.format.Time;
     58 import android.util.Log;
     59 import android.util.SparseArray;
     60 import android.util.SparseBooleanArray;
     61 import android.view.ActionMode;
     62 import android.view.LayoutInflater;
     63 import android.view.Menu;
     64 import android.view.MenuItem;
     65 import android.view.View;
     66 import android.view.ViewGroup;
     67 import android.widget.AbsListView;
     68 import android.widget.AbsListView.MultiChoiceModeListener;
     69 import android.widget.AbsListView.RecyclerListener;
     70 import android.widget.AdapterView;
     71 import android.widget.AdapterView.OnItemClickListener;
     72 import android.widget.BaseAdapter;
     73 import android.widget.GridView;
     74 import android.widget.ImageView;
     75 import android.widget.ListView;
     76 import android.widget.TextView;
     77 import android.widget.Toast;
     78 
     79 import com.android.documentsui.DocumentsActivity.State;
     80 import com.android.documentsui.ProviderExecutor.Preemptable;
     81 import com.android.documentsui.RecentsProvider.StateColumns;
     82 import com.android.documentsui.model.DocumentInfo;
     83 import com.android.documentsui.model.RootInfo;
     84 import com.google.android.collect.Lists;
     85 
     86 import java.util.ArrayList;
     87 import java.util.List;
     88 
     89 /**
     90  * Display the documents inside a single directory.
     91  */
     92 public class DirectoryFragment extends Fragment {
     93 
     94     private View mEmptyView;
     95     private ListView mListView;
     96     private GridView mGridView;
     97 
     98     private AbsListView mCurrentView;
     99 
    100     public static final int TYPE_NORMAL = 1;
    101     public static final int TYPE_SEARCH = 2;
    102     public static final int TYPE_RECENT_OPEN = 3;
    103 
    104     public static final int ANIM_NONE = 1;
    105     public static final int ANIM_SIDE = 2;
    106     public static final int ANIM_DOWN = 3;
    107     public static final int ANIM_UP = 4;
    108 
    109     private int mType = TYPE_NORMAL;
    110     private String mStateKey;
    111 
    112     private int mLastMode = MODE_UNKNOWN;
    113     private int mLastSortOrder = SORT_ORDER_UNKNOWN;
    114     private boolean mLastShowSize = false;
    115 
    116     private boolean mHideGridTitles = false;
    117 
    118     private boolean mSvelteRecents;
    119     private Point mThumbSize;
    120 
    121     private DocumentsAdapter mAdapter;
    122     private LoaderCallbacks<DirectoryResult> mCallbacks;
    123 
    124     private static final String EXTRA_TYPE = "type";
    125     private static final String EXTRA_ROOT = "root";
    126     private static final String EXTRA_DOC = "doc";
    127     private static final String EXTRA_QUERY = "query";
    128     private static final String EXTRA_IGNORE_STATE = "ignoreState";
    129 
    130     private final int mLoaderId = 42;
    131 
    132     public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
    133         show(fm, TYPE_NORMAL, root, doc, null, anim);
    134     }
    135 
    136     public static void showSearch(FragmentManager fm, RootInfo root, String query, int anim) {
    137         show(fm, TYPE_SEARCH, root, null, query, anim);
    138     }
    139 
    140     public static void showRecentsOpen(FragmentManager fm, int anim) {
    141         show(fm, TYPE_RECENT_OPEN, null, null, null, anim);
    142     }
    143 
    144     private static void show(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
    145             String query, int anim) {
    146         final Bundle args = new Bundle();
    147         args.putInt(EXTRA_TYPE, type);
    148         args.putParcelable(EXTRA_ROOT, root);
    149         args.putParcelable(EXTRA_DOC, doc);
    150         args.putString(EXTRA_QUERY, query);
    151 
    152         final FragmentTransaction ft = fm.beginTransaction();
    153         switch (anim) {
    154             case ANIM_SIDE:
    155                 args.putBoolean(EXTRA_IGNORE_STATE, true);
    156                 break;
    157             case ANIM_DOWN:
    158                 args.putBoolean(EXTRA_IGNORE_STATE, true);
    159                 ft.setCustomAnimations(R.animator.dir_down, R.animator.dir_frozen);
    160                 break;
    161             case ANIM_UP:
    162                 ft.setCustomAnimations(R.animator.dir_frozen, R.animator.dir_up);
    163                 break;
    164         }
    165 
    166         final DirectoryFragment fragment = new DirectoryFragment();
    167         fragment.setArguments(args);
    168 
    169         ft.replace(R.id.container_directory, fragment);
    170         ft.commitAllowingStateLoss();
    171     }
    172 
    173     private static String buildStateKey(RootInfo root, DocumentInfo doc) {
    174         final StringBuilder builder = new StringBuilder();
    175         builder.append(root != null ? root.authority : "null").append(';');
    176         builder.append(root != null ? root.rootId : "null").append(';');
    177         builder.append(doc != null ? doc.documentId : "null");
    178         return builder.toString();
    179     }
    180 
    181     public static DirectoryFragment get(FragmentManager fm) {
    182         // TODO: deal with multiple directories shown at once
    183         return (DirectoryFragment) fm.findFragmentById(R.id.container_directory);
    184     }
    185 
    186     @Override
    187     public View onCreateView(
    188             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    189         final Context context = inflater.getContext();
    190         final Resources res = context.getResources();
    191         final View view = inflater.inflate(R.layout.fragment_directory, container, false);
    192 
    193         mEmptyView = view.findViewById(android.R.id.empty);
    194 
    195         mListView = (ListView) view.findViewById(R.id.list);
    196         mListView.setOnItemClickListener(mItemListener);
    197         mListView.setMultiChoiceModeListener(mMultiListener);
    198         mListView.setRecyclerListener(mRecycleListener);
    199 
    200         // Indent our list divider to align with text
    201         final Drawable divider = mListView.getDivider();
    202         final boolean insetLeft = res.getBoolean(R.bool.list_divider_inset_left);
    203         final int insetSize = res.getDimensionPixelSize(R.dimen.list_divider_inset);
    204         if (insetLeft) {
    205             mListView.setDivider(new InsetDrawable(divider, insetSize, 0, 0, 0));
    206         } else {
    207             mListView.setDivider(new InsetDrawable(divider, 0, 0, insetSize, 0));
    208         }
    209 
    210         mGridView = (GridView) view.findViewById(R.id.grid);
    211         mGridView.setOnItemClickListener(mItemListener);
    212         mGridView.setMultiChoiceModeListener(mMultiListener);
    213         mGridView.setRecyclerListener(mRecycleListener);
    214 
    215         return view;
    216     }
    217 
    218     @Override
    219     public void onDestroyView() {
    220         super.onDestroyView();
    221 
    222         // Cancel any outstanding thumbnail requests
    223         final ViewGroup target = (mListView.getAdapter() != null) ? mListView : mGridView;
    224         final int count = target.getChildCount();
    225         for (int i = 0; i < count; i++) {
    226             final View view = target.getChildAt(i);
    227             mRecycleListener.onMovedToScrapHeap(view);
    228         }
    229 
    230         // Tear down any selection in progress
    231         mListView.setChoiceMode(AbsListView.CHOICE_MODE_NONE);
    232         mGridView.setChoiceMode(AbsListView.CHOICE_MODE_NONE);
    233     }
    234 
    235     @Override
    236     public void onActivityCreated(Bundle savedInstanceState) {
    237         super.onActivityCreated(savedInstanceState);
    238 
    239         final Context context = getActivity();
    240         final State state = getDisplayState(DirectoryFragment.this);
    241 
    242         final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
    243         final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
    244 
    245         mAdapter = new DocumentsAdapter();
    246         mType = getArguments().getInt(EXTRA_TYPE);
    247         mStateKey = buildStateKey(root, doc);
    248 
    249         if (mType == TYPE_RECENT_OPEN) {
    250             // Hide titles when showing recents for picking images/videos
    251             mHideGridTitles = MimePredicate.mimeMatches(
    252                     MimePredicate.VISUAL_MIMES, state.acceptMimes);
    253         } else {
    254             mHideGridTitles = (doc != null) && doc.isGridTitlesHidden();
    255         }
    256 
    257         final ActivityManager am = (ActivityManager) context.getSystemService(
    258                 Context.ACTIVITY_SERVICE);
    259         mSvelteRecents = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
    260 
    261         mCallbacks = new LoaderCallbacks<DirectoryResult>() {
    262             @Override
    263             public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
    264                 final String query = getArguments().getString(EXTRA_QUERY);
    265 
    266                 Uri contentsUri;
    267                 switch (mType) {
    268                     case TYPE_NORMAL:
    269                         contentsUri = DocumentsContract.buildChildDocumentsUri(
    270                                 doc.authority, doc.documentId);
    271                         if (state.action == ACTION_MANAGE) {
    272                             contentsUri = DocumentsContract.setManageMode(contentsUri);
    273                         }
    274                         return new DirectoryLoader(
    275                                 context, mType, root, doc, contentsUri, state.userSortOrder);
    276                     case TYPE_SEARCH:
    277                         contentsUri = DocumentsContract.buildSearchDocumentsUri(
    278                                 root.authority, root.rootId, query);
    279                         if (state.action == ACTION_MANAGE) {
    280                             contentsUri = DocumentsContract.setManageMode(contentsUri);
    281                         }
    282                         return new DirectoryLoader(
    283                                 context, mType, root, doc, contentsUri, state.userSortOrder);
    284                     case TYPE_RECENT_OPEN:
    285                         final RootsCache roots = DocumentsApplication.getRootsCache(context);
    286                         return new RecentLoader(context, roots, state);
    287                     default:
    288                         throw new IllegalStateException("Unknown type " + mType);
    289                 }
    290             }
    291 
    292             @Override
    293             public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
    294                 if (!isAdded()) return;
    295 
    296                 mAdapter.swapResult(result);
    297 
    298                 // Push latest state up to UI
    299                 // TODO: if mode change was racing with us, don't overwrite it
    300                 if (result.mode != MODE_UNKNOWN) {
    301                     state.derivedMode = result.mode;
    302                 }
    303                 state.derivedSortOrder = result.sortOrder;
    304                 ((DocumentsActivity) context).onStateChanged();
    305 
    306                 updateDisplayState();
    307 
    308                 // When launched into empty recents, show drawer
    309                 if (mType == TYPE_RECENT_OPEN && mAdapter.isEmpty() && !state.stackTouched) {
    310                     ((DocumentsActivity) context).setRootsDrawerOpen(true);
    311                 }
    312 
    313                 // Restore any previous instance state
    314                 final SparseArray<Parcelable> container = state.dirState.remove(mStateKey);
    315                 if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) {
    316                     getView().restoreHierarchyState(container);
    317                 } else if (mLastSortOrder != state.derivedSortOrder) {
    318                     mListView.smoothScrollToPosition(0);
    319                     mGridView.smoothScrollToPosition(0);
    320                 }
    321 
    322                 mLastSortOrder = state.derivedSortOrder;
    323             }
    324 
    325             @Override
    326             public void onLoaderReset(Loader<DirectoryResult> loader) {
    327                 mAdapter.swapResult(null);
    328             }
    329         };
    330 
    331         // Kick off loader at least once
    332         getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
    333 
    334         updateDisplayState();
    335     }
    336 
    337     @Override
    338     public void onStop() {
    339         super.onStop();
    340 
    341         // Remember last scroll location
    342         final SparseArray<Parcelable> container = new SparseArray<Parcelable>();
    343         getView().saveHierarchyState(container);
    344         final State state = getDisplayState(this);
    345         state.dirState.put(mStateKey, container);
    346     }
    347 
    348     @Override
    349     public void onResume() {
    350         super.onResume();
    351         updateDisplayState();
    352     }
    353 
    354     public void onDisplayStateChanged() {
    355         updateDisplayState();
    356     }
    357 
    358     public void onUserSortOrderChanged() {
    359         // Sort order change always triggers reload; we'll trigger state change
    360         // on the flip side.
    361         getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
    362     }
    363 
    364     public void onUserModeChanged() {
    365         final ContentResolver resolver = getActivity().getContentResolver();
    366         final State state = getDisplayState(this);
    367 
    368         final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
    369         final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
    370 
    371         if (root != null && doc != null) {
    372             final Uri stateUri = RecentsProvider.buildState(
    373                     root.authority, root.rootId, doc.documentId);
    374             final ContentValues values = new ContentValues();
    375             values.put(StateColumns.MODE, state.userMode);
    376 
    377             new AsyncTask<Void, Void, Void>() {
    378                 @Override
    379                 protected Void doInBackground(Void... params) {
    380                     resolver.insert(stateUri, values);
    381                     return null;
    382                 }
    383             }.execute();
    384         }
    385 
    386         // Mode change is just visual change; no need to kick loader, and
    387         // deliver change event immediately.
    388         state.derivedMode = state.userMode;
    389         ((DocumentsActivity) getActivity()).onStateChanged();
    390 
    391         updateDisplayState();
    392     }
    393 
    394     private void updateDisplayState() {
    395         final State state = getDisplayState(this);
    396 
    397         if (mLastMode == state.derivedMode && mLastShowSize == state.showSize) return;
    398         mLastMode = state.derivedMode;
    399         mLastShowSize = state.showSize;
    400 
    401         mListView.setVisibility(state.derivedMode == MODE_LIST ? View.VISIBLE : View.GONE);
    402         mGridView.setVisibility(state.derivedMode == MODE_GRID ? View.VISIBLE : View.GONE);
    403 
    404         final int choiceMode;
    405         if (state.allowMultiple) {
    406             choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL;
    407         } else {
    408             choiceMode = ListView.CHOICE_MODE_NONE;
    409         }
    410 
    411         final int thumbSize;
    412         if (state.derivedMode == MODE_GRID) {
    413             thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width);
    414             mListView.setAdapter(null);
    415             mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
    416             mGridView.setAdapter(mAdapter);
    417             mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width));
    418             mGridView.setNumColumns(GridView.AUTO_FIT);
    419             mGridView.setChoiceMode(choiceMode);
    420             mCurrentView = mGridView;
    421         } else if (state.derivedMode == MODE_LIST) {
    422             thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size);
    423             mGridView.setAdapter(null);
    424             mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE);
    425             mListView.setAdapter(mAdapter);
    426             mListView.setChoiceMode(choiceMode);
    427             mCurrentView = mListView;
    428         } else {
    429             throw new IllegalStateException("Unknown state " + state.derivedMode);
    430         }
    431 
    432         mThumbSize = new Point(thumbSize, thumbSize);
    433     }
    434 
    435     private OnItemClickListener mItemListener = new OnItemClickListener() {
    436         @Override
    437         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    438             final Cursor cursor = mAdapter.getItem(position);
    439             if (cursor != null) {
    440                 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
    441                 final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
    442                 if (isDocumentEnabled(docMimeType, docFlags)) {
    443                     final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
    444                     ((DocumentsActivity) getActivity()).onDocumentPicked(doc);
    445                 }
    446             }
    447         }
    448     };
    449 
    450     private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() {
    451         @Override
    452         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    453             mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
    454             mode.setTitle(getResources()
    455                     .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount()));
    456             return true;
    457         }
    458 
    459         @Override
    460         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
    461             final State state = getDisplayState(DirectoryFragment.this);
    462 
    463             final MenuItem open = menu.findItem(R.id.menu_open);
    464             final MenuItem share = menu.findItem(R.id.menu_share);
    465             final MenuItem delete = menu.findItem(R.id.menu_delete);
    466 
    467             final boolean manageMode = state.action == ACTION_MANAGE;
    468             open.setVisible(!manageMode);
    469             share.setVisible(manageMode);
    470             delete.setVisible(manageMode);
    471 
    472             return true;
    473         }
    474 
    475         @Override
    476         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
    477             final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions();
    478             final ArrayList<DocumentInfo> docs = Lists.newArrayList();
    479             final int size = checked.size();
    480             for (int i = 0; i < size; i++) {
    481                 if (checked.valueAt(i)) {
    482                     final Cursor cursor = mAdapter.getItem(checked.keyAt(i));
    483                     final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
    484                     docs.add(doc);
    485                 }
    486             }
    487 
    488             final int id = item.getItemId();
    489             if (id == R.id.menu_open) {
    490                 DocumentsActivity.get(DirectoryFragment.this).onDocumentsPicked(docs);
    491                 mode.finish();
    492                 return true;
    493 
    494             } else if (id == R.id.menu_share) {
    495                 onShareDocuments(docs);
    496                 mode.finish();
    497                 return true;
    498 
    499             } else if (id == R.id.menu_delete) {
    500                 onDeleteDocuments(docs);
    501                 mode.finish();
    502                 return true;
    503 
    504             } else {
    505                 return false;
    506             }
    507         }
    508 
    509         @Override
    510         public void onDestroyActionMode(ActionMode mode) {
    511             // ignored
    512         }
    513 
    514         @Override
    515         public void onItemCheckedStateChanged(
    516                 ActionMode mode, int position, long id, boolean checked) {
    517             if (checked) {
    518                 // Directories and footer items cannot be checked
    519                 boolean valid = false;
    520 
    521                 final Cursor cursor = mAdapter.getItem(position);
    522                 if (cursor != null) {
    523                     final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
    524                     final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
    525                     if (!Document.MIME_TYPE_DIR.equals(docMimeType)) {
    526                         valid = isDocumentEnabled(docMimeType, docFlags);
    527                     }
    528                 }
    529 
    530                 if (!valid) {
    531                     mCurrentView.setItemChecked(position, false);
    532                 }
    533             }
    534 
    535             mode.setTitle(getResources()
    536                     .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount()));
    537         }
    538     };
    539 
    540     private RecyclerListener mRecycleListener = new RecyclerListener() {
    541         @Override
    542         public void onMovedToScrapHeap(View view) {
    543             final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
    544             if (iconThumb != null) {
    545                 final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
    546                 if (oldTask != null) {
    547                     oldTask.preempt();
    548                     iconThumb.setTag(null);
    549                 }
    550             }
    551         }
    552     };
    553 
    554     private void onShareDocuments(List<DocumentInfo> docs) {
    555         Intent intent;
    556         if (docs.size() == 1) {
    557             final DocumentInfo doc = docs.get(0);
    558 
    559             intent = new Intent(Intent.ACTION_SEND);
    560             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    561             intent.addCategory(Intent.CATEGORY_DEFAULT);
    562             intent.setType(doc.mimeType);
    563             intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
    564 
    565         } else if (docs.size() > 1) {
    566             intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
    567             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    568             intent.addCategory(Intent.CATEGORY_DEFAULT);
    569 
    570             final ArrayList<String> mimeTypes = Lists.newArrayList();
    571             final ArrayList<Uri> uris = Lists.newArrayList();
    572             for (DocumentInfo doc : docs) {
    573                 mimeTypes.add(doc.mimeType);
    574                 uris.add(doc.derivedUri);
    575             }
    576 
    577             intent.setType(findCommonMimeType(mimeTypes));
    578             intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
    579 
    580         } else {
    581             return;
    582         }
    583 
    584         intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
    585         startActivity(intent);
    586     }
    587 
    588     private void onDeleteDocuments(List<DocumentInfo> docs) {
    589         final Context context = getActivity();
    590         final ContentResolver resolver = context.getContentResolver();
    591 
    592         boolean hadTrouble = false;
    593         for (DocumentInfo doc : docs) {
    594             if (!doc.isDeleteSupported()) {
    595                 Log.w(TAG, "Skipping " + doc);
    596                 hadTrouble = true;
    597                 continue;
    598             }
    599 
    600             ContentProviderClient client = null;
    601             try {
    602                 client = DocumentsApplication.acquireUnstableProviderOrThrow(
    603                         resolver, doc.derivedUri.getAuthority());
    604                 DocumentsContract.deleteDocument(client, doc.derivedUri);
    605             } catch (Exception e) {
    606                 Log.w(TAG, "Failed to delete " + doc);
    607                 hadTrouble = true;
    608             } finally {
    609                 ContentProviderClient.releaseQuietly(client);
    610             }
    611         }
    612 
    613         if (hadTrouble) {
    614             Toast.makeText(context, R.string.toast_failed_delete, Toast.LENGTH_SHORT).show();
    615         }
    616     }
    617 
    618     private static State getDisplayState(Fragment fragment) {
    619         return ((DocumentsActivity) fragment.getActivity()).getDisplayState();
    620     }
    621 
    622     private static abstract class Footer {
    623         private final int mItemViewType;
    624 
    625         public Footer(int itemViewType) {
    626             mItemViewType = itemViewType;
    627         }
    628 
    629         public abstract View getView(View convertView, ViewGroup parent);
    630 
    631         public int getItemViewType() {
    632             return mItemViewType;
    633         }
    634     }
    635 
    636     private class LoadingFooter extends Footer {
    637         public LoadingFooter() {
    638             super(1);
    639         }
    640 
    641         @Override
    642         public View getView(View convertView, ViewGroup parent) {
    643             final Context context = parent.getContext();
    644             final State state = getDisplayState(DirectoryFragment.this);
    645 
    646             if (convertView == null) {
    647                 final LayoutInflater inflater = LayoutInflater.from(context);
    648                 if (state.derivedMode == MODE_LIST) {
    649                     convertView = inflater.inflate(R.layout.item_loading_list, parent, false);
    650                 } else if (state.derivedMode == MODE_GRID) {
    651                     convertView = inflater.inflate(R.layout.item_loading_grid, parent, false);
    652                 } else {
    653                     throw new IllegalStateException();
    654                 }
    655             }
    656 
    657             return convertView;
    658         }
    659     }
    660 
    661     private class MessageFooter extends Footer {
    662         private final int mIcon;
    663         private final String mMessage;
    664 
    665         public MessageFooter(int itemViewType, int icon, String message) {
    666             super(itemViewType);
    667             mIcon = icon;
    668             mMessage = message;
    669         }
    670 
    671         @Override
    672         public View getView(View convertView, ViewGroup parent) {
    673             final Context context = parent.getContext();
    674             final State state = getDisplayState(DirectoryFragment.this);
    675 
    676             if (convertView == null) {
    677                 final LayoutInflater inflater = LayoutInflater.from(context);
    678                 if (state.derivedMode == MODE_LIST) {
    679                     convertView = inflater.inflate(R.layout.item_message_list, parent, false);
    680                 } else if (state.derivedMode == MODE_GRID) {
    681                     convertView = inflater.inflate(R.layout.item_message_grid, parent, false);
    682                 } else {
    683                     throw new IllegalStateException();
    684                 }
    685             }
    686 
    687             final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
    688             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
    689             icon.setImageResource(mIcon);
    690             title.setText(mMessage);
    691             return convertView;
    692         }
    693     }
    694 
    695     private class DocumentsAdapter extends BaseAdapter {
    696         private Cursor mCursor;
    697         private int mCursorCount;
    698 
    699         private List<Footer> mFooters = Lists.newArrayList();
    700 
    701         public void swapResult(DirectoryResult result) {
    702             mCursor = result != null ? result.cursor : null;
    703             mCursorCount = mCursor != null ? mCursor.getCount() : 0;
    704 
    705             mFooters.clear();
    706 
    707             final Bundle extras = mCursor != null ? mCursor.getExtras() : null;
    708             if (extras != null) {
    709                 final String info = extras.getString(DocumentsContract.EXTRA_INFO);
    710                 if (info != null) {
    711                     mFooters.add(new MessageFooter(2, R.drawable.ic_dialog_info, info));
    712                 }
    713                 final String error = extras.getString(DocumentsContract.EXTRA_ERROR);
    714                 if (error != null) {
    715                     mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, error));
    716                 }
    717                 if (extras.getBoolean(DocumentsContract.EXTRA_LOADING, false)) {
    718                     mFooters.add(new LoadingFooter());
    719                 }
    720             }
    721 
    722             if (result != null && result.exception != null) {
    723                 mFooters.add(new MessageFooter(
    724                         3, R.drawable.ic_dialog_alert, getString(R.string.query_error)));
    725             }
    726 
    727             if (isEmpty()) {
    728                 mEmptyView.setVisibility(View.VISIBLE);
    729             } else {
    730                 mEmptyView.setVisibility(View.GONE);
    731             }
    732 
    733             notifyDataSetChanged();
    734         }
    735 
    736         @Override
    737         public View getView(int position, View convertView, ViewGroup parent) {
    738             if (position < mCursorCount) {
    739                 return getDocumentView(position, convertView, parent);
    740             } else {
    741                 position -= mCursorCount;
    742                 convertView = mFooters.get(position).getView(convertView, parent);
    743                 // Only the view itself is disabled; contents inside shouldn't
    744                 // be dimmed.
    745                 convertView.setEnabled(false);
    746                 return convertView;
    747             }
    748         }
    749 
    750         private View getDocumentView(int position, View convertView, ViewGroup parent) {
    751             final Context context = parent.getContext();
    752             final State state = getDisplayState(DirectoryFragment.this);
    753 
    754             final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
    755 
    756             final RootsCache roots = DocumentsApplication.getRootsCache(context);
    757             final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
    758                     context, mThumbSize);
    759 
    760             if (convertView == null) {
    761                 final LayoutInflater inflater = LayoutInflater.from(context);
    762                 if (state.derivedMode == MODE_LIST) {
    763                     convertView = inflater.inflate(R.layout.item_doc_list, parent, false);
    764                 } else if (state.derivedMode == MODE_GRID) {
    765                     convertView = inflater.inflate(R.layout.item_doc_grid, parent, false);
    766                 } else {
    767                     throw new IllegalStateException();
    768                 }
    769             }
    770 
    771             final Cursor cursor = getItem(position);
    772 
    773             final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
    774             final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID);
    775             final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
    776             final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
    777             final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
    778             final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
    779             final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON);
    780             final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
    781             final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
    782             final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
    783 
    784             final View line1 = convertView.findViewById(R.id.line1);
    785             final View line2 = convertView.findViewById(R.id.line2);
    786 
    787             final ImageView iconMime = (ImageView) convertView.findViewById(R.id.icon_mime);
    788             final ImageView iconThumb = (ImageView) convertView.findViewById(R.id.icon_thumb);
    789             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
    790             final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1);
    791             final ImageView icon2 = (ImageView) convertView.findViewById(android.R.id.icon2);
    792             final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
    793             final TextView date = (TextView) convertView.findViewById(R.id.date);
    794             final TextView size = (TextView) convertView.findViewById(R.id.size);
    795 
    796             final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
    797             if (oldTask != null) {
    798                 oldTask.preempt();
    799                 iconThumb.setTag(null);
    800             }
    801 
    802             iconMime.animate().cancel();
    803             iconThumb.animate().cancel();
    804 
    805             final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
    806             final boolean allowThumbnail = (state.derivedMode == MODE_GRID)
    807                     || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, docMimeType);
    808             final boolean showThumbnail = supportsThumbnail && allowThumbnail && !mSvelteRecents;
    809 
    810             final boolean enabled = isDocumentEnabled(docMimeType, docFlags);
    811             final float iconAlpha = (state.derivedMode == MODE_LIST && !enabled) ? 0.5f : 1f;
    812 
    813             boolean cacheHit = false;
    814             if (showThumbnail) {
    815                 final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
    816                 final Bitmap cachedResult = thumbs.get(uri);
    817                 if (cachedResult != null) {
    818                     iconThumb.setImageBitmap(cachedResult);
    819                     cacheHit = true;
    820                 } else {
    821                     iconThumb.setImageDrawable(null);
    822                     final ThumbnailAsyncTask task = new ThumbnailAsyncTask(
    823                             uri, iconMime, iconThumb, mThumbSize, iconAlpha);
    824                     iconThumb.setTag(task);
    825                     ProviderExecutor.forAuthority(docAuthority).execute(task);
    826                 }
    827             }
    828 
    829             // Always throw MIME icon into place, even when a thumbnail is being
    830             // loaded in background.
    831             if (cacheHit) {
    832                 iconMime.setAlpha(0f);
    833                 iconMime.setImageDrawable(null);
    834                 iconThumb.setAlpha(1f);
    835             } else {
    836                 iconMime.setAlpha(1f);
    837                 iconThumb.setAlpha(0f);
    838                 iconThumb.setImageDrawable(null);
    839                 if (docIcon != 0) {
    840                     iconMime.setImageDrawable(
    841                             IconUtils.loadPackageIcon(context, docAuthority, docIcon));
    842                 } else {
    843                     iconMime.setImageDrawable(IconUtils.loadMimeIcon(
    844                             context, docMimeType, docAuthority, docId, state.derivedMode));
    845                 }
    846             }
    847 
    848             boolean hasLine1 = false;
    849             boolean hasLine2 = false;
    850 
    851             final boolean hideTitle = (state.derivedMode == MODE_GRID) && mHideGridTitles;
    852             if (!hideTitle) {
    853                 title.setText(docDisplayName);
    854                 hasLine1 = true;
    855             }
    856 
    857             Drawable iconDrawable = null;
    858             if (mType == TYPE_RECENT_OPEN) {
    859                 // We've already had to enumerate roots before any results can
    860                 // be shown, so this will never block.
    861                 final RootInfo root = roots.getRootBlocking(docAuthority, docRootId);
    862                 if (state.derivedMode == MODE_GRID) {
    863                     iconDrawable = root.loadGridIcon(context);
    864                 } else {
    865                     iconDrawable = root.loadIcon(context);
    866                 }
    867 
    868                 if (summary != null) {
    869                     final boolean alwaysShowSummary = getResources()
    870                             .getBoolean(R.bool.always_show_summary);
    871                     if (alwaysShowSummary) {
    872                         summary.setText(root.getDirectoryString());
    873                         summary.setVisibility(View.VISIBLE);
    874                         hasLine2 = true;
    875                     } else {
    876                         if (iconDrawable != null && roots.isIconUniqueBlocking(root)) {
    877                             // No summary needed if icon speaks for itself
    878                             summary.setVisibility(View.INVISIBLE);
    879                         } else {
    880                             summary.setText(root.getDirectoryString());
    881                             summary.setVisibility(View.VISIBLE);
    882                             summary.setTextAlignment(TextView.TEXT_ALIGNMENT_TEXT_END);
    883                             hasLine2 = true;
    884                         }
    885                     }
    886                 }
    887             } else {
    888                 // Directories showing thumbnails in grid mode get a little icon
    889                 // hint to remind user they're a directory.
    890                 if (Document.MIME_TYPE_DIR.equals(docMimeType) && state.derivedMode == MODE_GRID
    891                         && showThumbnail) {
    892                     iconDrawable = IconUtils.applyTintAttr(context, R.drawable.ic_doc_folder,
    893                             android.R.attr.textColorPrimaryInverse);
    894                 }
    895 
    896                 if (summary != null) {
    897                     if (docSummary != null) {
    898                         summary.setText(docSummary);
    899                         summary.setVisibility(View.VISIBLE);
    900                         hasLine2 = true;
    901                     } else {
    902                         summary.setVisibility(View.INVISIBLE);
    903                     }
    904                 }
    905             }
    906 
    907             if (icon1 != null) icon1.setVisibility(View.GONE);
    908             if (icon2 != null) icon2.setVisibility(View.GONE);
    909 
    910             if (iconDrawable != null) {
    911                 if (hasLine1) {
    912                     icon1.setVisibility(View.VISIBLE);
    913                     icon1.setImageDrawable(iconDrawable);
    914                 } else {
    915                     icon2.setVisibility(View.VISIBLE);
    916                     icon2.setImageDrawable(iconDrawable);
    917                 }
    918             }
    919 
    920             if (docLastModified == -1) {
    921                 date.setText(null);
    922             } else {
    923                 date.setText(formatTime(context, docLastModified));
    924                 hasLine2 = true;
    925             }
    926 
    927             if (state.showSize) {
    928                 size.setVisibility(View.VISIBLE);
    929                 if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
    930                     size.setText(null);
    931                 } else {
    932                     size.setText(Formatter.formatFileSize(context, docSize));
    933                     hasLine2 = true;
    934                 }
    935             } else {
    936                 size.setVisibility(View.GONE);
    937             }
    938 
    939             if (line1 != null) {
    940                 line1.setVisibility(hasLine1 ? View.VISIBLE : View.GONE);
    941             }
    942             if (line2 != null) {
    943                 line2.setVisibility(hasLine2 ? View.VISIBLE : View.GONE);
    944             }
    945 
    946             setEnabledRecursive(convertView, enabled);
    947 
    948             iconMime.setAlpha(iconAlpha);
    949             iconThumb.setAlpha(iconAlpha);
    950             if (icon1 != null) icon1.setAlpha(iconAlpha);
    951             if (icon2 != null) icon2.setAlpha(iconAlpha);
    952 
    953             return convertView;
    954         }
    955 
    956         @Override
    957         public int getCount() {
    958             return mCursorCount + mFooters.size();
    959         }
    960 
    961         @Override
    962         public Cursor getItem(int position) {
    963             if (position < mCursorCount) {
    964                 mCursor.moveToPosition(position);
    965                 return mCursor;
    966             } else {
    967                 return null;
    968             }
    969         }
    970 
    971         @Override
    972         public long getItemId(int position) {
    973             return position;
    974         }
    975 
    976         @Override
    977         public int getViewTypeCount() {
    978             return 4;
    979         }
    980 
    981         @Override
    982         public int getItemViewType(int position) {
    983             if (position < mCursorCount) {
    984                 return 0;
    985             } else {
    986                 position -= mCursorCount;
    987                 return mFooters.get(position).getItemViewType();
    988             }
    989         }
    990     }
    991 
    992     private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap>
    993             implements Preemptable {
    994         private final Uri mUri;
    995         private final ImageView mIconMime;
    996         private final ImageView mIconThumb;
    997         private final Point mThumbSize;
    998         private final float mTargetAlpha;
    999         private final CancellationSignal mSignal;
   1000 
   1001         public ThumbnailAsyncTask(Uri uri, ImageView iconMime, ImageView iconThumb, Point thumbSize,
   1002                 float targetAlpha) {
   1003             mUri = uri;
   1004             mIconMime = iconMime;
   1005             mIconThumb = iconThumb;
   1006             mThumbSize = thumbSize;
   1007             mTargetAlpha = targetAlpha;
   1008             mSignal = new CancellationSignal();
   1009         }
   1010 
   1011         @Override
   1012         public void preempt() {
   1013             cancel(false);
   1014             mSignal.cancel();
   1015         }
   1016 
   1017         @Override
   1018         protected Bitmap doInBackground(Uri... params) {
   1019             if (isCancelled()) return null;
   1020 
   1021             final Context context = mIconThumb.getContext();
   1022             final ContentResolver resolver = context.getContentResolver();
   1023 
   1024             ContentProviderClient client = null;
   1025             Bitmap result = null;
   1026             try {
   1027                 client = DocumentsApplication.acquireUnstableProviderOrThrow(
   1028                         resolver, mUri.getAuthority());
   1029                 result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal);
   1030                 if (result != null) {
   1031                     final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
   1032                             context, mThumbSize);
   1033                     thumbs.put(mUri, result);
   1034                 }
   1035             } catch (Exception e) {
   1036                 if (!(e instanceof OperationCanceledException)) {
   1037                     Log.w(TAG, "Failed to load thumbnail for " + mUri + ": " + e);
   1038                 }
   1039             } finally {
   1040                 ContentProviderClient.releaseQuietly(client);
   1041             }
   1042             return result;
   1043         }
   1044 
   1045         @Override
   1046         protected void onPostExecute(Bitmap result) {
   1047             if (mIconThumb.getTag() == this && result != null) {
   1048                 mIconThumb.setTag(null);
   1049                 mIconThumb.setImageBitmap(result);
   1050 
   1051                 mIconMime.setAlpha(mTargetAlpha);
   1052                 mIconMime.animate().alpha(0f).start();
   1053                 mIconThumb.setAlpha(0f);
   1054                 mIconThumb.animate().alpha(mTargetAlpha).start();
   1055             }
   1056         }
   1057     }
   1058 
   1059     private static String formatTime(Context context, long when) {
   1060         // TODO: DateUtils should make this easier
   1061         Time then = new Time();
   1062         then.set(when);
   1063         Time now = new Time();
   1064         now.setToNow();
   1065 
   1066         int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT
   1067                 | DateUtils.FORMAT_ABBREV_ALL;
   1068 
   1069         if (then.year != now.year) {
   1070             flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
   1071         } else if (then.yearDay != now.yearDay) {
   1072             flags |= DateUtils.FORMAT_SHOW_DATE;
   1073         } else {
   1074             flags |= DateUtils.FORMAT_SHOW_TIME;
   1075         }
   1076 
   1077         return DateUtils.formatDateTime(context, when, flags);
   1078     }
   1079 
   1080     private String findCommonMimeType(List<String> mimeTypes) {
   1081         String[] commonType = mimeTypes.get(0).split("/");
   1082         if (commonType.length != 2) {
   1083             return "*/*";
   1084         }
   1085 
   1086         for (int i = 1; i < mimeTypes.size(); i++) {
   1087             String[] type = mimeTypes.get(i).split("/");
   1088             if (type.length != 2) continue;
   1089 
   1090             if (!commonType[1].equals(type[1])) {
   1091                 commonType[1] = "*";
   1092             }
   1093 
   1094             if (!commonType[0].equals(type[0])) {
   1095                 commonType[0] = "*";
   1096                 commonType[1] = "*";
   1097                 break;
   1098             }
   1099         }
   1100 
   1101         return commonType[0] + "/" + commonType[1];
   1102     }
   1103 
   1104     private void setEnabledRecursive(View v, boolean enabled) {
   1105         if (v == null) return;
   1106         if (v.isEnabled() == enabled) return;
   1107         v.setEnabled(enabled);
   1108 
   1109         if (v instanceof ViewGroup) {
   1110             final ViewGroup vg = (ViewGroup) v;
   1111             for (int i = vg.getChildCount() - 1; i >= 0; i--) {
   1112                 setEnabledRecursive(vg.getChildAt(i), enabled);
   1113             }
   1114         }
   1115     }
   1116 
   1117     private boolean isDocumentEnabled(String docMimeType, int docFlags) {
   1118         final State state = getDisplayState(DirectoryFragment.this);
   1119 
   1120         // Directories are always enabled
   1121         if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
   1122             return true;
   1123         }
   1124 
   1125         // Read-only files are disabled when creating
   1126         if (state.action == ACTION_CREATE && (docFlags & Document.FLAG_SUPPORTS_WRITE) == 0) {
   1127             return false;
   1128         }
   1129 
   1130         return MimePredicate.mimeMatches(state.acceptMimes, docMimeType);
   1131     }
   1132 }
   1133