summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2013-08-02 10:33:21 -0700
committerJeff Sharkey <jsharkey@android.com>2013-08-02 11:05:11 -0700
commit92d7e697a864a3e18bef4ef256bb3eb339a66b4e (patch)
treee5cce35e071787bfb81a8341716071925f1c86c4
parent7e258b31e70464bb6d80b8b42f0cef8e4417bd6a (diff)
downloadframeworks_base-92d7e697a864a3e18bef4ef256bb3eb339a66b4e.zip
frameworks_base-92d7e697a864a3e18bef4ef256bb3eb339a66b4e.tar.gz
frameworks_base-92d7e697a864a3e18bef4ef256bb3eb339a66b4e.tar.bz2
Reference docs by ROOT_ID and DOC_ID; recents.
The same document may be present with different sematics under multiple storage roots, so always reference using both ROOT_ID and DOC_ID. This enables backends to revoke permissions for an entire root, such as when an account is removed. Start building provider to remember recently accessed documents. Change-Id: I75befa2e61393dec12fcc7fd27f631fcddae46fa
-rw-r--r--api/current.txt10
-rw-r--r--core/java/android/provider/DocumentsContract.java52
-rw-r--r--packages/DocumentsUI/AndroidManifest.xml5
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java42
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java80
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java177
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/TestActivity.java24
-rw-r--r--packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java172
8 files changed, 399 insertions, 163 deletions
diff --git a/api/current.txt b/api/current.txt
index 944b1c2..6bf8fe0 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -20267,7 +20267,9 @@ package android.provider {
public final class DocumentsContract {
ctor public DocumentsContract();
method public static android.net.Uri buildContentsUri(android.net.Uri);
- method public static android.net.Uri buildDocumentUri(java.lang.String, java.lang.String);
+ method public static android.net.Uri buildDocumentUri(java.lang.String, java.lang.String, java.lang.String);
+ method public static android.net.Uri buildDocumentUri(android.net.Uri, java.lang.String);
+ method public static android.net.Uri buildRootUri(java.lang.String, java.lang.String);
method public static android.net.Uri buildRootsUri(java.lang.String);
method public static android.net.Uri buildSearchUri(android.net.Uri, java.lang.String);
method public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, android.net.Uri, android.graphics.Point);
@@ -20282,7 +20284,7 @@ package android.provider {
field public static final int FLAG_SUPPORTS_THUMBNAIL = 8; // 0x8
field public static final java.lang.String MIME_TYPE_DIRECTORY = "vnd.android.cursor.dir/doc";
field public static final java.lang.String PARAM_QUERY = "query";
- field public static final java.lang.String ROOT_GUID = "0";
+ field public static final java.lang.String ROOT_DOC_ID = "0";
field public static final int ROOT_TYPE_DEVICE = 3; // 0x3
field public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; // 0x4
field public static final int ROOT_TYPE_SERVICE = 1; // 0x1
@@ -20290,16 +20292,16 @@ package android.provider {
}
public static abstract interface DocumentsContract.DocumentColumns implements android.provider.OpenableColumns {
+ field public static final java.lang.String DOC_ID = "doc_id";
field public static final java.lang.String FLAGS = "flags";
- field public static final java.lang.String GUID = "guid";
field public static final java.lang.String LAST_MODIFIED = "last_modified";
field public static final java.lang.String MIME_TYPE = "mime_type";
}
public static abstract interface DocumentsContract.RootColumns {
field public static final java.lang.String AVAILABLE_BYTES = "available_bytes";
- field public static final java.lang.String GUID = "guid";
field public static final java.lang.String ICON = "icon";
+ field public static final java.lang.String ROOT_ID = "root_id";
field public static final java.lang.String ROOT_TYPE = "root_type";
field public static final java.lang.String SUMMARY = "summary";
field public static final java.lang.String TITLE = "title";
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 979a5a3..30c9a0d 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -43,9 +43,10 @@ public final class DocumentsContract {
private static final String TAG = "Documents";
// content://com.example/roots/
- // content://com.example/docs/0/
- // content://com.example/docs/0/contents/
- // content://com.example/docs/0/search/?query=pony
+ // content://com.example/roots/sdcard/
+ // content://com.example/roots/sdcard/docs/0/
+ // content://com.example/roots/sdcard/docs/0/contents/
+ // content://com.example/roots/sdcard/docs/0/search/?query=pony
/**
* MIME type of a document which is a directory that may contain additional
@@ -59,10 +60,10 @@ public final class DocumentsContract {
public static final String META_DATA_DOCUMENT_PROVIDER = "android.content.DOCUMENT_PROVIDER";
/**
- * {@link DocumentColumns#GUID} value representing the root directory of a
- * storage backend.
+ * {@link DocumentColumns#DOC_ID} value representing the root directory of a
+ * storage root.
*/
- public static final String ROOT_GUID = "0";
+ public static final String ROOT_DOC_ID = "0";
/**
* Flag indicating that a document is a directory that supports creation of
@@ -139,20 +140,28 @@ public final class DocumentsContract {
public static final String PARAM_QUERY = "query";
/**
- * Build URI representing the custom roots in a storage backend.
+ * Build URI representing the roots in a storage backend.
*/
public static Uri buildRootsUri(String authority) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority).appendPath(PATH_ROOTS).build();
}
+ public static Uri buildRootUri(String authority, String rootId) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority).appendPath(PATH_ROOTS).appendPath(rootId).build();
+ }
+
+ public static Uri buildDocumentUri(String authority, String rootId, String docId) {
+ return buildDocumentUri(buildRootUri(authority, rootId), docId);
+ }
+
/**
- * Build URI representing the given {@link DocumentColumns#GUID} in a
- * storage backend.
+ * Build URI representing the given {@link DocumentColumns#DOC_ID} in a
+ * storage root.
*/
- public static Uri buildDocumentUri(String authority, String guid) {
- return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
- .authority(authority).appendPath(PATH_DOCS).appendPath(guid).build();
+ public static Uri buildDocumentUri(Uri rootUri, String docId) {
+ return rootUri.buildUpon().appendPath(PATH_DOCS).appendPath(docId).build();
}
/**
@@ -184,15 +193,13 @@ public final class DocumentsContract {
*/
public interface DocumentColumns extends OpenableColumns {
/**
- * The globally unique ID for a document within a storage backend.
- * Values <em>must</em> never change once returned. This field is
- * read-only to document clients.
+ * The ID for a document under a storage backend root. Values
+ * <em>must</em> never change once returned. This field is read-only to
+ * document clients.
* <p>
* Type: STRING
- *
- * @see DocumentsContract#ROOT_GUID
*/
- public static final String GUID = "guid";
+ public static final String DOC_ID = "doc_id";
/**
* MIME type of a document, matching the value returned by
@@ -237,6 +244,8 @@ public final class DocumentsContract {
* @see DocumentsContract#buildRootsUri(String)
*/
public interface RootColumns {
+ public static final String ROOT_ID = "root_id";
+
/**
* Storage root type, use for clustering.
* <p>
@@ -248,13 +257,6 @@ public final class DocumentsContract {
public static final String ROOT_TYPE = "root_type";
/**
- * GUID of directory entry for this storage root.
- * <p>
- * Type: STRING
- */
- public static final String GUID = "guid";
-
- /**
* Icon resource ID for this storage root, or {@code 0} to use the
* default {@link ProviderInfo#icon}.
* <p>
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index 453ef45..b88099e 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -21,6 +21,11 @@
</intent-filter>
</activity>
+ <provider
+ android:name=".RecentsProvider"
+ android:authorities="com.android.documentsui.recents"
+ android:exported="false" />
+
<!-- TODO: remove when we have real clients -->
<activity android:name=".TestActivity" android:enabled="false">
<intent-filter>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index 1f22613..ef97dd5 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -59,6 +59,8 @@ import java.util.ArrayList;
public class DirectoryFragment extends Fragment {
// TODO: show storage backend in item views when requested
+ // TODO: apply sort order locally
+ // TODO: apply MIME filtering locally
private ListView mListView;
private GridView mGridView;
@@ -70,14 +72,16 @@ public class DirectoryFragment extends Fragment {
private int mFlags;
- private static final String EXTRA_URI = "uri";
+ private static final String EXTRA_ROOT_URI = "rootUri";
+ private static final String EXTRA_DOCS_URI = "docsUri";
private static final int LOADER_DOCUMENTS = 2;
- public static void show(
- FragmentManager fm, Uri uri, String displayName, boolean addToBackStack) {
+ public static void show(FragmentManager fm, Uri rootUri, Uri docsUri, String displayName,
+ boolean addToBackStack) {
final Bundle args = new Bundle();
- args.putParcelable(EXTRA_URI, uri);
+ args.putParcelable(EXTRA_ROOT_URI, rootUri);
+ args.putParcelable(EXTRA_DOCS_URI, docsUri);
final DirectoryFragment fragment = new DirectoryFragment();
fragment.setArguments(args);
@@ -116,8 +120,8 @@ public class DirectoryFragment extends Fragment {
updateMode();
// TODO: migrate flags query to loader
- final Uri uri = getArguments().getParcelable(EXTRA_URI);
- mFlags = getDocumentFlags(context, uri);
+ final Uri docsUri = getArguments().getParcelable(EXTRA_DOCS_URI);
+ mFlags = getDocumentFlags(context, docsUri);
mCallbacks = new LoaderCallbacks<Cursor>() {
@Override
@@ -133,10 +137,10 @@ public class DirectoryFragment extends Fragment {
}
final Uri contentsUri;
- if (uri.getQueryParameter(DocumentsContract.PARAM_QUERY) != null) {
- contentsUri = uri;
+ if (docsUri.getQueryParameter(DocumentsContract.PARAM_QUERY) != null) {
+ contentsUri = docsUri;
} else {
- contentsUri = DocumentsContract.buildContentsUri(uri);
+ contentsUri = DocumentsContract.buildContentsUri(docsUri);
}
return new CursorLoader(context, contentsUri, null, null, null, sortOrder);
@@ -162,8 +166,8 @@ public class DirectoryFragment extends Fragment {
getLoaderManager().restartLoader(LOADER_DOCUMENTS, getArguments(), mCallbacks);
// TODO: clean up tracking of current directory
- final Uri uri = getArguments().getParcelable(EXTRA_URI);
- ((DocumentsActivity) getActivity()).onDirectoryChanged(uri, mFlags);
+ final Uri docsUri = getArguments().getParcelable(EXTRA_DOCS_URI);
+ ((DocumentsActivity) getActivity()).onDirectoryChanged(docsUri, mFlags);
}
@Override
@@ -245,8 +249,8 @@ public class DirectoryFragment extends Fragment {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final Cursor cursor = (Cursor) mAdapter.getItem(position);
- final Uri uri = getArguments().getParcelable(EXTRA_URI);
- final Document doc = Document.fromCursor(uri.getAuthority(), cursor);
+ final Uri rootUri = getArguments().getParcelable(EXTRA_ROOT_URI);
+ final Document doc = Document.fromCursor(rootUri, cursor);
((DocumentsActivity) getActivity()).onDocumentPicked(doc);
}
};
@@ -266,7 +270,7 @@ public class DirectoryFragment extends Fragment {
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == R.id.menu_open) {
- final Uri uri = getArguments().getParcelable(EXTRA_URI);
+ final Uri rootUri = getArguments().getParcelable(EXTRA_ROOT_URI);
final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions();
final ArrayList<Document> docs = Lists.newArrayList();
@@ -274,7 +278,7 @@ public class DirectoryFragment extends Fragment {
for (int i = 0; i < size; i++) {
if (checked.valueAt(i)) {
final Cursor cursor = (Cursor) mAdapter.getItem(checked.keyAt(i));
- docs.add(Document.fromCursor(uri.getAuthority(), cursor));
+ docs.add(Document.fromCursor(rootUri, cursor));
}
}
@@ -336,17 +340,17 @@ public class DirectoryFragment extends Fragment {
final TextView summary = (TextView) view.findViewById(android.R.id.summary);
final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
- final String guid = getCursorString(cursor, DocumentColumns.GUID);
+ final String docId = getCursorString(cursor, DocumentColumns.DOC_ID);
final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
final int flags = getCursorInt(cursor, DocumentColumns.FLAGS);
- final Uri uri = getArguments().getParcelable(EXTRA_URI);
- final String authority = uri.getAuthority();
+ final Uri rootUri = getArguments().getParcelable(EXTRA_ROOT_URI);
+ final String authority = rootUri.getAuthority();
if ((flags & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL) != 0) {
- final Uri childUri = DocumentsContract.buildDocumentUri(authority, guid);
+ final Uri childUri = DocumentsContract.buildDocumentUri(rootUri, docId);
icon.setImageURI(childUri);
} else {
icon.setImageDrawable(
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index dcd02d2..405ef36 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -91,7 +91,6 @@ public class DocumentsActivity extends Activity {
private static final String TAG = "Documents";
// TODO: fragment to show recently opened documents
- // TODO: pull actionbar icon from current backend
private static final String TAG_CREATE_DIRECTORY = "create_directory";
@@ -250,7 +249,8 @@ public class DocumentsActivity extends Activity {
public boolean onQueryTextSubmit(String query) {
// TODO: clear existing directory stack?
final Uri searchUri = DocumentsContract.buildSearchUri(mCurrentDir, query);
- DirectoryFragment.show(getFragmentManager(), searchUri, query, true);
+ DirectoryFragment.show(
+ getFragmentManager(), mCurrentRoot.rootUri, searchUri, query, true);
mSearchView.setIconified(true);
return true;
}
@@ -398,7 +398,7 @@ public class DocumentsActivity extends Activity {
final FragmentManager fm = getFragmentManager();
if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(doc.mimeType)) {
// Nested directory picked, recurse using new fragment
- DirectoryFragment.show(fm, doc.uri, doc.displayName, true);
+ DirectoryFragment.show(fm, doc.rootUri, doc.uri, doc.displayName, true);
} else if (mAction == ACTION_OPEN) {
// Explicit file picked, return
onFinished(doc.uri);
@@ -418,6 +418,8 @@ public class DocumentsActivity extends Activity {
}
public void onSaveRequested(String mimeType, String displayName) {
+ // TODO: handle overwrite by using last-selected GUID
+
final ContentValues values = new ContentValues();
values.put(DocumentColumns.MIME_TYPE, mimeType);
values.put(DocumentColumns.DISPLAY_NAME, displayName);
@@ -426,7 +428,6 @@ public class DocumentsActivity extends Activity {
if (uri != null) {
onFinished(uri);
} else {
- // TODO: ask for overwrite confirmation
Toast.makeText(this, R.string.save_error, Toast.LENGTH_SHORT).show();
}
}
@@ -470,35 +471,29 @@ public class DocumentsActivity extends Activity {
public static class Root {
public DocumentsProviderInfo info;
public int rootType;
+ public Uri rootUri;
public Uri uri;
public Drawable icon;
public String title;
public String summary;
- public static Root fromInfo(Context context, DocumentsProviderInfo info) {
+ public static Root fromCursor(
+ Context context, DocumentsProviderInfo info, Cursor cursor) {
+ final String rootId = cursor.getString(cursor.getColumnIndex(RootColumns.ROOT_ID));
+
final Root root = new Root();
final PackageManager pm = context.getPackageManager();
root.info = info;
- root.rootType = DocumentsContract.ROOT_TYPE_SERVICE;
+ root.rootType = cursor.getInt(cursor.getColumnIndex(RootColumns.ROOT_TYPE));
+ root.rootUri = DocumentsContract.buildRootUri(info.providerInfo.authority, rootId);
root.uri = DocumentsContract.buildDocumentUri(
- info.providerInfo.authority, DocumentsContract.ROOT_GUID);
+ root.rootUri, DocumentsContract.ROOT_DOC_ID);
root.icon = info.providerInfo.loadIcon(pm);
root.title = info.providerInfo.loadLabel(pm).toString();
root.summary = null;
- return root;
- }
-
- public static Root fromCursor(
- Context context, DocumentsProviderInfo info, Cursor cursor) {
- final Root root = fromInfo(context, info);
-
- root.rootType = cursor.getInt(cursor.getColumnIndex(RootColumns.ROOT_TYPE));
- root.uri = DocumentsContract.buildDocumentUri(info.providerInfo.authority,
- cursor.getString(cursor.getColumnIndex(RootColumns.GUID)));
- final PackageManager pm = context.getPackageManager();
final int icon = cursor.getInt(cursor.getColumnIndex(RootColumns.ICON));
if (icon != 0) {
try {
@@ -534,21 +529,24 @@ public class DocumentsActivity extends Activity {
}
public static class Document {
+ public Uri rootUri;
public Uri uri;
public String mimeType;
public String displayName;
- public static Document fromCursor(String authority, Cursor cursor) {
+ public static Document fromCursor(Uri rootUri, Cursor cursor) {
final Document doc = new Document();
- final String guid = getCursorString(cursor, DocumentColumns.GUID);
- doc.uri = DocumentsContract.buildDocumentUri(authority, guid);
+ final String docId = getCursorString(cursor, DocumentColumns.DOC_ID);
+ doc.rootUri = rootUri;
+ doc.uri = DocumentsContract.buildDocumentUri(rootUri, docId);
doc.mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
doc.displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
return doc;
}
- public static Document fromUri(ContentResolver resolver, Uri uri) {
+ public static Document fromUri(ContentResolver resolver, Uri rootUri, Uri uri) {
final Document doc = new Document();
+ doc.rootUri = rootUri;
doc.uri = uri;
final Cursor cursor = resolver.query(uri, null, null, null, null);
@@ -639,19 +637,16 @@ public class DocumentsActivity extends Activity {
sProviders.put(info.providerInfo.authority, info);
- if (info.customRoots) {
- // TODO: populate roots on background thread, and cache results
- final Uri uri = DocumentsContract.buildRootsUri(providerInfo.authority);
- final Cursor cursor = getContentResolver().query(uri, null, null, null, null);
- try {
- while (cursor.moveToNext()) {
- sRoots.add(Root.fromCursor(this, info, cursor));
- }
- } finally {
- cursor.close();
+ // TODO: remove deprecated customRoots flag
+ // TODO: populate roots on background thread, and cache results
+ final Uri uri = DocumentsContract.buildRootsUri(providerInfo.authority);
+ final Cursor cursor = getContentResolver().query(uri, null, null, null, null);
+ try {
+ while (cursor.moveToNext()) {
+ sRoots.add(Root.fromCursor(this, info, cursor));
}
- } else if (info != null) {
- sRoots.add(Root.fromInfo(this, info));
+ } finally {
+ cursor.close();
}
}
}
@@ -721,8 +716,8 @@ public class DocumentsActivity extends Activity {
}
mCurrentRoot = mRootsAdapter.getItem(position);
- DirectoryFragment.show(
- getFragmentManager(), mCurrentRoot.uri, mCurrentRoot.title, false);
+ DirectoryFragment.show(getFragmentManager(), mCurrentRoot.rootUri, mCurrentRoot.uri,
+ mCurrentRoot.title, false);
mDrawerLayout.closeDrawers();
}
@@ -784,13 +779,16 @@ public class DocumentsActivity extends Activity {
values.put(DocumentColumns.MIME_TYPE, DocumentsContract.MIME_TYPE_DIRECTORY);
values.put(DocumentColumns.DISPLAY_NAME, displayName);
- // TODO: handle errors from remote side
final DocumentsActivity activity = (DocumentsActivity) getActivity();
final Uri uri = resolver.insert(activity.mCurrentDir, values);
-
- // Navigate into newly created child
- final Document doc = Document.fromUri(resolver, uri);
- activity.onDocumentPicked(doc);
+ if (uri != null) {
+ // Navigate into newly created child
+ final Document doc = Document.fromUri(
+ resolver, activity.mCurrentRoot.rootUri, uri);
+ activity.onDocumentPicked(doc);
+ } else {
+ Toast.makeText(context, R.string.save_error, Toast.LENGTH_SHORT).show();
+ }
}
});
builder.setNegativeButton(android.R.string.cancel, null);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
new file mode 100644
index 0000000..e6ee614
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
@@ -0,0 +1,177 @@
+/*
+ * 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.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.text.format.DateUtils;
+import android.util.Log;
+
+public class RecentsProvider extends ContentProvider {
+ private static final String TAG = "RecentsProvider";
+
+ public static final String AUTHORITY = "com.android.documentsui.recents";
+
+ private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ private static final int URI_RECENT_OPEN = 1;
+ private static final int URI_RECENT_CREATE = 2;
+ private static final int URI_RESUME = 3;
+
+ static {
+ sMatcher.addURI(AUTHORITY, "recent_open", URI_RECENT_OPEN);
+ sMatcher.addURI(AUTHORITY, "recent_create", URI_RECENT_CREATE);
+ sMatcher.addURI(AUTHORITY, "resume/*", URI_RESUME);
+ }
+
+ private static final String TABLE_RECENT_OPEN = "recent_open";
+ private static final String TABLE_RECENT_CREATE = "recent_create";
+ private static final String TABLE_RESUME = "resume";
+
+ /**
+ * String of URIs pointing at a storage backend, stored as a JSON array,
+ * starting with root.
+ */
+ public static final String COL_PATH = "path";
+ public static final String COL_PACKAGE_NAME = "package_name";
+ public static final String COL_TIMESTAMP = "timestamp";
+
+ private DatabaseHelper mHelper;
+
+ private static class DatabaseHelper extends SQLiteOpenHelper {
+ private static final String DB_NAME = "recents";
+
+ private static final int VERSION_INIT = 1;
+
+ public DatabaseHelper(Context context) {
+ super(context, DB_NAME, null, VERSION_INIT);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + TABLE_RECENT_OPEN + " (" +
+ COL_PATH + " TEXT," +
+ COL_TIMESTAMP + " INTEGER," +
+ ")");
+
+ db.execSQL("CREATE TABLE " + TABLE_RECENT_CREATE + " (" +
+ COL_PATH + " TEXT," +
+ COL_TIMESTAMP + " INTEGER," +
+ ")");
+
+ db.execSQL("CREATE TABLE " + TABLE_RESUME + " (" +
+ COL_PACKAGE_NAME + " TEXT PRIMARY KEY ON CONFLICT REPLACE," +
+ COL_PATH + " TEXT," +
+ COL_TIMESTAMP + " INTEGER," +
+ ")");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.w(TAG, "Upgrading database; wiping app data");
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_RECENT_OPEN);
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_RECENT_CREATE);
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_RESUME);
+ onCreate(db);
+ }
+ }
+
+ @Override
+ public boolean onCreate() {
+ mHelper = new DatabaseHelper(getContext());
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ final SQLiteDatabase db = mHelper.getReadableDatabase();
+ switch (sMatcher.match(uri)) {
+ case URI_RECENT_OPEN: {
+ return db.query(TABLE_RECENT_OPEN, projection,
+ buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, null);
+ }
+ case URI_RECENT_CREATE: {
+ return db.query(TABLE_RECENT_CREATE, projection,
+ buildWhereYounger(DateUtils.WEEK_IN_MILLIS), null, null, null, null);
+ }
+ case URI_RESUME: {
+ final String packageName = uri.getPathSegments().get(1);
+ return db.query(TABLE_RESUME, projection, COL_PACKAGE_NAME + "=?",
+ new String[] { packageName }, null, null, null);
+ }
+ default: {
+ throw new UnsupportedOperationException("Unsupported Uri " + uri);
+ }
+ }
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ final SQLiteDatabase db = mHelper.getWritableDatabase();
+ switch (sMatcher.match(uri)) {
+ case URI_RECENT_OPEN: {
+ db.insert(TABLE_RECENT_OPEN, null, values);
+ db.delete(TABLE_RECENT_OPEN, buildWhereOlder(DateUtils.WEEK_IN_MILLIS), null);
+ return uri;
+ }
+ case URI_RECENT_CREATE: {
+ db.insert(TABLE_RECENT_CREATE, null, values);
+ db.delete(TABLE_RECENT_CREATE, buildWhereOlder(DateUtils.WEEK_IN_MILLIS), null);
+ return uri;
+ }
+ case URI_RESUME: {
+ final String packageName = uri.getPathSegments().get(1);
+ values.put(COL_PACKAGE_NAME, packageName);
+ db.insert(TABLE_RESUME, null, values);
+ return uri;
+ }
+ default: {
+ 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);
+ }
+
+ private static String buildWhereOlder(long deltaMillis) {
+ return COL_TIMESTAMP + "<" + (System.currentTimeMillis() - deltaMillis);
+ }
+
+ private static String buildWhereYounger(long deltaMillis) {
+ return COL_TIMESTAMP + ">" + (System.currentTimeMillis() - deltaMillis);
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java b/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java
index b15d123..a086a43 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java
@@ -19,7 +19,9 @@ package com.android.documentsui;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
import android.os.Bundle;
+import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
@@ -27,7 +29,15 @@ import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.TextView;
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+import java.io.IOException;
+import java.io.InputStream;
+
public class TestActivity extends Activity {
+ private static final String TAG = "TestActivity";
+
private TextView mResult;
@Override
@@ -113,5 +123,19 @@ public class TestActivity extends Activity {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
mResult.setText("resultCode=" + resultCode + ", data=" + String.valueOf(data));
+
+ final Uri uri = data != null ? data.getData() : null;
+ if (uri != null) {
+ InputStream is = null;
+ try {
+ is = getContentResolver().openInputStream(uri);
+ final int length = Streams.readFullyNoClose(is).length;
+ Log.d(TAG, "read length=" + length);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to read " + uri, e);
+ } finally {
+ IoUtils.closeQuietly(is);
+ }
+ }
}
}
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index bf5811b..dd7472b 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -45,15 +45,15 @@ public class ExternalStorageProvider extends ContentProvider {
private static final String AUTHORITY = "com.android.externalstorage";
- // TODO: support searching
// TODO: support multiple storage devices
private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int URI_ROOTS = 1;
- private static final int URI_DOCS_ID = 2;
- private static final int URI_DOCS_ID_CONTENTS = 3;
- private static final int URI_DOCS_ID_SEARCH = 4;
+ 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;
private HashMap<String, Root> mRoots = Maps.newHashMap();
@@ -68,9 +68,10 @@ public class ExternalStorageProvider extends ContentProvider {
static {
sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
- sMatcher.addURI(AUTHORITY, "docs/*", URI_DOCS_ID);
- sMatcher.addURI(AUTHORITY, "docs/*/contents", URI_DOCS_ID_CONTENTS);
- sMatcher.addURI(AUTHORITY, "docs/*/search", URI_DOCS_ID_SEARCH);
+ 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);
}
@Override
@@ -79,7 +80,7 @@ public class ExternalStorageProvider extends ContentProvider {
final Root root = new Root();
root.rootType = DocumentsContract.ROOT_TYPE_DEVICE_ADVANCED;
- root.name = "internal";
+ root.name = "primary";
root.title = getContext().getString(R.string.root_internal_storage);
root.path = Environment.getExternalStorageDirectory();
mRoots.put(root.name, root);
@@ -90,49 +91,59 @@ public class ExternalStorageProvider extends ContentProvider {
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
- final int match = sMatcher.match(uri);
- if (match == URI_ROOTS) {
- // TODO: support custom projections
- projection = new String[] {
- RootColumns.ROOT_TYPE, RootColumns.GUID, RootColumns.ICON, RootColumns.TITLE,
- RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES };
-
- final MatrixCursor cursor = new MatrixCursor(projection);
- for (Root root : mRoots.values()) {
- final String guid = fileToGuid(root.path);
- cursor.addRow(new Object[] {
- root.rootType, guid, root.icon, root.title, root.summary,
- root.path.getFreeSpace() });
- }
- return cursor;
- }
// TODO: support custom projections
- projection = new String[] {
- BaseColumns._ID,
- DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE, DocumentColumns.GUID,
- DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, DocumentColumns.FLAGS };
+ final String[] rootsProjection = new String[] {
+ BaseColumns._ID, RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON,
+ RootColumns.TITLE, RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES };
+ final String[] docsProjection = new String[] {
+ BaseColumns._ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
+ DocumentColumns.DOC_ID, DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED,
+ DocumentColumns.FLAGS };
+
+ switch (sMatcher.match(uri)) {
+ case URI_ROOTS: {
+ final MatrixCursor cursor = new MatrixCursor(rootsProjection);
+ for (Root root : mRoots.values()) {
+ includeRoot(cursor, root);
+ }
+ return cursor;
+ }
+ case URI_ROOTS_ID: {
+ final String root = uri.getPathSegments().get(1);
- final MatrixCursor cursor = new MatrixCursor(projection);
- switch (match) {
+ final MatrixCursor cursor = new MatrixCursor(rootsProjection);
+ includeRoot(cursor, mRoots.get(root));
+ return cursor;
+ }
case URI_DOCS_ID: {
- final String guid = uri.getPathSegments().get(1);
- includeFile(cursor, guid);
- break;
+ final Root root = mRoots.get(uri.getPathSegments().get(1));
+ final String docId = uri.getPathSegments().get(3);
+
+ final MatrixCursor cursor = new MatrixCursor(docsProjection);
+ final File file = docIdToFile(root, docId);
+ includeFile(cursor, root, file);
+ return cursor;
}
case URI_DOCS_ID_CONTENTS: {
- final String guid = uri.getPathSegments().get(1);
- final File parent = guidToFile(guid);
+ final Root root = mRoots.get(uri.getPathSegments().get(1));
+ final String docId = uri.getPathSegments().get(3);
+
+ final MatrixCursor cursor = new MatrixCursor(docsProjection);
+ final File parent = docIdToFile(root, docId);
for (File file : parent.listFiles()) {
- includeFile(cursor, fileToGuid(file));
+ includeFile(cursor, root, file);
}
- break;
+ return cursor;
}
case URI_DOCS_ID_SEARCH: {
- final String guid = uri.getPathSegments().get(1);
- final File parent = guidToFile(guid);
+ final Root root = mRoots.get(uri.getPathSegments().get(1));
+ final String docId = uri.getPathSegments().get(3);
final String query = uri.getQueryParameter(DocumentsContract.PARAM_QUERY).toLowerCase();
+ final MatrixCursor cursor = new MatrixCursor(docsProjection);
+ final File parent = docIdToFile(root, docId);
+
final LinkedList<File> pending = new LinkedList<File>();
pending.add(parent);
while (!pending.isEmpty() && cursor.getCount() < 20) {
@@ -143,49 +154,52 @@ public class ExternalStorageProvider extends ContentProvider {
}
} else {
if (file.getName().toLowerCase().contains(query)) {
- includeFile(cursor, fileToGuid(file));
+ includeFile(cursor, root, file);
}
}
}
- break;
+ return cursor;
}
default: {
- cursor.close();
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
-
- return cursor;
}
- private String fileToGuid(File file) {
+ private String fileToDocId(Root root, File file) {
+ String rootPath = root.path.getAbsolutePath();
final String path = file.getAbsolutePath();
- for (Root root : mRoots.values()) {
- final String rootPath = root.path.getAbsolutePath();
- if (path.startsWith(rootPath)) {
- return root.name + ':' + Uri.encode(path.substring(rootPath.length()));
- }
+ if (path.equals(rootPath)) {
+ return DocumentsContract.ROOT_DOC_ID;
}
- throw new IllegalArgumentException("Failed to find root for " + file);
+ if (!rootPath.endsWith("/")) {
+ rootPath += "/";
+ }
+ if (!path.startsWith(rootPath)) {
+ throw new IllegalArgumentException("File " + path + " outside root " + root.path);
+ } else {
+ return path.substring(rootPath.length());
+ }
}
- private File guidToFile(String guid) {
- final int split = guid.indexOf(':');
- final String name = guid.substring(0, split);
- final Root root = mRoots.get(name);
- if (root != null) {
- final String path = Uri.decode(guid.substring(split + 1));
- return new File(root.path, path);
+ private File docIdToFile(Root root, String docId) {
+ if (DocumentsContract.ROOT_DOC_ID.equals(docId)) {
+ return root.path;
+ } else {
+ return new File(root.path, docId);
}
+ }
- throw new IllegalArgumentException("Failed to find root for " + guid);
+ private void includeRoot(MatrixCursor cursor, Root root) {
+ cursor.addRow(new Object[] {
+ root.name.hashCode(), root.name, root.rootType, root.icon, root.title, root.summary,
+ root.path.getFreeSpace() });
}
- private void includeFile(MatrixCursor cursor, String guid) {
+ private void includeFile(MatrixCursor cursor, Root root, File file) {
int flags = 0;
- final File file = guidToFile(guid);
if (file.isDirectory()) {
flags |= DocumentsContract.FLAG_SUPPORTS_SEARCH;
}
@@ -202,17 +216,19 @@ public class ExternalStorageProvider extends ContentProvider {
flags |= DocumentsContract.FLAG_SUPPORTS_THUMBNAIL;
}
- final long id = guid.hashCode();
+ final String docId = fileToDocId(root, file);
+ final long id = docId.hashCode();
cursor.addRow(new Object[] {
- id, file.getName(), file.length(), guid, mimeType, file.lastModified(), flags });
+ id, file.getName(), file.length(), docId, mimeType, file.lastModified(), flags });
}
@Override
public String getType(Uri uri) {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
- final String guid = uri.getPathSegments().get(1);
- return getTypeForFile(guidToFile(guid));
+ final Root root = mRoots.get(uri.getPathSegments().get(1));
+ final String docId = uri.getPathSegments().get(3);
+ return getTypeForFile(docIdToFile(root, docId));
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
@@ -245,10 +261,11 @@ public class ExternalStorageProvider extends ContentProvider {
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
- final String guid = uri.getPathSegments().get(1);
- final File file = guidToFile(guid);
+ final Root root = mRoots.get(uri.getPathSegments().get(1));
+ final String docId = uri.getPathSegments().get(3);
// TODO: offer as thumbnail
+ final File file = docIdToFile(root, docId);
return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(uri, mode));
}
default: {
@@ -261,8 +278,10 @@ public class ExternalStorageProvider extends ContentProvider {
public Uri insert(Uri uri, ContentValues values) {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
- final String guid = uri.getPathSegments().get(1);
- final File parent = guidToFile(guid);
+ final Root root = mRoots.get(uri.getPathSegments().get(1));
+ final String docId = uri.getPathSegments().get(3);
+
+ final File parent = docIdToFile(root, docId);
final String mimeType = values.getAsString(DocumentColumns.MIME_TYPE);
final String name = validateDisplayName(
@@ -285,7 +304,8 @@ public class ExternalStorageProvider extends ContentProvider {
}
}
- return DocumentsContract.buildDocumentUri(AUTHORITY, fileToGuid(file));
+ final String newDocId = fileToDocId(root, file);
+ return DocumentsContract.buildDocumentUri(AUTHORITY, root.name, newDocId);
}
default: {
throw new UnsupportedOperationException("Unsupported Uri " + uri);
@@ -297,8 +317,10 @@ public class ExternalStorageProvider extends ContentProvider {
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
- final String guid = uri.getPathSegments().get(1);
- final File file = guidToFile(guid);
+ final Root root = mRoots.get(uri.getPathSegments().get(1));
+ final String docId = uri.getPathSegments().get(3);
+
+ final File file = docIdToFile(root, docId);
final File newFile = new File(
file.getParentFile(), values.getAsString(DocumentColumns.DISPLAY_NAME));
return file.renameTo(newFile) ? 1 : 0;
@@ -313,8 +335,10 @@ public class ExternalStorageProvider extends ContentProvider {
public int delete(Uri uri, String selection, String[] selectionArgs) {
switch (sMatcher.match(uri)) {
case URI_DOCS_ID: {
- final String guid = uri.getPathSegments().get(1);
- final File file = guidToFile(guid);
+ final Root root = mRoots.get(uri.getPathSegments().get(1));
+ final String docId = uri.getPathSegments().get(3);
+
+ final File file = docIdToFile(root, docId);
return file.delete() ? 1 : 0;
}
default: {