diff options
6 files changed, 1172 insertions, 75 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/SQLiteStatementBuilder.java b/core/java/android/database/sqlite/SQLiteStatementBuilder.java new file mode 100644 index 000000000000..e2efb2f8c39b --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteStatementBuilder.java @@ -0,0 +1,1036 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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; + +/** + * This is a convenience class that helps build SQL queries to be sent to + * {@link SQLiteDatabase} objects. + * @hide + */ +public class SQLiteStatementBuilder { +    private static final String TAG = "SQLiteStatementBuilder"; +    private static final Pattern sLimitPattern = +            Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?"); + +    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; + +    public SQLiteStatementBuilder() { +        mDistinct = false; +        mFactory = null; +    } + +    /** +     * Mark the query as DISTINCT. +     * +     * @param distinct if true the query is DISTINCT, otherwise it isn't +     */ +    public void setDistinct(boolean distinct) { +        mDistinct = distinct; +    } + +    /** +     * Returns the list of tables being queried +     * +     * @return the list of tables being queried +     */ +    public String getTables() { +        return mTables; +    } + +    /** +     * Sets the list of tables to query. Multiple tables can be specified to perform a join. +     * For example: +     *   setTables("foo, bar") +     *   setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)") +     * +     * @param inTables the list of tables to query on +     */ +    public void setTables(String inTables) { +        mTables = inTables; +    } + +    /** {@hide} */ +    public @Nullable String getWhere() { +        return (mWhereClause != null) ? mWhereClause.toString() : null; +    } + +    /** {@hide} */ +    public String[] getWhereArgs() { +        return mWhereArgs; +    } + +    /** +     * 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. +     */ +    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); +        } +        mWhereClause.append(inWhere); +        mWhereArgs = ArrayUtils.concat(String.class, mWhereArgs, inWhereArgs); +    } + +    /** +     * Append a standalone expression to the {@code WHERE} clause of this query. +     * <p> +     * This method differs from {@link #appendWhere(CharSequence)} in that it +     * automatically appends {@code AND} to any existing {@code WHERE} clause +     * already under construction before appending the given standalone +     * expression. +     * +     * @param inWhere the standalone expression to append to the {@code WHERE} +     *            clause. It will be wrapped in parentheses when it's appended. +     */ +    public void appendWhereExpression(@NonNull CharSequence inWhere) { +        appendWhereExpression(inWhere, EmptyArray.STRING); +    } + +    /** +     * Append a standalone expression to the {@code WHERE} clause of this query. +     * <p> +     * This method differs from {@link #appendWhere(CharSequence)} in that it +     * automatically appends {@code AND} to any existing {@code WHERE} clause +     * already under construction before appending the given standalone +     * expression. +     * +     * @param inWhere the standalone expression to append to the {@code WHERE} +     *            clause. It will be wrapped in parentheses when it's appended. +     * @param inWhereArgs list of arguments to be bound to any '?' occurrences +     *            in the standalone expression. +     */ +    public void appendWhereExpression(@NonNull CharSequence inWhere, String... inWhereArgs) { +        if (mWhereClause == null) { +            mWhereClause = new StringBuilder(inWhere.length() + 16); +        } +        if (mWhereClause.length() > 0) { +            mWhereClause.append(" AND "); +        } +        mWhereClause.append('(').append(inWhere).append(')'); +        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); +    } + +    /** +     * Sets the projection map for the query.  The projection map maps +     * from column names that the caller passes into query to database +     * column names. This is useful for renaming columns as well as +     * disambiguating column names when doing joins. For example you +     * could map "name" to "people.name".  If a projection map is set +     * it must contain all column names the user may request, even if +     * the key and value are the same. +     * +     * @param columnMap maps from the user column names to the database column names +     */ +    public void setProjectionMap(Map<String, String> columnMap) { +        mProjectionMap = columnMap; +    } + +    /** +     * Sets the cursor factory to be used for the query.  You can use +     * one factory for all queries on a database but it is normally +     * easier to specify the factory when doing this query. +     * +     * @param factory the factory to use. +     */ +    public void setCursorFactory(SQLiteDatabase.CursorFactory factory) { +        mFactory = factory; +    } + +    /** +     * When set, the selection is verified against malicious arguments. +     * When using this class to create a statement using +     * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)}, +     * non-numeric limits will raise an exception. If a projection map is specified, fields +     * not in that map will be ignored. +     * If this class is used to execute the statement directly using +     * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)} +     * or +     * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)}, +     * additionally also parenthesis escaping selection are caught. +     * +     * To summarize: To get maximum protection against malicious third party apps (for example +     * content provider consumers), make sure to do the following: +     * <ul> +     * <li>Set this value to true</li> +     * <li>Use a projection map</li> +     * <li>Use one of the query overloads instead of getting the statement as a sql string</li> +     * </ul> +     * By default, this value is false. +     */ +    public void setStrict(boolean strict) { +        mStrict = strict; +    } + +    /** +     * Build an SQL query string from the given clauses. +     * +     * @param distinct true if you want each row to be unique, false otherwise. +     * @param tables The table names to compile the query against. +     * @param columns 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 where 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 groupBy A filter declaring how to group rows, formatted as an SQL +     *            GROUP BY clause (excluding the GROUP BY itself). Passing null +     *            will cause the rows to not be grouped. +     * @param having A filter declare which row groups to include in the cursor, +     *            if row grouping is being used, formatted as an SQL HAVING +     *            clause (excluding the HAVING itself). Passing null will cause +     *            all row groups to be included, and is required when row +     *            grouping is not being used. +     * @param orderBy 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 limit Limits the number of rows returned by the query, +     *            formatted as LIMIT clause. Passing null denotes no LIMIT clause. +     * @return the SQL query string +     */ +    public static String buildQueryString( +            boolean distinct, String tables, String[] columns, String where, +            String groupBy, String having, String orderBy, String limit) { +        if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) { +            throw new IllegalArgumentException( +                    "HAVING clauses are only permitted when using a groupBy clause"); +        } +        if (!TextUtils.isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) { +            throw new IllegalArgumentException("invalid LIMIT clauses:" + limit); +        } + +        StringBuilder query = new StringBuilder(120); + +        query.append("SELECT "); +        if (distinct) { +            query.append("DISTINCT "); +        } +        if (columns != null && columns.length != 0) { +            appendColumns(query, columns); +        } else { +            query.append("* "); +        } +        query.append("FROM "); +        query.append(tables); +        appendClause(query, " WHERE ", where); +        appendClause(query, " GROUP BY ", groupBy); +        appendClause(query, " HAVING ", having); +        appendClause(query, " ORDER BY ", orderBy); +        appendClause(query, " LIMIT ", limit); + +        return query.toString(); +    } + +    private static void appendClause(StringBuilder s, String name, String clause) { +        if (!TextUtils.isEmpty(clause)) { +            s.append(name); +            s.append(clause); +        } +    } + +    /** +     * Add the names that are non-null in columns to s, separating +     * them with commas. +     */ +    public static void appendColumns(StringBuilder s, String[] columns) { +        int n = columns.length; + +        for (int i = 0; i < n; i++) { +            String column = columns[i]; + +            if (column != null) { +                if (i > 0) { +                    s.append(", "); +                } +                s.append(column); +            } +        } +        s.append(' '); +    } + +    /** +     * 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, +     *   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 groupBy A filter declaring how to group rows, formatted +     *   as an SQL GROUP BY clause (excluding the GROUP BY +     *   itself). Passing null will cause the rows to not be grouped. +     * @param having A filter declare which row groups to include in +     *   the cursor, if row grouping is being used, formatted as an +     *   SQL HAVING clause (excluding the HAVING itself).  Passing +     *   null will cause all row groups to be included, and is +     *   required when row grouping is not being used. +     * @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. +     * @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 groupBy, +            @Nullable String having, +            @Nullable String sortOrder) { +        return query(db, projection, selection, selectionArgs, groupBy, having, sortOrder, +                null /* limit */, 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, +     *   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 groupBy A filter declaring how to group rows, formatted +     *   as an SQL GROUP BY clause (excluding the GROUP BY +     *   itself). Passing null will cause the rows to not be grouped. +     * @param having A filter declare which row groups to include in +     *   the cursor, if row grouping is being used, formatted as an +     *   SQL HAVING clause (excluding the HAVING itself).  Passing +     *   null will cause all row groups to be included, and is +     *   required when row grouping is not being used. +     * @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 limit Limits the number of rows returned by the query, +     *   formatted as LIMIT clause. Passing null denotes no LIMIT clause. +     * @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 groupBy, +            @Nullable String having, +            @Nullable String sortOrder, +            @Nullable String limit) { +        return query(db, projection, selection, selectionArgs, +                groupBy, having, sortOrder, limit, null); +    } + +    /** +     * 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, +     *   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, +     *   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 groupBy A filter declaring how to group rows, formatted +     *   as an SQL GROUP BY clause (excluding the GROUP BY +     *   itself). Passing null will cause the rows to not be grouped. +     * @param having A filter declare which row groups to include in +     *   the cursor, if row grouping is being used, formatted as an +     *   SQL HAVING clause (excluding the HAVING itself).  Passing +     *   null will cause all row groups to be included, and is +     *   required when row grouping is not being used. +     * @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 limit Limits the number of rows returned by the query, +     *   formatted as LIMIT clause. Passing null denotes no LIMIT clause. +     * @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 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 (queryArgs == null) { +            queryArgs = Bundle.EMPTY; +        } + +        // Final SQL that we will execute +        final String sql; + +        final String unwrappedSql = 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 (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. + +            // NOTE: The ordering of the below operations is important; we must +            // execute the wrapped query to ensure the untrusted clause has been +            // fully isolated. + +            // TODO: decode SORT ORDER and LIMIT clauses, since they can contain +            // "expr" inside that need to be validated + +            final String wrappedSql = buildQuery(projection, +                    wrap(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)); + +            // Validate the unwrapped query +            db.validateSql(unwrappedSql, cancellationSignal); + +            // Execute wrapped query for extra protection +            sql = wrappedSql; +        } else { +            // Execute unwrapped query +            sql = unwrappedSql; +        } + +        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, 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++) { +            final Object arg = rawValues.valueAt(i); +            updateArgs[i] = (arg != null) ? arg.toString() : null; +        } + +        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. +     * +     * @param projectionIn 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 groupBy A filter declaring how to group rows, formatted +     *   as an SQL GROUP BY clause (excluding the GROUP BY itself). +     *   Passing null will cause the rows to not be grouped. +     * @param having A filter declare which row groups to include in +     *   the cursor, if row grouping is being used, formatted as an +     *   SQL HAVING clause (excluding the HAVING itself).  Passing +     *   null will cause all row groups to be included, and is +     *   required when row grouping is not being used. +     * @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 limit Limits the number of rows returned by the query, +     *   formatted as LIMIT clause. Passing null denotes no LIMIT clause. +     * @return the resulting SQL SELECT statement +     */ +    public String buildQuery( +            String[] projectionIn, String selection, String groupBy, +            String having, String sortOrder, String limit) { +        String[] projection = computeProjection(projectionIn); +        String where = computeWhere(selection); + +        return buildQueryString( +                mDistinct, mTables, projection, where, +                groupBy, having, sortOrder, limit); +    } + +    /** +     * @deprecated This method's signature is misleading since no SQL parameter +     * substitution is carried out.  The selection arguments parameter does not get +     * used at all.  To avoid confusion, call +     * {@link #buildQuery(String[], String, String, String, String, String)} instead. +     */ +    @Deprecated +    public String buildQuery( +            String[] projectionIn, String selection, String[] selectionArgs, +            String groupBy, String having, String sortOrder, String limit) { +        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 +     * in buildUnionQuery. +     * +     * @param typeDiscriminatorColumn the name of the result column +     *   whose cells will contain the name of the table from which +     *   each row was drawn. +     * @param unionColumns the names of the columns to appear in the +     *   result.  This may include columns that do not appear in the +     *   table this SELECT is querying (i.e. mTables), but that do +     *   appear in one of the other tables in the UNION query that we +     *   are constructing. +     * @param columnsPresentInTable a Set of the names of the columns +     *   that appear in this table (i.e. in the table whose name is +     *   mTables).  Since columns in unionColumns include columns that +     *   appear only in other tables, we use this array to distinguish +     *   which ones actually are present.  Other columns will have +     *   NULL values for results from this subquery. +     * @param computedColumnsOffset all columns in unionColumns before +     *   this index are included under the assumption that they're +     *   computed and therefore won't appear in columnsPresentInTable, +     *   e.g. "date * 1000 as normalized_date" +     * @param typeDiscriminatorValue the value used for the +     *   type-discriminator column in this subquery +     * @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 groupBy A filter declaring how to group rows, formatted +     *   as an SQL GROUP BY clause (excluding the GROUP BY itself). +     *   Passing null will cause the rows to not be grouped. +     * @param having A filter declare which row groups to include in +     *   the cursor, if row grouping is being used, formatted as an +     *   SQL HAVING clause (excluding the HAVING itself).  Passing +     *   null will cause all row groups to be included, and is +     *   required when row grouping is not being used. +     * @return the resulting SQL SELECT statement +     */ +    public String buildUnionSubQuery( +            String typeDiscriminatorColumn, +            String[] unionColumns, +            Set<String> columnsPresentInTable, +            int computedColumnsOffset, +            String typeDiscriminatorValue, +            String selection, +            String groupBy, +            String having) { +        int unionColumnsCount = unionColumns.length; +        String[] projectionIn = new String[unionColumnsCount]; + +        for (int i = 0; i < unionColumnsCount; i++) { +            String unionColumn = unionColumns[i]; + +            if (unionColumn.equals(typeDiscriminatorColumn)) { +                projectionIn[i] = "'" + typeDiscriminatorValue + "' AS " +                        + typeDiscriminatorColumn; +            } else if (i <= computedColumnsOffset +                       || columnsPresentInTable.contains(unionColumn)) { +                projectionIn[i] = unionColumn; +            } else { +                projectionIn[i] = "NULL AS " + unionColumn; +            } +        } +        return buildQuery( +                projectionIn, selection, groupBy, having, +                null /* sortOrder */, +                null /* limit */); +    } + +    /** +     * @deprecated This method's signature is misleading since no SQL parameter +     * substitution is carried out.  The selection arguments parameter does not get +     * used at all.  To avoid confusion, call +     * {@link #buildUnionSubQuery} +     * instead. +     */ +    @Deprecated +    public String buildUnionSubQuery( +            String typeDiscriminatorColumn, +            String[] unionColumns, +            Set<String> columnsPresentInTable, +            int computedColumnsOffset, +            String typeDiscriminatorValue, +            String selection, +            String[] selectionArgs, +            String groupBy, +            String having) { +        return buildUnionSubQuery( +                typeDiscriminatorColumn, unionColumns, columnsPresentInTable, +                computedColumnsOffset, typeDiscriminatorValue, selection, +                groupBy, having); +    } + +    /** +     * Given a set of subqueries, all of which are SELECT statements, +     * construct a query that returns the union of what those +     * subqueries return. +     * @param subQueries an array of SQL SELECT statements, all of +     *   which must have the same columns as the same positions in +     *   their results +     * @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 limit The limit clause, which applies to the entire union result set +     * +     * @return the resulting SQL SELECT statement +     */ +    public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) { +        StringBuilder query = new StringBuilder(128); +        int subQueryCount = subQueries.length; +        String unionOperator = mDistinct ? " UNION " : " UNION ALL "; + +        for (int i = 0; i < subQueryCount; i++) { +            if (i > 0) { +                query.append(unionOperator); +            } +            query.append(subQueries[i]); +        } +        appendClause(query, " ORDER BY ", sortOrder); +        appendClause(query, " LIMIT ", limit); +        return query.toString(); +    } + +    private @Nullable String[] computeProjection(@Nullable String[] projectionIn) { +        if (projectionIn != null && projectionIn.length > 0) { +            if (mProjectionMap != null) { +                String[] projection = new String[projectionIn.length]; +                int length = projectionIn.length; + +                for (int i = 0; i < length; i++) { +                    String userColumn = projectionIn[i]; +                    String column = mProjectionMap.get(userColumn); + +                    if (column != null) { +                        projection[i] = column; +                        continue; +                    } + +                    if (!mStrict && +                            ( userColumn.contains(" AS ") || userColumn.contains(" as "))) { +                        /* A column alias already exist */ +                        projection[i] = userColumn; +                        continue; +                    } + +                    throw new IllegalArgumentException("Invalid column " +                            + projectionIn[i] + " from tables " + mTables); +                } +                return projection; +            } else { +                return projectionIn; +            } +        } else if (mProjectionMap != null) { +            // Return all columns in projection map. +            Set<Entry<String, String>> entrySet = mProjectionMap.entrySet(); +            String[] projection = new String[entrySet.size()]; +            Iterator<Entry<String, String>> entryIter = entrySet.iterator(); +            int i = 0; + +            while (entryIter.hasNext()) { +                Entry<String, String> entry = entryIter.next(); + +                // Don't include the _count column when people ask for no projection. +                if (entry.getKey().equals(BaseColumns._COUNT)) { +                    continue; +                } +                projection[i++] = entry.getValue(); +            } +            return projection; +        } +        return null; +    } + +    private @NonNull String computeWhere(@Nullable String selection) { +        final boolean hasUser = selection != null && selection.length() > 0; +        final boolean hasInternal = mWhereClause != null && mWhereClause.length() > 0; + +        if (hasUser || hasInternal) { +            final StringBuilder where = new StringBuilder(); +            if (hasUser) { +                where.append('(').append(selection).append(')'); +            } +            if (hasUser && hasInternal) { +                where.append(" AND "); +            } +            if (hasInternal) { +                where.append('(').append(mWhereClause.toString()).append(')'); +            } +            return where.toString(); +        } else { +            return null; +        } +    } + +    /** +     * Wrap given argument in parenthesis, unless it's {@code null} or +     * {@code ()}, in which case return it verbatim. +     */ +    private @Nullable String wrap(@Nullable String arg) { +        if (arg == null) { +            return null; +        } else if (arg.equals("")) { +            return arg; +        } else { +            return "(" + arg + ")"; +        } +    } + +    private static void maybePutString(@NonNull Bundle bundle, @NonNull String key, +            @Nullable String value) { +        if (value != null) { +            bundle.putString(key, value); +        } +    } + +    private static void maybePutStringArray(@NonNull Bundle bundle, @NonNull String key, +            @Nullable String[] value) { +        if (value != null) { +            bundle.putStringArray(key, value); +        } +    } +} diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index be645fe707c6..c3d33ca84ee1 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -308,6 +308,23 @@ public class ArrayUtils {          return array;      } +    @SuppressWarnings("unchecked") +    public static @NonNull <T> T[] concat(Class<T> kind, @Nullable T[] a, @Nullable T[] b) { +        final int an = (a != null) ? a.length : 0; +        final int bn = (b != null) ? b.length : 0; +        if (an == 0 && bn == 0) { +            if (kind == String.class) { +                return (T[]) EmptyArray.STRING; +            } else if (kind == Object.class) { +                return (T[]) EmptyArray.OBJECT; +            } +        } +        final T[] res = (T[]) Array.newInstance(kind, an + bn); +        if (an > 0) System.arraycopy(a, 0, res, 0, an); +        if (bn > 0) System.arraycopy(b, 0, res, an, bn); +        return res; +    } +      /**       * Adds value to given array if not already present, providing set-like       * behavior. diff --git a/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java index 433d4d214b97..6464ad3e9709 100644 --- a/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java +++ b/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java @@ -16,9 +16,8 @@  package com.android.internal.util; -import android.test.MoreAsserts; +import static org.junit.Assert.assertArrayEquals; -import java.util.Arrays;  import junit.framework.TestCase;  /** @@ -92,29 +91,29 @@ public class ArrayUtilsTest extends TestCase {      }      public void testAppendInt() throws Exception { -        MoreAsserts.assertEquals(new int[] { 1 }, +        assertArrayEquals(new int[] { 1 },                  ArrayUtils.appendInt(null, 1)); -        MoreAsserts.assertEquals(new int[] { 1 }, +        assertArrayEquals(new int[] { 1 },                  ArrayUtils.appendInt(new int[] { }, 1)); -        MoreAsserts.assertEquals(new int[] { 1, 2 }, +        assertArrayEquals(new int[] { 1, 2 },                  ArrayUtils.appendInt(new int[] { 1 }, 2)); -        MoreAsserts.assertEquals(new int[] { 1, 2 }, +        assertArrayEquals(new int[] { 1, 2 },                  ArrayUtils.appendInt(new int[] { 1, 2 }, 1));      }      public void testRemoveInt() throws Exception {          assertNull(ArrayUtils.removeInt(null, 1)); -        MoreAsserts.assertEquals(new int[] { }, +        assertArrayEquals(new int[] { },                  ArrayUtils.removeInt(new int[] { }, 1)); -        MoreAsserts.assertEquals(new int[] { 1, 2, 3, }, +        assertArrayEquals(new int[] { 1, 2, 3, },                  ArrayUtils.removeInt(new int[] { 1, 2, 3}, 4)); -        MoreAsserts.assertEquals(new int[] { 2, 3, }, +        assertArrayEquals(new int[] { 2, 3, },                  ArrayUtils.removeInt(new int[] { 1, 2, 3}, 1)); -        MoreAsserts.assertEquals(new int[] { 1, 3, }, +        assertArrayEquals(new int[] { 1, 3, },                  ArrayUtils.removeInt(new int[] { 1, 2, 3}, 2)); -        MoreAsserts.assertEquals(new int[] { 1, 2, }, +        assertArrayEquals(new int[] { 1, 2, },                  ArrayUtils.removeInt(new int[] { 1, 2, 3}, 3)); -        MoreAsserts.assertEquals(new int[] { 2, 3, 1 }, +        assertArrayEquals(new int[] { 2, 3, 1 },                  ArrayUtils.removeInt(new int[] { 1, 2, 3, 1 }, 1));      } @@ -129,30 +128,51 @@ public class ArrayUtilsTest extends TestCase {      }      public void testAppendLong() throws Exception { -        MoreAsserts.assertEquals(new long[] { 1 }, +        assertArrayEquals(new long[] { 1 },                  ArrayUtils.appendLong(null, 1)); -        MoreAsserts.assertEquals(new long[] { 1 }, +        assertArrayEquals(new long[] { 1 },                  ArrayUtils.appendLong(new long[] { }, 1)); -        MoreAsserts.assertEquals(new long[] { 1, 2 }, +        assertArrayEquals(new long[] { 1, 2 },                  ArrayUtils.appendLong(new long[] { 1 }, 2)); -        MoreAsserts.assertEquals(new long[] { 1, 2 }, +        assertArrayEquals(new long[] { 1, 2 },                  ArrayUtils.appendLong(new long[] { 1, 2 }, 1));      }      public void testRemoveLong() throws Exception {          assertNull(ArrayUtils.removeLong(null, 1)); -        MoreAsserts.assertEquals(new long[] { }, +        assertArrayEquals(new long[] { },                  ArrayUtils.removeLong(new long[] { }, 1)); -        MoreAsserts.assertEquals(new long[] { 1, 2, 3, }, +        assertArrayEquals(new long[] { 1, 2, 3, },                  ArrayUtils.removeLong(new long[] { 1, 2, 3}, 4)); -        MoreAsserts.assertEquals(new long[] { 2, 3, }, +        assertArrayEquals(new long[] { 2, 3, },                  ArrayUtils.removeLong(new long[] { 1, 2, 3}, 1)); -        MoreAsserts.assertEquals(new long[] { 1, 3, }, +        assertArrayEquals(new long[] { 1, 3, },                  ArrayUtils.removeLong(new long[] { 1, 2, 3}, 2)); -        MoreAsserts.assertEquals(new long[] { 1, 2, }, +        assertArrayEquals(new long[] { 1, 2, },                  ArrayUtils.removeLong(new long[] { 1, 2, 3}, 3)); -        MoreAsserts.assertEquals(new long[] { 2, 3, 1 }, +        assertArrayEquals(new long[] { 2, 3, 1 },                  ArrayUtils.removeLong(new long[] { 1, 2, 3, 1 }, 1));      } +    public void testConcatEmpty() throws Exception { +        assertArrayEquals(new Long[] {}, +                ArrayUtils.concat(Long.class, null, null)); +        assertArrayEquals(new Long[] {}, +                ArrayUtils.concat(Long.class, new Long[] {}, null)); +        assertArrayEquals(new Long[] {}, +                ArrayUtils.concat(Long.class, null, new Long[] {})); +        assertArrayEquals(new Long[] {}, +                ArrayUtils.concat(Long.class, new Long[] {}, new Long[] {})); +    } + +    public void testConcat() throws Exception { +        assertArrayEquals(new Long[] { 1L }, +                ArrayUtils.concat(Long.class, new Long[] { 1L }, new Long[] {})); +        assertArrayEquals(new Long[] { 1L }, +                ArrayUtils.concat(Long.class, new Long[] {}, new Long[] { 1L })); +        assertArrayEquals(new Long[] { 1L, 2L }, +                ArrayUtils.concat(Long.class, new Long[] { 1L }, new Long[] { 2L })); +        assertArrayEquals(new Long[] { 1L, 2L, 3L, 4L }, +                ArrayUtils.concat(Long.class, new Long[] { 1L, 2L }, new Long[] { 3L, 4L })); +    }  } |