summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/current.txt1
-rw-r--r--core/java/android/database/DatabaseUtils.java89
-rw-r--r--core/java/android/database/sqlite/SQLiteQueryBuilder.java28
-rw-r--r--core/java/android/database/sqlite/SQLiteStatementBuilder.java1036
-rw-r--r--core/java/com/android/internal/util/ArrayUtils.java2
-rw-r--r--core/tests/coretests/src/android/database/DatabaseUtilsTest.java66
-rw-r--r--core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java20
7 files changed, 192 insertions, 1050 deletions
diff --git a/api/current.txt b/api/current.txt
index 7809410bf34c..ac1a1b972d60 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -12674,6 +12674,7 @@ package android.database.sqlite {
method public static void appendColumns(java.lang.StringBuilder, java.lang.String[]);
method public void appendWhere(java.lang.CharSequence);
method public void appendWhereEscapeString(java.lang.String);
+ method public void appendWhereStandalone(java.lang.CharSequence);
method public java.lang.String buildQuery(java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String);
method public deprecated java.lang.String buildQuery(java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String);
method public static java.lang.String buildQueryString(boolean, java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String);
diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java
index 3d019f07cb84..7a8ab60c502d 100644
--- a/core/java/android/database/DatabaseUtils.java
+++ b/core/java/android/database/DatabaseUtils.java
@@ -16,6 +16,7 @@
package android.database;
+import android.annotation.Nullable;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
@@ -34,6 +35,8 @@ import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.util.Log;
+import com.android.internal.util.ArrayUtils;
+
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.text.Collator;
@@ -216,6 +219,92 @@ public class DatabaseUtils {
}
/**
+ * Bind the given selection with the given selection arguments.
+ * <p>
+ * Internally assumes that '?' is only ever used for arguments, and doesn't
+ * appear as a literal or escaped value.
+ * <p>
+ * This method is typically useful for trusted code that needs to cook up a
+ * fully-bound selection.
+ *
+ * @hide
+ */
+ public static @Nullable String bindSelection(@Nullable String selection,
+ @Nullable Object... selectionArgs) {
+ if (selection == null) return null;
+ // If no arguments provided, so we can't bind anything
+ if (ArrayUtils.isEmpty(selectionArgs)) return selection;
+ // If no bindings requested, so we can shortcut
+ if (selection.indexOf('?') == -1) return selection;
+
+ // Track the chars immediately before and after each bind request, to
+ // decide if it needs additional whitespace added
+ char before = ' ';
+ char after = ' ';
+
+ int argIndex = 0;
+ final int len = selection.length();
+ final StringBuilder res = new StringBuilder(len);
+ for (int i = 0; i < len; ) {
+ char c = selection.charAt(i++);
+ if (c == '?') {
+ // Assume this bind request is guarded until we find a specific
+ // trailing character below
+ after = ' ';
+
+ // Sniff forward to see if the selection is requesting a
+ // specific argument index
+ int start = i;
+ for (; i < len; i++) {
+ c = selection.charAt(i);
+ if (c < '0' || c > '9') {
+ after = c;
+ break;
+ }
+ }
+ if (start != i) {
+ argIndex = Integer.parseInt(selection.substring(start, i)) - 1;
+ }
+
+ // Manually bind the argument into the selection, adding
+ // whitespace when needed for clarity
+ final Object arg = selectionArgs[argIndex++];
+ if (before != ' ' && before != '=') res.append(' ');
+ switch (DatabaseUtils.getTypeOfObject(arg)) {
+ case Cursor.FIELD_TYPE_NULL:
+ res.append("NULL");
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ res.append(((Number) arg).longValue());
+ break;
+ case Cursor.FIELD_TYPE_FLOAT:
+ res.append(((Number) arg).doubleValue());
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ throw new IllegalArgumentException("Blobs not supported");
+ case Cursor.FIELD_TYPE_STRING:
+ default:
+ if (arg instanceof Boolean) {
+ // Provide compatibility with legacy applications which may pass
+ // Boolean values in bind args.
+ res.append(((Boolean) arg).booleanValue() ? 1 : 0);
+ } else {
+ res.append('\'');
+ res.append(arg.toString());
+ res.append('\'');
+ }
+ break;
+ }
+ if (after != ' ') res.append(' ');
+ } else {
+ res.append(c);
+ before = c;
+ }
+ }
+ return res.toString();
+ }
+
+ /**
* Returns data type of the given object's value.
*<p>
* Returned values are
diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java
index 3298140b4a9b..06560f2e4887 100644
--- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java
+++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java
@@ -43,8 +43,7 @@ import java.util.regex.Pattern;
* This is a convenience class that helps build SQL queries to be sent to
* {@link SQLiteDatabase} objects.
*/
-public class SQLiteQueryBuilder
-{
+public class SQLiteQueryBuilder {
private static final String TAG = "SQLiteQueryBuilder";
private static final Pattern sLimitPattern =
Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?");
@@ -100,7 +99,7 @@ public class SQLiteQueryBuilder
*
* @param inWhere the chunk of text to append to the WHERE clause.
*/
- public void appendWhere(CharSequence inWhere) {
+ public void appendWhere(@NonNull CharSequence inWhere) {
if (mWhereClause == null) {
mWhereClause = new StringBuilder(inWhere.length() + 16);
}
@@ -117,7 +116,7 @@ public class SQLiteQueryBuilder
* @param inWhere the chunk of text to append to the WHERE clause. it will be escaped
* to avoid SQL injection attacks
*/
- public void appendWhereEscapeString(String inWhere) {
+ public void appendWhereEscapeString(@NonNull String inWhere) {
if (mWhereClause == null) {
mWhereClause = new StringBuilder(inWhere.length() + 16);
}
@@ -125,6 +124,27 @@ public class SQLiteQueryBuilder
}
/**
+ * Add a standalone chunk 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 wrapped in parentheses.
+ *
+ * @param inWhere the standalone expression to append to the {@code WHERE}
+ * clause. It will be wrapped in parentheses when it's appended.
+ */
+ public void appendWhereStandalone(@NonNull CharSequence inWhere) {
+ if (mWhereClause == null) {
+ mWhereClause = new StringBuilder(inWhere.length() + 16);
+ }
+ if (mWhereClause.length() > 0) {
+ mWhereClause.append(" AND ");
+ }
+ mWhereClause.append('(').append(inWhere).append(')');
+ }
+
+ /**
* 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
diff --git a/core/java/android/database/sqlite/SQLiteStatementBuilder.java b/core/java/android/database/sqlite/SQLiteStatementBuilder.java
deleted file mode 100644
index e2efb2f8c39b..000000000000
--- a/core/java/android/database/sqlite/SQLiteStatementBuilder.java
+++ /dev/null
@@ -1,1036 +0,0 @@
-/*
- * 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 (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
- * </pre>
- *
- * @param inWhere the chunk of text to append to the {@code WHERE} clause.
- */
- public void appendWhere(@NonNull CharSequence inWhere) {
- appendWhere(inWhere, EmptyArray.STRING);
- }
-
- /**
- * Append a chunk to the {@code WHERE} clause of the query. All chunks
- * appended are surrounded by parenthesis and {@code AND}ed with the
- * selection passed to {@link #query}. The final {@code WHERE} clause looks
- * like:
- *
- * <pre>
- * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
- * </pre>
- *
- * @param inWhere the chunk of text to append to the {@code WHERE} clause.
- * @param inWhereArgs list of arguments to be bound to any '?' occurrences
- * in the where clause.
- */
- public void appendWhere(@NonNull CharSequence inWhere, String... inWhereArgs) {
- if (mWhereClause == null) {
- mWhereClause = new StringBuilder(inWhere.length() + 16);
- }
- 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 (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
- * </pre>
- *
- * @param inWhere the chunk of text to append to the {@code WHERE} clause.
- * It will be escaped to avoid SQL injection attacks.
- */
- public void appendWhereEscapeString(@NonNull String inWhere) {
- appendWhereEscapeString(inWhere, EmptyArray.STRING);
- }
-
- /**
- * Append a chunk to the {@code WHERE} clause of the query. All chunks
- * appended are surrounded by parenthesis and {@code AND}ed with the
- * selection passed to {@link #query}. The final {@code WHERE} clause looks
- * like this:
- *
- * <pre>
- * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
- * </pre>
- *
- * @param inWhere the chunk of text to append to the {@code WHERE} clause.
- * It will be escaped to avoid SQL injection attacks.
- * @param inWhereArgs list of arguments to be bound to any '?' occurrences
- * in the where clause.
- */
- public void appendWhereEscapeString(@NonNull String inWhere, String... inWhereArgs) {
- if (mWhereClause == null) {
- mWhereClause = new StringBuilder(inWhere.length() + 16);
- }
- DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
- mWhereArgs = ArrayUtils.concat(String.class, mWhereArgs, inWhereArgs);
- }
-
- /**
- * 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 c3d33ca84ee1..4b662670f5e7 100644
--- a/core/java/com/android/internal/util/ArrayUtils.java
+++ b/core/java/com/android/internal/util/ArrayUtils.java
@@ -309,7 +309,7 @@ public class ArrayUtils {
}
@SuppressWarnings("unchecked")
- public static @NonNull <T> T[] concat(Class<T> kind, @Nullable T[] a, @Nullable T[] b) {
+ public static @NonNull <T> T[] concatElements(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) {
diff --git a/core/tests/coretests/src/android/database/DatabaseUtilsTest.java b/core/tests/coretests/src/android/database/DatabaseUtilsTest.java
new file mode 100644
index 000000000000..7c206d7eecf7
--- /dev/null
+++ b/core/tests/coretests/src/android/database/DatabaseUtilsTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2018 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;
+
+import static android.database.DatabaseUtils.bindSelection;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class DatabaseUtilsTest {
+ private static final Object[] ARGS = { "baz", 4, null };
+
+ @Test
+ public void testBindSelection_none() throws Exception {
+ assertEquals(null,
+ bindSelection(null, ARGS));
+ assertEquals("",
+ bindSelection("", ARGS));
+ assertEquals("foo=bar",
+ bindSelection("foo=bar", ARGS));
+ }
+
+ @Test
+ public void testBindSelection_normal() throws Exception {
+ assertEquals("foo='baz'",
+ bindSelection("foo=?", ARGS));
+ assertEquals("foo='baz' AND bar=4",
+ bindSelection("foo=? AND bar=?", ARGS));
+ assertEquals("foo='baz' AND bar=4 AND meow=NULL",
+ bindSelection("foo=? AND bar=? AND meow=?", ARGS));
+ }
+
+ @Test
+ public void testBindSelection_whitespace() throws Exception {
+ assertEquals("BETWEEN 5 AND 10",
+ bindSelection("BETWEEN? AND ?", 5, 10));
+ assertEquals("IN 'foo'",
+ bindSelection("IN?", "foo"));
+ }
+
+ @Test
+ public void testBindSelection_indexed() throws Exception {
+ assertEquals("foo=10 AND bar=11 AND meow=1",
+ bindSelection("foo=?10 AND bar=? AND meow=?1",
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12));
+ }
+}
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 6464ad3e9709..39bb84a20d7a 100644
--- a/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java
+++ b/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java
@@ -16,6 +16,8 @@
package com.android.internal.util;
+import static com.android.internal.util.ArrayUtils.concatElements;
+
import static org.junit.Assert.assertArrayEquals;
import junit.framework.TestCase;
@@ -156,23 +158,23 @@ public class ArrayUtilsTest extends TestCase {
public void testConcatEmpty() throws Exception {
assertArrayEquals(new Long[] {},
- ArrayUtils.concat(Long.class, null, null));
+ concatElements(Long.class, null, null));
assertArrayEquals(new Long[] {},
- ArrayUtils.concat(Long.class, new Long[] {}, null));
+ concatElements(Long.class, new Long[] {}, null));
assertArrayEquals(new Long[] {},
- ArrayUtils.concat(Long.class, null, new Long[] {}));
+ concatElements(Long.class, null, new Long[] {}));
assertArrayEquals(new Long[] {},
- ArrayUtils.concat(Long.class, new Long[] {}, new Long[] {}));
+ concatElements(Long.class, new Long[] {}, new Long[] {}));
}
- public void testConcat() throws Exception {
+ public void testconcatElements() throws Exception {
assertArrayEquals(new Long[] { 1L },
- ArrayUtils.concat(Long.class, new Long[] { 1L }, new Long[] {}));
+ concatElements(Long.class, new Long[] { 1L }, new Long[] {}));
assertArrayEquals(new Long[] { 1L },
- ArrayUtils.concat(Long.class, new Long[] {}, new Long[] { 1L }));
+ concatElements(Long.class, new Long[] {}, new Long[] { 1L }));
assertArrayEquals(new Long[] { 1L, 2L },
- ArrayUtils.concat(Long.class, new Long[] { 1L }, new Long[] { 2L }));
+ concatElements(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 }));
+ concatElements(Long.class, new Long[] { 1L, 2L }, new Long[] { 3L, 4L }));
}
}