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

Commit bcc66c6b authored by Jeff Sharkey's avatar Jeff Sharkey Committed by Android (Google) Code Review
Browse files

Merge "New recents behavior to match spec." into klp-dev

parents 22493bab d82b26b5
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -5,7 +5,7 @@ LOCAL_MODULE_TAGS := optional

LOCAL_SRC_FILES := $(call all-subdir-java-files)

LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 guava

LOCAL_PACKAGE_NAME := DocumentsUI
LOCAL_CERTIFICATE := platform
+53 −34
Original line number Diff line number Diff line
@@ -64,6 +64,7 @@ import android.widget.Toast;

import com.android.documentsui.DocumentsActivity.State;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.RootInfo;
import com.android.internal.util.Predicate;
import com.google.android.collect.Lists;

@@ -86,6 +87,7 @@ public class DirectoryFragment extends Fragment {

    public static final int TYPE_NORMAL = 1;
    public static final int TYPE_SEARCH = 2;
    public static final int TYPE_RECENT_OPEN = 3;

    private int mType = TYPE_NORMAL;

@@ -95,7 +97,10 @@ public class DirectoryFragment extends Fragment {
    private LoaderCallbacks<DirectoryResult> mCallbacks;

    private static final String EXTRA_TYPE = "type";
    private static final String EXTRA_URI = "uri";
    private static final String EXTRA_AUTHORITY = "authority";
    private static final String EXTRA_ROOT_ID = "rootId";
    private static final String EXTRA_DOC_ID = "docId";
    private static final String EXTRA_QUERY = "query";

    private static AtomicInteger sLoaderId = new AtomicInteger(4000);

@@ -104,24 +109,26 @@ public class DirectoryFragment extends Fragment {
    private final int mLoaderId = sLoaderId.incrementAndGet();

    public static void showNormal(FragmentManager fm, Uri uri) {
        show(fm, TYPE_NORMAL, uri);
        show(fm, TYPE_NORMAL, uri.getAuthority(), null, DocumentsContract.getDocumentId(uri), null);
    }

    public static void showSearch(FragmentManager fm, Uri uri, String query) {
        final Uri searchUri = DocumentsContract.buildSearchDocumentsUri(
                uri.getAuthority(), DocumentsContract.getDocumentId(uri), query);
        show(fm, TYPE_SEARCH, searchUri);
        show(fm, TYPE_SEARCH, uri.getAuthority(), null, DocumentsContract.getDocumentId(uri),
                query);
    }

    @Deprecated
    public static void showRecentsOpen(FragmentManager fm) {
        // TODO: new recents behavior
        show(fm, TYPE_RECENT_OPEN, null, null, null, null);
    }

    private static void show(FragmentManager fm, int type, Uri uri) {
    private static void show(FragmentManager fm, int type, String authority, String rootId,
            String docId, String query) {
        final Bundle args = new Bundle();
        args.putInt(EXTRA_TYPE, type);
        args.putParcelable(EXTRA_URI, uri);
        args.putString(EXTRA_AUTHORITY, authority);
        args.putString(EXTRA_ROOT_ID, rootId);
        args.putString(EXTRA_DOC_ID, docId);
        args.putString(EXTRA_QUERY, query);

        final DirectoryFragment fragment = new DirectoryFragment();
        fragment.setArguments(args);
@@ -160,9 +167,8 @@ public class DirectoryFragment extends Fragment {
        super.onActivityCreated(savedInstanceState);

        final Context context = getActivity();
        final Uri uri = getArguments().getParcelable(EXTRA_URI);

        mAdapter = new DocumentsAdapter(uri.getAuthority());
        mAdapter = new DocumentsAdapter();
        mType = getArguments().getInt(EXTRA_TYPE);

        mCallbacks = new LoaderCallbacks<DirectoryResult>() {
@@ -170,15 +176,26 @@ public class DirectoryFragment extends Fragment {
            public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
                final State state = getDisplayState(DirectoryFragment.this);

                final String authority = getArguments().getString(EXTRA_AUTHORITY);
                final String rootId = getArguments().getString(EXTRA_ROOT_ID);
                final String docId = getArguments().getString(EXTRA_DOC_ID);
                final String query = getArguments().getString(EXTRA_QUERY);

                Uri contentsUri;
                if (mType == TYPE_NORMAL) {
                    contentsUri = DocumentsContract.buildChildDocumentsUri(
                            uri.getAuthority(), DocumentsContract.getDocumentId(uri));
                } else {
                    contentsUri = uri;
                }
                switch (mType) {
                    case TYPE_NORMAL:
                        contentsUri = DocumentsContract.buildChildDocumentsUri(authority, docId);
                        return new DirectoryLoader(context, rootId, contentsUri, state.sortOrder);
                    case TYPE_SEARCH:
                        contentsUri = DocumentsContract.buildSearchDocumentsUri(
                                authority, docId, query);
                        return new DirectoryLoader(context, rootId, contentsUri, state.sortOrder);
                    case TYPE_RECENT_OPEN:
                        return new RecentLoader(context);
                    default:
                        throw new IllegalStateException("Unknown type " + mType);

                return new DirectoryLoader(context, contentsUri, state.sortOrder);
                }
            }

            @Override
@@ -246,8 +263,7 @@ public class DirectoryFragment extends Fragment {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            final Cursor cursor = mAdapter.getItem(position);
            final Uri uri = getArguments().getParcelable(EXTRA_URI);
            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(uri, cursor);
            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
            if (mFilter.apply(doc)) {
                ((DocumentsActivity) getActivity()).onDocumentPicked(doc);
            }
@@ -285,8 +301,7 @@ public class DirectoryFragment extends Fragment {
            for (int i = 0; i < size; i++) {
                if (checked.valueAt(i)) {
                    final Cursor cursor = mAdapter.getItem(checked.keyAt(i));
                    final Uri uri = getArguments().getParcelable(EXTRA_URI);
                    final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(uri, cursor);
                    final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
                    docs.add(doc);
                }
            }
@@ -401,14 +416,8 @@ public class DirectoryFragment extends Fragment {
    }

    private class DocumentsAdapter extends BaseAdapter {
        private final String mAuthority;

        private Cursor mCursor;

        public DocumentsAdapter(String authority) {
            mAuthority = authority;
        }

        public void swapCursor(Cursor cursor) {
            mCursor = cursor;

@@ -443,6 +452,8 @@ public class DirectoryFragment extends Fragment {

            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);
@@ -466,7 +477,7 @@ public class DirectoryFragment extends Fragment {
            }

            if ((docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0) {
                final Uri uri = DocumentsContract.buildDocumentUri(mAuthority, docId);
                final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
                final Bitmap cachedResult = thumbs.get(uri);
                if (cachedResult != null) {
                    icon.setImageBitmap(cachedResult);
@@ -477,13 +488,20 @@ public class DirectoryFragment extends Fragment {
                    task.execute(uri);
                }
            } else if (docIcon != 0) {
                icon.setImageDrawable(DocumentInfo.loadIcon(context, mAuthority, docIcon));
                icon.setImageDrawable(DocumentInfo.loadIcon(context, docAuthority, docIcon));
            } else {
                icon.setImageDrawable(RootsCache.resolveDocumentIcon(context, docMimeType));
            }

            title.setText(docDisplayName);

            if (mType == TYPE_RECENT_OPEN) {
                final RootInfo root = roots.getRoot(docAuthority, docRootId);
                icon1.setVisibility(View.VISIBLE);
                icon1.setImageDrawable(root.loadIcon(context));
                summary.setText(root.getDirectoryString());
                summary.setVisibility(View.VISIBLE);
            } else {
                icon1.setVisibility(View.GONE);
                if (docSummary != null) {
                    summary.setText(docSummary);
@@ -491,6 +509,7 @@ public class DirectoryFragment extends Fragment {
                } else {
                    summary.setVisibility(View.INVISIBLE);
                }
            }

            if (summaryGrid != null) {
                summaryGrid.setVisibility(
+12 −6
Original line number Diff line number Diff line
@@ -48,14 +48,16 @@ class DirectoryResult implements AutoCloseable {
public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
    private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();

    private final String mRootId;
    private final Uri mUri;
    private final int mSortOrder;

    private CancellationSignal mSignal;
    private DirectoryResult mResult;

    public DirectoryLoader(Context context, Uri uri, int sortOrder) {
    public DirectoryLoader(Context context, String rootId, Uri uri, int sortOrder) {
        super(context);
        mRootId = rootId;
        mUri = uri;
        mSortOrder = sortOrder;
    }
@@ -69,12 +71,16 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
            mSignal = new CancellationSignal();
        }
        final DirectoryResult result = new DirectoryResult();
        final String authority = mUri.getAuthority();
        try {
            result.client = getContext()
                    .getContentResolver().acquireUnstableContentProviderClient(mUri.getAuthority());
                    .getContentResolver().acquireUnstableContentProviderClient(authority);
            final Cursor cursor = result.client.query(
                    mUri, null, null, null, getQuerySortOrder(), mSignal);
            result.cursor = new SortingCursorWrapper(cursor, mSortOrder);
                    mUri, null, null, null, getQuerySortOrder(mSortOrder), mSignal);
            final Cursor withRoot = new RootCursorWrapper(mUri.getAuthority(), mRootId, cursor, -1);
            final Cursor sorted = new SortingCursorWrapper(withRoot, mSortOrder);

            result.cursor = sorted;
            result.cursor.registerContentObserver(mObserver);
        } catch (Exception e) {
            result.exception = e;
@@ -149,8 +155,8 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
        getContext().getContentResolver().unregisterContentObserver(mObserver);
    }

    private String getQuerySortOrder() {
        switch (mSortOrder) {
    public static String getQuerySortOrder(int sortOrder) {
        switch (sortOrder) {
            case SORT_ORDER_DISPLAY_NAME:
                return Document.COLUMN_DISPLAY_NAME + " ASC";
            case SORT_ORDER_LAST_MODIFIED:
+253 −0
Original line number Diff line number Diff line
/*
 * 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.
 */

package com.android.documentsui;

import static com.android.documentsui.DocumentsActivity.TAG;

import android.content.AsyncTaskLoader;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.database.MergeCursor;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Root;
import android.util.Log;

import com.android.documentsui.DocumentsActivity.State;
import com.android.documentsui.model.RootInfo;
import com.google.android.collect.Maps;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.AbstractFuture;

import libcore.io.IoUtils;

import java.io.Closeable;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {

    public static final int MAX_OUTSTANDING_RECENTS = 2;

    /**
     * Time to wait for first pass to complete before returning partial results.
     */
    public static final int MAX_FIRST_PASS_WAIT_MILLIS = 500;

    /**
     * Maximum documents from a single root.
     */
    public static final int MAX_DOCS_FROM_ROOT = 24;

    private static final ExecutorService sExecutor = buildExecutor();

    /**
     * Create a bounded thread pool for fetching recents; it creates threads as
     * needed (up to maximum) and reclaims them when finished.
     */
    private static ExecutorService buildExecutor() {
        // Create a bounded thread pool for fetching recents; it creates
        // threads as needed (up to maximum) and reclaims them when finished.
        final ThreadPoolExecutor executor = new ThreadPoolExecutor(
                MAX_OUTSTANDING_RECENTS, MAX_OUTSTANDING_RECENTS, 10, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>());
        executor.allowCoreThreadTimeOut(true);
        return executor;
    }

    private final HashMap<RootInfo, RecentTask> mTasks = Maps.newHashMap();

    private final int mSortOrder = State.SORT_ORDER_LAST_MODIFIED;

    private CountDownLatch mFirstPassLatch;
    private volatile boolean mFirstPassDone;

    private DirectoryResult mResult;

    // TODO: create better transfer of ownership around cursor to ensure its
    // closed in all edge cases.

    public class RecentTask extends AbstractFuture<Cursor> implements Runnable, Closeable {
        public final String authority;
        public final String rootId;

        private Cursor mWithRoot;

        public RecentTask(String authority, String rootId) {
            this.authority = authority;
            this.rootId = rootId;
        }

        @Override
        public void run() {
            if (isCancelled()) return;

            final ContentResolver resolver = getContext().getContentResolver();
            final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
                    authority);
            try {
                final Uri uri = DocumentsContract.buildRecentDocumentsUri(authority, rootId);
                final Cursor cursor = client.query(
                        uri, null, null, null, DirectoryLoader.getQuerySortOrder(mSortOrder));
                mWithRoot = new RootCursorWrapper(authority, rootId, cursor, MAX_DOCS_FROM_ROOT);
                set(mWithRoot);

                mFirstPassLatch.countDown();
                if (mFirstPassDone) {
                    onContentChanged();
                }

            } catch (Exception e) {
                setException(e);
            } finally {
                ContentProviderClient.closeQuietly(client);
            }
        }

        @Override
        public void close() throws IOException {
            IoUtils.closeQuietly(mWithRoot);
        }
    }

    public RecentLoader(Context context) {
        super(context);
    }

    @Override
    public DirectoryResult loadInBackground() {
        if (mFirstPassLatch == null) {
            // First time through we kick off all the recent tasks, and wait
            // around to see if everyone finishes quickly.

            final RootsCache roots = DocumentsApplication.getRootsCache(getContext());
            for (RootInfo root : roots.getRoots()) {
                if ((root.flags & Root.FLAG_SUPPORTS_RECENTS) != 0) {
                    final RecentTask task = new RecentTask(root.authority, root.rootId);
                    mTasks.put(root, task);
                }
            }

            mFirstPassLatch = new CountDownLatch(mTasks.size());
            for (RecentTask task : mTasks.values()) {
                sExecutor.execute(task);
            }

            try {
                mFirstPassLatch.await(MAX_FIRST_PASS_WAIT_MILLIS, TimeUnit.MILLISECONDS);
                mFirstPassDone = true;
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        // Collect all finished tasks
        List<Cursor> cursors = Lists.newArrayList();
        for (RecentTask task : mTasks.values()) {
            if (task.isDone()) {
                try {
                    cursors.add(task.get());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } catch (ExecutionException e) {
                    Log.w(TAG, "Failed to load " + task.authority + ", " + task.rootId, e);
                }
            }
        }

        final DirectoryResult result = new DirectoryResult();
        if (cursors.size() > 0) {
            final MergeCursor merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
            final SortingCursorWrapper sorted = new SortingCursorWrapper(
                    merged, State.SORT_ORDER_LAST_MODIFIED) {
                @Override
                public void close() {
                    // Ignored, since we manage cursor lifecycle internally
                }
            };
            result.cursor = sorted;
        }
        return result;
    }

    @Override
    public void cancelLoadInBackground() {
        super.cancelLoadInBackground();
    }

    @Override
    public void deliverResult(DirectoryResult result) {
        if (isReset()) {
            IoUtils.closeQuietly(result);
            return;
        }
        DirectoryResult oldResult = mResult;
        mResult = result;

        if (isStarted()) {
            super.deliverResult(result);
        }

        if (oldResult != null && oldResult != result) {
            IoUtils.closeQuietly(oldResult);
        }
    }

    @Override
    protected void onStartLoading() {
        if (mResult != null) {
            deliverResult(mResult);
        }
        if (takeContentChanged() || mResult == null) {
            forceLoad();
        }
    }

    @Override
    protected void onStopLoading() {
        cancelLoad();
    }

    @Override
    public void onCanceled(DirectoryResult result) {
        IoUtils.closeQuietly(result);
    }

    @Override
    protected void onReset() {
        super.onReset();

        // Ensure the loader is stopped
        onStopLoading();

        for (RecentTask task : mTasks.values()) {
            IoUtils.closeQuietly(task);
        }

        IoUtils.closeQuietly(mResult);
        mResult = null;
    }
}
+132 −0
Original line number Diff line number Diff line
/*
 * 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.
 */

package com.android.documentsui;

import android.database.AbstractCursor;
import android.database.Cursor;

/**
 * Cursor wrapper that adds columns to identify which root a document came from.
 */
public class RootCursorWrapper extends AbstractCursor {
    private final String mAuthority;
    private final String mRootId;

    private final Cursor mCursor;
    private final int mCount;

    private final String[] mColumnNames;

    private final int mAuthorityIndex;
    private final int mRootIdIndex;

    public static final String COLUMN_AUTHORITY = "android:authority";
    public static final String COLUMN_ROOT_ID = "android:rootId";

    public RootCursorWrapper(String authority, String rootId, Cursor cursor, int maxCount) {
        mAuthority = authority;
        mRootId = rootId;
        mCursor = cursor;

        final int count = cursor.getCount();
        if (maxCount > 0 && count > maxCount) {
            mCount = maxCount;
        } else {
            mCount = count;
        }

        if (cursor.getColumnIndex(COLUMN_AUTHORITY) != -1
                || cursor.getColumnIndex(COLUMN_ROOT_ID) != -1) {
            throw new IllegalArgumentException("Cursor contains internal columns!");
        }
        final String[] before = cursor.getColumnNames();
        mColumnNames = new String[before.length + 2];
        System.arraycopy(before, 0, mColumnNames, 0, before.length);
        mAuthorityIndex = before.length;
        mRootIdIndex = before.length + 1;
        mColumnNames[mAuthorityIndex] = COLUMN_AUTHORITY;
        mColumnNames[mRootIdIndex] = COLUMN_ROOT_ID;
    }

    @Override
    public void close() {
        super.close();
        mCursor.close();
    }

    @Override
    public boolean onMove(int oldPosition, int newPosition) {
        return mCursor.moveToPosition(newPosition);
    }

    @Override
    public String[] getColumnNames() {
        return mColumnNames;
    }

    @Override
    public int getCount() {
        return mCount;
    }

    @Override
    public double getDouble(int column) {
        return mCursor.getDouble(column);
    }

    @Override
    public float getFloat(int column) {
        return mCursor.getFloat(column);
    }

    @Override
    public int getInt(int column) {
        return mCursor.getInt(column);
    }

    @Override
    public long getLong(int column) {
        return mCursor.getLong(column);
    }

    @Override
    public short getShort(int column) {
        return mCursor.getShort(column);
    }

    @Override
    public String getString(int column) {
        if (column == mAuthorityIndex) {
            return mAuthority;
        } else if (column == mRootIdIndex) {
            return mRootId;
        } else {
            return mCursor.getString(column);
        }
    }

    @Override
    public int getType(int column) {
        return mCursor.getType(column);
    }

    @Override
    public boolean isNull(int column) {
        return mCursor.isNull(column);
    }

}
Loading