diff options
author | Jeff Sharkey <jsharkey@android.com> | 2014-04-23 07:15:32 +0000 |
---|---|---|
committer | Android Git Automerger <android-git-automerger@android.com> | 2014-04-23 07:15:32 +0000 |
commit | 3e5991c1eb3a54960675307335d24fca2fe3fa6d (patch) | |
tree | 2e3fc17c942818ec93439de00b7dbd3cc001a18f | |
parent | d5f8b4d26a26a4c77a49fd3ea32710e6278ce99c (diff) | |
parent | 21de56a94668e0fda1b8bb4ee4f99a09b40d28fd (diff) | |
download | frameworks_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.
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); } |