summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2014-04-05 19:05:24 -0700
committerJeff Sharkey <jsharkey@android.com>2014-04-22 22:18:21 -0700
commit21de56a94668e0fda1b8bb4ee4f99a09b40d28fd (patch)
tree3cadec8c73dd2ea2ed4cbe5744a52128e5d24a8a
parent846318a3250fa95f47a9decfbffb05a31dbd0006 (diff)
downloadframeworks_base-21de56a94668e0fda1b8bb4ee4f99a09b40d28fd.zip
frameworks_base-21de56a94668e0fda1b8bb4ee4f99a09b40d28fd.tar.gz
frameworks_base-21de56a94668e0fda1b8bb4ee4f99a09b40d28fd.tar.bz2
Add directory selection to DocumentsProvider.
Introduce new ACTION_PICK_DIRECTORY that allows users to grant access to an entire document subtree. Instead of requiring grants for each individual document, this leverages new prefix URI permission grants by defining new "via"-style URIs: content://com.example/via/12/document/24/ This references document 24 by using a prefix grant given for document 12. Internally, we use isChildDocument() to enforce that 24 is actually a descendant (child, grandchild, etc) of 12. Since this is an optional API, providers indicate support with Root.FLAG_SUPPORTS_DIR_SELECTION. Extend DocumentsUI to support picking directories. Expose createDocument() API to work with returned directories. Offer to canonicalize via-style URIs into direct URIs, generating exact permission grants along the way. Override openAssetFile() to pass through CancellationSignal. Move testing code into ApiDemos. Bug: 10607375 Change-Id: Ifffc1cff878870f8152eb6ca0199c5d014b9cb07
-rw-r--r--api/current.txt11
-rw-r--r--core/java/android/content/Intent.java34
-rw-r--r--core/java/android/os/FileUtils.java4
-rw-r--r--core/java/android/provider/DocumentsContract.java154
-rw-r--r--core/java/android/provider/DocumentsProvider.java181
-rw-r--r--packages/DocumentsUI/AndroidManifest.xml18
-rw-r--r--packages/DocumentsUI/res/layout/fragment_pick.xml48
-rw-r--r--packages/DocumentsUI/res/values/strings.xml2
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java63
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/PickFragment.java89
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RootsCache.java6
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/TestActivity.java268
-rw-r--r--packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java23
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java17
14 files changed, 558 insertions, 360 deletions
diff --git a/api/current.txt b/api/current.txt
index 6c51201..1055c24 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6927,6 +6927,7 @@ package android.content {
field public static final java.lang.String ACTION_PASTE = "android.intent.action.PASTE";
field public static final java.lang.String ACTION_PICK = "android.intent.action.PICK";
field public static final java.lang.String ACTION_PICK_ACTIVITY = "android.intent.action.PICK_ACTIVITY";
+ field public static final java.lang.String ACTION_PICK_DIRECTORY = "android.intent.action.PICK_DIRECTORY";
field public static final java.lang.String ACTION_POWER_CONNECTED = "android.intent.action.ACTION_POWER_CONNECTED";
field public static final java.lang.String ACTION_POWER_DISCONNECTED = "android.intent.action.ACTION_POWER_DISCONNECTED";
field public static final java.lang.String ACTION_POWER_USAGE_SUMMARY = "android.intent.action.POWER_USAGE_SUMMARY";
@@ -22480,16 +22481,21 @@ package android.provider {
public final class DocumentsContract {
method public static android.net.Uri buildChildDocumentsUri(java.lang.String, java.lang.String);
+ method public static android.net.Uri buildChildDocumentsViaUri(android.net.Uri, java.lang.String);
method public static android.net.Uri buildDocumentUri(java.lang.String, java.lang.String);
+ method public static android.net.Uri buildDocumentViaUri(android.net.Uri, java.lang.String);
method public static android.net.Uri buildRecentDocumentsUri(java.lang.String, 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 buildSearchDocumentsUri(java.lang.String, java.lang.String, java.lang.String);
+ method public static android.net.Uri buildViaUri(java.lang.String, java.lang.String);
+ method public static android.net.Uri createDocument(android.content.ContentResolver, android.net.Uri, java.lang.String, java.lang.String);
method public static boolean deleteDocument(android.content.ContentResolver, android.net.Uri);
method public static java.lang.String getDocumentId(android.net.Uri);
method public static android.graphics.Bitmap getDocumentThumbnail(android.content.ContentResolver, android.net.Uri, android.graphics.Point, android.os.CancellationSignal);
method public static java.lang.String getRootId(android.net.Uri);
method public static java.lang.String getSearchDocumentsQuery(android.net.Uri);
+ method public static java.lang.String getViaDocumentId(android.net.Uri);
method public static boolean isDocumentUri(android.content.Context, android.net.Uri);
field public static final java.lang.String EXTRA_ERROR = "error";
field public static final java.lang.String EXTRA_INFO = "info";
@@ -22526,6 +22532,7 @@ package android.provider {
field public static final java.lang.String COLUMN_TITLE = "title";
field public static final int FLAG_LOCAL_ONLY = 2; // 0x2
field public static final int FLAG_SUPPORTS_CREATE = 1; // 0x1
+ field public static final int FLAG_SUPPORTS_DIR_SELECTION = 16; // 0x10
field public static final int FLAG_SUPPORTS_RECENTS = 4; // 0x4
field public static final int FLAG_SUPPORTS_SEARCH = 8; // 0x8
}
@@ -22538,6 +22545,9 @@ package android.provider {
method public java.lang.String getDocumentType(java.lang.String) throws java.io.FileNotFoundException;
method public final java.lang.String getType(android.net.Uri);
method public final android.net.Uri insert(android.net.Uri, android.content.ContentValues);
+ method public boolean isChildDocument(java.lang.String, java.lang.String);
+ method public final android.content.res.AssetFileDescriptor openAssetFile(android.net.Uri, java.lang.String) throws java.io.FileNotFoundException;
+ method public final android.content.res.AssetFileDescriptor openAssetFile(android.net.Uri, java.lang.String, android.os.CancellationSignal) throws java.io.FileNotFoundException;
method public abstract android.os.ParcelFileDescriptor openDocument(java.lang.String, java.lang.String, android.os.CancellationSignal) throws java.io.FileNotFoundException;
method public android.content.res.AssetFileDescriptor openDocumentThumbnail(java.lang.String, android.graphics.Point, android.os.CancellationSignal) throws java.io.FileNotFoundException;
method public final android.os.ParcelFileDescriptor openFile(android.net.Uri, java.lang.String) throws java.io.FileNotFoundException;
@@ -22550,6 +22560,7 @@ package android.provider {
method public android.database.Cursor queryRecentDocuments(java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException;
method public abstract android.database.Cursor queryRoots(java.lang.String[]) throws java.io.FileNotFoundException;
method public android.database.Cursor querySearchDocuments(java.lang.String, java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException;
+ method public final void revokeDocumentPermission(java.lang.String);
method public final int update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[]);
}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 67b6737..c0f04af 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2700,9 +2700,11 @@ public class Intent implements Parcelable, Cloneable {
* take the persistable permissions using
* {@link ContentResolver#takePersistableUriPermission(Uri, int)}.
* <p>
- * Callers can restrict document selection to a specific kind of data, such
- * as photos, by setting one or more MIME types in
- * {@link #EXTRA_MIME_TYPES}.
+ * Callers must indicate the acceptable document MIME types through
+ * {@link #setType(String)}. For example, to select photos, use
+ * {@code image/*}. If multiple disjoint MIME types are acceptable, define
+ * them in {@link #EXTRA_MIME_TYPES} and {@link #setType(String)} to
+ * {@literal *}/*.
* <p>
* If the caller can handle multiple returned items (the user performing
* multiple selection), then you can specify {@link #EXTRA_ALLOW_MULTIPLE}
@@ -2712,9 +2714,10 @@ public class Intent implements Parcelable, Cloneable {
* returned URIs can be opened with
* {@link ContentResolver#openFileDescriptor(Uri, String)}.
* <p>
- * Output: The URI of the item that was picked. This must be a
- * {@code content://} URI so that any receiver can access it. If multiple
- * documents were selected, they are returned in {@link #getClipData()}.
+ * Output: The URI of the item that was picked, returned in
+ * {@link #getData()}. This must be a {@code content://} URI so that any
+ * receiver can access it. If multiple documents were selected, they are
+ * returned in {@link #getClipData()}.
*
* @see DocumentsContract
* @see #ACTION_CREATE_DOCUMENT
@@ -2756,6 +2759,24 @@ public class Intent implements Parcelable, Cloneable {
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT";
+ /**
+ * Activity Action: Allow the user to pick a directory. When invoked, the
+ * system will display the various {@link DocumentsProvider} instances
+ * installed on the device, letting the user navigate through them. Apps can
+ * fully manage documents within the returned directory.
+ * <p>
+ * To gain access to descendant (child, grandchild, etc) documents, use
+ * {@link DocumentsContract#buildDocumentViaUri(Uri, String)} and
+ * {@link DocumentsContract#buildChildDocumentsViaUri(Uri, String)} using
+ * the returned directory URI.
+ * <p>
+ * Output: The URI representing the selected directory.
+ *
+ * @see DocumentsContract
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_PICK_DIRECTORY = "android.intent.action.PICK_DIRECTORY";
+
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// Standard intent categories (see addCategory()).
@@ -3334,6 +3355,7 @@ public class Intent implements Parcelable, Cloneable {
* @see #ACTION_GET_CONTENT
* @see #ACTION_OPEN_DOCUMENT
* @see #ACTION_CREATE_DOCUMENT
+ * @see #ACTION_PICK_DIRECTORY
*/
public static final String EXTRA_LOCAL_ONLY =
"android.intent.extra.LOCAL_ONLY";
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
index dc18dee..1089f27 100644
--- a/core/java/android/os/FileUtils.java
+++ b/core/java/android/os/FileUtils.java
@@ -370,8 +370,8 @@ public class FileUtils {
* attacks.
*/
public static boolean contains(File dir, File file) {
- String dirPath = dir.getPath();
- String filePath = file.getPath();
+ String dirPath = dir.getAbsolutePath();
+ String filePath = file.getAbsolutePath();
if (dirPath.equals(filePath)) {
return true;
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index f0520b5..9a768e0 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -57,6 +57,10 @@ import java.util.List;
* <p>
* To create a document provider, extend {@link DocumentsProvider}, which
* provides a foundational implementation of this contract.
+ * <p>
+ * All client apps must hold a valid URI permission grant to access documents,
+ * typically issued when a user makes a selection through
+ * {@link Intent#ACTION_OPEN_DOCUMENT} or {@link Intent#ACTION_CREATE_DOCUMENT}.
*
* @see DocumentsProvider
*/
@@ -69,6 +73,8 @@ public final class DocumentsContract {
// content://com.example/root/sdcard/search/?query=pony
// content://com.example/document/12/
// content://com.example/document/12/children/
+ // content://com.example/via/12/document/24/
+ // content://com.example/via/12/document/24/children/
private DocumentsContract() {
}
@@ -425,6 +431,14 @@ public final class DocumentsContract {
public static final int FLAG_SUPPORTS_SEARCH = 1 << 3;
/**
+ * Flag indicating that this root supports directory selection.
+ *
+ * @see #COLUMN_FLAGS
+ * @see DocumentsProvider#isChildDocument(String, String)
+ */
+ public static final int FLAG_SUPPORTS_DIR_SELECTION = 1 << 4;
+
+ /**
* Flag indicating that this root is currently empty. This may be used
* to hide the root when opening documents, but the root will still be
* shown when creating documents and {@link #FLAG_SUPPORTS_CREATE} is
@@ -484,12 +498,15 @@ public final class DocumentsContract {
/** {@hide} */
public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
+ /** {@hide} */
+ public static final String EXTRA_URI = "uri";
private static final String PATH_ROOT = "root";
private static final String PATH_RECENT = "recent";
private static final String PATH_DOCUMENT = "document";
private static final String PATH_CHILDREN = "children";
private static final String PATH_SEARCH = "search";
+ private static final String PATH_VIA = "via";
private static final String PARAM_QUERY = "query";
private static final String PARAM_MANAGE = "manage";
@@ -532,6 +549,17 @@ public final class DocumentsContract {
}
/**
+ * Build URI representing access to descendant documents of the given
+ * {@link Document#COLUMN_DOCUMENT_ID}.
+ *
+ * @see #getViaDocumentId(Uri)
+ */
+ public static Uri buildViaUri(String authority, String documentId) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
+ .appendPath(PATH_VIA).appendPath(documentId).build();
+ }
+
+ /**
* Build URI representing the given {@link Document#COLUMN_DOCUMENT_ID} in a
* document provider. When queried, a provider will return a single row with
* columns defined by {@link Document}.
@@ -545,6 +573,41 @@ public final class DocumentsContract {
}
/**
+ * Build URI representing the given {@link Document#COLUMN_DOCUMENT_ID} in a
+ * document provider. Instead of directly accessing the target document,
+ * gain access via another document. The target document must be a
+ * descendant (child, grandchild, etc) of the via document.
+ * <p>
+ * This is typically used to access documents under a user-selected
+ * directory, since it doesn't require the user to separately confirm each
+ * new document access.
+ *
+ * @param viaUri a related document (directory) that the caller is
+ * leveraging to gain access to the target document. The target
+ * document must be a descendant of this directory.
+ * @param documentId the target document, which the caller may not have
+ * direct access to.
+ * @see Intent#ACTION_PICK_DIRECTORY
+ * @see DocumentsProvider#isChildDocument(String, String)
+ * @see #buildDocumentUri(String, String)
+ */
+ public static Uri buildDocumentViaUri(Uri viaUri, String documentId) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(viaUri.getAuthority()).appendPath(PATH_VIA)
+ .appendPath(getViaDocumentId(viaUri)).appendPath(PATH_DOCUMENT)
+ .appendPath(documentId).build();
+ }
+
+ /** {@hide} */
+ public static Uri buildDocumentMaybeViaUri(Uri baseUri, String documentId) {
+ if (isViaUri(baseUri)) {
+ return buildDocumentViaUri(baseUri, documentId);
+ } else {
+ return buildDocumentUri(baseUri.getAuthority(), documentId);
+ }
+ }
+
+ /**
* Build URI representing the children of the given directory in a document
* provider. When queried, a provider will return zero or more rows with
* columns defined by {@link Document}.
@@ -562,6 +625,32 @@ public final class DocumentsContract {
}
/**
+ * Build URI representing the children of the given directory in a document
+ * provider. Instead of directly accessing the target document, gain access
+ * via another document. The target document must be a descendant (child,
+ * grandchild, etc) of the via document.
+ * <p>
+ * This is typically used to access documents under a user-selected
+ * directory, since it doesn't require the user to separately confirm each
+ * new document access.
+ *
+ * @param viaUri a related document (directory) that the caller is
+ * leveraging to gain access to the target document. The target
+ * document must be a descendant of this directory.
+ * @param parentDocumentId the target document, which the caller may not
+ * have direct access to.
+ * @see Intent#ACTION_PICK_DIRECTORY
+ * @see DocumentsProvider#isChildDocument(String, String)
+ * @see #buildChildDocumentsUri(String, String)
+ */
+ public static Uri buildChildDocumentsViaUri(Uri viaUri, String parentDocumentId) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(viaUri.getAuthority()).appendPath(PATH_VIA)
+ .appendPath(getViaDocumentId(viaUri)).appendPath(PATH_DOCUMENT)
+ .appendPath(parentDocumentId).appendPath(PATH_CHILDREN).build();
+ }
+
+ /**
* Build URI representing a search for matching documents under a specific
* root in a document provider. When queried, a provider will return zero or
* more rows with columns defined by {@link Document}.
@@ -580,21 +669,31 @@ public final class DocumentsContract {
/**
* Test if the given URI represents a {@link Document} backed by a
* {@link DocumentsProvider}.
+ *
+ * @see #buildDocumentUri(String, String)
+ * @see #buildDocumentViaUri(Uri, String)
*/
public static boolean isDocumentUri(Context context, Uri uri) {
final List<String> paths = uri.getPathSegments();
- if (paths.size() < 2) {
- return false;
- }
- if (!PATH_DOCUMENT.equals(paths.get(0))) {
- return false;
+ if (paths.size() >= 2
+ && (PATH_DOCUMENT.equals(paths.get(0)) || PATH_VIA.equals(paths.get(0)))) {
+ return isDocumentsProvider(context, uri.getAuthority());
}
+ return false;
+ }
+
+ /** {@hide} */
+ public static boolean isViaUri(Uri uri) {
+ final List<String> paths = uri.getPathSegments();
+ return (paths.size() >= 2 && PATH_VIA.equals(paths.get(0)));
+ }
+ private static boolean isDocumentsProvider(Context context, String authority) {
final Intent intent = new Intent(PROVIDER_INTERFACE);
final List<ResolveInfo> infos = context.getPackageManager()
.queryIntentContentProviders(intent, 0);
for (ResolveInfo info : infos) {
- if (uri.getAuthority().equals(info.providerInfo.authority)) {
+ if (authority.equals(info.providerInfo.authority)) {
return true;
}
}
@@ -606,27 +705,40 @@ public final class DocumentsContract {
*/
public static String getRootId(Uri rootUri) {
final List<String> paths = rootUri.getPathSegments();
- if (paths.size() < 2) {
- throw new IllegalArgumentException("Not a root: " + rootUri);
- }
- if (!PATH_ROOT.equals(paths.get(0))) {
- throw new IllegalArgumentException("Not a root: " + rootUri);
+ if (paths.size() >= 2 && PATH_ROOT.equals(paths.get(0))) {
+ return paths.get(1);
}
- return paths.get(1);
+ throw new IllegalArgumentException("Invalid URI: " + rootUri);
}
/**
* Extract the {@link Document#COLUMN_DOCUMENT_ID} from the given URI.
+ *
+ * @see #isDocumentUri(Context, Uri)
*/
public static String getDocumentId(Uri documentUri) {
final List<String> paths = documentUri.getPathSegments();
- if (paths.size() < 2) {
- throw new IllegalArgumentException("Not a document: " + documentUri);
+ if (paths.size() >= 2 && PATH_DOCUMENT.equals(paths.get(0))) {
+ return paths.get(1);
}
- if (!PATH_DOCUMENT.equals(paths.get(0))) {
- throw new IllegalArgumentException("Not a document: " + documentUri);
+ if (paths.size() >= 4 && PATH_VIA.equals(paths.get(0))
+ && PATH_DOCUMENT.equals(paths.get(2))) {
+ return paths.get(3);
}
- return paths.get(1);
+ throw new IllegalArgumentException("Invalid URI: " + documentUri);
+ }
+
+ /**
+ * Extract the via {@link Document#COLUMN_DOCUMENT_ID} from the given URI.
+ *
+ * @see #isViaUri(Uri)
+ */
+ public static String getViaDocumentId(Uri documentUri) {
+ final List<String> paths = documentUri.getPathSegments();
+ if (paths.size() >= 2 && PATH_VIA.equals(paths.get(0))) {
+ return paths.get(1);
+ }
+ throw new IllegalArgumentException("Invalid URI: " + documentUri);
}
/**
@@ -758,7 +870,6 @@ public final class DocumentsContract {
* @param mimeType MIME type of new document
* @param displayName name of new document
* @return newly created document, or {@code null} if failed
- * @hide
*/
public static Uri createDocument(ContentResolver resolver, Uri parentDocumentUri,
String mimeType, String displayName) {
@@ -778,13 +889,12 @@ public final class DocumentsContract {
public static Uri createDocument(ContentProviderClient client, Uri parentDocumentUri,
String mimeType, String displayName) throws RemoteException {
final Bundle in = new Bundle();
- in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(parentDocumentUri));
+ in.putParcelable(DocumentsContract.EXTRA_URI, parentDocumentUri);
in.putString(Document.COLUMN_MIME_TYPE, mimeType);
in.putString(Document.COLUMN_DISPLAY_NAME, displayName);
final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null, in);
- return buildDocumentUri(
- parentDocumentUri.getAuthority(), out.getString(Document.COLUMN_DOCUMENT_ID));
+ return out.getParcelable(DocumentsContract.EXTRA_URI);
}
/**
@@ -811,7 +921,7 @@ public final class DocumentsContract {
public static void deleteDocument(ContentProviderClient client, Uri documentUri)
throws RemoteException {
final Bundle in = new Bundle();
- in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(documentUri));
+ in.putParcelable(DocumentsContract.EXTRA_URI, documentUri);
client.call(METHOD_DELETE_DOCUMENT, null, in);
}
diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java
index 49816f8..1a7a00f2 100644
--- a/core/java/android/provider/DocumentsProvider.java
+++ b/core/java/android/provider/DocumentsProvider.java
@@ -46,6 +46,7 @@ import android.util.Log;
import libcore.io.IoUtils;
import java.io.FileNotFoundException;
+import java.util.Objects;
/**
* Base class for a document provider. A document provider offers read and write
@@ -125,6 +126,8 @@ public abstract class DocumentsProvider extends ContentProvider {
private static final int MATCH_SEARCH = 4;
private static final int MATCH_DOCUMENT = 5;
private static final int MATCH_CHILDREN = 6;
+ private static final int MATCH_DOCUMENT_VIA = 7;
+ private static final int MATCH_CHILDREN_VIA = 8;
private String mAuthority;
@@ -144,6 +147,8 @@ public abstract class DocumentsProvider extends ContentProvider {
mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH);
mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT);
mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN);
+ mMatcher.addURI(mAuthority, "via/*/document/*", MATCH_DOCUMENT_VIA);
+ mMatcher.addURI(mAuthority, "via/*/document/*/children", MATCH_CHILDREN_VIA);
// Sanity check our setup
if (!info.exported) {
@@ -161,6 +166,35 @@ public abstract class DocumentsProvider extends ContentProvider {
}
/**
+ * Test if a document is descendant (child, grandchild, etc) from the given
+ * parent. Providers must override this to support directory selection. You
+ * should avoid making network requests to keep this request fast.
+ *
+ * @param parentDocumentId parent to verify against.
+ * @param documentId child to verify.
+ * @return if given document is a descendant of the given parent.
+ * @see DocumentsContract.Root#FLAG_SUPPORTS_DIR_SELECTION
+ */
+ public boolean isChildDocument(String parentDocumentId, String documentId) {
+ return false;
+ }
+
+ /** {@hide} */
+ private void enforceVia(Uri documentUri) {
+ if (DocumentsContract.isViaUri(documentUri)) {
+ final String parent = DocumentsContract.getViaDocumentId(documentUri);
+ final String child = DocumentsContract.getDocumentId(documentUri);
+ if (Objects.equals(parent, child)) {
+ return;
+ }
+ if (!isChildDocument(parent, child)) {
+ throw new SecurityException(
+ "Document " + child + " is not a descendant of " + parent);
+ }
+ }
+ }
+
+ /**
* Create a new document and return its newly generated
* {@link Document#COLUMN_DOCUMENT_ID}. You must allocate a new
* {@link Document#COLUMN_DOCUMENT_ID} to represent the document, which must
@@ -182,9 +216,10 @@ public abstract class DocumentsProvider extends ContentProvider {
/**
* Delete the requested document. Upon returning, any URI permission grants
- * for the requested document will be revoked. If additional documents were
- * deleted as a side effect of this call, such as documents inside a
- * directory, the implementor is responsible for revoking those permissions.
+ * for the given document will be revoked. If additional documents were
+ * deleted as a side effect of this call (such as documents inside a
+ * directory) the implementor is responsible for revoking those permissions
+ * using {@link #revokeDocumentPermission(String)}.
*
* @param documentId the document to delete.
*/
@@ -420,8 +455,12 @@ public abstract class DocumentsProvider extends ContentProvider {
return querySearchDocuments(
getRootId(uri), getSearchDocumentsQuery(uri), projection);
case MATCH_DOCUMENT:
+ case MATCH_DOCUMENT_VIA:
+ enforceVia(uri);
return queryDocument(getDocumentId(uri), projection);
case MATCH_CHILDREN:
+ case MATCH_CHILDREN_VIA:
+ enforceVia(uri);
if (DocumentsContract.isManageMode(uri)) {
return queryChildDocumentsForManage(
getDocumentId(uri), projection, sortOrder);
@@ -449,6 +488,8 @@ public abstract class DocumentsProvider extends ContentProvider {
case MATCH_ROOT:
return DocumentsContract.Root.MIME_TYPE_ITEM;
case MATCH_DOCUMENT:
+ case MATCH_DOCUMENT_VIA:
+ enforceVia(uri);
return getDocumentType(getDocumentId(uri));
default:
return null;
@@ -460,6 +501,49 @@ public abstract class DocumentsProvider extends ContentProvider {
}
/**
+ * Implementation is provided by the parent class. Can be overridden to
+ * provide additional functionality, but subclasses <em>must</em> always
+ * call the superclass. If the superclass returns {@code null}, the subclass
+ * may implement custom behavior.
+ * <p>
+ * This is typically used to resolve a "via" URI into a concrete document
+ * reference, issuing a narrower single-document URI permission grant along
+ * the way.
+ *
+ * @see DocumentsContract#buildDocumentViaUri(Uri, String)
+ */
+ @Override
+ public Uri canonicalize(Uri uri) {
+ final Context context = getContext();
+ switch (mMatcher.match(uri)) {
+ case MATCH_DOCUMENT_VIA:
+ enforceVia(uri);
+
+ final Uri narrowUri = DocumentsContract.buildDocumentUri(uri.getAuthority(),
+ DocumentsContract.getDocumentId(uri));
+
+ // Caller may only have prefix grant, so extend them a grant to
+ // the narrow Uri. Caller already holds read grant to get here,
+ // so check for any other modes we should extend.
+ int modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
+ if (context.checkCallingOrSelfUriPermission(uri,
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+ == PackageManager.PERMISSION_GRANTED) {
+ modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
+ }
+ if (context.checkCallingOrSelfUriPermission(uri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
+ == PackageManager.PERMISSION_GRANTED) {
+ modeFlags |= Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION;
+ }
+ context.grantUriPermission(getCallingPackage(), narrowUri, modeFlags);
+ return narrowUri;
+ }
+ return null;
+ }
+
+ /**
* Implementation is provided by the parent class. Throws by default, and
* cannot be overriden.
*
@@ -496,54 +580,47 @@ public abstract class DocumentsProvider extends ContentProvider {
* provide additional functionality, but subclasses <em>must</em> always
* call the superclass. If the superclass returns {@code null}, the subclass
* may implement custom behavior.
- *
- * @see #openDocument(String, String, CancellationSignal)
- * @see #deleteDocument(String)
*/
@Override
public Bundle call(String method, String arg, Bundle extras) {
- final Context context = getContext();
-
if (!method.startsWith("android:")) {
- // Let non-platform methods pass through
+ // Ignore non-platform methods
return super.call(method, arg, extras);
}
- final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID);
- final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId);
+ final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
+ final String authority = documentUri.getAuthority();
+ final String documentId = DocumentsContract.getDocumentId(documentUri);
- // Require that caller can manage requested document
- final boolean callerHasManage =
- context.checkCallingOrSelfPermission(android.Manifest.permission.MANAGE_DOCUMENTS)
- == PackageManager.PERMISSION_GRANTED;
- enforceWritePermissionInner(documentUri);
+ if (!mAuthority.equals(authority)) {
+ throw new SecurityException(
+ "Requested authority " + authority + " doesn't match provider " + mAuthority);
+ }
+ enforceVia(documentUri);
final Bundle out = new Bundle();
try {
if (METHOD_CREATE_DOCUMENT.equals(method)) {
+ enforceWritePermissionInner(documentUri);
+
final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE);
final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME);
final String newDocumentId = createDocument(documentId, mimeType, displayName);
- out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId);
-
- // Extend permission grant towards caller if needed
- if (!callerHasManage) {
- final Uri newDocumentUri = DocumentsContract.buildDocumentUri(
- mAuthority, newDocumentId);
- context.grantUriPermission(getCallingPackage(), newDocumentUri,
- Intent.FLAG_GRANT_READ_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
- }
+
+ // No need to issue new grants here, since caller either has
+ // manage permission or a prefix grant. We might generate a
+ // "via" style URI if that's how they called us.
+ final Uri newDocumentUri = DocumentsContract.buildDocumentMaybeViaUri(documentUri,
+ newDocumentId);
+ out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri);
} else if (METHOD_DELETE_DOCUMENT.equals(method)) {
+ enforceWritePermissionInner(documentUri);
deleteDocument(documentId);
// Document no longer exists, clean up any grants
- context.revokeUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
+ revokeDocumentPermission(documentId);
} else {
throw new UnsupportedOperationException("Method not supported " + method);
@@ -555,12 +632,25 @@ public abstract class DocumentsProvider extends ContentProvider {
}
/**
+ * Revoke any active permission grants for the given
+ * {@link Document#COLUMN_DOCUMENT_ID}, usually called when a document
+ * becomes invalid. Follows the same semantics as
+ * {@link Context#revokeUriPermission(Uri, int)}.
+ */
+ public final void revokeDocumentPermission(String documentId) {
+ final Context context = getContext();
+ context.revokeUriPermission(DocumentsContract.buildDocumentUri(mAuthority, documentId), ~0);
+ context.revokeUriPermission(DocumentsContract.buildViaUri(mAuthority, documentId), ~0);
+ }
+
+ /**
* Implementation is provided by the parent class. Cannot be overriden.
*
* @see #openDocument(String, String, CancellationSignal)
*/
@Override
public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+ enforceVia(uri);
return openDocument(getDocumentId(uri), mode, null);
}
@@ -572,17 +662,47 @@ public abstract class DocumentsProvider extends ContentProvider {
@Override
public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
throws FileNotFoundException {
+ enforceVia(uri);
return openDocument(getDocumentId(uri), mode, signal);
}
/**
* Implementation is provided by the parent class. Cannot be overriden.
*
+ * @see #openDocument(String, String, CancellationSignal)
+ */
+ @Override
+ @SuppressWarnings("resource")
+ public final AssetFileDescriptor openAssetFile(Uri uri, String mode)
+ throws FileNotFoundException {
+ enforceVia(uri);
+ final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, null);
+ return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
+ }
+
+ /**
+ * Implementation is provided by the parent class. Cannot be overriden.
+ *
+ * @see #openDocument(String, String, CancellationSignal)
+ */
+ @Override
+ @SuppressWarnings("resource")
+ public final AssetFileDescriptor openAssetFile(Uri uri, String mode, CancellationSignal signal)
+ throws FileNotFoundException {
+ enforceVia(uri);
+ final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, signal);
+ return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
+ }
+
+ /**
+ * Implementation is provided by the parent class. Cannot be overriden.
+ *
* @see #openDocumentThumbnail(String, Point, CancellationSignal)
*/
@Override
public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
throws FileNotFoundException {
+ enforceVia(uri);
if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
return openDocumentThumbnail(getDocumentId(uri), sizeHint, null);
@@ -600,6 +720,7 @@ public abstract class DocumentsProvider extends ContentProvider {
public final AssetFileDescriptor openTypedAssetFile(
Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
throws FileNotFoundException {
+ enforceVia(uri);
if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal);
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index 6b77a7c..159ee66 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -9,18 +9,17 @@
android:label="@string/app_label"
android:supportsRtl="true">
- <!-- TODO: allow rotation when state saving is in better shape -->
<activity
android:name=".DocumentsActivity"
android:theme="@style/Theme"
android:icon="@drawable/ic_doc_text">
- <intent-filter android:priority="100">
+ <intent-filter>
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="*/*" />
</intent-filter>
- <intent-filter android:priority="100">
+ <intent-filter>
<action android:name="android.intent.action.CREATE_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
@@ -33,6 +32,10 @@
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
+ <action android:name="android.intent.action.PICK_DIRECTORY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <intent-filter>
<action android:name="android.provider.action.MANAGE_ROOT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.document/root" />
@@ -57,14 +60,5 @@
<data android:scheme="package" />
</intent-filter>
</receiver>
-
- <!-- TODO: remove when we have real clients -->
- <activity android:name=".TestActivity" android:enabled="false">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.DEFAULT" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
</application>
</manifest>
diff --git a/packages/DocumentsUI/res/layout/fragment_pick.xml b/packages/DocumentsUI/res/layout/fragment_pick.xml
new file mode 100644
index 0000000..4a2fd03
--- /dev/null
+++ b/packages/DocumentsUI/res/layout/fragment_pick.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- Le sigh, this really should be an asset -->
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="#ccc" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:baselineAligned="false"
+ android:gravity="center_vertical"
+ android:background="#ddd"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall">
+
+ <Button
+ android:id="@android:id/button1"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackground"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textAllCaps="false"
+ android:padding="8dp" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 92c30ba..c1a9d72 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -44,6 +44,8 @@
<string name="menu_share">Share</string>
<!-- Menu item title that deletes the selected documents [CHAR LIMIT=24] -->
<string name="menu_delete">Delete</string>
+ <!-- Menu item title that selects the current directory [CHAR LIMIT=48] -->
+ <string name="menu_select">Select \"<xliff:g id="directory" example="My Directory">^1</xliff:g>\"</string>
<!-- Action mode title summarizing the number of documents selected [CHAR LIMIT=32] -->
<string name="mode_selected_count"><xliff:g id="count" example="3">%1$d</xliff:g> selected</string>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 4212e96..9f76991 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -24,6 +24,7 @@ import static com.android.documentsui.DocumentsActivity.State.ACTION_CREATE;
import static com.android.documentsui.DocumentsActivity.State.ACTION_GET_CONTENT;
import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE;
import static com.android.documentsui.DocumentsActivity.State.ACTION_OPEN;
+import static com.android.documentsui.DocumentsActivity.State.ACTION_PICK_DIRECTORY;
import static com.android.documentsui.DocumentsActivity.State.MODE_GRID;
import static com.android.documentsui.DocumentsActivity.State.MODE_LIST;
@@ -202,6 +203,8 @@ public class DocumentsActivity extends Activity {
final String mimeType = getIntent().getType();
final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
SaveFragment.show(getFragmentManager(), mimeType, title);
+ } else if (mState.action == ACTION_PICK_DIRECTORY) {
+ PickFragment.show(getFragmentManager());
}
if (mState.action == ACTION_GET_CONTENT) {
@@ -209,7 +212,8 @@ public class DocumentsActivity extends Activity {
moreApps.setComponent(null);
moreApps.setPackage(null);
RootsFragment.show(getFragmentManager(), moreApps);
- } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE) {
+ } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE
+ || mState.action == ACTION_PICK_DIRECTORY) {
RootsFragment.show(getFragmentManager(), null);
}
@@ -236,6 +240,8 @@ public class DocumentsActivity extends Activity {
mState.action = ACTION_CREATE;
} else if (Intent.ACTION_GET_CONTENT.equals(action)) {
mState.action = ACTION_GET_CONTENT;
+ } else if (Intent.ACTION_PICK_DIRECTORY.equals(action)) {
+ mState.action = ACTION_PICK_DIRECTORY;
} else if (DocumentsContract.ACTION_MANAGE_ROOT.equals(action)) {
mState.action = ACTION_MANAGE;
}
@@ -434,7 +440,8 @@ public class DocumentsActivity extends Activity {
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
actionBar.setIcon(new ColorDrawable());
- if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
+ if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT
+ || mState.action == ACTION_PICK_DIRECTORY) {
actionBar.setTitle(R.string.title_open);
} else if (mState.action == ACTION_CREATE) {
actionBar.setTitle(R.string.title_save);
@@ -576,7 +583,7 @@ public class DocumentsActivity extends Activity {
sortSize.setVisible(mState.showSize);
final boolean searchVisible;
- if (mState.action == ACTION_CREATE) {
+ if (mState.action == ACTION_CREATE || mState.action == ACTION_PICK_DIRECTORY) {
createDir.setVisible(cwd != null && cwd.isCreateSupported());
searchVisible = false;
@@ -586,7 +593,9 @@ public class DocumentsActivity extends Activity {
list.setVisible(false);
}
- SaveFragment.get(fm).setSaveEnabled(cwd != null && cwd.isCreateSupported());
+ if (mState.action == ACTION_CREATE) {
+ SaveFragment.get(fm).setSaveEnabled(cwd != null && cwd.isCreateSupported());
+ }
} else {
createDir.setVisible(false);
@@ -819,7 +828,7 @@ public class DocumentsActivity extends Activity {
if (cwd == null) {
// No directory means recents
- if (mState.action == ACTION_CREATE) {
+ if (mState.action == ACTION_CREATE || mState.action == ACTION_PICK_DIRECTORY) {
RecentsCreateFragment.show(fm);
} else {
DirectoryFragment.showRecentsOpen(fm, anim);
@@ -848,6 +857,15 @@ public class DocumentsActivity extends Activity {
}
}
+ if (mState.action == ACTION_PICK_DIRECTORY) {
+ final PickFragment pick = PickFragment.get(fm);
+ if (pick != null) {
+ final CharSequence displayName = (mState.stack.size() <= 1) ? root.title
+ : cwd.displayName;
+ pick.setPickTarget(cwd, displayName);
+ }
+ }
+
final RootsFragment roots = RootsFragment.get(fm);
if (roots != null) {
roots.onCurrentRootChanged();
@@ -1002,12 +1020,18 @@ public class DocumentsActivity extends Activity {
new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor());
}
+ public void onPickRequested(DocumentInfo pickTarget) {
+ final Uri viaUri = DocumentsContract.buildViaUri(pickTarget.authority,
+ pickTarget.documentId);
+ new PickFinishTask(viaUri).executeOnExecutor(getCurrentExecutor());
+ }
+
private void saveStackBlocking() {
final ContentResolver resolver = getContentResolver();
final ContentValues values = new ContentValues();
final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack);
- if (mState.action == ACTION_CREATE) {
+ if (mState.action == ACTION_CREATE || mState.action == ACTION_PICK_DIRECTORY) {
// Remember stack for last create
values.clear();
values.put(RecentColumns.KEY, mState.stack.buildKey());
@@ -1040,6 +1064,11 @@ public class DocumentsActivity extends Activity {
if (mState.action == ACTION_GET_CONTENT) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ } else if (mState.action == ACTION_PICK_DIRECTORY) {
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
} else {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
@@ -1121,6 +1150,25 @@ public class DocumentsActivity extends Activity {
}
}
+ private class PickFinishTask extends AsyncTask<Void, Void, Void> {
+ private final Uri mUri;
+
+ public PickFinishTask(Uri uri) {
+ mUri = uri;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ saveStackBlocking();
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ onFinished(mUri);
+ }
+ }
+
public static class State implements android.os.Parcelable {
public int action;
public String[] acceptMimes;
@@ -1154,7 +1202,8 @@ public class DocumentsActivity extends Activity {
public static final int ACTION_OPEN = 1;
public static final int ACTION_CREATE = 2;
public static final int ACTION_GET_CONTENT = 3;
- public static final int ACTION_MANAGE = 4;
+ public static final int ACTION_PICK_DIRECTORY = 4;
+ public static final int ACTION_MANAGE = 5;
public static final int MODE_UNKNOWN = 0;
public static final int MODE_LIST = 1;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java b/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
new file mode 100644
index 0000000..a9e488a1
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/PickFragment.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2014 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.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import com.android.documentsui.model.DocumentInfo;
+
+import java.util.Locale;
+
+/**
+ * Display pick confirmation bar, usually for selecting a directory.
+ */
+public class PickFragment extends Fragment {
+ public static final String TAG = "PickFragment";
+
+ private DocumentInfo mPickTarget;
+
+ private View mContainer;
+ private Button mPick;
+
+ public static void show(FragmentManager fm) {
+ final PickFragment fragment = new PickFragment();
+
+ final FragmentTransaction ft = fm.beginTransaction();
+ ft.replace(R.id.container_save, fragment, TAG);
+ ft.commitAllowingStateLoss();
+ }
+
+ public static PickFragment get(FragmentManager fm) {
+ return (PickFragment) fm.findFragmentByTag(TAG);
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ mContainer = inflater.inflate(R.layout.fragment_pick, container, false);
+
+ mPick = (Button) mContainer.findViewById(android.R.id.button1);
+ mPick.setOnClickListener(mPickListener);
+
+ setPickTarget(null, null);
+
+ return mContainer;
+ }
+
+ private View.OnClickListener mPickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final DocumentsActivity activity = DocumentsActivity.get(PickFragment.this);
+ activity.onPickRequested(mPickTarget);
+ }
+ };
+
+ public void setPickTarget(DocumentInfo pickTarget, CharSequence displayName) {
+ mPickTarget = pickTarget;
+
+ if (mPickTarget != null) {
+ mContainer.setVisibility(View.VISIBLE);
+ final Locale locale = getResources().getConfiguration().locale;
+ final String raw = getString(R.string.menu_select).toUpperCase(locale);
+ mPick.setText(TextUtils.expandTemplate(raw, displayName));
+ } else {
+ mContainer.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
index f1dca1d..933dbe0 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
@@ -104,7 +104,8 @@ public class RootsCache {
mRecentsRoot.authority = null;
mRecentsRoot.rootId = null;
mRecentsRoot.icon = R.drawable.ic_root_recent;
- mRecentsRoot.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE;
+ mRecentsRoot.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE
+ | Root.FLAG_SUPPORTS_DIR_SELECTION;
mRecentsRoot.title = mContext.getString(R.string.root_recent);
mRecentsRoot.availableBytes = -1;
@@ -349,12 +350,15 @@ public class RootsCache {
final List<RootInfo> matching = Lists.newArrayList();
for (RootInfo root : roots) {
final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0;
+ final boolean supportsDir = (root.flags & Root.FLAG_SUPPORTS_DIR_SELECTION) != 0;
final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0;
final boolean localOnly = (root.flags & Root.FLAG_LOCAL_ONLY) != 0;
final boolean empty = (root.flags & Root.FLAG_EMPTY) != 0;
// Exclude read-only devices when creating
if (state.action == State.ACTION_CREATE && !supportsCreate) continue;
+ // Exclude roots that don't support directory picking
+ if (state.action == State.ACTION_PICK_DIRECTORY && !supportsDir) continue;
// Exclude advanced devices when not requested
if (!state.showAdvanced && advanced) continue;
// Exclude non-local devices when local only
diff --git a/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java b/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java
deleted file mode 100644
index 1a47308..0000000
--- a/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
- * 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.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.DocumentsContract;
-import android.util.Log;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.Button;
-import android.widget.CheckBox;
-import android.widget.LinearLayout;
-import android.widget.ScrollView;
-import android.widget.TextView;
-
-import libcore.io.IoUtils;
-import libcore.io.Streams;
-
-import java.io.InputStream;
-import java.io.OutputStream;
-
-public class TestActivity extends Activity {
- private static final String TAG = "TestActivity";
-
- private static final int CODE_READ = 42;
- private static final int CODE_WRITE = 43;
-
- private TextView mResult;
-
- @Override
- public void onCreate(Bundle icicle) {
- super.onCreate(icicle);
-
- final Context context = this;
-
- final LinearLayout view = new LinearLayout(context);
- view.setOrientation(LinearLayout.VERTICAL);
-
- mResult = new TextView(context);
- view.addView(mResult);
-
- final CheckBox multiple = new CheckBox(context);
- multiple.setText("ALLOW_MULTIPLE");
- view.addView(multiple);
- final CheckBox localOnly = new CheckBox(context);
- localOnly.setText("LOCAL_ONLY");
- view.addView(localOnly);
-
- Button button;
- button = new Button(context);
- button.setText("OPEN_DOC */*");
- button.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("*/*");
- if (multiple.isChecked()) {
- intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
- }
- if (localOnly.isChecked()) {
- intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
- }
- startActivityForResult(intent, CODE_READ);
- }
- });
- view.addView(button);
-
- button = new Button(context);
- button.setText("OPEN_DOC image/*");
- button.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("image/*");
- if (multiple.isChecked()) {
- intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
- }
- if (localOnly.isChecked()) {
- intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
- }
- startActivityForResult(intent, CODE_READ);
- }
- });
- view.addView(button);
-
- button = new Button(context);
- button.setText("OPEN_DOC audio/ogg");
- button.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("audio/ogg");
- if (multiple.isChecked()) {
- intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
- }
- if (localOnly.isChecked()) {
- intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
- }
- startActivityForResult(intent, CODE_READ);
- }
- });
- view.addView(button);
-
- button = new Button(context);
- button.setText("OPEN_DOC text/plain, application/msword");
- button.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("*/*");
- intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {
- "text/plain", "application/msword" });
- if (multiple.isChecked()) {
- intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
- }
- if (localOnly.isChecked()) {
- intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
- }
- startActivityForResult(intent, CODE_READ);
- }
- });
- view.addView(button);
-
- button = new Button(context);
- button.setText("CREATE_DOC text/plain");
- button.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("text/plain");
- intent.putExtra(Intent.EXTRA_TITLE, "foobar.txt");
- if (localOnly.isChecked()) {
- intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
- }
- startActivityForResult(intent, CODE_WRITE);
- }
- });
- view.addView(button);
-
- button = new Button(context);
- button.setText("CREATE_DOC image/png");
- button.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("image/png");
- intent.putExtra(Intent.EXTRA_TITLE, "mypicture.png");
- if (localOnly.isChecked()) {
- intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
- }
- startActivityForResult(intent, CODE_WRITE);
- }
- });
- view.addView(button);
-
- button = new Button(context);
- button.setText("GET_CONTENT */*");
- button.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("*/*");
- if (multiple.isChecked()) {
- intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
- }
- if (localOnly.isChecked()) {
- intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
- }
- startActivityForResult(Intent.createChooser(intent, "Kittens!"), CODE_READ);
- }
- });
- view.addView(button);
-
- final ScrollView scroll = new ScrollView(context);
- scroll.addView(view);
-
- setContentView(scroll);
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- mResult.setText(null);
- String result = "resultCode=" + resultCode + ", data=" + String.valueOf(data);
-
- if (requestCode == CODE_READ) {
- final Uri uri = data != null ? data.getData() : null;
- if (uri != null) {
- if (DocumentsContract.isDocumentUri(this, uri)) {
- result += "; DOC_ID";
- }
- try {
- getContentResolver().takePersistableUriPermission(
- uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
- } catch (SecurityException e) {
- result += "; FAILED TO TAKE";
- Log.e(TAG, "Failed to take", e);
- }
- InputStream is = null;
- try {
- is = getContentResolver().openInputStream(uri);
- final int length = Streams.readFullyNoClose(is).length;
- result += "; read length=" + length;
- } catch (Exception e) {
- result += "; ERROR";
- Log.e(TAG, "Failed to read " + uri, e);
- } finally {
- IoUtils.closeQuietly(is);
- }
- } else {
- result += "no uri?";
- }
- } else if (requestCode == CODE_WRITE) {
- final Uri uri = data != null ? data.getData() : null;
- if (uri != null) {
- if (DocumentsContract.isDocumentUri(this, uri)) {
- result += "; DOC_ID";
- }
- try {
- getContentResolver().takePersistableUriPermission(
- uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- } catch (SecurityException e) {
- result += "; FAILED TO TAKE";
- Log.e(TAG, "Failed to take", e);
- }
- OutputStream os = null;
- try {
- os = getContentResolver().openOutputStream(uri);
- os.write("THE COMPLETE WORKS OF SHAKESPEARE".getBytes());
- } catch (Exception e) {
- result += "; ERROR";
- Log.e(TAG, "Failed to write " + uri, e);
- } finally {
- IoUtils.closeQuietly(os);
- }
- } else {
- result += "no uri?";
- }
- }
-
- Log.d(TAG, result);
- mResult.setText(result);
- }
-}
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 559e052..16fc3e5 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -27,6 +27,7 @@ import android.net.Uri;
import android.os.CancellationSignal;
import android.os.Environment;
import android.os.FileObserver;
+import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
@@ -143,7 +144,7 @@ public class ExternalStorageProvider extends DocumentsProvider {
final RootInfo root = new RootInfo();
root.rootId = rootId;
root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED
- | Root.FLAG_SUPPORTS_SEARCH;
+ | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_DIR_SELECTION;
if (ROOT_ID_PRIMARY_EMULATED.equals(rootId)) {
root.title = getContext().getString(R.string.root_internal_storage);
} else {
@@ -240,8 +241,8 @@ public class ExternalStorageProvider extends DocumentsProvider {
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else {
flags |= Document.FLAG_SUPPORTS_WRITE;
+ flags |= Document.FLAG_SUPPORTS_DELETE;
}
- flags |= Document.FLAG_SUPPORTS_DELETE;
}
final String displayName = file.getName();
@@ -284,11 +285,26 @@ public class ExternalStorageProvider extends DocumentsProvider {
}
@Override
+ public boolean isChildDocument(String parentDocId, String docId) {
+ try {
+ final File parent = getFileForDocId(parentDocId).getCanonicalFile();
+ final File doc = getFileForDocId(docId).getCanonicalFile();
+ return FileUtils.contains(parent, doc);
+ } catch (IOException e) {
+ throw new IllegalArgumentException(
+ "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e);
+ }
+ }
+
+ @Override
public String createDocument(String docId, String mimeType, String displayName)
throws FileNotFoundException {
final File parent = getFileForDocId(docId);
- File file;
+ if (!parent.isDirectory()) {
+ throw new IllegalArgumentException("Parent document isn't a directory");
+ }
+ File file;
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
file = new File(parent, displayName);
if (!file.mkdir()) {
@@ -317,6 +333,7 @@ public class ExternalStorageProvider extends DocumentsProvider {
@Override
public void deleteDocument(String docId) throws FileNotFoundException {
+ // TODO: extend to delete directories
final File file = getFileForDocId(docId);
if (!file.delete()) {
throw new IllegalStateException("Failed to delete " + file);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1bbdf3b..51296c1 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -5979,8 +5979,11 @@ public final class ActivityManagerService extends ActivityManagerNative
return perm;
}
- private final boolean checkUriPermissionLocked(
- Uri uri, int uid, final int modeFlags, int minStrength) {
+ private final boolean checkUriPermissionLocked(Uri uri, int uid, final int modeFlags) {
+ final boolean persistable = (modeFlags & Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0;
+ final int minStrength = persistable ? UriPermission.STRENGTH_PERSISTABLE
+ : UriPermission.STRENGTH_OWNED;
+
// Root gets to do everything.
if (uid == 0) {
return true;
@@ -6024,8 +6027,8 @@ public final class ActivityManagerService extends ActivityManagerNative
if (pid == MY_PID) {
return PackageManager.PERMISSION_GRANTED;
}
- synchronized(this) {
- return checkUriPermissionLocked(uri, uid, modeFlags, UriPermission.STRENGTH_OWNED)
+ synchronized (this) {
+ return checkUriPermissionLocked(uri, uid, modeFlags)
? PackageManager.PERMISSION_GRANTED
: PackageManager.PERMISSION_DENIED;
}
@@ -6137,11 +6140,7 @@ public final class ActivityManagerService extends ActivityManagerNative
if (callingUid != Process.myUid()) {
if (!checkHoldingPermissionsLocked(pm, pi, uri, callingUid, modeFlags)) {
// Require they hold a strong enough Uri permission
- final boolean persistable =
- (modeFlags & Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0;
- final int minStrength = persistable ? UriPermission.STRENGTH_PERSISTABLE
- : UriPermission.STRENGTH_OWNED;
- if (!checkUriPermissionLocked(uri, callingUid, modeFlags, minStrength)) {
+ if (!checkUriPermissionLocked(uri, callingUid, modeFlags)) {
throw new SecurityException("Uid " + callingUid
+ " does not have permission to uri " + uri);
}