Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a35ac2dd authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

More UX work for thumbnails, search, management.

Hide non-finished downloads from normal picker UI, but keep them
around in management mode.  Uses a Uri query parameter and a hidden
API on DocumentsProvider.

Scale thumbnails to fit viewport, always show MIME icon while waiting
on thumbnails, and crossfade between them.  Cancel thumbnail tasks
when views are recycled.

Filter directories out of search results for now.  Also leave sort
ordering from backend intact, since it's custom ranking.  Fix
SearchView interaction to dismiss properly and restore across
orientation and drawer state changes.

Hide most actions when drawer is open.  Invalidate RootInfo cache
when locale changes.  Apply sort ordering when showing recent create
directories.  Hide recent summary string when icon is enough for user
to disambiguate.

Bug: 10667184, 10665663
Change-Id: I331d3272a08c497f88dc659d9e112231cb35aa69
parent ea5b3bfe
Loading
Loading
Loading
Loading
+18 −4
Original line number Diff line number Diff line
@@ -32,13 +32,27 @@
            android:layout_weight="1"
            android:background="#fff">

            <ImageView
            <FrameLayout
                android:id="@android:id/icon"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <ImageView
                    android:id="@+id/icon_mime"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:scaleType="centerInside"
                    android:contentDescription="@null" />

                <ImageView
                    android:id="@+id/icon_thumb"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:scaleType="centerCrop"
                    android:contentDescription="@null" />

            </FrameLayout>

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
+18 −4
Original line number Diff line number Diff line
@@ -25,16 +25,30 @@
    android:paddingBottom="8dip"
    android:orientation="horizontal">

    <ImageView
    <FrameLayout
        android:id="@android:id/icon"
        android:layout_width="@dimen/icon_size"
        android:layout_height="@dimen/icon_size"
        android:layout_marginStart="12dp"
        android:layout_marginEnd="20dp"
        android:layout_gravity="center_vertical"
        android:layout_gravity="center_vertical">

        <ImageView
            android:id="@+id/icon_mime"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerInside"
            android:contentDescription="@null" />

        <ImageView
            android:id="@+id/icon_thumb"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            android:contentDescription="@null" />

    </FrameLayout>

    <LinearLayout
        android:layout_width="0dip"
        android:layout_height="wrap_content"
+10 −3
Original line number Diff line number Diff line
@@ -14,6 +14,13 @@
     limitations under the License.
-->

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="12dp">

    <TextView
        android:id="@android:id/title"
        style="?android:attr/listSeparatorTextViewStyle" />

</FrameLayout>
+96 −31
Original line number Diff line number Diff line
@@ -38,9 +38,11 @@ import android.content.Loader;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.text.format.DateUtils;
@@ -56,6 +58,7 @@ 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;
@@ -162,10 +165,12 @@ public class DirectoryFragment extends Fragment {
        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;
    }
@@ -192,13 +197,19 @@ public class DirectoryFragment extends Fragment {
                    case TYPE_NORMAL:
                        contentsUri = DocumentsContract.buildChildDocumentsUri(
                                doc.authority, doc.documentId);
                        if (state.action == ACTION_MANAGE) {
                            contentsUri = DocumentsContract.setManageMode(contentsUri);
                        }
                        return new DirectoryLoader(
                                context, root, doc, contentsUri, state.userSortOrder);
                                context, mType, root, doc, contentsUri, state.userSortOrder);
                    case TYPE_SEARCH:
                        contentsUri = DocumentsContract.buildSearchDocumentsUri(
                                doc.authority, doc.documentId, query);
                        if (state.action == ACTION_MANAGE) {
                            contentsUri = DocumentsContract.setManageMode(contentsUri);
                        }
                        return new DirectoryLoader(
                                context, root, doc, contentsUri, state.userSortOrder);
                                context, mType, root, doc, contentsUri, state.userSortOrder);
                    case TYPE_RECENT_OPEN:
                        final RootsCache roots = DocumentsApplication.getRootsCache(context);
                        final List<RootInfo> matchingRoots = roots.getMatchingRoots(state);
