import android.annotation.RequiresPermission;
import android.annotation.TestApi;
import android.annotation.UserIdInt;
-import android.app.ActivityManagerNative;
+import android.app.ActivityManager;
import android.app.ActivityThread;
import android.app.AppGlobals;
import android.content.pm.PackageManager.NameNotFoundException;
public static final String EXTRA_SIZE = "android.content.extra.SIZE";
/**
+ * An extra boolean describing whether a particular provider supports refresh
+ * or not. If a provider supports refresh, it should include this key in its
+ * returned Cursor as part of its query call.
+ *
+ */
+ public static final String EXTRA_REFRESH_SUPPORTED = "android.content.extra.REFRESH_SUPPORTED";
+
+ /**
+ * Key for an SQL style selection string that may be present in the query Bundle argument
+ * passed to {@link ContentProvider#query(Uri, String[], Bundle, CancellationSignal)}
+ * when called by a legacy client.
+ *
+ * <p>Clients should never include user supplied values directly in the selection string,
+ * as this presents an avenue for SQL injection attacks. In lieu of this, a client
+ * should use standard placeholder notation to represent values in a selection string,
+ * then supply a corresponding value in {@value #QUERY_ARG_SQL_SELECTION_ARGS}.
+ *
+ * <p><b>Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher are strongly
+ * encourage to use structured query arguments in lieu of opaque SQL query clauses.</b>
+ *
+ * @see #QUERY_ARG_SORT_COLUMNS
+ * @see #QUERY_ARG_SORT_DIRECTION
+ * @see #QUERY_ARG_SORT_COLLATION
+ */
+ public static final String QUERY_ARG_SQL_SELECTION = "android:query-arg-sql-selection";
+
+ /**
+ * Key for SQL selection string arguments list.
+ *
+ * <p>Clients should never include user supplied values directly in the selection string,
+ * as this presents an avenue for SQL injection attacks. In lieu of this, a client
+ * should use standard placeholder notation to represent values in a selection string,
+ * then supply a corresponding value in {@value #QUERY_ARG_SQL_SELECTION_ARGS}.
+ *
+ * <p><b>Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher are strongly
+ * encourage to use structured query arguments in lieu of opaque SQL query clauses.</b>
+ *
+ * @see #QUERY_ARG_SORT_COLUMNS
+ * @see #QUERY_ARG_SORT_DIRECTION
+ * @see #QUERY_ARG_SORT_COLLATION
+ */
+ public static final String QUERY_ARG_SQL_SELECTION_ARGS =
+ "android:query-arg-sql-selection-args";
+
+ /**
+ * Key for an SQL style sort string that may be present in the query Bundle argument
+ * passed to {@link ContentProvider#query(Uri, String[], Bundle, CancellationSignal)}
+ * when called by a legacy client.
+ *
+ * <p><b>Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher are strongly
+ * encourage to use structured query arguments in lieu of opaque SQL query clauses.</b>
+ *
+ * @see #QUERY_ARG_SORT_COLUMNS
+ * @see #QUERY_ARG_SORT_DIRECTION
+ * @see #QUERY_ARG_SORT_COLLATION
+ */
+ public static final String QUERY_ARG_SQL_SORT_ORDER = "android:query-arg-sql-sort-order";
+
+ /**
+ * Specifies the list of columns against which to sort results. When first column values
+ * are identical, records are then sorted based on second column values, and so on.
+ *
+ * <p>Columns present in this list must also be included in the projection
+ * supplied to {@link ContentResolver#query(Uri, String[], Bundle, CancellationSignal)}.
+ *
+ * <p>Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher:
+ *
+ * <li>{@link ContentProvider} implementations: When preparing data in
+ * {@link ContentProvider#query(Uri, String[], Bundle, CancellationSignal)}, if sort columns
+ * is reflected in the returned Cursor, it is strongly recommended that
+ * {@link #QUERY_ARG_SORT_COLUMNS} then be included in the array of honored arguments
+ * reflected in {@link Cursor} extras {@link Bundle} under {@link #EXTRA_HONORED_ARGS}.
+ *
+ * <li>When querying a provider, where no QUERY_ARG_SQL* otherwise exists in the
+ * arguments {@link Bundle}, the Content framework will attempt to synthesize
+ * an QUERY_ARG_SQL* argument using the corresponding QUERY_ARG_SORT* values.
+ */
+ public static final String QUERY_ARG_SORT_COLUMNS = "android:query-arg-sort-columns";
+
+ /**
+ * Specifies desired sort order. When unspecified a provider may provide a default
+ * sort direction, or choose to return unsorted results.
+ *
+ * <p>Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher:
+ *
+ * <li>{@link ContentProvider} implementations: When preparing data in
+ * {@link ContentProvider#query(Uri, String[], Bundle, CancellationSignal)}, if sort direction
+ * is reflected in the returned Cursor, it is strongly recommended that
+ * {@link #QUERY_ARG_SORT_DIRECTION} then be included in the array of honored arguments
+ * reflected in {@link Cursor} extras {@link Bundle} under {@link #EXTRA_HONORED_ARGS}.
+ *
+ * <li>When querying a provider, where no QUERY_ARG_SQL* otherwise exists in the
+ * arguments {@link Bundle}, the Content framework will attempt to synthesize
+ * a QUERY_ARG_SQL* argument using the corresponding QUERY_ARG_SORT* values.
+ *
+ * @see #QUERY_SORT_DIRECTION_ASCENDING
+ * @see #QUERY_SORT_DIRECTION_DESCENDING
+ */
+ public static final String QUERY_ARG_SORT_DIRECTION = "android:query-arg-sort-direction";
+
+ /**
+ * Allows client to specify a hint to the provider declaring which collation
+ * to use when sorting text values.
+ *
+ * <p>Providers may support custom collators. When specifying a custom collator
+ * the value is determined by the Provider.
+ *
+ * <li>{@link ContentProvider} implementations: When preparing data in
+ * {@link ContentProvider#query(Uri, String[], Bundle, CancellationSignal)}, if sort collation
+ * is reflected in the returned Cursor, it is strongly recommended that
+ * {@link #QUERY_ARG_SORT_COLLATION} then be included in the array of honored arguments
+ * reflected in {@link Cursor} extras {@link Bundle} under {@link #EXTRA_HONORED_ARGS}.
+ *
+ * <li>When querying a provider, where no QUERY_ARG_SQL* otherwise exists in the
+ * arguments {@link Bundle}, the Content framework will attempt to synthesize
+ * a QUERY_ARG_SQL* argument using the corresponding QUERY_ARG_SORT* values.
+ *
+ * @see java.text.Collator#PRIMARY
+ * @see java.text.Collator#SECONDARY
+ * @see java.text.Collator#TERTIARY
+ * @see java.text.Collator#IDENTICAL
+ */
+ public static final String QUERY_ARG_SORT_COLLATION = "android:query-arg-sort-collation";
+
+ /**
+ * Allows provider to report back to client which query keys are honored in a Cursor.
+ *
+ * <p>Key identifying a {@code String[]} containing all QUERY_ARG_SORT* arguments
+ * honored by the provider. Include this in {@link Cursor} extras {@link Bundle}
+ * when any QUERY_ARG_SORT* value was honored during the preparation of the
+ * results {@link Cursor}.
+ *
+ * <p>If present, ALL honored arguments are enumerated in this extra’s payload.
+ *
+ * @see #QUERY_ARG_SORT_COLUMNS
+ * @see #QUERY_ARG_SORT_DIRECTION
+ * @see #QUERY_ARG_SORT_COLLATION
+ */
+ public static final String EXTRA_HONORED_ARGS = "android.content.extra.HONORED_ARGS";
+
+ /** @hide */
+ @IntDef(flag = false, value = {
+ QUERY_SORT_DIRECTION_ASCENDING,
+ QUERY_SORT_DIRECTION_DESCENDING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SortDirection {}
+ public static final int QUERY_SORT_DIRECTION_ASCENDING = 0;
+ public static final int QUERY_SORT_DIRECTION_DESCENDING = 1;
+
+ /**
+ * @see {@link java.text.Collector} for details on respective collation strength.
+ * @hide
+ */
+ @IntDef(flag = false, value = {
+ java.text.Collator.PRIMARY,
+ java.text.Collator.SECONDARY,
+ java.text.Collator.TERTIARY,
+ java.text.Collator.IDENTICAL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface QueryCollator {}
+
+ /**
+ * Specifies the offset row index within a Cursor.
+ */
+ public static final String QUERY_ARG_OFFSET = "android:query-arg-offset";
+
+ /**
+ * Specifies the max number of rows to include in a Cursor.
+ */
+ public static final String QUERY_ARG_LIMIT = "android:query-arg-limit";
+
+ /**
+ * Added to {@link Cursor} extras {@link Bundle} to indicate total row count of
+ * recordset when paging is supported. Providers must include this when
+ * implementing paging support.
+ *
+ * <p>A provider may return -1 that row count of the recordset is unknown.
+ *
+ * <p>Providers having returned -1 in a previous query are recommended to
+ * send content change notification once (if) full recordset size becomes
+ * known.
+ */
+ public static final String EXTRA_TOTAL_COUNT = "android.content.extra.TOTAL_COUNT";
+
+ /**
* This is the Android platform's base MIME type for a content: URI
* containing a Cursor of a single item. Applications should use this
* as the base type along with their own sub-type of their content: URIs
public ContentResolver(Context context) {
mContext = context != null ? context : ActivityThread.currentApplication();
mPackageName = mContext.getOpPackageName();
+ mTargetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
}
/** @hide */
}
try {
- String type = ActivityManagerNative.getDefault().getProviderMimeType(
+ String type = ActivityManager.getService().getProviderMimeType(
ContentProvider.getUriWithoutUserId(url), resolveUserId(url));
return type;
} catch (RemoteException e) {
* @return A Cursor object, which is positioned before the first entry, or null
* @see Cursor
*/
- public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
+ public final @Nullable Cursor query(@RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder,
@Nullable CancellationSignal cancellationSignal) {
+ Bundle queryArgs = createSqlQueryBundle(selection, selectionArgs, sortOrder);
+ return query(uri, projection, queryArgs, cancellationSignal);
+ }
+
+ /**
+ * Query the given URI, returning a {@link Cursor} over the result set
+ * with support for cancellation.
+ *
+ * <p>For best performance, the caller should follow these guidelines:
+ *
+ * <li>Provide an explicit projection, to prevent reading data from storage
+ * that aren't going to be used.
+ *
+ * Provider must identify which QUERY_ARG_SORT* arguments were honored during
+ * the preparation of the result set by including the respective argument keys
+ * in the {@link Cursor} extras {@link Bundle}. See {@link #EXTRA_HONORED_ARGS}
+ * for details.
+ *
+ * @see #QUERY_ARG_SORT_COLUMNS, #QUERY_ARG_SORT_DIRECTION, #QUERY_ARG_SORT_COLLATION.
+ *
+ * @param uri The URI, using the content:// scheme, for the content to
+ * retrieve.
+ * @param projection A list of which columns to return. Passing null will
+ * return all columns, which is inefficient.
+ * @param queryArgs A Bundle containing any arguments to the query.
+ * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
+ * If the operation is canceled, then {@link OperationCanceledException} will be thrown
+ * when the query is executed.
+ * @return A Cursor object, which is positioned before the first entry, or null
+ * @see Cursor
+ */
+ public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
+ @Nullable String[] projection, @Nullable Bundle queryArgs,
+ @Nullable CancellationSignal cancellationSignal) {
Preconditions.checkNotNull(uri, "uri");
IContentProvider unstableProvider = acquireUnstableProvider(uri);
if (unstableProvider == null) {
}
try {
qCursor = unstableProvider.query(mPackageName, uri, projection,
- selection, selectionArgs, sortOrder, remoteCancellationSignal);
+ queryArgs, remoteCancellationSignal);
} catch (DeadObjectException e) {
// The remote process has died... but we only hold an unstable
// reference though, so we might recover!!! Let's try!!!!
if (stableProvider == null) {
return null;
}
- qCursor = stableProvider.query(mPackageName, uri, projection,
- selection, selectionArgs, sortOrder, remoteCancellationSignal);
+ qCursor = stableProvider.query(
+ mPackageName, uri, projection, queryArgs, remoteCancellationSignal);
}
if (qCursor == null) {
return null;
// Force query execution. Might fail and throw a runtime exception here.
qCursor.getCount();
long durationMillis = SystemClock.uptimeMillis() - startTime;
- maybeLogQueryToEventLog(durationMillis, uri, projection, selection, sortOrder);
+ maybeLogQueryToEventLog(durationMillis, uri, projection, queryArgs);
// Wrap the cursor object into CursorWrapperInner object.
final IContentProvider provider = (stableProvider != null) ? stableProvider
}
/**
+ * 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>
+ *
+ * @param url 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 final boolean refresh(@NonNull Uri url, @Nullable Bundle args,
+ @Nullable CancellationSignal cancellationSignal) {
+ Preconditions.checkNotNull(url, "url");
+ IContentProvider provider = acquireProvider(url);
+ if (provider == null) {
+ return false;
+ }
+
+ try {
+ ICancellationSignal remoteCancellationSignal = null;
+ if (cancellationSignal != null) {
+ cancellationSignal.throwIfCanceled();
+ remoteCancellationSignal = provider.createCancellationSignal();
+ cancellationSignal.setRemote(remoteCancellationSignal);
+ }
+ return provider.refresh(mPackageName, url, args, remoteCancellationSignal);
+ } catch (RemoteException e) {
+ // Arbitrary and not worth documenting, as Activity
+ // Manager will kill this process shortly anyway.
+ return false;
+ } finally {
+ releaseProvider(provider);
+ }
+ }
+
+ /**
* Open a stream on to the content associated with a content URI. If there
* is no data associated with the URI, FileNotFoundException is thrown.
*
/**
* Register an observer class that gets callbacks when data identified by a
* given content URI changes.
- *
- * @param uri The URI to watch for changes. This can be a specific row URI, or a base URI
- * for a whole class of content.
- * @param notifyForDescendants When false, the observer will be notified whenever a
- * change occurs to the exact URI specified by <code>uri</code> or to one of the
- * URI's ancestors in the path hierarchy. When true, the observer will also be notified
- * whenever a change occurs to the URI's descendants in the path hierarchy.
+ * <p>
+ * Starting in {@link android.os.Build.VERSION_CODES#O}, all content
+ * notifications must be backed by a valid {@link ContentProvider}.
+ *
+ * @param uri The URI to watch for changes. This can be a specific row URI,
+ * or a base URI for a whole class of content.
+ * @param notifyForDescendants When false, the observer will be notified
+ * whenever a change occurs to the exact URI specified by
+ * <code>uri</code> or to one of the URI's ancestors in the path
+ * hierarchy. When true, the observer will also be notified
+ * whenever a change occurs to the URI's descendants in the path
+ * hierarchy.
* @param observer The object that receives callbacks when changes occur.
* @see #unregisterContentObserver
*/
ContentProvider.getUriWithoutUserId(uri),
notifyForDescendants,
observer,
- ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId()));
+ ContentProvider.getUserIdFromUri(uri, mContext.getUserId()));
}
/** @hide - designated user version */
ContentObserver observer, @UserIdInt int userHandle) {
try {
getContentService().registerContentObserver(uri, notifyForDescendents,
- observer.getContentObserver(), userHandle);
+ observer.getContentObserver(), userHandle, mTargetSdkVersion);
} catch (RemoteException e) {
}
}
}
/**
- * Notify registered observers that a row was updated and attempt to sync changes
- * to the network.
- * To register, call {@link #registerContentObserver(android.net.Uri , boolean, android.database.ContentObserver) registerContentObserver()}.
- * By default, CursorAdapter objects will get this notification.
+ * Notify registered observers that a row was updated and attempt to sync
+ * changes to the network.
+ * <p>
+ * To observe events sent through this call, use
+ * {@link #registerContentObserver(Uri, boolean, ContentObserver)}.
+ * <p>
+ * Starting in {@link android.os.Build.VERSION_CODES#O}, all content
+ * notifications must be backed by a valid {@link ContentProvider}.
*
* @param uri The uri of the content that was changed.
- * @param observer The observer that originated the change, may be <code>null</null>.
- * The observer that originated the change will only receive the notification if it
- * has requested to receive self-change notifications by implementing
- * {@link ContentObserver#deliverSelfNotifications()} to return true.
+ * @param observer The observer that originated the change, may be
+ * <code>null</null>. The observer that originated the change
+ * will only receive the notification if it has requested to
+ * receive self-change notifications by implementing
+ * {@link ContentObserver#deliverSelfNotifications()} to return
+ * true.
*/
public void notifyChange(@NonNull Uri uri, @Nullable ContentObserver observer) {
notifyChange(uri, observer, true /* sync to network */);
/**
* Notify registered observers that a row was updated.
- * To register, call {@link #registerContentObserver(android.net.Uri , boolean, android.database.ContentObserver) registerContentObserver()}.
- * By default, CursorAdapter objects will get this notification.
- * If syncToNetwork is true, this will attempt to schedule a local sync using the sync
- * adapter that's registered for the authority of the provided uri. No account will be
- * passed to the sync adapter, so all matching accounts will be synchronized.
+ * <p>
+ * To observe events sent through this call, use
+ * {@link #registerContentObserver(Uri, boolean, ContentObserver)}.
+ * <p>
+ * If syncToNetwork is true, this will attempt to schedule a local sync
+ * using the sync adapter that's registered for the authority of the
+ * provided uri. No account will be passed to the sync adapter, so all
+ * matching accounts will be synchronized.
+ * <p>
+ * Starting in {@link android.os.Build.VERSION_CODES#O}, all content
+ * notifications must be backed by a valid {@link ContentProvider}.
*
* @param uri The uri of the content that was changed.
- * @param observer The observer that originated the change, may be <code>null</null>.
- * The observer that originated the change will only receive the notification if it
- * has requested to receive self-change notifications by implementing
- * {@link ContentObserver#deliverSelfNotifications()} to return true.
+ * @param observer The observer that originated the change, may be
+ * <code>null</null>. The observer that originated the change
+ * will only receive the notification if it has requested to
+ * receive self-change notifications by implementing
+ * {@link ContentObserver#deliverSelfNotifications()} to return
+ * true.
* @param syncToNetwork If true, same as {@link #NOTIFY_SYNC_TO_NETWORK}.
* @see #requestSync(android.accounts.Account, String, android.os.Bundle)
*/
ContentProvider.getUriWithoutUserId(uri),
observer,
syncToNetwork,
- ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId()));
+ ContentProvider.getUserIdFromUri(uri, mContext.getUserId()));
}
/**
* Notify registered observers that a row was updated.
- * To register, call {@link #registerContentObserver(android.net.Uri, boolean, android.database.ContentObserver) registerContentObserver()}.
- * By default, CursorAdapter objects will get this notification.
- * If syncToNetwork is true, this will attempt to schedule a local sync using the sync
- * adapter that's registered for the authority of the provided uri. No account will be
- * passed to the sync adapter, so all matching accounts will be synchronized.
+ * <p>
+ * To observe events sent through this call, use
+ * {@link #registerContentObserver(Uri, boolean, ContentObserver)}.
+ * <p>
+ * If syncToNetwork is true, this will attempt to schedule a local sync
+ * using the sync adapter that's registered for the authority of the
+ * provided uri. No account will be passed to the sync adapter, so all
+ * matching accounts will be synchronized.
+ * <p>
+ * Starting in {@link android.os.Build.VERSION_CODES#O}, all content
+ * notifications must be backed by a valid {@link ContentProvider}.
*
* @param uri The uri of the content that was changed.
- * @param observer The observer that originated the change, may be <code>null</null>.
- * The observer that originated the change will only receive the notification if it
- * has requested to receive self-change notifications by implementing
- * {@link ContentObserver#deliverSelfNotifications()} to return true.
+ * @param observer The observer that originated the change, may be
+ * <code>null</null>. The observer that originated the change
+ * will only receive the notification if it has requested to
+ * receive self-change notifications by implementing
+ * {@link ContentObserver#deliverSelfNotifications()} to return
+ * true.
* @param flags Additional flags: {@link #NOTIFY_SYNC_TO_NETWORK}.
* @see #requestSync(android.accounts.Account, String, android.os.Bundle)
*/
ContentProvider.getUriWithoutUserId(uri),
observer,
flags,
- ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId()));
+ ContentProvider.getUserIdFromUri(uri, mContext.getUserId()));
}
/**
uri, observer == null ? null : observer.getContentObserver(),
observer != null && observer.deliverSelfNotifications(),
syncToNetwork ? NOTIFY_SYNC_TO_NETWORK : 0,
- userHandle);
+ userHandle, mTargetSdkVersion);
} catch (RemoteException e) {
}
}
getContentService().notifyChange(
uri, observer == null ? null : observer.getContentObserver(),
observer != null && observer.deliverSelfNotifications(), flags,
- userHandle);
+ userHandle, mTargetSdkVersion);
} catch (RemoteException e) {
}
}
@Intent.AccessUriMode int modeFlags) {
Preconditions.checkNotNull(uri, "uri");
try {
- ActivityManagerNative.getDefault().takePersistableUriPermission(
+ ActivityManager.getService().takePersistableUriPermission(
ContentProvider.getUriWithoutUserId(uri), modeFlags, resolveUserId(uri));
} catch (RemoteException e) {
}
@Intent.AccessUriMode int modeFlags) {
Preconditions.checkNotNull(uri, "uri");
try {
- ActivityManagerNative.getDefault().releasePersistableUriPermission(
+ ActivityManager.getService().releasePersistableUriPermission(
ContentProvider.getUriWithoutUserId(uri), modeFlags, resolveUserId(uri));
} catch (RemoteException e) {
}
*/
public @NonNull List<UriPermission> getPersistedUriPermissions() {
try {
- return ActivityManagerNative.getDefault()
+ return ActivityManager.getService()
.getPersistedUriPermissions(mPackageName, true).getList();
} catch (RemoteException e) {
throw new RuntimeException("Activity manager has died", e);
*/
public @NonNull List<UriPermission> getOutgoingPersistedUriPermissions() {
try {
- return ActivityManagerNative.getDefault()
+ return ActivityManager.getService()
.getPersistedUriPermissions(mPackageName, false).getList();
} catch (RemoteException e) {
throw new RuntimeException("Activity manager has died", e);
}
try {
ISyncStatusObserver.Stub observer = new ISyncStatusObserver.Stub() {
+ @Override
public void onStatusChanged(int which) throws RemoteException {
callback.onStatusChanged(which);
}
}
}
+ /** {@hide} */
+ public int getTargetSdkVersion() {
+ return mTargetSdkVersion;
+ }
+
/**
* Returns sampling percentage for a given duration.
*
return (int) (100 * durationMillis / SLOW_THRESHOLD_MILLIS) + 1;
}
- private void maybeLogQueryToEventLog(long durationMillis,
- Uri uri, String[] projection,
- String selection, String sortOrder) {
+ private void maybeLogQueryToEventLog(
+ long durationMillis, Uri uri, String[] projection, @Nullable Bundle queryArgs) {
if (!ENABLE_CONTENT_SAMPLE) return;
int samplePercent = samplePercentForDuration(durationMillis);
if (samplePercent < 100) {
}
}
+ // Ensure a non-null bundle.
+ queryArgs = (queryArgs != null) ? queryArgs : Bundle.EMPTY;
+
StringBuilder projectionBuffer = new StringBuilder(100);
if (projection != null) {
for (int i = 0; i < projection.length; ++i) {
EventLogTags.CONTENT_QUERY_SAMPLE,
uri.toString(),
projectionBuffer.toString(),
- selection != null ? selection : "",
- sortOrder != null ? sortOrder : "",
+ queryArgs.getString(QUERY_ARG_SQL_SELECTION, ""),
+ queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER, ""),
durationMillis,
blockingPackage != null ? blockingPackage : "",
samplePercent);
@Override
protected void finalize() throws Throwable {
try {
- mCloseGuard.warnIfOpen();
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+
close();
} finally {
super.finalize();
private final Context mContext;
final String mPackageName;
+ final int mTargetSdkVersion;
private static final String TAG = "ContentResolver";
public Drawable getTypeDrawable(String mimeType) {
return MimeIconUtils.loadMimeIcon(mContext, mimeType);
}
+
+ /**
+ * @hide
+ */
+ public static @Nullable Bundle createSqlQueryBundle(
+ @Nullable String selection,
+ @Nullable String[] selectionArgs,
+ @Nullable String sortOrder) {
+
+ if (selection == null && selectionArgs == null && sortOrder == null) {
+ return null;
+ }
+
+ Bundle queryArgs = new Bundle();
+ if (selection != null) {
+ queryArgs.putString(QUERY_ARG_SQL_SELECTION, selection);
+ }
+ if (selectionArgs != null) {
+ queryArgs.putStringArray(QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs);
+ }
+ if (sortOrder != null) {
+ queryArgs.putString(QUERY_ARG_SQL_SORT_ORDER, sortOrder);
+ }
+ return queryArgs;
+ }
+
+ /**
+ * Returns structured sort args formatted as an SQL sort clause.
+ *
+ * NOTE: Collator clauses are suitable for use with non text fields. We might
+ * choose to omit any collation clause since we don't know the underlying
+ * type of data to be collated. Imperical testing shows that sqlite3 doesn't
+ * appear to care much about the presence of collate clauses in queries
+ * when ordering by numeric fields. For this reason we include collate
+ * clause unilaterally when {@link #QUERY_ARG_SORT_COLLATION} is present
+ * in query args bundle.
+ *
+ * TODO: Would be nice to explicitly validate that colums referenced in
+ * {@link #QUERY_ARG_SORT_COLUMNS} are present in the associated projection.
+ *
+ * @hide
+ */
+ public static String createSqlSortClause(Bundle queryArgs) {
+ String[] columns = queryArgs.getStringArray(QUERY_ARG_SORT_COLUMNS);
+ if (columns == null || columns.length == 0) {
+ throw new IllegalArgumentException("Can't create sort clause without columns.");
+ }
+
+ String query = TextUtils.join(", ", columns);
+
+ // Interpret PRIMARY and SECONDARY collation strength as no-case collation based
+ // on their javadoc descriptions.
+ int collation = queryArgs.getInt(
+ ContentResolver.QUERY_ARG_SORT_COLLATION, java.text.Collator.IDENTICAL);
+ if (collation == java.text.Collator.PRIMARY || collation == java.text.Collator.SECONDARY) {
+ query += " COLLATE NOCASE";
+ }
+
+ int sortDir = queryArgs.getInt(QUERY_ARG_SORT_DIRECTION, Integer.MIN_VALUE);
+ if (sortDir != Integer.MIN_VALUE) {
+ switch (sortDir) {
+ case QUERY_SORT_DIRECTION_ASCENDING:
+ query += " ASC";
+ break;
+ case QUERY_SORT_DIRECTION_DESCENDING:
+ query += " DESC";
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported sort direction value."
+ + " See ContentResolver documentation for details.");
+ }
+ }
+ return query;
+ }
}