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 (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;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 (&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.
      */
-    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 (&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 {@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.
+     * @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 }));
+    }
 }