summaryrefslogtreecommitdiff
path: root/core/java/android
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android')
-rw-r--r--core/java/android/content/ContentResolver.java7
-rw-r--r--core/java/android/content/ContentValues.java120
-rw-r--r--core/java/android/database/sqlite/SQLiteDatabase.java3
-rw-r--r--core/java/android/database/sqlite/SQLiteQueryBuilder.java467
4 files changed, 476 insertions, 121 deletions
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index b138b9d62ea2..ac98e12bda3e 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -261,6 +261,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 54857bb55f2e..da2049c8f6d7 100644
--- a/core/java/android/content/ContentValues.java
+++ b/core/java/android/content/ContentValues.java
@@ -19,6 +19,7 @@ package android.content;
import android.annotation.UnsupportedAppUsage;
import android.os.Parcel;
import android.os.Parcelable;
+import android.util.ArrayMap;
import android.util.Log;
import java.util.ArrayList;
@@ -33,17 +34,21 @@ 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
@UnsupportedAppUsage
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<>();
}
/**
@@ -52,7 +57,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);
}
/**
@@ -61,19 +66,24 @@ 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
*/
+ @Deprecated
@UnsupportedAppUsage
- private ContentValues(HashMap<String, Object> values) {
- mValues = values;
+ 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
@@ -81,12 +91,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();
}
/**
@@ -96,7 +111,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);
}
/**
@@ -105,7 +120,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);
}
/**
@@ -115,7 +130,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);
}
/**
@@ -125,7 +140,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);
}
/**
@@ -135,7 +150,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);
}
/**
@@ -145,7 +160,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);
}
/**
@@ -155,7 +170,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);
}
/**
@@ -165,7 +180,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);
}
/**
@@ -175,7 +190,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);
}
/**
@@ -185,7 +200,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);
}
/**
@@ -194,7 +209,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);
}
/**
@@ -203,7 +218,7 @@ public final class ContentValues implements Parcelable {
* @return the number of values
*/
public int size() {
- return mValues.size();
+ return mMap.size();
}
/**
@@ -214,7 +229,7 @@ public final class ContentValues implements Parcelable {
* TODO: consider exposing this new method publicly
*/
public boolean isEmpty() {
- return mValues.isEmpty();
+ return mMap.isEmpty();
}
/**
@@ -223,14 +238,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();
}
/**
@@ -240,7 +255,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);
}
/**
@@ -252,7 +267,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);
}
/**
@@ -262,7 +277,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;
}
@@ -273,7 +288,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) {
@@ -298,7 +313,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) {
@@ -323,7 +338,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) {
@@ -348,7 +363,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) {
@@ -373,7 +388,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) {
@@ -398,7 +413,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) {
@@ -423,7 +438,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) {
@@ -451,7 +466,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 {
@@ -465,7 +480,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();
}
/**
@@ -474,30 +489,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);
}
/**
@@ -507,7 +523,7 @@ public final class ContentValues implements Parcelable {
@Deprecated
@UnsupportedAppUsage
public void putStringArrayList(String key, ArrayList<String> value) {
- mValues.put(key, value);
+ mMap.put(key, value);
}
/**
@@ -518,7 +534,7 @@ public final class ContentValues implements Parcelable {
@Deprecated
@UnsupportedAppUsage
public ArrayList<String> getStringArrayList(String key) {
- return (ArrayList<String>) mValues.get(key);
+ return (ArrayList<String>) mMap.get(key);
}
/**
@@ -528,7 +544,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 25d98f7edf06..01557c59f8ac 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -193,8 +193,9 @@ public final class SQLiteDatabase extends SQLiteClosable {
*/
public static final int CONFLICT_NONE = 0;
+ /** {@hide} */
@UnsupportedAppUsage
- private static final String[] CONFLICT_VALUES = new String[]
+ public static final String[] CONFLICT_VALUES = new String[]
{"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "};
/**
diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java
index 1bd44fa5c2c6..ce610f17ae73 100644
--- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java
+++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java
@@ -16,18 +16,40 @@
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.UnsupportedAppUsage;
+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;
@@ -35,8 +57,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*)?");
@@ -46,6 +67,7 @@ public class SQLiteQueryBuilder
private String mTables = "";
@UnsupportedAppUsage
private StringBuilder mWhereClause = null; // lazily created
+ private String[] mWhereArgs = EmptyArray.STRING;
@UnsupportedAppUsage
private boolean mDistinct;
private SQLiteDatabase.CursorFactory mFactory;
@@ -86,43 +108,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 (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;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 (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;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 (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;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 (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;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);
}
/**
@@ -172,8 +243,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;
}
/**
@@ -267,7 +338,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,
@@ -292,10 +363,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 */);
}
@@ -304,7 +379,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,
@@ -331,10 +406,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);
}
@@ -343,7 +423,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,
@@ -373,14 +488,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
@@ -388,25 +548,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));
+
+ final String[] sqlArgs = ArrayUtils.concat(String.class,
+ queryArgs.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS), mWhereArgs);
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Performing query: " + sql);
+ 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.
@@ -438,28 +702,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);
}
@@ -476,6 +722,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
@@ -601,7 +883,7 @@ public class SQLiteQueryBuilder
}
@UnsupportedAppUsage
- 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];
@@ -624,7 +906,7 @@ public class SQLiteQueryBuilder
}
throw new IllegalArgumentException("Invalid column "
- + projectionIn[i]);
+ + projectionIn[i] + " from tables " + mTables);
}
return projection;
} else {
@@ -650,4 +932,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);
+ }
+ }
}