@@ -425,6 +436,20 @@ public class DirectoryFragment extends Fragment {
        }
    };

    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.reallyCancel();
                    iconThumb.setTag(null);
                }
            }
        }
    };

    private void onShareDocuments(List<DocumentInfo> docs) {
        Intent intent;
        if (docs.size() == 1) {
@@ -632,7 +657,9 @@ public class DirectoryFragment extends Fragment {
            final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
            final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);

            final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
            final View icon = convertView.findViewById(android.R.id.icon);
            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 View line2 = convertView.findViewById(R.id.line2);
            final ImageView icon1 = (ImageView) convertView.findViewById(android.R.id.icon1);
@@ -640,30 +667,49 @@ public class DirectoryFragment extends Fragment {
            final TextView date = (TextView) convertView.findViewById(R.id.date);
            final TextView size = (TextView) convertView.findViewById(R.id.size);

            final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) icon.getTag();
            final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
            if (oldTask != null) {
                oldTask.cancel(false);
                oldTask.reallyCancel();
                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(LIST_THUMBNAIL_MIMES, docMimeType);

            boolean cacheHit = false;
            if (supportsThumbnail && allowThumbnail) {
                final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
                final Bitmap cachedResult = thumbs.get(uri);
                if (cachedResult != null) {
                    icon.setImageBitmap(cachedResult);
                    iconThumb.setImageBitmap(cachedResult);
                    cacheHit = true;
                } else {
                    final ThumbnailAsyncTask task = new ThumbnailAsyncTask(icon, mThumbSize);
                    icon.setImageBitmap(null);
                    icon.setTag(task);
                    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, uri);
                    iconThumb.setImageDrawable(null);
                    final ThumbnailAsyncTask task = new ThumbnailAsyncTask(
                            uri, iconMime, iconThumb, mThumbSize);
                    iconThumb.setTag(task);
                    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
                }
            }
            } else if (docIcon != 0) {
                icon.setImageDrawable(IconUtils.loadPackageIcon(context, docAuthority, docIcon));

            // Always throw MIME icon into place, even when a thumbnail is being
            // loaded in background.
            if (cacheHit) {
                iconMime.setAlpha(0f);
                iconThumb.setAlpha(1f);
            } else {
                icon.setImageDrawable(IconUtils.loadMimeIcon(context, docMimeType));
                iconMime.setAlpha(1f);
                iconThumb.setAlpha(0f);
                if (docIcon != 0) {
                    iconMime.setImageDrawable(
                            IconUtils.loadPackageIcon(context, docAuthority, docIcon));
                } else {
                    iconMime.setImageDrawable(IconUtils.loadMimeIcon(context, docMimeType));
                }
            }

            title.setText(docDisplayName);
@@ -672,12 +718,19 @@ public class DirectoryFragment extends Fragment {

            if (mType == TYPE_RECENT_OPEN) {
                final RootInfo root = roots.getRoot(docAuthority, docRootId);
                final Drawable iconDrawable = root.loadIcon(context);
                icon1.setVisibility(View.VISIBLE);
                icon1.setImageDrawable(root.loadIcon(context));
                icon1.setImageDrawable(iconDrawable);

                if (iconDrawable != null && roots.isIconUnique(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 {
                icon1.setVisibility(View.GONE);
                if (docSummary != null) {
@@ -762,32 +815,39 @@ public class DirectoryFragment extends Fragment {
    }

    private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap> {
        private final ImageView mTarget;
        private final Uri mUri;
        private final ImageView mIconMime;
        private final ImageView mIconThumb;
        private final Point mThumbSize;
        private final CancellationSignal mSignal;

        public ThumbnailAsyncTask(ImageView target, Point thumbSize) {
            mTarget = target;
        public ThumbnailAsyncTask(
                Uri uri, ImageView iconMime, ImageView iconThumb, Point thumbSize) {
            mUri = uri;
            mIconMime = iconMime;
            mIconThumb = iconThumb;
            mThumbSize = thumbSize;
            mSignal = new CancellationSignal();
        }

        @Override
        protected void onPreExecute() {
            mTarget.setTag(this);
        public void reallyCancel() {
            cancel(false);
            mSignal.cancel();
        }

        @Override
        protected Bitmap doInBackground(Uri... params) {
            final Context context = mTarget.getContext();
            final Uri uri = params[0];
            final Context context = mIconThumb.getContext();

            Bitmap result = null;
            try {
                // TODO: switch to using unstable provider
                result = DocumentsContract.getDocumentThumbnail(
                        context.getContentResolver(), uri, mThumbSize, null);
                        context.getContentResolver(), mUri, mThumbSize, mSignal);
                if (result != null) {
                    final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
                            context, mThumbSize);
                    thumbs.put(uri, result);
                    thumbs.put(mUri, result);
                }
            } catch (Exception e) {
                Log.w(TAG, "Failed to load thumbnail: " + e);
@@ -797,9 +857,14 @@ public class DirectoryFragment extends Fragment {

        @Override
        protected void onPostExecute(Bitmap result) {
            if (mTarget.getTag() == this) {
                mTarget.setImageBitmap(result);
                mTarget.setTag(null);
            if (mIconThumb.getTag() == this && result != null) {
                mIconThumb.setTag(null);
                mIconThumb.setImageBitmap(result);

                mIconMime.setAlpha(1f);
                mIconMime.animate().alpha(0f).start();
                mIconThumb.setAlpha(0f);
                mIconThumb.animate().alpha(1f).start();
            }
        }
    }
+20 −6
Original line number Diff line number Diff line
@@ -62,6 +62,7 @@ class DirectoryResult implements AutoCloseable {
public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
    private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();

    private final int mType;
    private final RootInfo mRoot;
    private final DocumentInfo mDoc;
    private final Uri mUri;
@@ -70,9 +71,10 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
    private CancellationSignal mSignal;
    private DirectoryResult mResult;

    public DirectoryLoader(
            Context context, RootInfo root, DocumentInfo doc, Uri uri, int userSortOrder) {
    public DirectoryLoader(Context context, int type, RootInfo root, DocumentInfo doc, Uri uri,
            int userSortOrder) {
        super(context);
        mType = type;
        mRoot = root;
        mDoc = doc;
        mUri = uri;
@@ -128,6 +130,11 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
            }
        }

        // Search always uses ranking from provider
        if (mType == DirectoryFragment.TYPE_SEARCH) {
            result.sortOrder = State.SORT_ORDER_UNKNOWN;
        }

        Log.d(TAG, "userMode=" + userMode + ", userSortOrder=" + mUserSortOrder + " --> mode="
                + result.mode + ", sortOrder=" + result.sortOrder);

@@ -137,11 +144,18 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
                    mUri, null, null, null, getQuerySortOrder(result.sortOrder), mSignal);
            cursor.registerContentObserver(mObserver);

            final Cursor withRoot = new RootCursorWrapper(
                    mUri.getAuthority(), mRoot.rootId, cursor, -1);
            final Cursor sorted = new SortingCursorWrapper(withRoot, result.sortOrder);
            cursor = new RootCursorWrapper(mUri.getAuthority(), mRoot.rootId, cursor, -1);

            if (mType == DirectoryFragment.TYPE_SEARCH) {
                // Filter directories out of search results, for now
                cursor = new FilteringCursorWrapper(cursor, null, new String[] {
                        Document.MIME_TYPE_DIR });
            } else {
                // Normal directories should have sorting applied
                cursor = new SortingCursorWrapper(cursor, result.sortOrder);
            }

            result.cursor = sorted;
            result.cursor = cursor;
        } catch (Exception e) {
            Log.d(TAG, "Failed to query", e);
            result.exception = e;
Loading