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 ac90035bd6cb..69a3b74640b4 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 f83eeda3e06e..eb5c720d6309 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 })); + } } |