diff options
9 files changed, 388 insertions, 80 deletions
diff --git a/packages/DocumentsUI/res/layout/fragment_directory.xml b/packages/DocumentsUI/res/layout/fragment_directory.xml index 8dbd1de..67c5954 100644 --- a/packages/DocumentsUI/res/layout/fragment_directory.xml +++ b/packages/DocumentsUI/res/layout/fragment_directory.xml @@ -42,4 +42,12 @@ android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:visibility="gone" /> + <Button + android:id="@+id/more" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:text="@string/more" + android:visibility="gone" /> + </FrameLayout> diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml index 2b83183..928ba85 100644 --- a/packages/DocumentsUI/res/values/strings.xml +++ b/packages/DocumentsUI/res/values/strings.xml @@ -60,4 +60,7 @@ <string name="toast_no_application">Can\'t open file</string> <string name="toast_failed_delete">Unable to delete some documents</string> + <string name="more">More</string> + <string name="loading">Loading\u2026</string> + </resources> diff --git a/packages/DocumentsUI/res/xml/document_provider.xml b/packages/DocumentsUI/res/xml/document_provider.xml new file mode 100644 index 0000000..77891cb --- /dev/null +++ b/packages/DocumentsUI/res/xml/document_provider.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<documents-provider xmlns:android="http://schemas.android.com/apk/res/android" + android:customRoots="true"> +</documents-provider> diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java index d3421e7..dd9aee5 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java @@ -32,6 +32,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.Loader; +import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Point; import android.net.Uri; @@ -54,6 +55,7 @@ import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.BaseAdapter; +import android.widget.Button; import android.widget.GridView; import android.widget.ImageView; import android.widget.ListView; @@ -79,6 +81,7 @@ public class DirectoryFragment extends Fragment { private View mEmptyView; private ListView mListView; private GridView mGridView; + private Button mMoreView; private AbsListView mCurrentView; @@ -93,7 +96,7 @@ public class DirectoryFragment extends Fragment { private Point mThumbSize; private DocumentsAdapter mAdapter; - private LoaderCallbacks<List<Document>> mCallbacks; + private LoaderCallbacks<DirectoryResult> mCallbacks; private static final String EXTRA_TYPE = "type"; private static final String EXTRA_URI = "uri"; @@ -150,14 +153,16 @@ public class DirectoryFragment extends Fragment { mGridView.setOnItemClickListener(mItemListener); mGridView.setMultiChoiceModeListener(mMultiListener); + mMoreView = (Button) view.findViewById(R.id.more); + mAdapter = new DocumentsAdapter(); final Uri uri = getArguments().getParcelable(EXTRA_URI); mType = getArguments().getInt(EXTRA_TYPE); - mCallbacks = new LoaderCallbacks<List<Document>>() { + mCallbacks = new LoaderCallbacks<DirectoryResult>() { @Override - public Loader<List<Document>> onCreateLoader(int id, Bundle args) { + public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { final DisplayState state = getDisplayState(DirectoryFragment.this); mFilter = new MimePredicate(state.acceptMimes); @@ -189,12 +194,34 @@ public class DirectoryFragment extends Fragment { } @Override - public void onLoadFinished(Loader<List<Document>> loader, List<Document> data) { - mAdapter.swapDocuments(data); + public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { + mAdapter.swapDocuments(result.contents); + + final Cursor cursor = result.cursor; + if (cursor != null && cursor.getExtras() + .getBoolean(DocumentsContract.EXTRA_HAS_MORE, false)) { + mMoreView.setText(R.string.more); + mMoreView.setVisibility(View.VISIBLE); + mMoreView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mMoreView.setText(R.string.loading); + final Bundle bundle = new Bundle(); + bundle.putBoolean(DocumentsContract.EXTRA_REQUEST_MORE, true); + try { + cursor.respond(bundle); + } catch (Exception e) { + Log.w(TAG, "Failed to respond: " + e); + } + } + }); + } else { + mMoreView.setVisibility(View.GONE); + } } @Override - public void onLoaderReset(Loader<List<Document>> loader) { + public void onLoaderReset(Loader<DirectoryResult> loader) { mAdapter.swapDocuments(null); } }; @@ -407,7 +434,7 @@ public class DirectoryFragment extends Fragment { public void swapDocuments(List<Document> documents) { mDocuments = documents; - if (documents != null && documents.isEmpty()) { + if (mDocuments != null && mDocuments.isEmpty()) { mEmptyView.setVisibility(View.VISIBLE); } else { mEmptyView.setVisibility(View.GONE); diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java index c99d6af..14d6fd5 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java @@ -36,29 +36,27 @@ import com.google.android.collect.Lists; import libcore.io.IoUtils; import java.io.FileNotFoundException; -import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.LinkedList; import java.util.List; -public class DirectoryLoader extends UriDerivativeLoader<List<Document>> { +class DirectoryResult implements AutoCloseable { + Cursor cursor; + List<Document> contents = Lists.newArrayList(); + Exception e; + + @Override + public void close() throws Exception { + IoUtils.closeQuietly(cursor); + } +} + +public class DirectoryLoader extends UriDerivativeLoader<Uri, DirectoryResult> { private final int mType; private Predicate<Document> mFilter; private Comparator<Document> mSortOrder; - /** - * Stub result that represents an internal error. - */ - public static class ExceptionResult extends LinkedList<Document> { - public final Exception e; - - public ExceptionResult(Exception e) { - this.e = e; - } - } - public DirectoryLoader(Context context, Uri uri, int type, Predicate<Document> filter, Comparator<Document> sortOrder) { super(context, uri); @@ -68,53 +66,49 @@ public class DirectoryLoader extends UriDerivativeLoader<List<Document>> { } @Override - public List<Document> loadInBackground(Uri uri, CancellationSignal signal) { + public DirectoryResult loadInBackground(Uri uri, CancellationSignal signal) { + final DirectoryResult result = new DirectoryResult(); try { - return loadInBackgroundInternal(uri, signal); + loadInBackgroundInternal(result, uri, signal); } catch (Exception e) { - return new ExceptionResult(e); + result.e = e; } + return result; } - private List<Document> loadInBackgroundInternal(Uri uri, CancellationSignal signal) { - final ArrayList<Document> result = Lists.newArrayList(); - - // TODO: subscribe to the notify uri from query - + private void loadInBackgroundInternal( + DirectoryResult result, Uri uri, CancellationSignal signal) { final ContentResolver resolver = getContext().getContentResolver(); final Cursor cursor = resolver.query(uri, null, null, null, getQuerySortOrder(), signal); - try { - while (cursor != null && cursor.moveToNext()) { - Document doc = null; - switch (mType) { - case TYPE_NORMAL: - case TYPE_SEARCH: - doc = Document.fromDirectoryCursor(uri, cursor); - break; - case TYPE_RECENT_OPEN: - try { - doc = Document.fromRecentOpenCursor(resolver, cursor); - } catch (FileNotFoundException e) { - Log.w(TAG, "Failed to find recent: " + e); - } - break; - default: - throw new IllegalArgumentException("Unknown type"); - } - - if (doc != null && (mFilter == null || mFilter.apply(doc))) { - result.add(doc); - } + result.cursor = cursor; + result.cursor.registerContentObserver(mObserver); + + while (cursor.moveToNext()) { + Document doc = null; + switch (mType) { + case TYPE_NORMAL: + case TYPE_SEARCH: + doc = Document.fromDirectoryCursor(uri, cursor); + break; + case TYPE_RECENT_OPEN: + try { + doc = Document.fromRecentOpenCursor(resolver, cursor); + } catch (FileNotFoundException e) { + Log.w(TAG, "Failed to find recent: " + e); + } + break; + default: + throw new IllegalArgumentException("Unknown type"); + } + + if (doc != null && (mFilter == null || mFilter.apply(doc))) { + result.contents.add(doc); } - } finally { - IoUtils.closeQuietly(cursor); } if (mSortOrder != null) { - Collections.sort(result, mSortOrder); + Collections.sort(result.contents, mSortOrder); } - - return result; } private String getQuerySortOrder() { diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java index cd8adac..5466dbf 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java @@ -124,7 +124,7 @@ public class RecentsCreateFragment extends Fragment { } }; - public static class RecentsCreateLoader extends UriDerivativeLoader<List<DocumentStack>> { + public static class RecentsCreateLoader extends UriDerivativeLoader<Uri, List<DocumentStack>> { public RecentsCreateLoader(Context context) { super(context, RecentsProvider.buildRecentCreate()); } diff --git a/packages/DocumentsUI/src/com/android/documentsui/UriDerivativeLoader.java b/packages/DocumentsUI/src/com/android/documentsui/UriDerivativeLoader.java index 1b88af4..1a5bb0c 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/UriDerivativeLoader.java +++ b/packages/DocumentsUI/src/com/android/documentsui/UriDerivativeLoader.java @@ -19,7 +19,6 @@ package com.android.documentsui; import android.content.AsyncTaskLoader; import android.content.Context; import android.database.ContentObserver; -import android.net.Uri; import android.os.CancellationSignal; import android.os.OperationCanceledException; @@ -28,17 +27,16 @@ import android.os.OperationCanceledException; * changes while started, manages {@link CancellationSignal}, and caches * returned results. */ -public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> { - private final ForceLoadContentObserver mObserver; - private boolean mObserving; +public abstract class UriDerivativeLoader<P, R> extends AsyncTaskLoader<R> { + final ForceLoadContentObserver mObserver; - private final Uri mUri; + private final P mParam; - private T mResult; + private R mResult; private CancellationSignal mCancellationSignal; @Override - public final T loadInBackground() { + public final R loadInBackground() { synchronized (this) { if (isLoadInBackgroundCanceled()) { throw new OperationCanceledException(); @@ -46,7 +44,7 @@ public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> { mCancellationSignal = new CancellationSignal(); } try { - return loadInBackground(mUri, mCancellationSignal); + return loadInBackground(mParam, mCancellationSignal); } finally { synchronized (this) { mCancellationSignal = null; @@ -54,7 +52,7 @@ public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> { } } - public abstract T loadInBackground(Uri uri, CancellationSignal signal); + public abstract R loadInBackground(P param, CancellationSignal signal); @Override public void cancelLoadInBackground() { @@ -68,12 +66,12 @@ public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> { } @Override - public void deliverResult(T result) { + public void deliverResult(R result) { if (isReset()) { closeQuietly(result); return; } - T oldResult = mResult; + R oldResult = mResult; mResult = result; if (isStarted()) { @@ -85,18 +83,14 @@ public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> { } } - public UriDerivativeLoader(Context context, Uri uri) { + public UriDerivativeLoader(Context context, P param) { super(context); mObserver = new ForceLoadContentObserver(); - mUri = uri; + mParam = param; } @Override protected void onStartLoading() { - if (!mObserving) { - getContext().getContentResolver().registerContentObserver(mUri, false, mObserver); - mObserving = true; - } if (mResult != null) { deliverResult(mResult); } @@ -111,7 +105,7 @@ public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> { } @Override - public void onCanceled(T result) { + public void onCanceled(R result) { closeQuietly(result); } @@ -125,13 +119,10 @@ public abstract class UriDerivativeLoader<T> extends AsyncTaskLoader<T> { closeQuietly(mResult); mResult = null; - if (mObserving) { - getContext().getContentResolver().unregisterContentObserver(mObserver); - mObserving = false; - } + getContext().getContentResolver().unregisterContentObserver(mObserver); } - private void closeQuietly(T result) { + private void closeQuietly(R result) { if (result instanceof AutoCloseable) { try { ((AutoCloseable) result).close(); diff --git a/packages/ExternalStorageProvider/AndroidManifest.xml b/packages/ExternalStorageProvider/AndroidManifest.xml index 5272166..8bd2a6d 100644 --- a/packages/ExternalStorageProvider/AndroidManifest.xml +++ b/packages/ExternalStorageProvider/AndroidManifest.xml @@ -15,5 +15,18 @@ android:name="android.content.DOCUMENT_PROVIDER" android:resource="@xml/document_provider" /> </provider> + + <!-- TODO: remove when we have real providers --> + <provider + android:name=".CloudTestDocumentsProvider" + android:authorities="com.android.externalstorage.cloudtest" + android:grantUriPermissions="true" + android:exported="true" + android:enabled="false" + android:permission="android.permission.MANAGE_DOCUMENTS"> + <meta-data + android:name="android.content.DOCUMENT_PROVIDER" + android:resource="@xml/document_provider" /> + </provider> </application> </manifest> diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/CloudTestDocumentsProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/CloudTestDocumentsProvider.java new file mode 100644 index 0000000..119d92e --- /dev/null +++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/CloudTestDocumentsProvider.java @@ -0,0 +1,253 @@ +/* + * 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.externalstorage; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.database.MatrixCursor.RowBuilder; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.os.SystemClock; +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.DocumentColumns; +import android.provider.DocumentsContract.Documents; +import android.provider.DocumentsContract.RootColumns; +import android.provider.DocumentsContract.Roots; +import android.util.Log; + +import com.google.android.collect.Lists; + +import libcore.io.IoUtils; + +import java.io.FileNotFoundException; +import java.util.List; + +public class CloudTestDocumentsProvider extends ContentProvider { + private static final String TAG = "CloudTest"; + + private static final String AUTHORITY = "com.android.externalstorage.cloudtest"; + + private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH); + + private static final int URI_ROOTS = 1; + private static final int URI_ROOTS_ID = 2; + private static final int URI_DOCS_ID = 3; + private static final int URI_DOCS_ID_CONTENTS = 4; + private static final int URI_DOCS_ID_SEARCH = 5; + + static { + sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS); + sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID); + sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID); + sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS); + sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH); + } + + private static final String[] ALL_ROOTS_COLUMNS = new String[] { + RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON, RootColumns.TITLE, + RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES + }; + + private static final String[] ALL_DOCUMENTS_COLUMNS = new String[] { + DocumentColumns.DOC_ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE, + DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, DocumentColumns.FLAGS + }; + + private List<String> mKnownDocs = Lists.newArrayList("meow.png", "kittens.pdf"); + + private int mPage; + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + switch (sMatcher.match(uri)) { + case URI_ROOTS: { + final MatrixCursor result = new MatrixCursor( + projection != null ? projection : ALL_ROOTS_COLUMNS); + includeDefaultRoot(result); + return result; + } + case URI_ROOTS_ID: { + final MatrixCursor result = new MatrixCursor( + projection != null ? projection : ALL_ROOTS_COLUMNS); + includeDefaultRoot(result); + return result; + } + case URI_DOCS_ID: { + final String docId = DocumentsContract.getDocId(uri); + final MatrixCursor result = new MatrixCursor( + projection != null ? projection : ALL_DOCUMENTS_COLUMNS); + includeDoc(result, docId); + return result; + } + case URI_DOCS_ID_CONTENTS: { + final CloudCursor result = new CloudCursor( + projection != null ? projection : ALL_DOCUMENTS_COLUMNS, uri); + for (String docId : mKnownDocs) { + includeDoc(result, docId); + } + if (mPage < 3) { + result.setHasMore(); + } + result.setNotificationUri(getContext().getContentResolver(), uri); + return result; + } + default: { + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + } + } + + private void includeDefaultRoot(MatrixCursor result) { + final RowBuilder row = result.newRow(); + row.offer(RootColumns.ROOT_ID, "testroot"); + row.offer(RootColumns.ROOT_TYPE, Roots.ROOT_TYPE_SERVICE); + row.offer(RootColumns.TITLE, "_TestTitle"); + row.offer(RootColumns.SUMMARY, "_TestSummary"); + } + + private void includeDoc(MatrixCursor result, String docId) { + int flags = 0; + + final String mimeType; + if (Documents.DOC_ID_ROOT.equals(docId)) { + mimeType = Documents.MIME_TYPE_DIR; + } else { + mimeType = "application/octet-stream"; + } + + final RowBuilder row = result.newRow(); + row.offer(DocumentColumns.DOC_ID, docId); + row.offer(DocumentColumns.DISPLAY_NAME, docId); + row.offer(DocumentColumns.MIME_TYPE, mimeType); + row.offer(DocumentColumns.LAST_MODIFIED, System.currentTimeMillis()); + row.offer(DocumentColumns.FLAGS, flags); + } + + private class CloudCursor extends MatrixCursor { + private final Uri mUri; + private Bundle mExtras = new Bundle(); + + public CloudCursor(String[] columnNames, Uri uri) { + super(columnNames); + mUri = uri; + } + + public void setHasMore() { + mExtras.putBoolean(DocumentsContract.EXTRA_HAS_MORE, true); + } + + @Override + public Bundle getExtras() { + Log.d(TAG, "getExtras() " + mExtras); + return mExtras; + } + + @Override + public Bundle respond(Bundle extras) { + extras.size(); + Log.d(TAG, "respond() " + extras); + if (extras.getBoolean(DocumentsContract.EXTRA_REQUEST_MORE, false)) { + new CloudTask().execute(mUri); + } + return Bundle.EMPTY; + } + } + + private class CloudTask extends AsyncTask<Uri, Void, Void> { + @Override + protected Void doInBackground(Uri... uris) { + final Uri uri = uris[0]; + + SystemClock.sleep(1000); + + // Grab some files from the cloud + for (int i = 0; i < 5; i++) { + mKnownDocs.add("cloud-page" + mPage + "-file" + i); + } + mPage++; + + Log.d(TAG, "Loaded more; notifying " + uri); + getContext().getContentResolver().notifyChange(uri, null, false); + return null; + } + } + + private interface TypeQuery { + final String[] PROJECTION = { + DocumentColumns.MIME_TYPE }; + + final int MIME_TYPE = 0; + } + + @Override + public String getType(Uri uri) { + switch (sMatcher.match(uri)) { + case URI_ROOTS: { + return Roots.MIME_TYPE_DIR; + } + case URI_ROOTS_ID: { + return Roots.MIME_TYPE_ITEM; + } + case URI_DOCS_ID: { + final Cursor cursor = query(uri, TypeQuery.PROJECTION, null, null, null); + try { + if (cursor.moveToFirst()) { + return cursor.getString(TypeQuery.MIME_TYPE); + } else { + return null; + } + } finally { + IoUtils.closeQuietly(cursor); + } + } + default: { + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + } + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } +} |