Revert SQLiteQueryBuilder for now.

We've encountered subtle bugs in how apps are using this public
API, so revert it back to exactly what shipped in the last
release, and move functionality to new SQLiteStatementBuilder
class, since we already have several customers using it.

Test: atest cts/tests/tests/provider/src/android/provider/cts/MediaStore*
Test: atest cts/tests/tests/database/src/android/database/sqlite/cts/SQLiteQueryBuilderTest.java
Bug: 111486645
Merged-In: Ief059e987f2421e19f6f57a94320c313946a26d7
Change-Id: Ief059e987f2421e19f6f57a94320c313946a26d7
This commit is contained in:
Jeff Sharkey
2018-07-19 09:30:16 -06:00
committed by Jeff Sharkey
parent 4039dc4901
commit 6c90f1ded2
3 changed files with 1104 additions and 463 deletions

View File

@@ -12668,30 +12668,22 @@ package android.database.sqlite {
ctor public SQLiteQueryBuilder();
method public static void appendColumns(java.lang.StringBuilder, java.lang.String[]);
method public void appendWhere(java.lang.CharSequence);
method public void appendWhere(java.lang.CharSequence, java.lang.String...);
method public void appendWhereEscapeString(java.lang.String);
method public void appendWhereEscapeString(java.lang.String, java.lang.String...);
method public void appendWhereExpression(java.lang.CharSequence);
method public void appendWhereExpression(java.lang.CharSequence, java.lang.String...);
method public java.lang.String buildQuery(java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String);
method public deprecated java.lang.String buildQuery(java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String);
method public static java.lang.String buildQueryString(boolean, java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String);
method public java.lang.String buildUnionQuery(java.lang.String[], java.lang.String, java.lang.String);
method public java.lang.String buildUnionSubQuery(java.lang.String, java.lang.String[], java.util.Set<java.lang.String>, int, java.lang.String, java.lang.String, java.lang.String, java.lang.String);
method public deprecated java.lang.String buildUnionSubQuery(java.lang.String, java.lang.String[], java.util.Set<java.lang.String>, int, java.lang.String, java.lang.String, java.lang.String[], java.lang.String, java.lang.String);
method public int delete(android.database.sqlite.SQLiteDatabase, java.lang.String, java.lang.String[]);
method public java.lang.String getTables();
method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String);
method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String);
method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, android.os.CancellationSignal);
method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, android.os.CancellationSignal);
method public android.database.Cursor query(android.database.sqlite.SQLiteDatabase, java.lang.String[], android.os.Bundle, android.os.CancellationSignal);
method public void setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory);
method public void setDistinct(boolean);
method public void setProjectionMap(java.util.Map<java.lang.String, java.lang.String>);
method public void setStrict(boolean);
method public void setTables(java.lang.String);
method public int update(android.database.sqlite.SQLiteDatabase, android.content.ContentValues, java.lang.String, java.lang.String[]);
}
public class SQLiteReadOnlyDatabaseException extends android.database.sqlite.SQLiteException {

View File

@@ -16,40 +16,18 @@
package android.database.sqlite;
import static android.content.ContentResolver.QUERY_ARG_SQL_GROUP_BY;
import static android.content.ContentResolver.QUERY_ARG_SQL_HAVING;
import static android.content.ContentResolver.QUERY_ARG_SQL_LIMIT;
import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION;
import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS;
import static android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER;
import android.annotation.UnsupportedAppUsage;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import com.android.internal.util.ArrayUtils;
import dalvik.system.VMRuntime;
import libcore.util.EmptyArray;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
@@ -57,7 +35,8 @@ 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*)?");
@@ -67,7 +46,6 @@ public class SQLiteQueryBuilder {
private String mTables = "";
@UnsupportedAppUsage
private StringBuilder mWhereClause = null; // lazily created
private String[] mWhereArgs = EmptyArray.STRING;
@UnsupportedAppUsage
private boolean mDistinct;
private SQLiteDatabase.CursorFactory mFactory;
@@ -108,131 +86,43 @@ public class SQLiteQueryBuilder {
mTables = inTables;
}
/** {@hide} */
public @Nullable String getWhere() {
return (mWhereClause != null) ? mWhereClause.toString() : null;
}
/** {@hide} */
public String[] getWhereArgs() {
return mWhereArgs;
}
/**
* Append a chunk to the {@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:
* Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
* by parenthesis and ANDed with the selection passed to {@link #query}. The final
* WHERE clause looks like:
*
* <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 inWhere the chunk of text to append to the 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) {
public void appendWhere(CharSequence inWhere) {
if (mWhereClause == null) {
mWhereClause = new StringBuilder(inWhere.length() + 16);
}
if (mWhereClause.length() == 0) {
mWhereClause.append('(');
}
mWhereClause.append(inWhere);
mWhereArgs = ArrayUtils.concat(String.class, mWhereArgs, inWhereArgs);
}
/**
* Append a 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.
* Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
* by parenthesis and ANDed with the selection passed to {@link #query}. The final
* WHERE clause looks like:
*
* @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.
* WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
*
* @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.
* @param inWhere the chunk of text to append to the WHERE clause. it will be escaped
* to avoid SQL injection attacks
*/
public void appendWhereExpression(@NonNull CharSequence inWhere, String... inWhereArgs) {
public void appendWhereEscapeString(String inWhere) {
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);
if (mWhereClause.length() == 0) {
mWhereClause.append('(');
}
DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
mWhereArgs = ArrayUtils.concat(String.class, mWhereArgs, inWhereArgs);
}
/**
@@ -282,8 +172,8 @@ public class SQLiteQueryBuilder {
* </ul>
* By default, this value is false.
*/
public void setStrict(boolean strict) {
mStrict = strict;
public void setStrict(boolean flag) {
mStrict = flag;
}
/**
@@ -377,7 +267,7 @@ public class SQLiteQueryBuilder {
* information passed into this method.
*
* @param db the database to query on
* @param projection A list of which columns to return. Passing
* @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,
@@ -402,14 +292,10 @@ public class SQLiteQueryBuilder {
* @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,
public Cursor query(SQLiteDatabase db, String[] projectionIn,
String selection, String[] selectionArgs, String groupBy,
String having, String sortOrder) {
return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
null /* limit */, null /* cancellationSignal */);
}
@@ -418,7 +304,7 @@ public class SQLiteQueryBuilder {
* information passed into this method.
*
* @param db the database to query on
* @param projection A list of which columns to return. Passing
* @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,
@@ -445,15 +331,10 @@ public class SQLiteQueryBuilder {
* @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,
public Cursor query(SQLiteDatabase db, String[] projectionIn,
String selection, String[] selectionArgs, String groupBy,
String having, String sortOrder, String limit) {
return query(db, projectionIn, selection, selectionArgs,
groupBy, having, sortOrder, limit, null);
}
@@ -462,42 +343,7 @@ public class SQLiteQueryBuilder {
* 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
* @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,
@@ -527,69 +373,14 @@ public class SQLiteQueryBuilder {
* @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) {
public Cursor query(SQLiteDatabase db, String[] projectionIn,
String selection, String[] selectionArgs, String groupBy,
String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
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) {
if (mStrict && selection != null && selection.length() > 0) {
// 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
@@ -597,135 +388,24 @@ public class SQLiteQueryBuilder {
// originally specified. An attacker cannot create an expression that
// would escape the SQL expression while maintaining balanced parentheses
// in both the wrapped and original forms.
// 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;
String sqlForValidation = buildQuery(projectionIn, "(" + selection + ")", groupBy,
having, sortOrder, limit);
db.validateSql(sqlForValidation, cancellationSignal); // will throw if query is invalid
}
final String[] sqlArgs = ArrayUtils.concat(String.class,
queryArgs.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS), mWhereArgs);
String sql = buildQuery(
projectionIn, selection, groupBy, having,
sortOrder, limit);
if (Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Performing query: " + sql);
}
return db.rawQueryWithFactory(
mFactory, sql, sqlArgs,
mFactory, sql, selectionArgs,
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
@@ -758,10 +438,28 @@ public class SQLiteQueryBuilder {
String[] projectionIn, String selection, String groupBy,
String having, String sortOrder, String limit) {
String[] projection = computeProjection(projectionIn);
String where = computeWhere(selection);
StringBuilder where = new StringBuilder();
boolean hasBaseWhereClause = mWhereClause != null && mWhereClause.length() > 0;
if (hasBaseWhereClause) {
where.append(mWhereClause.toString());
where.append(')');
}
// Tack on the user's selection, if present.
if (selection != null && selection.length() > 0) {
if (hasBaseWhereClause) {
where.append(" AND ");
}
where.append('(');
where.append(selection);
where.append(')');
}
return buildQueryString(
mDistinct, mTables, projection, where,
mDistinct, mTables, projection, where.toString(),
groupBy, having, sortOrder, limit);
}
@@ -778,42 +476,6 @@ public class SQLiteQueryBuilder {
return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit);
}
/** {@hide} */
public String buildUpdate(ContentValues values, String selection) {
if (values == null || values.isEmpty()) {
throw new IllegalArgumentException("Empty values");
}
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
sql.append(mTables);
sql.append(" SET ");
final ArrayMap<String, Object> rawValues = values.getValues();
for (int i = 0; i < rawValues.size(); i++) {
if (i > 0) {
sql.append(',');
}
sql.append(rawValues.keyAt(i));
sql.append("=?");
}
final String where = computeWhere(selection);
appendClause(sql, " WHERE ", where);
return sql.toString();
}
/** {@hide} */
public String buildDelete(String selection) {
StringBuilder sql = new StringBuilder(120);
sql.append("DELETE FROM ");
sql.append(mTables);
final String where = computeWhere(selection);
appendClause(sql, " WHERE ", where);
return sql.toString();
}
/**
* Construct a SELECT statement suitable for use in a group of
* SELECT statements that will be joined through UNION operators
@@ -939,7 +601,7 @@ public class SQLiteQueryBuilder {
}
@UnsupportedAppUsage
private @Nullable String[] computeProjection(@Nullable String[] projectionIn) {
private String[] computeProjection(String[] projectionIn) {
if (projectionIn != null && projectionIn.length > 0) {
if (mProjectionMap != null) {
String[] projection = new String[projectionIn.length];
@@ -962,7 +624,7 @@ public class SQLiteQueryBuilder {
}
throw new IllegalArgumentException("Invalid column "
+ projectionIn[i] + " from tables " + mTables);
+ projectionIn[i]);
}
return projection;
} else {
@@ -988,53 +650,4 @@ public class SQLiteQueryBuilder {
}
return null;
}
private @NonNull String computeWhere(@Nullable String selection) {
final boolean hasUser = selection != null && selection.length() > 0;
final boolean hasInternal = mWhereClause != null && mWhereClause.length() > 0;
if (hasUser || hasInternal) {
final StringBuilder where = new StringBuilder();
if (hasUser) {
where.append('(').append(selection).append(')');
}
if (hasUser && hasInternal) {
where.append(" AND ");
}
if (hasInternal) {
where.append('(').append(mWhereClause.toString()).append(')');
}
return where.toString();
} else {
return null;
}
}
/**
* Wrap given argument in parenthesis, unless it's {@code null} or
* {@code ()}, in which case return it verbatim.
*/
private @Nullable String wrap(@Nullable String arg) {
if (arg == null) {
return null;
} else if (arg.equals("")) {
return arg;
} else {
return "(" + arg + ")";
}
}
private static void maybePutString(@NonNull Bundle bundle, @NonNull String key,
@Nullable String value) {
if (value != null) {
bundle.putString(key, value);
}
}
private static void maybePutStringArray(@NonNull Bundle bundle, @NonNull String key,
@Nullable String[] value) {
if (value != null) {
bundle.putStringArray(key, value);
}
}
}

File diff suppressed because it is too large Load Diff