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