Merge "Extend SQLiteQueryBuilder for update and delete."
diff --git a/api/current.txt b/api/current.txt
index f5504d7..0dcecdd 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -12670,22 +12670,28 @@
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 af09f15..f923738 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -260,6 +260,13 @@
*/
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 6f93798..93fa403 100644
--- a/core/java/android/content/ContentValues.java
+++ b/core/java/android/content/ContentValues.java
@@ -18,6 +18,7 @@
import android.os.Parcel;
import android.os.Parcelable;
+import android.util.ArrayMap;
import android.util.Log;
import java.util.ArrayList;
@@ -32,16 +33,20 @@
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 @@
* @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 @@
* @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 @@
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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @return the number of values
*/
public int size() {
- return mValues.size();
+ return mMap.size();
}
/**
@@ -211,7 +226,7 @@
* TODO: consider exposing this new method publicly
*/
public boolean isEmpty() {
- return mValues.isEmpty();
+ return mMap.isEmpty();
}
/**
@@ -220,14 +235,14 @@
* @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 @@
* @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 @@
* 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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* {@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 @@
* @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 @@
* @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 @@
*/
@Deprecated
public void putStringArrayList(String key, ArrayList<String> value) {
- mValues.put(key, value);
+ mMap.put(key, value);
}
/**
@@ -513,7 +529,7 @@
@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 @@
@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 6adae25..a4b989a 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -189,7 +189,8 @@
*/
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 @@
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 c6c676f..3190771 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 @@
* 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 @@
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 @@
mTables = inTables;
}
- /**
- * 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:
- *
- * WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>)
- *
- * @param inWhere the chunk of text to append to the WHERE clause.
- */
- public void appendWhere(CharSequence inWhere) {
- if (mWhereClause == null) {
- mWhereClause = new StringBuilder(inWhere.length() + 16);
- }
- if (mWhereClause.length() == 0) {
- mWhereClause.append('(');
- }
- mWhereClause.append(inWhere);
+ /** {@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. it will be escaped
- * to avoid SQL injection attacks
+ * @param inWhere the chunk of text to append to the {@code WHERE} clause.
*/
- public void appendWhereEscapeString(String 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 {@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.
+ * @param inWhereArgs list of arguments to be bound to any '?' occurrences
+ * in the where clause.
+ */
+ public void appendWhereEscapeString(@NonNull String inWhere, String... inWhereArgs) {
+ if (mWhereClause == null) {
+ mWhereClause = new StringBuilder(inWhere.length() + 16);
}
DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
+ mWhereArgs = ArrayUtils.concat(String.class, mWhereArgs, inWhereArgs);
}
/**
@@ -168,8 +239,8 @@
* </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 @@
* 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 @@
* @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 @@
* 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 @@
* @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 @@
* 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 @@
* @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 @@
// 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 @@
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 @@
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 @@
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 @@
}
throw new IllegalArgumentException("Invalid column "
- + projectionIn[i]);
+ + projectionIn[i] + " from tables " + mTables);
}
return projection;
} else {
@@ -645,4 +927,53 @@
}
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 be645fe..c3d33ca8 100644
--- a/core/java/com/android/internal/util/ArrayUtils.java
+++ b/core/java/com/android/internal/util/ArrayUtils.java
@@ -308,6 +308,23 @@
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 433d4d2..6464ad3 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 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 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 }));
+ }
}