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>
<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>
--- /dev/null
+<?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>
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;
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;
private View mEmptyView;
private ListView mListView;
private GridView mGridView;
+ private Button mMoreView;
private AbsListView mCurrentView;
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";
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);
}
@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);
}
};
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);
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);
}
@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() {
}
};
- 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());
}
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.database.ContentObserver;
-import android.net.Uri;
import android.os.CancellationSignal;
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();
mCancellationSignal = new CancellationSignal();
}
try {
- return loadInBackground(mUri, mCancellationSignal);
+ return loadInBackground(mParam, mCancellationSignal);
} finally {
synchronized (this) {
mCancellationSignal = null;
}
}
- public abstract T loadInBackground(Uri uri, CancellationSignal signal);
+ public abstract R loadInBackground(P param, CancellationSignal signal);
@Override
public void cancelLoadInBackground() {
}
@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()) {
}
}
- 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);
}
}
@Override
- public void onCanceled(T result) {
+ public void onCanceled(R result) {
closeQuietly(result);
}
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();
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>
--- /dev/null
+/*
+ * 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);
+ }
+}