OSDN Git Service

RESTRICT AUTOMERGE: Recover shady content:// paths.
[android-x86/frameworks-base.git] / core / java / android / content / ContentProvider.java
index bc2d788..e2c898d 100644 (file)
@@ -39,10 +39,11 @@ import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.IBinder;
 import android.os.ICancellationSignal;
-import android.os.OperationCanceledException;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
+import android.os.RemoteException;
 import android.os.UserHandle;
+import android.os.storage.StorageManager;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -53,6 +54,7 @@ import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Objects;
 
 /**
  * Content providers are one of the primary building blocks of Android applications, providing
@@ -96,6 +98,7 @@ import java.util.Arrays;
  * developer guide.</p>
  */
 public abstract class ContentProvider implements ComponentCallbacks2 {
+
     private static final String TAG = "ContentProvider";
 
     /*
@@ -117,7 +120,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
     private boolean mNoPerms;
     private boolean mSingleUser;
 
-    private final ThreadLocal<String> mCallingPackage = new ThreadLocal<String>();
+    private final ThreadLocal<String> mCallingPackage = new ThreadLocal<>();
 
     private Transport mTransport = new Transport();
 
@@ -204,11 +207,10 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
         }
 
         @Override
-        public Cursor query(String callingPkg, Uri uri, String[] projection,
-                String selection, String[] selectionArgs, String sortOrder,
-                ICancellationSignal cancellationSignal) {
-            validateIncomingUri(uri);
-            uri = getUriWithoutUserId(uri);
+        public Cursor query(String callingPkg, Uri uri, @Nullable String[] projection,
+                @Nullable Bundle queryArgs, @Nullable ICancellationSignal cancellationSignal) {
+            uri = validateIncomingUri(uri);
+            uri = maybeGetUriWithoutUserId(uri);
             if (enforceReadPermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) {
                 // The caller has no access to the data, so return an empty cursor with
                 // the columns in the requested order. The caller may ask for an invalid
@@ -224,9 +226,9 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
                 // However, the caller may be expecting to access them my index. Hence,
                 // we have to execute the query as if allowed to get a cursor with the
                 // columns. We then use the column names to return an empty cursor.
-                Cursor cursor = ContentProvider.this.query(uri, projection, selection,
-                        selectionArgs, sortOrder, CancellationSignal.fromTransport(
-                                cancellationSignal));
+                Cursor cursor = ContentProvider.this.query(
+                        uri, projection, queryArgs,
+                        CancellationSignal.fromTransport(cancellationSignal));
                 if (cursor == null) {
                     return null;
                 }
@@ -237,7 +239,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
             final String original = setCallingPackage(callingPkg);
             try {
                 return ContentProvider.this.query(
-                        uri, projection, selection, selectionArgs, sortOrder,
+                        uri, projection, queryArgs,
                         CancellationSignal.fromTransport(cancellationSignal));
             } finally {
                 setCallingPackage(original);
@@ -246,16 +248,16 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
 
         @Override
         public String getType(Uri uri) {
-            validateIncomingUri(uri);
-            uri = getUriWithoutUserId(uri);
+            uri = validateIncomingUri(uri);
+            uri = maybeGetUriWithoutUserId(uri);
             return ContentProvider.this.getType(uri);
         }
 
         @Override
         public Uri insert(String callingPkg, Uri uri, ContentValues initialValues) {
-            validateIncomingUri(uri);
+            uri = validateIncomingUri(uri);
             int userId = getUserIdFromUri(uri);
-            uri = getUriWithoutUserId(uri);
+            uri = maybeGetUriWithoutUserId(uri);
             if (enforceWritePermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) {
                 return rejectInsert(uri, initialValues);
             }
@@ -269,8 +271,8 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
 
         @Override
         public int bulkInsert(String callingPkg, Uri uri, ContentValues[] initialValues) {
-            validateIncomingUri(uri);
-            uri = getUriWithoutUserId(uri);
+            uri = validateIncomingUri(uri);
+            uri = maybeGetUriWithoutUserId(uri);
             if (enforceWritePermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) {
                 return 0;
             }
@@ -291,11 +293,12 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
             for (int i = 0; i < numOperations; i++) {
                 ContentProviderOperation operation = operations.get(i);
                 Uri uri = operation.getUri();
-                validateIncomingUri(uri);
                 userIds[i] = getUserIdFromUri(uri);
-                if (userIds[i] != UserHandle.USER_CURRENT) {
-                    // Removing the user id from the uri.
-                    operation = new ContentProviderOperation(operation, true);
+                uri = validateIncomingUri(uri);
+                uri = maybeGetUriWithoutUserId(uri);
+                // Rebuild operation if we changed the Uri above
+                if (!Objects.equals(operation.getUri(), uri)) {
+                    operation = new ContentProviderOperation(operation, uri);
                     operations.set(i, operation);
                 }
                 if (operation.isReadOperation()) {
@@ -330,8 +333,8 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
 
         @Override
         public int delete(String callingPkg, Uri uri, String selection, String[] selectionArgs) {
-            validateIncomingUri(uri);
-            uri = getUriWithoutUserId(uri);
+            uri = validateIncomingUri(uri);
+            uri = maybeGetUriWithoutUserId(uri);
             if (enforceWritePermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) {
                 return 0;
             }
@@ -346,8 +349,8 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
         @Override
         public int update(String callingPkg, Uri uri, ContentValues values, String selection,
                 String[] selectionArgs) {
-            validateIncomingUri(uri);
-            uri = getUriWithoutUserId(uri);
+            uri = validateIncomingUri(uri);
+            uri = maybeGetUriWithoutUserId(uri);
             if (enforceWritePermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) {
                 return 0;
             }
@@ -363,8 +366,8 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
         public ParcelFileDescriptor openFile(
                 String callingPkg, Uri uri, String mode, ICancellationSignal cancellationSignal,
                 IBinder callerToken) throws FileNotFoundException {
-            validateIncomingUri(uri);
-            uri = getUriWithoutUserId(uri);
+            uri = validateIncomingUri(uri);
+            uri = maybeGetUriWithoutUserId(uri);
             enforceFilePermission(callingPkg, uri, mode, callerToken);
             final String original = setCallingPackage(callingPkg);
             try {
@@ -379,8 +382,8 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
         public AssetFileDescriptor openAssetFile(
                 String callingPkg, Uri uri, String mode, ICancellationSignal cancellationSignal)
                 throws FileNotFoundException {
-            validateIncomingUri(uri);
-            uri = getUriWithoutUserId(uri);
+            uri = validateIncomingUri(uri);
+            uri = maybeGetUriWithoutUserId(uri);
             enforceFilePermission(callingPkg, uri, mode, null);
             final String original = setCallingPackage(callingPkg);
             try {
@@ -405,8 +408,8 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
 
         @Override
         public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
-            validateIncomingUri(uri);
-            uri = getUriWithoutUserId(uri);
+            uri = validateIncomingUri(uri);
+            uri = maybeGetUriWithoutUserId(uri);
             return ContentProvider.this.getStreamTypes(uri, mimeTypeFilter);
         }
 
@@ -414,8 +417,8 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
         public AssetFileDescriptor openTypedAssetFile(String callingPkg, Uri uri, String mimeType,
                 Bundle opts, ICancellationSignal cancellationSignal) throws FileNotFoundException {
             Bundle.setDefusable(opts, true);
-            validateIncomingUri(uri);
-            uri = getUriWithoutUserId(uri);
+            uri = validateIncomingUri(uri);
+            uri = maybeGetUriWithoutUserId(uri);
             enforceFilePermission(callingPkg, uri, "r", null);
             final String original = setCallingPackage(callingPkg);
             try {
@@ -433,7 +436,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
 
         @Override
         public Uri canonicalize(String callingPkg, Uri uri) {
-            validateIncomingUri(uri);
+            uri = validateIncomingUri(uri);
             int userId = getUserIdFromUri(uri);
             uri = getUriWithoutUserId(uri);
             if (enforceReadPermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) {
@@ -449,7 +452,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
 
         @Override
         public Uri uncanonicalize(String callingPkg, Uri uri) {
-            validateIncomingUri(uri);
+            uri = validateIncomingUri(uri);
             int userId = getUserIdFromUri(uri);
             uri = getUriWithoutUserId(uri);
             if (enforceReadPermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) {
@@ -463,6 +466,23 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
             }
         }
 
+        @Override
+        public boolean refresh(String callingPkg, Uri uri, Bundle args,
+                ICancellationSignal cancellationSignal) throws RemoteException {
+            uri = validateIncomingUri(uri);
+            uri = getUriWithoutUserId(uri);
+            if (enforceReadPermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) {
+                return false;
+            }
+            final String original = setCallingPackage(callingPkg);
+            try {
+                return ContentProvider.this.refresh(uri, args,
+                        CancellationSignal.fromTransport(cancellationSignal));
+            } finally {
+                setCallingPackage(original);
+            }
+        }
+
         private void enforceFilePermission(String callingPkg, Uri uri, String mode,
                 IBinder callerToken) throws FileNotFoundException, SecurityException {
             if (mode != null && mode.indexOf('w') != -1) {
@@ -602,12 +622,17 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
             return MODE_IGNORED;
         }
 
-        final String failReason = mExported
-                ? " requires " + missingPerm + ", or grantUriPermission()"
-                : " requires the provider be exported, or grantUriPermission()";
+        final String suffix;
+        if (android.Manifest.permission.MANAGE_DOCUMENTS.equals(mReadPermission)) {
+            suffix = " requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs";
+        } else if (mExported) {
+            suffix = " requires " + missingPerm + ", or grantUriPermission()";
+        } else {
+            suffix = " requires the provider be exported, or grantUriPermission()";
+        }
         throw new SecurityException("Permission Denial: reading "
                 + ContentProvider.this.getClass().getName() + " uri " + uri + " from pid=" + pid
-                + ", uid=" + uid + failReason);
+                + ", uid=" + uid + suffix);
     }
 
     /** {@hide} */
@@ -875,6 +900,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
      * (Content providers do not usually care about things like screen
      * orientation, but may want to know about locale changes.)
      */
+    @Override
     public void onConfigurationChanged(Configuration newConfig) {
     }
 
@@ -886,15 +912,22 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
      * <p>The default content provider implementation does nothing.
      * Subclasses may override this method to take appropriate action.
      */
+    @Override
     public void onLowMemory() {
     }
 
+    @Override
     public void onTrimMemory(int level) {
     }
 
     /**
      * Implement this to handle query requests from clients.
-     * This method can be called from multiple threads, as described in
+     *
+     * <p>Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher should override
+     * {@link #query(Uri, String[], Bundle, CancellationSignal)} and provide a stub
+     * implementation of this method.
+     *
+     * <p>This method can be called from multiple threads, as described in
      * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes
      * and Threads</a>.
      * <p>
@@ -953,7 +986,11 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
 
     /**
      * Implement this to handle query requests from clients with support for cancellation.
-     * This method can be called from multiple threads, as described in
+     *
+     * <p>Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher should override
+     * {@link #query(Uri, String[], Bundle, CancellationSignal)} instead of this method.
+     *
+     * <p>This method can be called from multiple threads, as described in
      * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes
      * and Threads</a>.
      * <p>
@@ -1010,7 +1047,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
      * @param sortOrder How the rows in the cursor should be sorted.
      *      If {@code null} then the provider is free to define the sort order.
      * @param cancellationSignal A signal to cancel the operation in progress, or {@code null} if none.
-     * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+     * If the operation is canceled, then {@link android.os.OperationCanceledException} will be thrown
      * when the query is executed.
      * @return a Cursor or {@code null}.
      */
@@ -1021,6 +1058,104 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
     }
 
     /**
+     * Implement this to handle query requests where the arguments are packed into a {@link Bundle}.
+     * Arguments may include traditional SQL style query arguments. When present these
+     * should be handled  according to the contract established in
+     * {@link #query(Uri, String[], String, String[], String, CancellationSignal).
+     *
+     * <p>Traditional SQL arguments can be found in the bundle using the following keys:
+     * <li>{@link ContentResolver#QUERY_ARG_SQL_SELECTION}
+     * <li>{@link ContentResolver#QUERY_ARG_SQL_SELECTION_ARGS}
+     * <li>{@link ContentResolver#QUERY_ARG_SQL_SORT_ORDER}
+     *
+     * <p>This method can be called from multiple threads, as described in
+     * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes
+     * and Threads</a>.
+     *
+     * <p>
+     * Example client call:<p>
+     * <pre>// Request 20 records starting at row index 30.
+       Bundle queryArgs = new Bundle();
+       queryArgs.putInt(ContentResolver.QUERY_ARG_OFFSET, 30);
+       queryArgs.putInt(ContentResolver.QUERY_ARG_LIMIT, 20);
+
+       Cursor cursor = getContentResolver().query(
+                contentUri,    // Content Uri is specific to individual content providers.
+                projection,    // String[] describing which columns to return.
+                queryArgs,     // Query arguments.
+                null);         // Cancellation signal.</pre>
+     *
+     * Example implementation:<p>
+     * <pre>
+
+        int recordsetSize = 0x1000;  // Actual value is implementation specific.
+        queryArgs = queryArgs != null ? queryArgs : Bundle.EMPTY;  // ensure queryArgs is non-null
+
+        int offset = queryArgs.getInt(ContentResolver.QUERY_ARG_OFFSET, 0);
+        int limit = queryArgs.getInt(ContentResolver.QUERY_ARG_LIMIT, Integer.MIN_VALUE);
+
+        MatrixCursor c = new MatrixCursor(PROJECTION, limit);
+
+        // Calculate the number of items to include in the cursor.
+        int numItems = MathUtils.constrain(recordsetSize - offset, 0, limit);
+
+        // Build the paged result set....
+        for (int i = offset; i < offset + numItems; i++) {
+            // populate row from your data.
+        }
+
+        Bundle extras = new Bundle();
+        c.setExtras(extras);
+
+        // Any QUERY_ARG_* key may be included if honored.
+        // In an actual implementation, include only keys that are both present in queryArgs
+        // and reflected in the Cursor output. For example, if QUERY_ARG_OFFSET were included
+        // in queryArgs, but was ignored because it contained an invalid value (like –273),
+        // then QUERY_ARG_OFFSET should be omitted.
+        extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, new String[] {
+            ContentResolver.QUERY_ARG_OFFSET,
+            ContentResolver.QUERY_ARG_LIMIT
+        });
+
+        extras.putInt(ContentResolver.EXTRA_TOTAL_COUNT, recordsetSize);
+
+        cursor.setNotificationUri(getContext().getContentResolver(), uri);
+
+        return cursor;</pre>
+     * <p>
+     * @see #query(Uri, String[], String, String[], String, CancellationSignal) for
+     *     implementation details.
+     *
+     * @param uri The URI to query. This will be the full URI sent by the client.
+     * @param projection The list of columns to put into the cursor.
+     *            If {@code null} provide a default set of columns.
+     * @param queryArgs A Bundle containing all additional information necessary for the query.
+     *            Values in the Bundle may include SQL style arguments.
+     * @param cancellationSignal A signal to cancel the operation in progress,
+     *            or {@code null}.
+     * @return a Cursor or {@code null}.
+     */
+    public @Nullable Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+            @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) {
+        queryArgs = queryArgs != null ? queryArgs : Bundle.EMPTY;
+
+        // if client doesn't supply an SQL sort order argument, attempt to build one from
+        // QUERY_ARG_SORT* arguments.
+        String sortClause = queryArgs.getString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER);
+        if (sortClause == null && queryArgs.containsKey(ContentResolver.QUERY_ARG_SORT_COLUMNS)) {
+            sortClause = ContentResolver.createSqlSortClause(queryArgs);
+        }
+
+        return query(
+                uri,
+                projection,
+                queryArgs.getString(ContentResolver.QUERY_ARG_SQL_SELECTION),
+                queryArgs.getStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS),
+                sortClause,
+                cancellationSignal);
+    }
+
+    /**
      * Implement this to handle requests for the MIME type of the data at the
      * given URI.  The returned MIME type should start with
      * <code>vnd.android.cursor.item</code> for a single record,
@@ -1093,6 +1228,33 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
     }
 
     /**
+     * Implement this to support refresh of content identified by {@code uri}. By default, this
+     * method returns false; providers who wish to implement this should return true to signal the
+     * client that the provider has tried refreshing with its own implementation.
+     * <p>
+     * This allows clients to request an explicit refresh of content identified by {@code uri}.
+     * <p>
+     * Client code should only invoke this method when there is a strong indication (such as a user
+     * initiated pull to refresh gesture) that the content is stale.
+     * <p>
+     * Remember to send {@link ContentResolver#notifyChange(Uri, android.database.ContentObserver)}
+     * notifications when content changes.
+     *
+     * @param uri The Uri identifying the data to refresh.
+     * @param args Additional options from the client. The definitions of these are specific to the
+     *            content provider being called.
+     * @param cancellationSignal A signal to cancel the operation in progress, or {@code null} if
+     *            none. For example, if you called refresh on a particular uri, you should call
+     *            {@link CancellationSignal#throwIfCanceled()} to check whether the client has
+     *            canceled the refresh request.
+     * @return true if the provider actually tried refreshing.
+     */
+    public boolean refresh(Uri uri, @Nullable Bundle args,
+            @Nullable CancellationSignal cancellationSignal) {
+        return false;
+    }
+
+    /**
      * @hide
      * Implementation when a caller has performed an insert on the content
      * provider, but that call has been rejected for the operation given
@@ -1215,6 +1377,12 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
      * android.os.Handler, android.os.ParcelFileDescriptor.OnCloseListener)},
      * {@link ParcelFileDescriptor#createReliablePipe()}, or
      * {@link ParcelFileDescriptor#createReliableSocketPair()}.
+     * <p>
+     * If you need to return a large file that isn't backed by a real file on
+     * disk, such as a file on a network share or cloud storage service,
+     * consider using
+     * {@link StorageManager#openProxyFileDescriptor(int, android.os.ProxyFileDescriptorCallback, android.os.Handler)}
+     * which will let you to stream the content on-demand.
      *
      * <p class="note">For use in Intents, you will want to implement {@link #getType}
      * to return the appropriate MIME type for the data returned here with
@@ -1367,7 +1535,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
      * no file associated with the given URI or the mode is invalid.
      * @throws SecurityException Throws SecurityException if the caller does
      * not have permission to access the file.
-     * 
+     *
      * @see #openFile(Uri, String)
      * @see #openFileHelper(Uri, String)
      * @see #getType(android.net.Uri)
@@ -1735,7 +1903,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
          */
         if (mContext == null) {
             mContext = context;
-            if (context != null) {
+            if (context != null && mTransport != null) {
                 mTransport.mAppOpsManager = (AppOpsManager) context.getSystemService(
                         Context.APP_OPS_SERVICE);
             }
@@ -1806,7 +1974,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
     /**
      * Implement this to shut down the ContentProvider instance. You can then
      * invoke this method in unit tests.
-     * 
+     *
      * <p>
      * Android normally handles ContentProvider startup and shutdown
      * automatically. You do not need to start up or shut down a
@@ -1844,12 +2012,14 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
     }
 
     /** @hide */
-    private void validateIncomingUri(Uri uri) throws SecurityException {
+    public Uri validateIncomingUri(Uri uri) throws SecurityException {
         String auth = uri.getAuthority();
-        int userId = getUserIdFromAuthority(auth, UserHandle.USER_CURRENT);
-        if (userId != UserHandle.USER_CURRENT && userId != mContext.getUserId()) {
-            throw new SecurityException("trying to query a ContentProvider in user "
-                    + mContext.getUserId() + " with a uri belonging to user " + userId);
+        if (!mSingleUser) {
+            int userId = getUserIdFromAuthority(auth, UserHandle.USER_CURRENT);
+            if (userId != UserHandle.USER_CURRENT && userId != mContext.getUserId()) {
+                throw new SecurityException("trying to query a ContentProvider in user "
+                        + mContext.getUserId() + " with a uri belonging to user " + userId);
+            }
         }
         if (!matchesOurAuthorities(getAuthorityWithoutUserId(auth))) {
             String message = "The authority of the uri " + uri + " does not match the one of the "
@@ -1861,6 +2031,27 @@ public abstract class ContentProvider implements ComponentCallbacks2 {
             }
             throw new SecurityException(message);
         }
+
+        // Normalize the path by removing any empty path segments, which can be
+        // a source of security issues.
+        final String encodedPath = uri.getEncodedPath();
+        if (encodedPath != null && encodedPath.indexOf("//") != -1) {
+            final Uri normalized = uri.buildUpon()
+                    .encodedPath(encodedPath.replaceAll("//+", "/")).build();
+            Log.w(TAG, "Normalized " + uri + " to " + normalized
+                    + " to avoid possible security issues");
+            return normalized;
+        } else {
+            return uri;
+        }
+    }
+
+    /** @hide */
+    private Uri maybeGetUriWithoutUserId(Uri uri) {
+        if (mSingleUser) {
+            return uri;
+        }
+        return getUriWithoutUserId(uri);
     }
 
     /** @hide */