summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2014-04-23 07:15:32 +0000
committerAndroid Git Automerger <android-git-automerger@android.com>2014-04-23 07:15:32 +0000
commit3e5991c1eb3a54960675307335d24fca2fe3fa6d (patch)
tree2e3fc17c942818ec93439de00b7dbd3cc001a18f
parentd5f8b4d26a26a4c77a49fd3ea32710e6278ce99c (diff)
parent21de56a94668e0fda1b8bb4ee4f99a09b40d28fd (diff)
downloadframeworks_base-3e5991c1eb3a54960675307335d24fca2fe3fa6d.zip
frameworks_base-3e5991c1eb3a54960675307335d24fca2fe3fa6d.tar.gz
frameworks_base-3e5991c1eb3a54960675307335d24fca2fe3fa6d.tar.bz2
am 21de56a9: Add directory selection to DocumentsProvider.
* commit '21de56a94668e0fda1b8bb4ee4f99a09b40d28fd': Add directory selection to DocumentsProvider.
-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 8080835..1e42f9a 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);
}