diff options
| -rw-r--r-- | api/current.txt | 6 | ||||
| -rw-r--r-- | core/java/android/content/ContentResolver.java | 7 | ||||
| -rw-r--r-- | core/java/android/content/ContentValues.java | 120 | ||||
| -rw-r--r-- | core/java/android/database/sqlite/SQLiteDatabase.java | 6 | ||||
| -rw-r--r-- | core/java/android/database/sqlite/SQLiteQueryBuilder.java | 467 | ||||
| -rw-r--r-- | core/java/com/android/internal/util/ArrayUtils.java | 17 | ||||
| -rw-r--r-- | core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java | 64 |
7 files changed, 543 insertions, 144 deletions
diff --git a/api/current.txt b/api/current.txt index f4c6a8f29d23..f8676dad891f 100644 --- a/api/current.txt +++ b/api/current.txt @@ -12670,22 +12670,28 @@ package android.database.sqlite { ctor public SQLiteQueryBuilder(); method public static void appendColumns(java.lang.StringBuilder, java.lang.String[]); method public void appendWhere(java.lang.CharSequence); + method public void appendWhere(java.lang.CharSequence, java.lang.String...); method public void appendWhereEscapeString(java.lang.String); + method public void appendWhereEscapeString(java.lang.String, java.lang.String...); method public java.lang.String buildQuery(java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String); method public deprecated java.lang.String buildQuery(java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String); method public static java.lang.String buildQueryString(boolean, java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String); method public java.lang.String buildUnionQuery(java.lang.String[], java.lang.String, java.lang.String); method public java.lang.String buildUnionSubQuery(java.lang.String, java.lang.String[], java.util.Set<java.lang.String>, int, java.lang.String, java.lang.String, java.lang.String, java.lang.String); method public deprecated java.lang.String buildUnionSubQuery(java.lang.String, java.lang.String[], java.util.Set<java.lang.String>, int, java.lang.String, java.lang.String, java.lang.String[], java.lang.String, java.lang.String); + method public int delete(android.database.sqlite.SQLiteDatabase, java.lang.String, java.lang.String[]); method public java.lang.String getTables(); method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String); method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String); + method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, android.os.CancellationSignal); method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, android.os.CancellationSignal); + method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], android.os.Bundle, android.os.CancellationSignal); method public void setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory); method public void setDistinct(boolean); method public void setProjectionMap(java.util.Map<java.lang.String, java.lang.String>); method public void setStrict(boolean); method public void setTables(java.lang.String); + method public int update(android.database.sqlite.SQLiteDatabase, android.content.ContentValues, java.lang.String, java.lang.String[]); } public class SQLiteReadOnlyDatabaseException extends android.database.sqlite.SQLiteException { diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index af09f15653d1..f923738c56d6 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -260,6 +260,13 @@ public abstract class ContentResolver { */ public static final String QUERY_ARG_SQL_SORT_ORDER = "android:query-arg-sql-sort-order"; + /** {@hide} */ + public static final String QUERY_ARG_SQL_GROUP_BY = "android:query-arg-sql-group-by"; + /** {@hide} */ + public static final String QUERY_ARG_SQL_HAVING = "android:query-arg-sql-having"; + /** {@hide} */ + public static final String QUERY_ARG_SQL_LIMIT = "android:query-arg-sql-limit"; + /** * 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. diff --git a/core/java/android/content/ContentValues.java b/core/java/android/content/ContentValues.java index 6f9379890a3a..93fa403b760b 100644 --- a/core/java/android/content/ContentValues.java +++ b/core/java/android/content/ContentValues.java @@ -18,6 +18,7 @@ package android.content; import android.os.Parcel; import android.os.Parcelable; +import android.util.ArrayMap; import android.util.Log; import java.util.ArrayList; @@ -32,16 +33,20 @@ import java.util.Set; public final class ContentValues implements Parcelable { public static final String TAG = "ContentValues"; - /** Holds the actual values */ + /** + * @hide + * @deprecated kept around for lame people doing reflection + */ + @Deprecated private HashMap<String, Object> mValues; + private final ArrayMap<String, Object> mMap; + /** * Creates an empty set of values using the default initial size */ public ContentValues() { - // Choosing a default size of 8 based on analysis of typical - // consumption by applications. - mValues = new HashMap<String, Object>(8); + mMap = new ArrayMap<>(); } /** @@ -50,7 +55,7 @@ public final class ContentValues implements Parcelable { * @param size the initial size of the set of values */ public ContentValues(int size) { - mValues = new HashMap<String, Object>(size, 1.0f); + mMap = new ArrayMap<>(size); } /** @@ -59,18 +64,23 @@ public final class ContentValues implements Parcelable { * @param from the values to copy */ public ContentValues(ContentValues from) { - mValues = new HashMap<String, Object>(from.mValues); + mMap = new ArrayMap<>(from.mMap); } /** - * Creates a set of values copied from the given HashMap. This is used - * by the Parcel unmarshalling code. - * - * @param values the values to start with - * {@hide} + * @hide + * @deprecated kept around for lame people doing reflection */ - private ContentValues(HashMap<String, Object> values) { - mValues = values; + @Deprecated + private ContentValues(HashMap<String, Object> from) { + mMap = new ArrayMap<>(); + mMap.putAll(from); + } + + /** {@hide} */ + private ContentValues(Parcel in) { + mMap = new ArrayMap<>(in.readInt()); + in.readArrayMap(mMap, null); } @Override @@ -78,12 +88,17 @@ public final class ContentValues implements Parcelable { if (!(object instanceof ContentValues)) { return false; } - return mValues.equals(((ContentValues) object).mValues); + return mMap.equals(((ContentValues) object).mMap); + } + + /** {@hide} */ + public ArrayMap<String, Object> getValues() { + return mMap; } @Override public int hashCode() { - return mValues.hashCode(); + return mMap.hashCode(); } /** @@ -93,7 +108,7 @@ public final class ContentValues implements Parcelable { * @param value the data for the value to put */ public void put(String key, String value) { - mValues.put(key, value); + mMap.put(key, value); } /** @@ -102,7 +117,7 @@ public final class ContentValues implements Parcelable { * @param other the ContentValues from which to copy */ public void putAll(ContentValues other) { - mValues.putAll(other.mValues); + mMap.putAll(other.mMap); } /** @@ -112,7 +127,7 @@ public final class ContentValues implements Parcelable { * @param value the data for the value to put */ public void put(String key, Byte value) { - mValues.put(key, value); + mMap.put(key, value); } /** @@ -122,7 +137,7 @@ public final class ContentValues implements Parcelable { * @param value the data for the value to put */ public void put(String key, Short value) { - mValues.put(key, value); + mMap.put(key, value); } /** @@ -132,7 +147,7 @@ public final class ContentValues implements Parcelable { * @param value the data for the value to put */ public void put(String key, Integer value) { - mValues.put(key, value); + mMap.put(key, value); } /** @@ -142,7 +157,7 @@ public final class ContentValues implements Parcelable { * @param value the data for the value to put */ public void put(String key, Long value) { - mValues.put(key, value); + mMap.put(key, value); } /** @@ -152,7 +167,7 @@ public final class ContentValues implements Parcelable { * @param value the data for the value to put */ public void put(String key, Float value) { - mValues.put(key, value); + mMap.put(key, value); } /** @@ -162,7 +177,7 @@ public final class ContentValues implements Parcelable { * @param value the data for the value to put */ public void put(String key, Double value) { - mValues.put(key, value); + mMap.put(key, value); } /** @@ -172,7 +187,7 @@ public final class ContentValues implements Parcelable { * @param value the data for the value to put */ public void put(String key, Boolean value) { - mValues.put(key, value); + mMap.put(key, value); } /** @@ -182,7 +197,7 @@ public final class ContentValues implements Parcelable { * @param value the data for the value to put */ public void put(String key, byte[] value) { - mValues.put(key, value); + mMap.put(key, value); } /** @@ -191,7 +206,7 @@ public final class ContentValues implements Parcelable { * @param key the name of the value to make null */ public void putNull(String key) { - mValues.put(key, null); + mMap.put(key, null); } /** @@ -200,7 +215,7 @@ public final class ContentValues implements Parcelable { * @return the number of values */ public int size() { - return mValues.size(); + return mMap.size(); } /** @@ -211,7 +226,7 @@ public final class ContentValues implements Parcelable { * TODO: consider exposing this new method publicly */ public boolean isEmpty() { - return mValues.isEmpty(); + return mMap.isEmpty(); } /** @@ -220,14 +235,14 @@ public final class ContentValues implements Parcelable { * @param key the name of the value to remove */ public void remove(String key) { - mValues.remove(key); + mMap.remove(key); } /** * Removes all values. */ public void clear() { - mValues.clear(); + mMap.clear(); } /** @@ -237,7 +252,7 @@ public final class ContentValues implements Parcelable { * @return {@code true} if the value is present, {@code false} otherwise */ public boolean containsKey(String key) { - return mValues.containsKey(key); + return mMap.containsKey(key); } /** @@ -249,7 +264,7 @@ public final class ContentValues implements Parcelable { * was previously added with the given {@code key} */ public Object get(String key) { - return mValues.get(key); + return mMap.get(key); } /** @@ -259,7 +274,7 @@ public final class ContentValues implements Parcelable { * @return the String for the value */ public String getAsString(String key) { - Object value = mValues.get(key); + Object value = mMap.get(key); return value != null ? value.toString() : null; } @@ -270,7 +285,7 @@ public final class ContentValues implements Parcelable { * @return the Long value, or {@code null} if the value is missing or cannot be converted */ public Long getAsLong(String key) { - Object value = mValues.get(key); + Object value = mMap.get(key); try { return value != null ? ((Number) value).longValue() : null; } catch (ClassCastException e) { @@ -295,7 +310,7 @@ public final class ContentValues implements Parcelable { * @return the Integer value, or {@code null} if the value is missing or cannot be converted */ public Integer getAsInteger(String key) { - Object value = mValues.get(key); + Object value = mMap.get(key); try { return value != null ? ((Number) value).intValue() : null; } catch (ClassCastException e) { @@ -320,7 +335,7 @@ public final class ContentValues implements Parcelable { * @return the Short value, or {@code null} if the value is missing or cannot be converted */ public Short getAsShort(String key) { - Object value = mValues.get(key); + Object value = mMap.get(key); try { return value != null ? ((Number) value).shortValue() : null; } catch (ClassCastException e) { @@ -345,7 +360,7 @@ public final class ContentValues implements Parcelable { * @return the Byte value, or {@code null} if the value is missing or cannot be converted */ public Byte getAsByte(String key) { - Object value = mValues.get(key); + Object value = mMap.get(key); try { return value != null ? ((Number) value).byteValue() : null; } catch (ClassCastException e) { @@ -370,7 +385,7 @@ public final class ContentValues implements Parcelable { * @return the Double value, or {@code null} if the value is missing or cannot be converted */ public Double getAsDouble(String key) { - Object value = mValues.get(key); + Object value = mMap.get(key); try { return value != null ? ((Number) value).doubleValue() : null; } catch (ClassCastException e) { @@ -395,7 +410,7 @@ public final class ContentValues implements Parcelable { * @return the Float value, or {@code null} if the value is missing or cannot be converted */ public Float getAsFloat(String key) { - Object value = mValues.get(key); + Object value = mMap.get(key); try { return value != null ? ((Number) value).floatValue() : null; } catch (ClassCastException e) { @@ -420,7 +435,7 @@ public final class ContentValues implements Parcelable { * @return the Boolean value, or {@code null} if the value is missing or cannot be converted */ public Boolean getAsBoolean(String key) { - Object value = mValues.get(key); + Object value = mMap.get(key); try { return (Boolean) value; } catch (ClassCastException e) { @@ -448,7 +463,7 @@ public final class ContentValues implements Parcelable { * {@code byte[]} */ public byte[] getAsByteArray(String key) { - Object value = mValues.get(key); + Object value = mMap.get(key); if (value instanceof byte[]) { return (byte[]) value; } else { @@ -462,7 +477,7 @@ public final class ContentValues implements Parcelable { * @return a set of all of the keys and values */ public Set<Map.Entry<String, Object>> valueSet() { - return mValues.entrySet(); + return mMap.entrySet(); } /** @@ -471,30 +486,31 @@ public final class ContentValues implements Parcelable { * @return a set of all of the keys */ public Set<String> keySet() { - return mValues.keySet(); + return mMap.keySet(); } public static final Parcelable.Creator<ContentValues> CREATOR = new Parcelable.Creator<ContentValues>() { - @SuppressWarnings({"deprecation", "unchecked"}) + @Override public ContentValues createFromParcel(Parcel in) { - // TODO - what ClassLoader should be passed to readHashMap? - HashMap<String, Object> values = in.readHashMap(null); - return new ContentValues(values); + return new ContentValues(in); } + @Override public ContentValues[] newArray(int size) { return new ContentValues[size]; } }; + @Override public int describeContents() { return 0; } - @SuppressWarnings("deprecation") + @Override public void writeToParcel(Parcel parcel, int flags) { - parcel.writeMap(mValues); + parcel.writeInt(mMap.size()); + parcel.writeArrayMap(mMap); } /** @@ -503,7 +519,7 @@ public final class ContentValues implements Parcelable { */ @Deprecated public void putStringArrayList(String key, ArrayList<String> value) { - mValues.put(key, value); + mMap.put(key, value); } /** @@ -513,7 +529,7 @@ public final class ContentValues implements Parcelable { @SuppressWarnings("unchecked") @Deprecated public ArrayList<String> getStringArrayList(String key) { - return (ArrayList<String>) mValues.get(key); + return (ArrayList<String>) mMap.get(key); } /** @@ -523,7 +539,7 @@ public final class ContentValues implements Parcelable { @Override public String toString() { StringBuilder sb = new StringBuilder(); - for (String name : mValues.keySet()) { + for (String name : mMap.keySet()) { String value = getAsString(name); if (sb.length() > 0) sb.append(" "); sb.append(name + "=" + value); diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 6adae25fc38d..a4b989a72492 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -189,7 +189,8 @@ public final class SQLiteDatabase extends SQLiteClosable { */ public static final int CONFLICT_NONE = 0; - private static final String[] CONFLICT_VALUES = new String[] + /** {@hide} */ + public static final String[] CONFLICT_VALUES = new String[] {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; /** @@ -1748,7 +1749,8 @@ public final class SQLiteDatabase extends SQLiteClosable { executeSql(sql, bindArgs); } - private int executeSql(String sql, Object[] bindArgs) throws SQLException { + /** {@hide} */ + public int executeSql(String sql, Object[] bindArgs) throws SQLException { acquireReference(); try { final int statementType = DatabaseUtils.getSqlStatementType(sql); diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java index c6c676f81758..31907717c168 100644 --- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java +++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java @@ -16,17 +16,39 @@ package android.database.sqlite; +import static android.content.ContentResolver.QUERY_ARG_SQL_GROUP_BY; +import static android.content.ContentResolver.QUERY_ARG_SQL_HAVING; +import static android.content.ContentResolver.QUERY_ARG_SQL_LIMIT; +import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION; +import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS; +import static android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ContentResolver; +import android.content.ContentValues; import android.database.Cursor; import android.database.DatabaseUtils; +import android.os.Build; +import android.os.Bundle; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.provider.BaseColumns; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.Log; +import com.android.internal.util.ArrayUtils; + +import dalvik.system.VMRuntime; + +import libcore.util.EmptyArray; + +import java.util.Arrays; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; @@ -34,8 +56,7 @@ import java.util.regex.Pattern; * This is a convenience class that helps build SQL queries to be sent to * {@link SQLiteDatabase} objects. */ -public class SQLiteQueryBuilder -{ +public class SQLiteQueryBuilder { private static final String TAG = "SQLiteQueryBuilder"; private static final Pattern sLimitPattern = Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?"); @@ -43,6 +64,7 @@ public class SQLiteQueryBuilder private Map<String, String> mProjectionMap = null; private String mTables = ""; private StringBuilder mWhereClause = null; // lazily created + private String[] mWhereArgs = EmptyArray.STRING; private boolean mDistinct; private SQLiteDatabase.CursorFactory mFactory; private boolean mStrict; @@ -82,43 +104,92 @@ public class SQLiteQueryBuilder mTables = inTables; } + /** {@hide} */ + public @Nullable String getWhere() { + return (mWhereClause != null) ? mWhereClause.toString() : null; + } + + /** {@hide} */ + public String[] getWhereArgs() { + return mWhereArgs; + } + /** - * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded - * by parenthesis and ANDed with the selection passed to {@link #query}. The final - * WHERE clause looks like: + * Append a chunk to the {@code WHERE} clause of the query. All chunks + * appended are surrounded by parenthesis and {@code AND}ed with the + * selection passed to {@link #query}. The final {@code WHERE} clause looks + * like: * + * <pre> * WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) + * </pre> * - * @param inWhere the chunk of text to append to the WHERE clause. + * @param inWhere the chunk of text to append to the {@code WHERE} clause. */ - public void appendWhere(CharSequence inWhere) { + public void appendWhere(@NonNull CharSequence inWhere) { + appendWhere(inWhere, EmptyArray.STRING); + } + + /** + * Append a chunk to the {@code WHERE} clause of the query. All chunks + * appended are surrounded by parenthesis and {@code AND}ed with the + * selection passed to {@link #query}. The final {@code WHERE} clause looks + * like: + * + * <pre> + * WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) + * </pre> + * + * @param inWhere the chunk of text to append to the {@code WHERE} clause. + * @param inWhereArgs list of arguments to be bound to any '?' occurrences + * in the where clause. + */ + public void appendWhere(@NonNull CharSequence inWhere, String... inWhereArgs) { if (mWhereClause == null) { mWhereClause = new StringBuilder(inWhere.length() + 16); } - if (mWhereClause.length() == 0) { - mWhereClause.append('('); - } mWhereClause.append(inWhere); + mWhereArgs = ArrayUtils.concat(String.class, mWhereArgs, inWhereArgs); + } + + /** + * Append a chunk to the {@code WHERE} clause of the query. All chunks + * appended are surrounded by parenthesis and {@code AND}ed with the + * selection passed to {@link #query}. The final {@code WHERE} clause looks + * like this: + * + * <pre> + * WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) + * </pre> + * + * @param inWhere the chunk of text to append to the {@code WHERE} clause. + * It will be escaped to avoid SQL injection attacks. + */ + public void appendWhereEscapeString(@NonNull String inWhere) { + appendWhereEscapeString(inWhere, EmptyArray.STRING); } /** - * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded - * by parenthesis and ANDed with the selection passed to {@link #query}. The final - * WHERE clause looks like: + * Append a chunk to the {@code WHERE} clause of the query. All chunks + * appended are surrounded by parenthesis and {@code AND}ed with the + * selection passed to {@link #query}. The final {@code WHERE} clause looks + * like this: * + * <pre> * WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) + * </pre> * - * @param inWhere the chunk of text to append to the WHERE clause. it will be escaped - * to avoid SQL injection attacks + * @param inWhere the chunk of text to append to the {@code WHERE} clause. + * It will be escaped to avoid SQL injection attacks. + * @param inWhereArgs list of arguments to be bound to any '?' occurrences + * in the where clause. */ - public void appendWhereEscapeString(String inWhere) { + public void appendWhereEscapeString(@NonNull String inWhere, String... inWhereArgs) { if (mWhereClause == null) { mWhereClause = new StringBuilder(inWhere.length() + 16); } - if (mWhereClause.length() == 0) { - mWhereClause.append('('); - } DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere); + mWhereArgs = ArrayUtils.concat(String.class, mWhereArgs, inWhereArgs); } /** @@ -168,8 +239,8 @@ public class SQLiteQueryBuilder * </ul> * By default, this value is false. */ - public void setStrict(boolean flag) { - mStrict = flag; + public void setStrict(boolean strict) { + mStrict = strict; } /** @@ -263,7 +334,7 @@ public class SQLiteQueryBuilder * information passed into this method. * * @param db the database to query on - * @param projectionIn A list of which columns to return. Passing + * @param projection A list of which columns to return. Passing * null will return all columns, which is discouraged to prevent * reading data from storage that isn't going to be used. * @param selection A filter declaring which rows to return, @@ -288,10 +359,14 @@ public class SQLiteQueryBuilder * @see android.content.ContentResolver#query(android.net.Uri, String[], * String, String[], String) */ - public Cursor query(SQLiteDatabase db, String[] projectionIn, - String selection, String[] selectionArgs, String groupBy, - String having, String sortOrder) { - return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder, + public @Nullable Cursor query(@NonNull SQLiteDatabase db, + @Nullable String[] projection, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String groupBy, + @Nullable String having, + @Nullable String sortOrder) { + return query(db, projection, selection, selectionArgs, groupBy, having, sortOrder, null /* limit */, null /* cancellationSignal */); } @@ -300,7 +375,7 @@ public class SQLiteQueryBuilder * information passed into this method. * * @param db the database to query on - * @param projectionIn A list of which columns to return. Passing + * @param projection A list of which columns to return. Passing * null will return all columns, which is discouraged to prevent * reading data from storage that isn't going to be used. * @param selection A filter declaring which rows to return, @@ -327,10 +402,15 @@ public class SQLiteQueryBuilder * @see android.content.ContentResolver#query(android.net.Uri, String[], * String, String[], String) */ - public Cursor query(SQLiteDatabase db, String[] projectionIn, - String selection, String[] selectionArgs, String groupBy, - String having, String sortOrder, String limit) { - return query(db, projectionIn, selection, selectionArgs, + public @Nullable Cursor query(@NonNull SQLiteDatabase db, + @Nullable String[] projection, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String groupBy, + @Nullable String having, + @Nullable String sortOrder, + @Nullable String limit) { + return query(db, projection, selection, selectionArgs, groupBy, having, sortOrder, limit, null); } @@ -339,7 +419,42 @@ public class SQLiteQueryBuilder * information passed into this method. * * @param db the database to query on - * @param projectionIn A list of which columns to return. Passing + * @param projection A list of which columns to return. Passing + * null will return all columns, which is discouraged to prevent + * reading data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE + * itself). Passing null will return all rows for the given URL. + * @param selectionArgs You may include ?s in selection, which + * will be replaced by the values from selectionArgs, in order + * that they appear in the selection. The values will be bound + * as Strings. + * @param sortOrder How to order the rows, formatted as an SQL + * ORDER BY clause (excluding the ORDER BY itself). Passing null + * will use the default sort order, which may be unordered. + * @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 over the result set + * @see android.content.ContentResolver#query(android.net.Uri, String[], + * String, String[], String) + */ + public @Nullable Cursor query(@NonNull SQLiteDatabase db, + @Nullable String[] projection, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String sortOrder, + @Nullable CancellationSignal cancellationSignal) { + return query(db, projection, selection, selectionArgs, null, null, sortOrder, null, + cancellationSignal); + } + + /** + * Perform a query by combining all current settings and the + * information passed into this method. + * + * @param db the database to query on + * @param projection A list of which columns to return. Passing * null will return all columns, which is discouraged to prevent * reading data from storage that isn't going to be used. * @param selection A filter declaring which rows to return, @@ -369,14 +484,59 @@ public class SQLiteQueryBuilder * @see android.content.ContentResolver#query(android.net.Uri, String[], * String, String[], String) */ - public Cursor query(SQLiteDatabase db, String[] projectionIn, - String selection, String[] selectionArgs, String groupBy, - String having, String sortOrder, String limit, CancellationSignal cancellationSignal) { - if (mTables == null) { + public @Nullable Cursor query(@NonNull SQLiteDatabase db, + @Nullable String[] projection, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String groupBy, + @Nullable String having, + @Nullable String sortOrder, + @Nullable String limit, + @Nullable CancellationSignal cancellationSignal) { + final Bundle queryArgs = new Bundle(); + maybePutString(queryArgs, QUERY_ARG_SQL_SELECTION, selection); + maybePutStringArray(queryArgs, QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs); + maybePutString(queryArgs, QUERY_ARG_SQL_GROUP_BY, groupBy); + maybePutString(queryArgs, QUERY_ARG_SQL_HAVING, having); + maybePutString(queryArgs, QUERY_ARG_SQL_SORT_ORDER, sortOrder); + maybePutString(queryArgs, QUERY_ARG_SQL_LIMIT, limit); + return query(db, projection, queryArgs, cancellationSignal); + } + + /** + * Perform a query by combining all current settings and the information + * passed into this method. + * + * @param db the database to query on + * @param projection A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param queryArgs A collection of arguments for the query, defined using + * keys such as {@link ContentResolver#QUERY_ARG_SQL_SELECTION} + * and {@link ContentResolver#QUERY_ARG_SQL_SELECTION_ARGS}. + * @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 over the result set + */ + public Cursor query(@NonNull SQLiteDatabase db, + @Nullable String[] projection, + @Nullable Bundle queryArgs, + @Nullable CancellationSignal cancellationSignal) { + Objects.requireNonNull(db, "No database defined"); + + if (VMRuntime.getRuntime().getTargetSdkVersion() >= Build.VERSION_CODES.Q) { + Objects.requireNonNull(mTables, "No tables defined"); + } else if (mTables == null) { return null; } - if (mStrict && selection != null && selection.length() > 0) { + if (queryArgs == null) { + queryArgs = Bundle.EMPTY; + } + + if (mStrict) { // Validate the user-supplied selection to detect syntactic anomalies // in the selection string that could indicate a SQL injection attempt. // The idea is to ensure that the selection clause is a valid SQL expression @@ -384,25 +544,129 @@ public class SQLiteQueryBuilder // originally specified. An attacker cannot create an expression that // would escape the SQL expression while maintaining balanced parentheses // in both the wrapped and original forms. - String sqlForValidation = buildQuery(projectionIn, "(" + selection + ")", groupBy, - having, sortOrder, limit); - db.validateSql(sqlForValidation, cancellationSignal); // will throw if query is invalid + + // TODO: decode SORT ORDER and LIMIT clauses, since they can contain + // "expr" inside that need to be validated + final String sql = buildQuery(projection, + wrap(queryArgs.getString(QUERY_ARG_SQL_SELECTION)), + wrap(queryArgs.getString(QUERY_ARG_SQL_GROUP_BY)), + wrap(queryArgs.getString(QUERY_ARG_SQL_HAVING)), + queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER), + queryArgs.getString(QUERY_ARG_SQL_LIMIT)); + db.validateSql(sql, cancellationSignal); // will throw if query is invalid } - String sql = buildQuery( - projectionIn, selection, groupBy, having, - sortOrder, limit); + final String sql = buildQuery(projection, + queryArgs.getString(QUERY_ARG_SQL_SELECTION), + queryArgs.getString(QUERY_ARG_SQL_GROUP_BY), + queryArgs.getString(QUERY_ARG_SQL_HAVING), + queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER), + queryArgs.getString(QUERY_ARG_SQL_LIMIT)); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Performing query: " + sql); + final String[] sqlArgs = ArrayUtils.concat(String.class, + queryArgs.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS), mWhereArgs); + + if (Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); } + return db.rawQueryWithFactory( - mFactory, sql, selectionArgs, + mFactory, sql, sqlArgs, SQLiteDatabase.findEditTable(mTables), cancellationSignal); // will throw if query is invalid } /** + * Perform an update by combining all current settings and the + * information passed into this method. + * + * @param db the database to update on + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE + * itself). Passing null will return all rows for the given URL. + * @param selectionArgs You may include ?s in selection, which + * will be replaced by the values from selectionArgs, in order + * that they appear in the selection. The values will be bound + * as Strings. + * @return the number of rows updated + */ + public int update(@NonNull SQLiteDatabase db, @NonNull ContentValues values, + @Nullable String selection, @Nullable String[] selectionArgs) { + Objects.requireNonNull(mTables, "No tables defined"); + Objects.requireNonNull(db, "No database defined"); + Objects.requireNonNull(values, "No values defined"); + + if (mStrict) { + // Validate the user-supplied selection to detect syntactic anomalies + // in the selection string that could indicate a SQL injection attempt. + // The idea is to ensure that the selection clause is a valid SQL expression + // by compiling it twice: once wrapped in parentheses and once as + // originally specified. An attacker cannot create an expression that + // would escape the SQL expression while maintaining balanced parentheses + // in both the wrapped and original forms. + final String sql = buildUpdate(values, wrap(selection)); + db.validateSql(sql, null); // will throw if query is invalid + } + + final ArrayMap<String, Object> rawValues = values.getValues(); + final String[] updateArgs = new String[rawValues.size()]; + for (int i = 0; i < updateArgs.length; i++) { + updateArgs[i] = String.valueOf(rawValues.valueAt(i)); + } + + final String sql = buildUpdate(values, selection); + final String[] sqlArgs = ArrayUtils.concat(String.class, updateArgs, + ArrayUtils.concat(String.class, selectionArgs, mWhereArgs)); + + if (Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); + } + + return db.executeSql(sql, sqlArgs); + } + + /** + * Perform a delete by combining all current settings and the + * information passed into this method. + * + * @param db the database to delete on + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE + * itself). Passing null will return all rows for the given URL. + * @param selectionArgs You may include ?s in selection, which + * will be replaced by the values from selectionArgs, in order + * that they appear in the selection. The values will be bound + * as Strings. + * @return the number of rows deleted + */ + public int delete(@NonNull SQLiteDatabase db, @Nullable String selection, + @Nullable String[] selectionArgs) { + Objects.requireNonNull(mTables, "No tables defined"); + Objects.requireNonNull(db, "No database defined"); + + if (mStrict) { + // Validate the user-supplied selection to detect syntactic anomalies + // in the selection string that could indicate a SQL injection attempt. + // The idea is to ensure that the selection clause is a valid SQL expression + // by compiling it twice: once wrapped in parentheses and once as + // originally specified. An attacker cannot create an expression that + // would escape the SQL expression while maintaining balanced parentheses + // in both the wrapped and original forms. + final String sql = buildDelete(wrap(selection)); + db.validateSql(sql, null); // will throw if query is invalid + } + + final String sql = buildDelete(selection); + final String[] sqlArgs = ArrayUtils.concat(String.class, selectionArgs, mWhereArgs); + + if (Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); + } + + return db.executeSql(sql, sqlArgs); + } + + /** * Construct a SELECT statement suitable for use in a group of * SELECT statements that will be joined through UNION operators * in buildUnionQuery. @@ -434,28 +698,10 @@ public class SQLiteQueryBuilder String[] projectionIn, String selection, String groupBy, String having, String sortOrder, String limit) { String[] projection = computeProjection(projectionIn); - - StringBuilder where = new StringBuilder(); - boolean hasBaseWhereClause = mWhereClause != null && mWhereClause.length() > 0; - - if (hasBaseWhereClause) { - where.append(mWhereClause.toString()); - where.append(')'); - } - - // Tack on the user's selection, if present. - if (selection != null && selection.length() > 0) { - if (hasBaseWhereClause) { - where.append(" AND "); - } - - where.append('('); - where.append(selection); - where.append(')'); - } + String where = computeWhere(selection); return buildQueryString( - mDistinct, mTables, projection, where.toString(), + mDistinct, mTables, projection, where, groupBy, having, sortOrder, limit); } @@ -472,6 +718,42 @@ public class SQLiteQueryBuilder return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit); } + /** {@hide} */ + public String buildUpdate(ContentValues values, String selection) { + if (values == null || values.isEmpty()) { + throw new IllegalArgumentException("Empty values"); + } + + StringBuilder sql = new StringBuilder(120); + sql.append("UPDATE "); + sql.append(mTables); + sql.append(" SET "); + + final ArrayMap<String, Object> rawValues = values.getValues(); + for (int i = 0; i < rawValues.size(); i++) { + if (i > 0) { + sql.append(','); + } + sql.append(rawValues.keyAt(i)); + sql.append("=?"); + } + + final String where = computeWhere(selection); + appendClause(sql, " WHERE ", where); + return sql.toString(); + } + + /** {@hide} */ + public String buildDelete(String selection) { + StringBuilder sql = new StringBuilder(120); + sql.append("DELETE FROM "); + sql.append(mTables); + + final String where = computeWhere(selection); + appendClause(sql, " WHERE ", where); + return sql.toString(); + } + /** * Construct a SELECT statement suitable for use in a group of * SELECT statements that will be joined through UNION operators @@ -596,7 +878,7 @@ public class SQLiteQueryBuilder return query.toString(); } - private String[] computeProjection(String[] projectionIn) { + private @Nullable String[] computeProjection(@Nullable String[] projectionIn) { if (projectionIn != null && projectionIn.length > 0) { if (mProjectionMap != null) { String[] projection = new String[projectionIn.length]; @@ -619,7 +901,7 @@ public class SQLiteQueryBuilder } throw new IllegalArgumentException("Invalid column " - + projectionIn[i]); + + projectionIn[i] + " from tables " + mTables); } return projection; } else { @@ -645,4 +927,53 @@ public class SQLiteQueryBuilder } return null; } + + private @NonNull String computeWhere(@Nullable String selection) { + final boolean hasUser = selection != null && selection.length() > 0; + final boolean hasInternal = mWhereClause != null && mWhereClause.length() > 0; + + if (hasUser || hasInternal) { + final StringBuilder where = new StringBuilder(); + if (hasUser) { + where.append('(').append(selection).append(')'); + } + if (hasUser && hasInternal) { + where.append(" AND "); + } + if (hasInternal) { + where.append('(').append(mWhereClause.toString()).append(')'); + } + return where.toString(); + } else { + return null; + } + } + + /** + * Wrap given argument in parenthesis, unless it's {@code null} or + * {@code ()}, in which case return it verbatim. + */ + private @Nullable String wrap(@Nullable String arg) { + if (arg == null) { + return null; + } else if (arg.equals("")) { + return arg; + } else { + return "(" + arg + ")"; + } + } + + private static void maybePutString(@NonNull Bundle bundle, @NonNull String key, + @Nullable String value) { + if (value != null) { + bundle.putString(key, value); + } + } + + private static void maybePutStringArray(@NonNull Bundle bundle, @NonNull String key, + @Nullable String[] value) { + if (value != null) { + bundle.putStringArray(key, value); + } + } } diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index be645fe707c6..c3d33ca84ee1 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -308,6 +308,23 @@ public class ArrayUtils { return array; } + @SuppressWarnings("unchecked") + public static @NonNull <T> T[] concat(Class<T> kind, @Nullable T[] a, @Nullable T[] b) { + final int an = (a != null) ? a.length : 0; + final int bn = (b != null) ? b.length : 0; + if (an == 0 && bn == 0) { + if (kind == String.class) { + return (T[]) EmptyArray.STRING; + } else if (kind == Object.class) { + return (T[]) EmptyArray.OBJECT; + } + } + final T[] res = (T[]) Array.newInstance(kind, an + bn); + if (an > 0) System.arraycopy(a, 0, res, 0, an); + if (bn > 0) System.arraycopy(b, 0, res, an, bn); + return res; + } + /** * Adds value to given array if not already present, providing set-like * behavior. diff --git a/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java index 433d4d214b97..6464ad3e9709 100644 --- a/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java +++ b/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java @@ -16,9 +16,8 @@ package com.android.internal.util; -import android.test.MoreAsserts; +import static org.junit.Assert.assertArrayEquals; -import java.util.Arrays; import junit.framework.TestCase; /** @@ -92,29 +91,29 @@ public class ArrayUtilsTest extends TestCase { } public void testAppendInt() throws Exception { - MoreAsserts.assertEquals(new int[] { 1 }, + assertArrayEquals(new int[] { 1 }, ArrayUtils.appendInt(null, 1)); - MoreAsserts.assertEquals(new int[] { 1 }, + assertArrayEquals(new int[] { 1 }, ArrayUtils.appendInt(new int[] { }, 1)); - MoreAsserts.assertEquals(new int[] { 1, 2 }, + assertArrayEquals(new int[] { 1, 2 }, ArrayUtils.appendInt(new int[] { 1 }, 2)); - MoreAsserts.assertEquals(new int[] { 1, 2 }, + assertArrayEquals(new int[] { 1, 2 }, ArrayUtils.appendInt(new int[] { 1, 2 }, 1)); } public void testRemoveInt() throws Exception { assertNull(ArrayUtils.removeInt(null, 1)); - MoreAsserts.assertEquals(new int[] { }, + assertArrayEquals(new int[] { }, ArrayUtils.removeInt(new int[] { }, 1)); - MoreAsserts.assertEquals(new int[] { 1, 2, 3, }, + assertArrayEquals(new int[] { 1, 2, 3, }, ArrayUtils.removeInt(new int[] { 1, 2, 3}, 4)); - MoreAsserts.assertEquals(new int[] { 2, 3, }, + assertArrayEquals(new int[] { 2, 3, }, ArrayUtils.removeInt(new int[] { 1, 2, 3}, 1)); - MoreAsserts.assertEquals(new int[] { 1, 3, }, + assertArrayEquals(new int[] { 1, 3, }, ArrayUtils.removeInt(new int[] { 1, 2, 3}, 2)); - MoreAsserts.assertEquals(new int[] { 1, 2, }, + assertArrayEquals(new int[] { 1, 2, }, ArrayUtils.removeInt(new int[] { 1, 2, 3}, 3)); - MoreAsserts.assertEquals(new int[] { 2, 3, 1 }, + assertArrayEquals(new int[] { 2, 3, 1 }, ArrayUtils.removeInt(new int[] { 1, 2, 3, 1 }, 1)); } @@ -129,30 +128,51 @@ public class ArrayUtilsTest extends TestCase { } public void testAppendLong() throws Exception { - MoreAsserts.assertEquals(new long[] { 1 }, + assertArrayEquals(new long[] { 1 }, ArrayUtils.appendLong(null, 1)); - MoreAsserts.assertEquals(new long[] { 1 }, + assertArrayEquals(new long[] { 1 }, ArrayUtils.appendLong(new long[] { }, 1)); - MoreAsserts.assertEquals(new long[] { 1, 2 }, + assertArrayEquals(new long[] { 1, 2 }, ArrayUtils.appendLong(new long[] { 1 }, 2)); - MoreAsserts.assertEquals(new long[] { 1, 2 }, + assertArrayEquals(new long[] { 1, 2 }, ArrayUtils.appendLong(new long[] { 1, 2 }, 1)); } public void testRemoveLong() throws Exception { assertNull(ArrayUtils.removeLong(null, 1)); - MoreAsserts.assertEquals(new long[] { }, + assertArrayEquals(new long[] { }, ArrayUtils.removeLong(new long[] { }, 1)); - MoreAsserts.assertEquals(new long[] { 1, 2, 3, }, + assertArrayEquals(new long[] { 1, 2, 3, }, ArrayUtils.removeLong(new long[] { 1, 2, 3}, 4)); - MoreAsserts.assertEquals(new long[] { 2, 3, }, + assertArrayEquals(new long[] { 2, 3, }, ArrayUtils.removeLong(new long[] { 1, 2, 3}, 1)); - MoreAsserts.assertEquals(new long[] { 1, 3, }, + assertArrayEquals(new long[] { 1, 3, }, ArrayUtils.removeLong(new long[] { 1, 2, 3}, 2)); - MoreAsserts.assertEquals(new long[] { 1, 2, }, + assertArrayEquals(new long[] { 1, 2, }, ArrayUtils.removeLong(new long[] { 1, 2, 3}, 3)); - MoreAsserts.assertEquals(new long[] { 2, 3, 1 }, + assertArrayEquals(new long[] { 2, 3, 1 }, ArrayUtils.removeLong(new long[] { 1, 2, 3, 1 }, 1)); } + public void testConcatEmpty() throws Exception { + assertArrayEquals(new Long[] {}, + ArrayUtils.concat(Long.class, null, null)); + assertArrayEquals(new Long[] {}, + ArrayUtils.concat(Long.class, new Long[] {}, null)); + assertArrayEquals(new Long[] {}, + ArrayUtils.concat(Long.class, null, new Long[] {})); + assertArrayEquals(new Long[] {}, + ArrayUtils.concat(Long.class, new Long[] {}, new Long[] {})); + } + + public void testConcat() throws Exception { + assertArrayEquals(new Long[] { 1L }, + ArrayUtils.concat(Long.class, new Long[] { 1L }, new Long[] {})); + assertArrayEquals(new Long[] { 1L }, + ArrayUtils.concat(Long.class, new Long[] {}, new Long[] { 1L })); + assertArrayEquals(new Long[] { 1L, 2L }, + ArrayUtils.concat(Long.class, new Long[] { 1L }, new Long[] { 2L })); + assertArrayEquals(new Long[] { 1L, 2L, 3L, 4L }, + ArrayUtils.concat(Long.class, new Long[] { 1L, 2L }, new Long[] { 3L, 4L })); + } } |