/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Per article 5 of the Apache 2.0 License, some modifications to this code * were made by the OmniROM Project. * * Modifications Copyright (C) 2013 The OmniROM Project * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 3 * of the License, or (at your option) any later version. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package com.android.documentsui; import static com.android.documentsui.DocumentsActivity.TAG; import static com.android.documentsui.DocumentsActivity.State.ACTION_CREATE; import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE; import static com.android.documentsui.DocumentsActivity.State.ACTION_STANDALONE; import static com.android.documentsui.DocumentsActivity.State.MODE_GRID; import static com.android.documentsui.DocumentsActivity.State.MODE_LIST; import static com.android.documentsui.DocumentsActivity.State.MODE_UNKNOWN; import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_UNKNOWN; import static com.android.documentsui.model.DocumentInfo.getCursorInt; import static com.android.documentsui.model.DocumentInfo.getCursorLong; import static com.android.documentsui.model.DocumentInfo.getCursorString; import android.app.ActivityManager; import android.app.AlertDialog; import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.app.LoaderManager.LoaderCallbacks; import android.app.ProgressDialog; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.Loader; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.os.Parcelable; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.text.format.DateUtils; import android.text.format.Formatter; import android.text.format.Time; import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.AbsListView.RecyclerListener; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.BaseAdapter; import android.widget.FrameLayout; import android.widget.GridView; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.android.documentsui.DocumentsActivity.State; import com.android.documentsui.ProviderExecutor.Preemptable; import com.android.documentsui.RecentsProvider.StateColumns; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.RootInfo; import com.google.android.collect.Lists; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.Executor; /** * Display the documents inside a single directory. */ public class DirectoryFragment extends Fragment { private View mEmptyView; private ListView mListView; private GridView mGridView; private AbsListView mCurrentView; public static final int TYPE_NORMAL = 1; public static final int TYPE_SEARCH = 2; public static final int TYPE_RECENT_OPEN = 3; public static final int ANIM_NONE = 1; public static final int ANIM_SIDE = 2; public static final int ANIM_DOWN = 3; public static final int ANIM_UP = 4; private int mType = TYPE_NORMAL; private String mStateKey; private int mLastMode = MODE_UNKNOWN; private int mLastSortOrder = SORT_ORDER_UNKNOWN; private boolean mLastShowSize = false; private boolean mHideGridTitles = false; private boolean mSvelteRecents; private Point mThumbSize; private DocumentsAdapter mAdapter; private LoaderCallbacks<DirectoryResult> mCallbacks; private static final String EXTRA_TYPE = "type"; private static final String EXTRA_ROOT = "root"; private static final String EXTRA_DOC = "doc"; private static final String EXTRA_QUERY = "query"; private static final String EXTRA_IGNORE_STATE = "ignoreState"; private final int mLoaderId = 42; public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) { show(fm, TYPE_NORMAL, root, doc, null, anim); } public static void showSearch(FragmentManager fm, RootInfo root, String query, int anim) { show(fm, TYPE_SEARCH, root, null, query, anim); } public static void showRecentsOpen(FragmentManager fm, int anim) { show(fm, TYPE_RECENT_OPEN, null, null, null, anim); } private static void show(FragmentManager fm, int type, RootInfo root, DocumentInfo doc, String query, int anim) { final Bundle args = new Bundle(); args.putInt(EXTRA_TYPE, type); args.putParcelable(EXTRA_ROOT, root); args.putParcelable(EXTRA_DOC, doc); args.putString(EXTRA_QUERY, query); final FragmentTransaction ft = fm.beginTransaction(); switch (anim) { case ANIM_SIDE: args.putBoolean(EXTRA_IGNORE_STATE, true); break; case ANIM_DOWN: args.putBoolean(EXTRA_IGNORE_STATE, true); ft.setCustomAnimations(R.animator.dir_down, R.animator.dir_frozen); break; case ANIM_UP: ft.setCustomAnimations(R.animator.dir_frozen, R.animator.dir_up); break; } final DirectoryFragment fragment = new DirectoryFragment(); fragment.setArguments(args); ft.replace(R.id.container_directory, fragment); ft.commitAllowingStateLoss(); } private static String buildStateKey(RootInfo root, DocumentInfo doc) { final StringBuilder builder = new StringBuilder(); builder.append(root != null ? root.authority : "null").append(';'); builder.append(root != null ? root.rootId : "null").append(';'); builder.append(doc != null ? doc.documentId : "null"); return builder.toString(); } public static DirectoryFragment get(FragmentManager fm) { // TODO: deal with multiple directories shown at once return (DirectoryFragment) fm.findFragmentById(R.id.container_directory); } @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final Context context = inflater.getContext(); final View view = inflater.inflate(R.layout.fragment_directory, container, false); mEmptyView = view.findViewById(android.R.id.empty); mListView = (ListView) view.findViewById(R.id.list); mListView.setOnItemClickListener(mItemListener); mListView.setMultiChoiceModeListener(mMultiListener); mListView.setRecyclerListener(mRecycleListener); mGridView = (GridView) view.findViewById(R.id.grid); mGridView.setOnItemClickListener(mItemListener); mGridView.setMultiChoiceModeListener(mMultiListener); mGridView.setRecyclerListener(mRecycleListener); return view; } @Override public void onDestroyView() { super.onDestroyView(); // Cancel any outstanding thumbnail requests final ViewGroup target = (mListView.getAdapter() != null) ? mListView : mGridView; final int count = target.getChildCount(); for (int i = 0; i < count; i++) { final View view = target.getChildAt(i); mRecycleListener.onMovedToScrapHeap(view); } // Tear down any selection in progress mListView.setChoiceMode(AbsListView.CHOICE_MODE_NONE); mGridView.setChoiceMode(AbsListView.CHOICE_MODE_NONE); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); final Context context = getActivity(); final State state = getDisplayState(DirectoryFragment.this); final RootInfo root = getArguments().getParcelable(EXTRA_ROOT); final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC); mAdapter = new DocumentsAdapter(); mType = getArguments().getInt(EXTRA_TYPE); mStateKey = buildStateKey(root, doc); if (mType == TYPE_RECENT_OPEN) { // Hide titles when showing recents for picking images/videos mHideGridTitles = MimePredicate.mimeMatches( MimePredicate.VISUAL_MIMES, state.acceptMimes); } else { mHideGridTitles = (doc != null) && doc.isGridTitlesHidden(); } final ActivityManager am = (ActivityManager) context.getSystemService( Context.ACTIVITY_SERVICE); mSvelteRecents = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN); mCallbacks = new LoaderCallbacks<DirectoryResult>() { @Override public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { final String query = getArguments().getString(EXTRA_QUERY); Uri contentsUri; switch (mType) { case TYPE_NORMAL: contentsUri = DocumentsContract.buildChildDocumentsUri( doc.authority, doc.documentId); if (state.action == ACTION_MANAGE) { contentsUri = DocumentsContract.setManageMode(contentsUri); } return new DirectoryLoader( context, mType, root, doc, contentsUri, state.userSortOrder); case TYPE_SEARCH: contentsUri = DocumentsContract.buildSearchDocumentsUri( root.authority, root.rootId, query); if (state.action == ACTION_MANAGE) { contentsUri = DocumentsContract.setManageMode(contentsUri); } return new DirectoryLoader( context, mType, root, doc, contentsUri, state.userSortOrder); case TYPE_RECENT_OPEN: final RootsCache roots = DocumentsApplication.getRootsCache(context); return new RecentLoader(context, roots, state); default: throw new IllegalStateException("Unknown type " + mType); } } @Override public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { if (!isAdded()) return; mAdapter.swapResult(result); // Push latest state up to UI // TODO: if mode change was racing with us, don't overwrite it if (result.mode != MODE_UNKNOWN) { state.derivedMode = result.mode; } state.derivedSortOrder = result.sortOrder; ((DocumentsActivity) context).onStateChanged(); updateDisplayState(); // When launched into empty recents, show drawer if (mType == TYPE_RECENT_OPEN && mAdapter.isEmpty() && !state.stackTouched) { ((DocumentsActivity) context).setRootsDrawerOpen(true); } // Restore any previous instance state final SparseArray<Parcelable> container = state.dirState.remove(mStateKey); if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) { getView().restoreHierarchyState(container); } else if (mLastSortOrder != state.derivedSortOrder) { mListView.smoothScrollToPosition(0); mGridView.smoothScrollToPosition(0); } mLastSortOrder = state.derivedSortOrder; } @Override public void onLoaderReset(Loader<DirectoryResult> loader) { mAdapter.swapResult(null); } }; // Kick off loader at least once getLoaderManager().restartLoader(mLoaderId, null, mCallbacks); updateDisplayState(); } @Override public void onStop() { super.onStop(); // Remember last scroll location final SparseArray<Parcelable> container = new SparseArray<Parcelable>(); getView().saveHierarchyState(container); final State state = getDisplayState(this); state.dirState.put(mStateKey, container); } @Override public void onResume() { super.onResume(); updateDisplayState(); } public void onUserSortOrderChanged() { // Sort order change always triggers reload; we'll trigger state change // on the flip side. getLoaderManager().restartLoader(mLoaderId, null, mCallbacks); } public void onUserModeChanged() { final ContentResolver resolver = getActivity().getContentResolver(); final State state = getDisplayState(this); final RootInfo root = getArguments().getParcelable(EXTRA_ROOT); final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC); if (root != null && doc != null) { final Uri stateUri = RecentsProvider.buildState( root.authority, root.rootId, doc.documentId); final ContentValues values = new ContentValues(); values.put(StateColumns.MODE, state.userMode); new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { resolver.insert(stateUri, values); return null; } }.execute(); } // Mode change is just visual change; no need to kick loader, and // deliver change event immediately. state.derivedMode = state.userMode; ((DocumentsActivity) getActivity()).onStateChanged(); updateDisplayState(); } private void updateDisplayState() { final State state = getDisplayState(this); if (mLastMode == state.derivedMode && mLastShowSize == state.showSize) return; mLastMode = state.derivedMode; mLastShowSize = state.showSize; mListView.setVisibility(state.derivedMode == MODE_LIST ? View.VISIBLE : View.GONE); mGridView.setVisibility(state.derivedMode == MODE_GRID ? View.VISIBLE : View.GONE); final int choiceMode; if (state.allowMultiple) { choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL; } else { choiceMode = ListView.CHOICE_MODE_NONE; } final int thumbSize; if (state.derivedMode == MODE_GRID) { thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width); mListView.setAdapter(null); mListView.setChoiceMode(ListView.CHOICE_MODE_NONE); mGridView.setAdapter(mAdapter); mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width)); mGridView.setNumColumns(GridView.AUTO_FIT); mGridView.setChoiceMode(choiceMode); mCurrentView = mGridView; } else if (state.derivedMode == MODE_LIST) { thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size); mGridView.setAdapter(null); mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE); mListView.setAdapter(mAdapter); mListView.setChoiceMode(choiceMode); mCurrentView = mListView; } else { throw new IllegalStateException("Unknown state " + state.derivedMode); } mThumbSize = new Point(thumbSize, thumbSize); } private OnItemClickListener mItemListener = new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { final Cursor cursor = mAdapter.getItem(position); if (cursor != null) { final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); if (isDocumentEnabled(docMimeType, docFlags)) { final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); ((DocumentsActivity) getActivity()).onDocumentPicked(doc); } } } }; private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { mode.getMenuInflater().inflate(R.menu.mode_directory, menu); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { final State state = getDisplayState(DirectoryFragment.this); final MenuItem open = menu.findItem(R.id.menu_open); final MenuItem share = menu.findItem(R.id.menu_share); final MenuItem delete = menu.findItem(R.id.menu_delete); final MenuItem copy = menu.findItem(R.id.menu_copy); final MenuItem cut = menu.findItem(R.id.menu_cut); final boolean manageMode = state.action == ACTION_MANAGE; final boolean stdMode = state.action == ACTION_STANDALONE; open.setVisible(!manageMode && !stdMode); share.setVisible(manageMode || stdMode); delete.setVisible(manageMode || stdMode); copy.setVisible(stdMode); cut.setVisible(stdMode); return true; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions(); final ArrayList<DocumentInfo> docs = Lists.newArrayList(); final int size = checked.size(); for (int i = 0; i < size; i++) { if (checked.valueAt(i)) { final Cursor cursor = mAdapter.getItem(checked.keyAt(i)); final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor); docs.add(doc); } } final int id = item.getItemId(); if (id == R.id.menu_open) { DocumentsActivity.get(DirectoryFragment.this).onDocumentsPicked(docs); mode.finish(); return true; } else if (id == R.id.menu_share) { onShareDocuments(docs); mode.finish(); return true; } else if (id == R.id.menu_delete) { onDeleteDocuments(docs); mode.finish(); return true; } else if (id == R.id.menu_copy) { onCopyDocuments(docs); mode.finish(); return true; } else if (id == R.id.menu_cut) { onCutDocuments(docs); mode.finish(); return true; } else { return false; } } @Override public void onDestroyActionMode(ActionMode mode) { // ignored } @Override public void onItemCheckedStateChanged( ActionMode mode, int position, long id, boolean checked) { if (checked) { // Directories and footer items cannot be checked boolean valid = false; boolean hasFolder = false; final Cursor cursor = mAdapter.getItem(position); if (cursor != null) { final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); final State state = getDisplayState(DirectoryFragment.this); if (Document.MIME_TYPE_DIR.equals(docMimeType)) { hasFolder = true; } if (!Document.MIME_TYPE_DIR.equals(docMimeType) || state.action == ACTION_STANDALONE) { valid = isDocumentEnabled(docMimeType, docFlags); } } if (hasFolder) { final Menu menu = mode.getMenu(); final MenuItem copy = menu.findItem(R.id.menu_copy); final MenuItem cut = menu.findItem(R.id.menu_cut); copy.setVisible(false); cut.setVisible(false); } if (!valid) { mCurrentView.setItemChecked(position, false); } } mode.setTitle(getResources() .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount())); } }; private RecyclerListener mRecycleListener = new RecyclerListener() { @Override public void onMovedToScrapHeap(View view) { final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb); if (iconThumb != null) { final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag(); if (oldTask != null) { oldTask.preempt(); iconThumb.setTag(null); } } } }; private void onShareDocuments(List<DocumentInfo> docs) { Intent intent; if (docs.size() == 1) { final DocumentInfo doc = docs.get(0); intent = new Intent(Intent.ACTION_SEND); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addCategory(Intent.CATEGORY_DEFAULT); intent.setType(doc.mimeType); intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri); } else if (docs.size() > 1) { intent = new Intent(Intent.ACTION_SEND_MULTIPLE); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addCategory(Intent.CATEGORY_DEFAULT); final ArrayList<String> mimeTypes = Lists.newArrayList(); final ArrayList<Uri> uris = Lists.newArrayList(); for (DocumentInfo doc : docs) { mimeTypes.add(doc.mimeType); uris.add(doc.derivedUri); } intent.setType(findCommonMimeType(mimeTypes)); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); } else { return; } intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via)); startActivity(intent); } private void onDeleteDocuments(final List<DocumentInfo> docs) { final Context context = getActivity(); final ContentResolver resolver = context.getContentResolver(); final Resources resources = context.getResources(); // Open a confirmation dialog AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { new DeleteFilesTask(docs.toArray(new DocumentInfo[0])).executeOnExecutor(getCurrentExecutor()); } }); builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { // User cancelled the dialog, ignore actions } }); builder.setTitle(R.string.dialog_delete_confirm_title) .setMessage(resources.getQuantityString(R.plurals.dialog_delete_confirm_message, docs.size(), docs.size())); AlertDialog dialog = builder.create(); dialog.show(); } private boolean onDeleteDocumentsImpl(final List<DocumentInfo> docs) { final Context context = getActivity(); final ContentResolver resolver = context.getContentResolver(); boolean hadTrouble = false; for (DocumentInfo doc : docs) { if (!doc.isDeleteSupported()) { Log.w(TAG, "Skipping " + doc); hadTrouble = true; continue; } ContentProviderClient client = null; try { client = DocumentsApplication.acquireUnstableProviderOrThrow( resolver, doc.derivedUri.getAuthority()); if (Document.MIME_TYPE_DIR.equals(doc.mimeType)) { // In order to delete a directory, we must delete its contents first. We // recursively do so. Uri contentsUri = DocumentsContract.buildChildDocumentsUri( doc.authority, doc.documentId); final RootInfo root = getArguments().getParcelable(EXTRA_ROOT); // We get the contents of the directory DirectoryLoader loader = new DirectoryLoader( context, mType, root, doc, contentsUri, SORT_ORDER_UNKNOWN); DirectoryResult result = loader.loadInBackground(); Cursor cursor = result.cursor; // Build a list of the docs to delete, and delete them ArrayList<DocumentInfo> docsToDelete = new ArrayList<DocumentInfo>(); for (int i = 0; i < cursor.getCount(); i++) { cursor.moveToPosition(i); final DocumentInfo subDoc = DocumentInfo.fromDirectoryCursor(cursor); docsToDelete.add(subDoc); } onDeleteDocumentsImpl(docsToDelete); } DocumentsContract.deleteDocument(client, doc.derivedUri); } catch (Exception e) { Log.w(TAG, "Failed to delete " + doc); hadTrouble = true; } finally { ContentProviderClient.releaseQuietly(client); } } return !hadTrouble; } private void onCopyDocuments(final List<DocumentInfo> docs) { ((DocumentsActivity) getActivity()).setClipboardDocuments(docs, true); } private void onCutDocuments(final List<DocumentInfo> docs) { ((DocumentsActivity) getActivity()).setClipboardDocuments(docs, false); } private static State getDisplayState(Fragment fragment) { return ((DocumentsActivity) fragment.getActivity()).getDisplayState(); } private static abstract class Footer { private final int mItemViewType; public Footer(int itemViewType) { mItemViewType = itemViewType; } public abstract View getView(View convertView, ViewGroup parent); public int getItemViewType() { return mItemViewType; } } private class LoadingFooter extends Footer { public LoadingFooter() { super(1); } @Override public View getView(View convertView, ViewGroup parent) { final Context context = parent.getContext(); final State state = getDisplayState(DirectoryFragment.this); if (convertView == null) { final LayoutInflater inflater = LayoutInflater.from(context); if (state.derivedMode == MODE_LIST) { convertView = inflater.inflate(R.layout.item_loading_list, parent, false); } else if (state.derivedMode == MODE_GRID) { convertView = inflater.inflate(R.layout.item_loading_grid, parent, false); } else { throw new IllegalStateException(); } } return convertView; } } private class MessageFooter extends Footer { private final int mIcon; private final String mMessage; public MessageFooter(int itemViewType, int icon, String message) { super(itemViewType); mIcon = icon; mMessage = message; } @Override public View getView(View convertView, ViewGroup parent) { final Context context = parent.getContext(); final State state = getDisplayState(DirectoryFragment.this); if (convertView == null) { final LayoutInflater inflater = LayoutInflater.from(context); if (state.derivedMode == MODE_LIST) { convertView = inflater.inflate(R.layout.item_message_list, parent, false); } else if (state.derivedMode == MODE_GRID) { convertView = inflater.inflate(R.layout.item_message_grid, parent, false); } else { throw new IllegalStateException(); } } final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); final TextView title = (TextView) convertView.findViewById(android.R.id.title); icon.setImageResource(mIcon); title.setText(mMessage); return convertView; } } private class DocumentsAdapter extends BaseAdapter { private Cursor mCursor; private int mCursorCount; private List<Footer> mFooters = Lists.newArrayList(); public void swapResult(DirectoryResult result) { mCursor = result != null ? result.cursor : null; mCursorCount = mCursor != null ? mCursor.getCount() : 0; mFooters.clear(); final Bundle extras = mCursor != null ? mCursor.getExtras() : null; if (extras != null) { final String info = extras.getString(DocumentsContract.EXTRA_INFO); if (info != null) { mFooters.add(new MessageFooter(2, R.drawable.ic_dialog_info, info)); } final String error = extras.getString(DocumentsContract.EXTRA_ERROR); if (error != null) { mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, error)); } if (extras.getBoolean(DocumentsContract.EXTRA_LOADING, false)) { mFooters.add(new LoadingFooter()); } } if (result != null && result.exception != null) { mFooters.add(new MessageFooter( 3, R.drawable.ic_dialog_alert, getString(R.string.query_error))); } if (isEmpty()) { mEmptyView.setVisibility(View.VISIBLE); } else { mEmptyView.setVisibility(View.GONE); } notifyDataSetChanged(); } @Override public View getView(int position, View convertView, ViewGroup parent) { if (position < mCursorCount) { return getDocumentView(position, convertView, parent); } else { position -= mCursorCount; convertView = mFooters.get(position).getView(convertView, parent); // Only the view itself is disabled; contents inside shouldn't // be dimmed. convertView.setEnabled(false); return convertView; } } private View getDocumentView(int position, View convertView, ViewGroup parent) { final Context context = parent.getContext(); final State state = getDisplayState(DirectoryFragment.this); final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC); final RootsCache roots = DocumentsApplication.getRootsCache(context); final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( context, mThumbSize); if (convertView == null) { final LayoutInflater inflater = LayoutInflater.from(context); if (state.derivedMode == MODE_LIST) { convertView = inflater.inflate(R.layout.item_doc_list, parent, false); } else if (state.derivedMode == MODE_GRID) { convertView = inflater.inflate(R.layout.item_doc_grid, parent, false); // Apply padding to grid items final FrameLayout grid = (FrameLayout) convertView; final int gridPadding = getResources() .getDimensionPixelSize(R.dimen.grid_padding); // Tricksy hobbitses! We need to fully clear the drawable so // the view doesn't clobber the new InsetDrawable callback // when setting back later. final Drawable fg = grid.getForeground(); final Drawable bg = grid.getBackground(); grid.setForeground(null); grid.setBackground(null); grid.setForeground(new InsetDrawable(fg, gridPadding)); grid.setBackground(new InsetDrawable(bg, gridPadding)); } else { throw new IllegalStateException(); } } final Cursor cursor = getItem(position); final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY); final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID); final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID); final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED); final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON); final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY); final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE); final View line1 = convertView.findViewById(R.id.line1); final View line2 = convertView.findViewById(R.id.line2); final ImageView iconMime = (ImageView) convertView.findViewById(R.id.icon_mime); final ImageView iconThumb = (ImageView) convertView.findViewById(R.id.icon_thumb); final TextView title = (TextView) convertView.findViewById(android.R.id.title); final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1); final ImageView icon2 = (ImageView) convertView.findViewById(android.R.id.icon2); final TextView summary = (TextView) convertView.findViewById(android.R.id.summary); final TextView date = (TextView) convertView.findViewById(R.id.date); final TextView size = (TextView) convertView.findViewById(R.id.size); final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag(); if (oldTask != null) { oldTask.preempt(); iconThumb.setTag(null); } iconMime.animate().cancel(); iconThumb.animate().cancel(); final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0; final boolean allowThumbnail = (state.derivedMode == MODE_GRID) || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, docMimeType); final boolean showThumbnail = supportsThumbnail && allowThumbnail && !mSvelteRecents; boolean cacheHit = false; if (showThumbnail) { final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId); final Bitmap cachedResult = thumbs.get(uri); if (cachedResult != null) { iconThumb.setImageBitmap(cachedResult); cacheHit = true; } else { iconThumb.setImageDrawable(null); final ThumbnailAsyncTask task = new ThumbnailAsyncTask( uri, iconMime, iconThumb, mThumbSize); iconThumb.setTag(task); ProviderExecutor.forAuthority(docAuthority).execute(task); } } // Always throw MIME icon into place, even when a thumbnail is being // loaded in background. if (cacheHit) { iconMime.setAlpha(0f); iconMime.setImageDrawable(null); iconThumb.setAlpha(1f); } else { iconMime.setAlpha(1f); iconThumb.setAlpha(0f); iconThumb.setImageDrawable(null); if (docIcon != 0) { iconMime.setImageDrawable( IconUtils.loadPackageIcon(context, docAuthority, docIcon)); } else { iconMime.setImageDrawable(IconUtils.loadMimeIcon( context, docMimeType, docAuthority, docId, state.derivedMode)); } } boolean hasLine1 = false; boolean hasLine2 = false; final boolean hideTitle = (state.derivedMode == MODE_GRID) && mHideGridTitles; if (!hideTitle) { title.setText(docDisplayName); hasLine1 = true; } Drawable iconDrawable = null; if (mType == TYPE_RECENT_OPEN) { // We've already had to enumerate roots before any results can // be shown, so this will never block. final RootInfo root = roots.getRootBlocking(docAuthority, docRootId); iconDrawable = root.loadIcon(context); if (summary != null) { final boolean alwaysShowSummary = getResources() .getBoolean(R.bool.always_show_summary); if (alwaysShowSummary) { summary.setText(root.getDirectoryString()); summary.setVisibility(View.VISIBLE); hasLine2 = true; } else { if (iconDrawable != null && roots.isIconUniqueBlocking(root)) { // No summary needed if icon speaks for itself summary.setVisibility(View.INVISIBLE); } else { summary.setText(root.getDirectoryString()); summary.setVisibility(View.VISIBLE); summary.setTextAlignment(TextView.TEXT_ALIGNMENT_TEXT_END); hasLine2 = true; } } } } else { // Directories showing thumbnails in grid mode get a little icon // hint to remind user they're a directory. if (Document.MIME_TYPE_DIR.equals(docMimeType) && state.derivedMode == MODE_GRID && showThumbnail) { iconDrawable = context.getResources().getDrawable(R.drawable.ic_root_folder); } if (summary != null) { if (docSummary != null) { summary.setText(docSummary); summary.setVisibility(View.VISIBLE); hasLine2 = true; } else { summary.setVisibility(View.INVISIBLE); } } } if (icon1 != null) icon1.setVisibility(View.GONE); if (icon2 != null) icon2.setVisibility(View.GONE); if (iconDrawable != null) { if (hasLine1) { icon1.setVisibility(View.VISIBLE); icon1.setImageDrawable(iconDrawable); } else { icon2.setVisibility(View.VISIBLE); icon2.setImageDrawable(iconDrawable); } } if (docLastModified == -1) { date.setText(null); } else { date.setText(formatTime(context, docLastModified)); hasLine2 = true; } if (state.showSize) { size.setVisibility(View.VISIBLE); if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) { size.setText(null); } else { size.setText(Formatter.formatFileSize(context, docSize)); hasLine2 = true; } } else { size.setVisibility(View.GONE); } if (line1 != null) { line1.setVisibility(hasLine1 ? View.VISIBLE : View.GONE); } if (line2 != null) { line2.setVisibility(hasLine2 ? View.VISIBLE : View.GONE); } final boolean enabled = isDocumentEnabled(docMimeType, docFlags); if (enabled) { setEnabledRecursive(convertView, true); iconMime.setAlpha(1f); iconThumb.setAlpha(1f); if (icon1 != null) icon1.setAlpha(1f); if (icon2 != null) icon2.setAlpha(1f); } else { setEnabledRecursive(convertView, false); iconMime.setAlpha(0.5f); iconThumb.setAlpha(0.5f); if (icon1 != null) icon1.setAlpha(0.5f); if (icon2 != null) icon2.setAlpha(0.5f); } return convertView; } @Override public int getCount() { return mCursorCount + mFooters.size(); } @Override public Cursor getItem(int position) { if (position < mCursorCount) { mCursor.moveToPosition(position); return mCursor; } else { return null; } } @Override public long getItemId(int position) { return position; } @Override public int getViewTypeCount() { return 4; } @Override public int getItemViewType(int position) { if (position < mCursorCount) { return 0; } else { position -= mCursorCount; return mFooters.get(position).getItemViewType(); } } } private class DeleteFilesTask extends AsyncTask<Void, Integer, Boolean> { private final DocumentInfo[] mDocs; private ProgressDialog mProgressDialog; public DeleteFilesTask(DocumentInfo... docs) { mDocs = docs; mProgressDialog = new ProgressDialog(getActivity()); mProgressDialog.setMessage(getString(R.string.delete_in_progress)); mProgressDialog.setIndeterminate(true); mProgressDialog.setCanceledOnTouchOutside(false); mProgressDialog.show(); } @Override protected Boolean doInBackground(Void... params) { ArrayList<DocumentInfo> docs = new ArrayList<DocumentInfo>(); Collections.addAll(docs, mDocs); boolean result = onDeleteDocumentsImpl(docs); return result; } @Override protected void onPostExecute(Boolean result) { mProgressDialog.dismiss(); if (result == false) { Toast.makeText(getActivity(), R.string.toast_failed_delete, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getActivity(), R.string.toast_success_delete, Toast.LENGTH_SHORT).show(); } // Reload files in the current folder getLoaderManager().restartLoader(mLoaderId, null, mCallbacks); updateDisplayState(); } } private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap> implements Preemptable { private final Uri mUri; private final ImageView mIconMime; private final ImageView mIconThumb; private final Point mThumbSize; private final CancellationSignal mSignal; public ThumbnailAsyncTask( Uri uri, ImageView iconMime, ImageView iconThumb, Point thumbSize) { mUri = uri; mIconMime = iconMime; mIconThumb = iconThumb; mThumbSize = thumbSize; mSignal = new CancellationSignal(); } @Override public void preempt() { cancel(false); mSignal.cancel(); } @Override protected Bitmap doInBackground(Uri... params) { if (isCancelled()) return null; final Context context = mIconThumb.getContext(); final ContentResolver resolver = context.getContentResolver(); ContentProviderClient client = null; Bitmap result = null; try { client = DocumentsApplication.acquireUnstableProviderOrThrow( resolver, mUri.getAuthority()); result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal); if (result != null) { final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache( context, mThumbSize); thumbs.put(mUri, result); } } catch (Exception e) { if (!(e instanceof OperationCanceledException)) { Log.w(TAG, "Failed to load thumbnail for " + mUri + ": " + e); } } finally { ContentProviderClient.releaseQuietly(client); } return result; } @Override protected void onPostExecute(Bitmap result) { if (mIconThumb.getTag() == this && result != null) { mIconThumb.setTag(null); mIconThumb.setImageBitmap(result); final float targetAlpha = mIconMime.isEnabled() ? 1f : 0.5f; mIconMime.setAlpha(targetAlpha); mIconMime.animate().alpha(0f).start(); mIconThumb.setAlpha(0f); mIconThumb.animate().alpha(targetAlpha).start(); } } } private static String formatTime(Context context, long when) { // TODO: DateUtils should make this easier Time then = new Time(); then.set(when); Time now = new Time(); now.setToNow(); int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT | DateUtils.FORMAT_ABBREV_ALL; if (then.year != now.year) { flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE; } else if (then.yearDay != now.yearDay) { flags |= DateUtils.FORMAT_SHOW_DATE; } else { flags |= DateUtils.FORMAT_SHOW_TIME; } return DateUtils.formatDateTime(context, when, flags); } private String findCommonMimeType(List<String> mimeTypes) { String[] commonType = mimeTypes.get(0).split("/"); if (commonType.length != 2) { return "*/*"; } for (int i = 1; i < mimeTypes.size(); i++) { String[] type = mimeTypes.get(i).split("/"); if (type.length != 2) continue; if (!commonType[1].equals(type[1])) { commonType[1] = "*"; } if (!commonType[0].equals(type[0])) { commonType[0] = "*"; commonType[1] = "*"; break; } } return commonType[0] + "/" + commonType[1]; } private void setEnabledRecursive(View v, boolean enabled) { if (v == null) return; if (v.isEnabled() == enabled) return; v.setEnabled(enabled); if (v instanceof ViewGroup) { final ViewGroup vg = (ViewGroup) v; for (int i = vg.getChildCount() - 1; i >= 0; i--) { setEnabledRecursive(vg.getChildAt(i), enabled); } } } private boolean isDocumentEnabled(String docMimeType, int docFlags) { final State state = getDisplayState(DirectoryFragment.this); // Directories are always enabled if (Document.MIME_TYPE_DIR.equals(docMimeType)) { return true; } // Read-only files are disabled when creating if (state.action == ACTION_CREATE && (docFlags & Document.FLAG_SUPPORTS_WRITE) == 0) { return false; } return MimePredicate.mimeMatches(state.acceptMimes, docMimeType); } public Executor getCurrentExecutor() { return ((DocumentsActivity) getActivity()).getCurrentExecutor(); } }