Merge "Reference docs by ROOT_ID and DOC_ID; recents."

This commit is contained in:
Jeff Sharkey
2013-08-02 18:10:09 +00:00
committed by Android (Google) Code Review
8 changed files with 403 additions and 167 deletions

View File

@ -20272,7 +20272,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);
@ -20287,7 +20289,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
@ -20295,16 +20297,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";

View File

@ -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();
}
/**
* Build URI representing the given {@link DocumentColumns#GUID} in a
* storage backend.
*/
public static Uri buildDocumentUri(String authority, String guid) {
public static Uri buildRootUri(String authority, String rootId) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority).appendPath(PATH_DOCS).appendPath(guid).build();
.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#DOC_ID} in a
* storage root.
*/
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>
@ -247,13 +256,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}.

View File

@ -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>

View File

@ -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(

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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 };
final MatrixCursor cursor = new MatrixCursor(projection);
switch (match) {
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(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);
}
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);
if (!rootPath.endsWith("/")) {
rootPath += "/";
}
if (!path.startsWith(rootPath)) {
throw new IllegalArgumentException("File " + path + " outside root " + root.path);
} else {
return path.substring(rootPath.length());
}
throw new IllegalArgumentException("Failed to find root for " + guid);
}
private void includeFile(MatrixCursor cursor, String guid) {
private File docIdToFile(Root root, String docId) {
if (DocumentsContract.ROOT_DOC_ID.equals(docId)) {
return root.path;
} else {
return new File(root.path, docId);
}
}
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, 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: {