1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.database.sqlite; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.content.ContentValues; 23 import android.database.Cursor; 24 import android.database.DatabaseUtils; 25 import android.os.Build; 26 import android.os.CancellationSignal; 27 import android.os.OperationCanceledException; 28 import android.provider.BaseColumns; 29 import android.text.TextUtils; 30 import android.util.ArrayMap; 31 import android.util.Log; 32 33 import com.android.internal.util.ArrayUtils; 34 35 import libcore.util.EmptyArray; 36 37 import java.util.Arrays; 38 import java.util.Iterator; 39 import java.util.List; 40 import java.util.Locale; 41 import java.util.Map; 42 import java.util.Map.Entry; 43 import java.util.Objects; 44 import java.util.Set; 45 import java.util.regex.Matcher; 46 import java.util.regex.Pattern; 47 48 /** 49 * This is a convenience class that helps build SQL queries to be sent to 50 * {@link SQLiteDatabase} objects. 51 */ 52 public class SQLiteQueryBuilder { 53 private static final String TAG = "SQLiteQueryBuilder"; 54 55 private static final Pattern sAggregationPattern = Pattern.compile( 56 "(?i)(AVG|COUNT|MAX|MIN|SUM|TOTAL|GROUP_CONCAT)\\((.+)\\)"); 57 58 private Map<String, String> mProjectionMap = null; 59 private List<Pattern> mProjectionGreylist = null; 60 61 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 62 private String mTables = ""; 63 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 64 private StringBuilder mWhereClause = null; // lazily created 65 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 66 private boolean mDistinct; 67 private SQLiteDatabase.CursorFactory mFactory; 68 69 private static final int STRICT_PARENTHESES = 1 << 0; 70 private static final int STRICT_COLUMNS = 1 << 1; 71 private static final int STRICT_GRAMMAR = 1 << 2; 72 73 private int mStrictFlags; 74 SQLiteQueryBuilder()75 public SQLiteQueryBuilder() { 76 mDistinct = false; 77 mFactory = null; 78 } 79 80 /** 81 * Mark the query as {@code DISTINCT}. 82 * 83 * @param distinct if true the query is {@code DISTINCT}, otherwise it isn't 84 */ setDistinct(boolean distinct)85 public void setDistinct(boolean distinct) { 86 mDistinct = distinct; 87 } 88 89 /** 90 * Get if the query is marked as {@code DISTINCT}, as last configured by 91 * {@link #setDistinct(boolean)}. 92 */ isDistinct()93 public boolean isDistinct() { 94 return mDistinct; 95 } 96 97 /** 98 * Returns the list of tables being queried 99 * 100 * @return the list of tables being queried 101 */ getTables()102 public @Nullable String getTables() { 103 return mTables; 104 } 105 106 /** 107 * Sets the list of tables to query. Multiple tables can be specified to perform a join. 108 * For example: 109 * setTables("foo, bar") 110 * setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)") 111 * 112 * @param inTables the list of tables to query on 113 */ setTables(@ullable String inTables)114 public void setTables(@Nullable String inTables) { 115 mTables = inTables; 116 } 117 118 /** 119 * Append a chunk to the {@code WHERE} clause of the query. All chunks appended are surrounded 120 * by parenthesis and {@code AND}ed with the selection passed to {@link #query}. The final 121 * {@code WHERE} clause looks like: 122 * <p> 123 * WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) 124 * 125 * @param inWhere the chunk of text to append to the {@code WHERE} clause. 126 */ appendWhere(@onNull CharSequence inWhere)127 public void appendWhere(@NonNull CharSequence inWhere) { 128 if (mWhereClause == null) { 129 mWhereClause = new StringBuilder(inWhere.length() + 16); 130 } 131 mWhereClause.append(inWhere); 132 } 133 134 /** 135 * Append a chunk to the {@code WHERE} clause of the query. All chunks appended are surrounded 136 * by parenthesis and ANDed with the selection passed to {@link #query}. The final 137 * {@code WHERE} clause looks like: 138 * <p> 139 * WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) 140 * 141 * @param inWhere the chunk of text to append to the {@code WHERE} clause. it will be escaped 142 * to avoid SQL injection attacks 143 */ appendWhereEscapeString(@onNull String inWhere)144 public void appendWhereEscapeString(@NonNull String inWhere) { 145 if (mWhereClause == null) { 146 mWhereClause = new StringBuilder(inWhere.length() + 16); 147 } 148 DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere); 149 } 150 151 /** 152 * Add a standalone chunk to the {@code WHERE} clause of this query. 153 * <p> 154 * This method differs from {@link #appendWhere(CharSequence)} in that it 155 * automatically appends {@code AND} to any existing {@code WHERE} clause 156 * already under construction before appending the given standalone 157 * expression wrapped in parentheses. 158 * 159 * @param inWhere the standalone expression to append to the {@code WHERE} 160 * clause. It will be wrapped in parentheses when it's appended. 161 */ appendWhereStandalone(@onNull CharSequence inWhere)162 public void appendWhereStandalone(@NonNull CharSequence inWhere) { 163 if (mWhereClause == null) { 164 mWhereClause = new StringBuilder(inWhere.length() + 16); 165 } 166 if (mWhereClause.length() > 0) { 167 mWhereClause.append(" AND "); 168 } 169 mWhereClause.append('(').append(inWhere).append(')'); 170 } 171 172 /** 173 * Sets the projection map for the query. The projection map maps 174 * from column names that the caller passes into query to database 175 * column names. This is useful for renaming columns as well as 176 * disambiguating column names when doing joins. For example you 177 * could map "name" to "people.name". If a projection map is set 178 * it must contain all column names the user may request, even if 179 * the key and value are the same. 180 * 181 * @param columnMap maps from the user column names to the database column names 182 */ setProjectionMap(@ullable Map<String, String> columnMap)183 public void setProjectionMap(@Nullable Map<String, String> columnMap) { 184 mProjectionMap = columnMap; 185 } 186 187 /** 188 * Gets the projection map for the query, as last configured by 189 * {@link #setProjectionMap(Map)}. 190 */ getProjectionMap()191 public @Nullable Map<String, String> getProjectionMap() { 192 return mProjectionMap; 193 } 194 195 /** 196 * Sets a projection greylist of columns that will be allowed through, even 197 * when {@link #setStrict(boolean)} is enabled. This provides a way for 198 * abusive custom columns like {@code COUNT(*)} to continue working. 199 * 200 * @hide 201 */ setProjectionGreylist(@ullable List<Pattern> projectionGreylist)202 public void setProjectionGreylist(@Nullable List<Pattern> projectionGreylist) { 203 mProjectionGreylist = projectionGreylist; 204 } 205 206 /** 207 * Gets the projection greylist for the query, as last configured by 208 * {@link #setProjectionGreylist(List)}. 209 * 210 * @hide 211 */ getProjectionGreylist()212 public @Nullable List<Pattern> getProjectionGreylist() { 213 return mProjectionGreylist; 214 } 215 216 /** 217 * @deprecated Projection aggregation is now always allowed 218 * 219 * @hide 220 */ 221 @Deprecated setProjectionAggregationAllowed(boolean projectionAggregationAllowed)222 public void setProjectionAggregationAllowed(boolean projectionAggregationAllowed) { 223 } 224 225 /** 226 * @deprecated Projection aggregation is now always allowed 227 * 228 * @hide 229 */ 230 @Deprecated isProjectionAggregationAllowed()231 public boolean isProjectionAggregationAllowed() { 232 return true; 233 } 234 235 /** 236 * Sets the cursor factory to be used for the query. You can use 237 * one factory for all queries on a database but it is normally 238 * easier to specify the factory when doing this query. 239 * 240 * @param factory the factory to use. 241 */ setCursorFactory(@ullable SQLiteDatabase.CursorFactory factory)242 public void setCursorFactory(@Nullable SQLiteDatabase.CursorFactory factory) { 243 mFactory = factory; 244 } 245 246 /** 247 * Gets the cursor factory to be used for the query, as last configured by 248 * {@link #setCursorFactory(android.database.sqlite.SQLiteDatabase.CursorFactory)}. 249 */ getCursorFactory()250 public @Nullable SQLiteDatabase.CursorFactory getCursorFactory() { 251 return mFactory; 252 } 253 254 /** 255 * When set, the selection is verified against malicious arguments. 256 * When using this class to create a statement using 257 * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)}, 258 * non-numeric limits will raise an exception. If a projection map is specified, fields 259 * not in that map will be ignored. 260 * If this class is used to execute the statement directly using 261 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)} 262 * or 263 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)}, 264 * additionally also parenthesis escaping selection are caught. 265 * 266 * To summarize: To get maximum protection against malicious third party apps (for example 267 * content provider consumers), make sure to do the following: 268 * <ul> 269 * <li>Set this value to true</li> 270 * <li>Use a projection map</li> 271 * <li>Use one of the query overloads instead of getting the statement as a sql string</li> 272 * </ul> 273 * By default, this value is false. 274 */ setStrict(boolean strict)275 public void setStrict(boolean strict) { 276 if (strict) { 277 mStrictFlags |= STRICT_PARENTHESES; 278 } else { 279 mStrictFlags &= ~STRICT_PARENTHESES; 280 } 281 } 282 283 /** 284 * Get if the query is marked as strict, as last configured by 285 * {@link #setStrict(boolean)}. 286 */ isStrict()287 public boolean isStrict() { 288 return (mStrictFlags & STRICT_PARENTHESES) != 0; 289 } 290 291 /** 292 * When enabled, verify that all projections and {@link ContentValues} only 293 * contain valid columns as defined by {@link #setProjectionMap(Map)}. 294 * <p> 295 * This enforcement applies to {@link #insert}, {@link #query}, and 296 * {@link #update} operations. Any enforcement failures will throw an 297 * {@link IllegalArgumentException}. 298 * 299 * {@hide} 300 */ setStrictColumns(boolean strictColumns)301 public void setStrictColumns(boolean strictColumns) { 302 if (strictColumns) { 303 mStrictFlags |= STRICT_COLUMNS; 304 } else { 305 mStrictFlags &= ~STRICT_COLUMNS; 306 } 307 } 308 309 /** 310 * Get if the query is marked as strict, as last configured by 311 * {@link #setStrictColumns(boolean)}. 312 * 313 * {@hide} 314 */ isStrictColumns()315 public boolean isStrictColumns() { 316 return (mStrictFlags & STRICT_COLUMNS) != 0; 317 } 318 319 /** 320 * When enabled, verify that all untrusted SQL conforms to a restricted SQL 321 * grammar. Here are the restrictions applied: 322 * <ul> 323 * <li>In {@code WHERE} and {@code HAVING} clauses: subqueries, raising, and 324 * windowing terms are rejected. 325 * <li>In {@code GROUP BY} clauses: only valid columns are allowed. 326 * <li>In {@code ORDER BY} clauses: only valid columns, collation, and 327 * ordering terms are allowed. 328 * <li>In {@code LIMIT} clauses: only numerical values and offset terms are 329 * allowed. 330 * </ul> 331 * All column references must be valid as defined by 332 * {@link #setProjectionMap(Map)}. 333 * <p> 334 * This enforcement applies to {@link #query}, {@link #update} and 335 * {@link #delete} operations. This enforcement does not apply to trusted 336 * inputs, such as those provided by {@link #appendWhere}. Any enforcement 337 * failures will throw an {@link IllegalArgumentException}. 338 * 339 * {@hide} 340 */ setStrictGrammar(boolean strictGrammar)341 public void setStrictGrammar(boolean strictGrammar) { 342 if (strictGrammar) { 343 mStrictFlags |= STRICT_GRAMMAR; 344 } else { 345 mStrictFlags &= ~STRICT_GRAMMAR; 346 } 347 } 348 349 /** 350 * Get if the query is marked as strict, as last configured by 351 * {@link #setStrictGrammar(boolean)}. 352 * 353 * {@hide} 354 */ isStrictGrammar()355 public boolean isStrictGrammar() { 356 return (mStrictFlags & STRICT_GRAMMAR) != 0; 357 } 358 359 /** 360 * Build an SQL query string from the given clauses. 361 * 362 * @param distinct true if you want each row to be unique, false otherwise. 363 * @param tables The table names to compile the query against. 364 * @param columns A list of which columns to return. Passing null will 365 * return all columns, which is discouraged to prevent reading 366 * data from storage that isn't going to be used. 367 * @param where A filter declaring which rows to return, formatted as an SQL 368 * {@code WHERE} clause (excluding the {@code WHERE} itself). Passing {@code null} will 369 * return all rows for the given URL. 370 * @param groupBy A filter declaring how to group rows, formatted as an SQL 371 * {@code GROUP BY} clause (excluding the {@code GROUP BY} itself). Passing {@code null} 372 * will cause the rows to not be grouped. 373 * @param having A filter declare which row groups to include in the cursor, 374 * if row grouping is being used, formatted as an SQL {@code HAVING} 375 * clause (excluding the {@code HAVING} itself). Passing null will cause 376 * all row groups to be included, and is required when row 377 * grouping is not being used. 378 * @param orderBy How to order the rows, formatted as an SQL {@code ORDER BY} clause 379 * (excluding the {@code ORDER BY} itself). Passing null will use the 380 * default sort order, which may be unordered. 381 * @param limit Limits the number of rows returned by the query, 382 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause. 383 * @return the SQL query string 384 */ buildQueryString( boolean distinct, String tables, String[] columns, String where, String groupBy, String having, String orderBy, String limit)385 public static String buildQueryString( 386 boolean distinct, String tables, String[] columns, String where, 387 String groupBy, String having, String orderBy, String limit) { 388 if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) { 389 throw new IllegalArgumentException( 390 "HAVING clauses are only permitted when using a groupBy clause"); 391 } 392 393 StringBuilder query = new StringBuilder(120); 394 395 query.append("SELECT "); 396 if (distinct) { 397 query.append("DISTINCT "); 398 } 399 if (columns != null && columns.length != 0) { 400 appendColumns(query, columns); 401 } else { 402 query.append("* "); 403 } 404 query.append("FROM "); 405 query.append(tables); 406 appendClause(query, " WHERE ", where); 407 appendClause(query, " GROUP BY ", groupBy); 408 appendClause(query, " HAVING ", having); 409 appendClause(query, " ORDER BY ", orderBy); 410 appendClause(query, " LIMIT ", limit); 411 412 return query.toString(); 413 } 414 appendClause(StringBuilder s, String name, String clause)415 private static void appendClause(StringBuilder s, String name, String clause) { 416 if (!TextUtils.isEmpty(clause)) { 417 s.append(name); 418 s.append(clause); 419 } 420 } 421 422 /** 423 * Add the names that are non-null in columns to s, separating 424 * them with commas. 425 */ appendColumns(StringBuilder s, String[] columns)426 public static void appendColumns(StringBuilder s, String[] columns) { 427 int n = columns.length; 428 429 for (int i = 0; i < n; i++) { 430 String column = columns[i]; 431 432 if (column != null) { 433 if (i > 0) { 434 s.append(", "); 435 } 436 s.append(column); 437 } 438 } 439 s.append(' '); 440 } 441 442 /** 443 * Perform a query by combining all current settings and the 444 * information passed into this method. 445 * 446 * @param db the database to query on 447 * @param projectionIn A list of which columns to return. Passing 448 * null will return all columns, which is discouraged to prevent 449 * reading data from storage that isn't going to be used. 450 * @param selection A filter declaring which rows to return, 451 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE} 452 * itself). Passing null will return all rows for the given URL. 453 * @param selectionArgs You may include ?s in selection, which 454 * will be replaced by the values from selectionArgs, in order 455 * that they appear in the selection. The values will be bound 456 * as Strings. 457 * @param groupBy A filter declaring how to group rows, formatted 458 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} 459 * itself). Passing null will cause the rows to not be grouped. 460 * @param having A filter declare which row groups to include in 461 * the cursor, if row grouping is being used, formatted as an 462 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing 463 * null will cause all row groups to be included, and is 464 * required when row grouping is not being used. 465 * @param sortOrder How to order the rows, formatted as an SQL 466 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null 467 * will use the default sort order, which may be unordered. 468 * @return a cursor over the result set 469 * @see android.content.ContentResolver#query(android.net.Uri, String[], 470 * String, String[], String) 471 */ query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder)472 public Cursor query(SQLiteDatabase db, String[] projectionIn, 473 String selection, String[] selectionArgs, String groupBy, 474 String having, String sortOrder) { 475 return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder, 476 null /* limit */, null /* cancellationSignal */); 477 } 478 479 /** 480 * Perform a query by combining all current settings and the 481 * information passed into this method. 482 * 483 * @param db the database to query on 484 * @param projectionIn A list of which columns to return. Passing 485 * null will return all columns, which is discouraged to prevent 486 * reading data from storage that isn't going to be used. 487 * @param selection A filter declaring which rows to return, 488 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE} 489 * itself). Passing null will return all rows for the given URL. 490 * @param selectionArgs You may include ?s in selection, which 491 * will be replaced by the values from selectionArgs, in order 492 * that they appear in the selection. The values will be bound 493 * as Strings. 494 * @param groupBy A filter declaring how to group rows, formatted 495 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} 496 * itself). Passing null will cause the rows to not be grouped. 497 * @param having A filter declare which row groups to include in 498 * the cursor, if row grouping is being used, formatted as an 499 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing 500 * null will cause all row groups to be included, and is 501 * required when row grouping is not being used. 502 * @param sortOrder How to order the rows, formatted as an SQL 503 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null 504 * will use the default sort order, which may be unordered. 505 * @param limit Limits the number of rows returned by the query, 506 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause. 507 * @return a cursor over the result set 508 * @see android.content.ContentResolver#query(android.net.Uri, String[], 509 * String, String[], String) 510 */ query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit)511 public Cursor query(SQLiteDatabase db, String[] projectionIn, 512 String selection, String[] selectionArgs, String groupBy, 513 String having, String sortOrder, String limit) { 514 return query(db, projectionIn, selection, selectionArgs, 515 groupBy, having, sortOrder, limit, null); 516 } 517 518 /** 519 * Perform a query by combining all current settings and the 520 * information passed into this method. 521 * 522 * @param db the database to query on 523 * @param projectionIn A list of which columns to return. Passing 524 * null will return all columns, which is discouraged to prevent 525 * reading data from storage that isn't going to be used. 526 * @param selection A filter declaring which rows to return, 527 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE} 528 * itself). Passing null will return all rows for the given URL. 529 * @param selectionArgs You may include ?s in selection, which 530 * will be replaced by the values from selectionArgs, in order 531 * that they appear in the selection. The values will be bound 532 * as Strings. 533 * @param groupBy A filter declaring how to group rows, formatted 534 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} 535 * itself). Passing null will cause the rows to not be grouped. 536 * @param having A filter declare which row groups to include in 537 * the cursor, if row grouping is being used, formatted as an 538 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing 539 * null will cause all row groups to be included, and is 540 * required when row grouping is not being used. 541 * @param sortOrder How to order the rows, formatted as an SQL 542 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null 543 * will use the default sort order, which may be unordered. 544 * @param limit Limits the number of rows returned by the query, 545 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause. 546 * @param cancellationSignal A signal to cancel the operation in progress, or null if none. 547 * If the operation is canceled, then {@link OperationCanceledException} will be thrown 548 * when the query is executed. 549 * @return a cursor over the result set 550 * @see android.content.ContentResolver#query(android.net.Uri, String[], 551 * String, String[], String) 552 */ query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit, CancellationSignal cancellationSignal)553 public Cursor query(SQLiteDatabase db, String[] projectionIn, 554 String selection, String[] selectionArgs, String groupBy, 555 String having, String sortOrder, String limit, CancellationSignal cancellationSignal) { 556 if (mTables == null) { 557 return null; 558 } 559 560 final String sql; 561 final String unwrappedSql = buildQuery( 562 projectionIn, selection, groupBy, having, 563 sortOrder, limit); 564 565 if (isStrictColumns()) { 566 enforceStrictColumns(projectionIn); 567 } 568 if (isStrictGrammar()) { 569 enforceStrictGrammar(selection, groupBy, having, sortOrder, limit); 570 } 571 if (isStrict()) { 572 // Validate the user-supplied selection to detect syntactic anomalies 573 // in the selection string that could indicate a SQL injection attempt. 574 // The idea is to ensure that the selection clause is a valid SQL expression 575 // by compiling it twice: once wrapped in parentheses and once as 576 // originally specified. An attacker cannot create an expression that 577 // would escape the SQL expression while maintaining balanced parentheses 578 // in both the wrapped and original forms. 579 580 // NOTE: The ordering of the below operations is important; we must 581 // execute the wrapped query to ensure the untrusted clause has been 582 // fully isolated. 583 584 // Validate the unwrapped query 585 db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid 586 587 // Execute wrapped query for extra protection 588 final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy, 589 wrap(having), sortOrder, limit); 590 sql = wrappedSql; 591 } else { 592 // Execute unwrapped query 593 sql = unwrappedSql; 594 } 595 596 final String[] sqlArgs = selectionArgs; 597 if (Log.isLoggable(TAG, Log.DEBUG)) { 598 if (Build.IS_DEBUGGABLE) { 599 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); 600 } else { 601 Log.d(TAG, sql); 602 } 603 } 604 return db.rawQueryWithFactory( 605 mFactory, sql, sqlArgs, 606 SQLiteDatabase.findEditTable(mTables), 607 cancellationSignal); // will throw if query is invalid 608 } 609 610 /** 611 * Perform an insert by combining all current settings and the 612 * information passed into this method. 613 * 614 * @param db the database to insert on 615 * @return the row ID of the newly inserted row, or -1 if an error occurred 616 * 617 * {@hide} 618 */ insert(@onNull SQLiteDatabase db, @NonNull ContentValues values)619 public long insert(@NonNull SQLiteDatabase db, @NonNull ContentValues values) { 620 Objects.requireNonNull(mTables, "No tables defined"); 621 Objects.requireNonNull(db, "No database defined"); 622 Objects.requireNonNull(values, "No values defined"); 623 624 if (isStrictColumns()) { 625 enforceStrictColumns(values); 626 } 627 628 final String sql = buildInsert(values); 629 630 final ArrayMap<String, Object> rawValues = values.getValues(); 631 final int valuesLength = rawValues.size(); 632 final Object[] sqlArgs = new Object[valuesLength]; 633 for (int i = 0; i < sqlArgs.length; i++) { 634 sqlArgs[i] = rawValues.valueAt(i); 635 } 636 if (Log.isLoggable(TAG, Log.DEBUG)) { 637 if (Build.IS_DEBUGGABLE) { 638 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); 639 } else { 640 Log.d(TAG, sql); 641 } 642 } 643 return db.executeSql(sql, sqlArgs); 644 } 645 646 /** 647 * Perform an update by combining all current settings and the 648 * information passed into this method. 649 * 650 * @param db the database to update on 651 * @param selection A filter declaring which rows to return, 652 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE} 653 * itself). Passing null will return all rows for the given URL. 654 * @param selectionArgs You may include ?s in selection, which 655 * will be replaced by the values from selectionArgs, in order 656 * that they appear in the selection. The values will be bound 657 * as Strings. 658 * @return the number of rows updated 659 */ update(@onNull SQLiteDatabase db, @NonNull ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)660 public int update(@NonNull SQLiteDatabase db, @NonNull ContentValues values, 661 @Nullable String selection, @Nullable String[] selectionArgs) { 662 Objects.requireNonNull(mTables, "No tables defined"); 663 Objects.requireNonNull(db, "No database defined"); 664 Objects.requireNonNull(values, "No values defined"); 665 666 final String sql; 667 final String unwrappedSql = buildUpdate(values, selection); 668 669 if (isStrictColumns()) { 670 enforceStrictColumns(values); 671 } 672 if (isStrictGrammar()) { 673 enforceStrictGrammar(selection, null, null, null, null); 674 } 675 if (isStrict()) { 676 // Validate the user-supplied selection to detect syntactic anomalies 677 // in the selection string that could indicate a SQL injection attempt. 678 // The idea is to ensure that the selection clause is a valid SQL expression 679 // by compiling it twice: once wrapped in parentheses and once as 680 // originally specified. An attacker cannot create an expression that 681 // would escape the SQL expression while maintaining balanced parentheses 682 // in both the wrapped and original forms. 683 684 // NOTE: The ordering of the below operations is important; we must 685 // execute the wrapped query to ensure the untrusted clause has been 686 // fully isolated. 687 688 // Validate the unwrapped query 689 db.validateSql(unwrappedSql, null); // will throw if query is invalid 690 691 // Execute wrapped query for extra protection 692 final String wrappedSql = buildUpdate(values, wrap(selection)); 693 sql = wrappedSql; 694 } else { 695 // Execute unwrapped query 696 sql = unwrappedSql; 697 } 698 699 if (selectionArgs == null) { 700 selectionArgs = EmptyArray.STRING; 701 } 702 final ArrayMap<String, Object> rawValues = values.getValues(); 703 final int valuesLength = rawValues.size(); 704 final Object[] sqlArgs = new Object[valuesLength + selectionArgs.length]; 705 for (int i = 0; i < sqlArgs.length; i++) { 706 if (i < valuesLength) { 707 sqlArgs[i] = rawValues.valueAt(i); 708 } else { 709 sqlArgs[i] = selectionArgs[i - valuesLength]; 710 } 711 } 712 if (Log.isLoggable(TAG, Log.DEBUG)) { 713 if (Build.IS_DEBUGGABLE) { 714 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); 715 } else { 716 Log.d(TAG, sql); 717 } 718 } 719 return db.executeSql(sql, sqlArgs); 720 } 721 722 /** 723 * Perform a delete by combining all current settings and the 724 * information passed into this method. 725 * 726 * @param db the database to delete on 727 * @param selection A filter declaring which rows to return, 728 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE} 729 * itself). Passing null will return all rows for the given URL. 730 * @param selectionArgs You may include ?s in selection, which 731 * will be replaced by the values from selectionArgs, in order 732 * that they appear in the selection. The values will be bound 733 * as Strings. 734 * @return the number of rows deleted 735 */ delete(@onNull SQLiteDatabase db, @Nullable String selection, @Nullable String[] selectionArgs)736 public int delete(@NonNull SQLiteDatabase db, @Nullable String selection, 737 @Nullable String[] selectionArgs) { 738 Objects.requireNonNull(mTables, "No tables defined"); 739 Objects.requireNonNull(db, "No database defined"); 740 741 final String sql; 742 final String unwrappedSql = buildDelete(selection); 743 744 if (isStrictGrammar()) { 745 enforceStrictGrammar(selection, null, null, null, null); 746 } 747 if (isStrict()) { 748 // Validate the user-supplied selection to detect syntactic anomalies 749 // in the selection string that could indicate a SQL injection attempt. 750 // The idea is to ensure that the selection clause is a valid SQL expression 751 // by compiling it twice: once wrapped in parentheses and once as 752 // originally specified. An attacker cannot create an expression that 753 // would escape the SQL expression while maintaining balanced parentheses 754 // in both the wrapped and original forms. 755 756 // NOTE: The ordering of the below operations is important; we must 757 // execute the wrapped query to ensure the untrusted clause has been 758 // fully isolated. 759 760 // Validate the unwrapped query 761 db.validateSql(unwrappedSql, null); // will throw if query is invalid 762 763 // Execute wrapped query for extra protection 764 final String wrappedSql = buildDelete(wrap(selection)); 765 sql = wrappedSql; 766 } else { 767 // Execute unwrapped query 768 sql = unwrappedSql; 769 } 770 771 final String[] sqlArgs = selectionArgs; 772 if (Log.isLoggable(TAG, Log.DEBUG)) { 773 if (Build.IS_DEBUGGABLE) { 774 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); 775 } else { 776 Log.d(TAG, sql); 777 } 778 } 779 return db.executeSql(sql, sqlArgs); 780 } 781 enforceStrictColumns(@ullable String[] projection)782 private void enforceStrictColumns(@Nullable String[] projection) { 783 Objects.requireNonNull(mProjectionMap, "No projection map defined"); 784 785 computeProjection(projection); 786 } 787 enforceStrictColumns(@onNull ContentValues values)788 private void enforceStrictColumns(@NonNull ContentValues values) { 789 Objects.requireNonNull(mProjectionMap, "No projection map defined"); 790 791 final ArrayMap<String, Object> rawValues = values.getValues(); 792 for (int i = 0; i < rawValues.size(); i++) { 793 final String column = rawValues.keyAt(i); 794 if (!mProjectionMap.containsKey(column)) { 795 throw new IllegalArgumentException("Invalid column " + column); 796 } 797 } 798 } 799 enforceStrictGrammar(@ullable String selection, @Nullable String groupBy, @Nullable String having, @Nullable String sortOrder, @Nullable String limit)800 private void enforceStrictGrammar(@Nullable String selection, @Nullable String groupBy, 801 @Nullable String having, @Nullable String sortOrder, @Nullable String limit) { 802 SQLiteTokenizer.tokenize(selection, SQLiteTokenizer.OPTION_NONE, 803 this::enforceStrictGrammarWhereHaving); 804 SQLiteTokenizer.tokenize(groupBy, SQLiteTokenizer.OPTION_NONE, 805 this::enforceStrictGrammarGroupBy); 806 SQLiteTokenizer.tokenize(having, SQLiteTokenizer.OPTION_NONE, 807 this::enforceStrictGrammarWhereHaving); 808 SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE, 809 this::enforceStrictGrammarOrderBy); 810 SQLiteTokenizer.tokenize(limit, SQLiteTokenizer.OPTION_NONE, 811 this::enforceStrictGrammarLimit); 812 } 813 enforceStrictGrammarWhereHaving(@onNull String token)814 private void enforceStrictGrammarWhereHaving(@NonNull String token) { 815 if (isTableOrColumn(token)) return; 816 if (SQLiteTokenizer.isFunction(token)) return; 817 if (SQLiteTokenizer.isType(token)) return; 818 819 // NOTE: we explicitly don't allow SELECT subqueries, since they could 820 // leak data that should have been filtered by the trusted where clause 821 switch (token.toUpperCase(Locale.US)) { 822 case "AND": case "AS": case "BETWEEN": case "BINARY": 823 case "CASE": case "CAST": case "COLLATE": case "DISTINCT": 824 case "ELSE": case "END": case "ESCAPE": case "EXISTS": 825 case "GLOB": case "IN": case "IS": case "ISNULL": 826 case "LIKE": case "MATCH": case "NOCASE": case "NOT": 827 case "NOTNULL": case "NULL": case "OR": case "REGEXP": 828 case "RTRIM": case "THEN": case "WHEN": 829 return; 830 } 831 throw new IllegalArgumentException("Invalid token " + token); 832 } 833 enforceStrictGrammarGroupBy(@onNull String token)834 private void enforceStrictGrammarGroupBy(@NonNull String token) { 835 if (isTableOrColumn(token)) return; 836 throw new IllegalArgumentException("Invalid token " + token); 837 } 838 enforceStrictGrammarOrderBy(@onNull String token)839 private void enforceStrictGrammarOrderBy(@NonNull String token) { 840 if (isTableOrColumn(token)) return; 841 switch (token.toUpperCase(Locale.US)) { 842 case "COLLATE": case "ASC": case "DESC": 843 case "BINARY": case "RTRIM": case "NOCASE": 844 return; 845 } 846 throw new IllegalArgumentException("Invalid token " + token); 847 } 848 enforceStrictGrammarLimit(@onNull String token)849 private void enforceStrictGrammarLimit(@NonNull String token) { 850 switch (token.toUpperCase(Locale.US)) { 851 case "OFFSET": 852 return; 853 } 854 throw new IllegalArgumentException("Invalid token " + token); 855 } 856 857 /** 858 * Construct a {@code SELECT} statement suitable for use in a group of 859 * {@code SELECT} statements that will be joined through {@code UNION} operators 860 * in buildUnionQuery. 861 * 862 * @param projectionIn A list of which columns to return. Passing 863 * null will return all columns, which is discouraged to 864 * prevent reading data from storage that isn't going to be 865 * used. 866 * @param selection A filter declaring which rows to return, 867 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE} 868 * itself). Passing null will return all rows for the given 869 * URL. 870 * @param groupBy A filter declaring how to group rows, formatted 871 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} itself). 872 * Passing null will cause the rows to not be grouped. 873 * @param having A filter declare which row groups to include in 874 * the cursor, if row grouping is being used, formatted as an 875 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing 876 * null will cause all row groups to be included, and is 877 * required when row grouping is not being used. 878 * @param sortOrder How to order the rows, formatted as an SQL 879 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null 880 * will use the default sort order, which may be unordered. 881 * @param limit Limits the number of rows returned by the query, 882 * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause. 883 * @return the resulting SQL {@code SELECT} statement 884 */ buildQuery( String[] projectionIn, String selection, String groupBy, String having, String sortOrder, String limit)885 public String buildQuery( 886 String[] projectionIn, String selection, String groupBy, 887 String having, String sortOrder, String limit) { 888 String[] projection = computeProjection(projectionIn); 889 String where = computeWhere(selection); 890 891 return buildQueryString( 892 mDistinct, mTables, projection, where, 893 groupBy, having, sortOrder, limit); 894 } 895 896 /** 897 * @deprecated This method's signature is misleading since no SQL parameter 898 * substitution is carried out. The selection arguments parameter does not get 899 * used at all. To avoid confusion, call 900 * {@link #buildQuery(String[], String, String, String, String, String)} instead. 901 */ 902 @Deprecated buildQuery( String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit)903 public String buildQuery( 904 String[] projectionIn, String selection, String[] selectionArgs, 905 String groupBy, String having, String sortOrder, String limit) { 906 return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit); 907 } 908 909 /** {@hide} */ buildInsert(ContentValues values)910 public String buildInsert(ContentValues values) { 911 if (values == null || values.isEmpty()) { 912 throw new IllegalArgumentException("Empty values"); 913 } 914 915 StringBuilder sql = new StringBuilder(120); 916 sql.append("INSERT INTO "); 917 sql.append(SQLiteDatabase.findEditTable(mTables)); 918 sql.append(" ("); 919 920 final ArrayMap<String, Object> rawValues = values.getValues(); 921 for (int i = 0; i < rawValues.size(); i++) { 922 if (i > 0) { 923 sql.append(','); 924 } 925 sql.append(rawValues.keyAt(i)); 926 } 927 sql.append(") VALUES ("); 928 for (int i = 0; i < rawValues.size(); i++) { 929 if (i > 0) { 930 sql.append(','); 931 } 932 sql.append('?'); 933 } 934 sql.append(")"); 935 return sql.toString(); 936 } 937 938 /** {@hide} */ buildUpdate(ContentValues values, String selection)939 public String buildUpdate(ContentValues values, String selection) { 940 if (values == null || values.isEmpty()) { 941 throw new IllegalArgumentException("Empty values"); 942 } 943 944 StringBuilder sql = new StringBuilder(120); 945 sql.append("UPDATE "); 946 sql.append(SQLiteDatabase.findEditTable(mTables)); 947 sql.append(" SET "); 948 949 final ArrayMap<String, Object> rawValues = values.getValues(); 950 for (int i = 0; i < rawValues.size(); i++) { 951 if (i > 0) { 952 sql.append(','); 953 } 954 sql.append(rawValues.keyAt(i)); 955 sql.append("=?"); 956 } 957 958 final String where = computeWhere(selection); 959 appendClause(sql, " WHERE ", where); 960 return sql.toString(); 961 } 962 963 /** {@hide} */ buildDelete(String selection)964 public String buildDelete(String selection) { 965 StringBuilder sql = new StringBuilder(120); 966 sql.append("DELETE FROM "); 967 sql.append(SQLiteDatabase.findEditTable(mTables)); 968 969 final String where = computeWhere(selection); 970 appendClause(sql, " WHERE ", where); 971 return sql.toString(); 972 } 973 974 /** 975 * Construct a {@code SELECT} statement suitable for use in a group of 976 * {@code SELECT} statements that will be joined through {@code UNION} operators 977 * in buildUnionQuery. 978 * 979 * @param typeDiscriminatorColumn the name of the result column 980 * whose cells will contain the name of the table from which 981 * each row was drawn. 982 * @param unionColumns the names of the columns to appear in the 983 * result. This may include columns that do not appear in the 984 * table this {@code SELECT} is querying (i.e. mTables), but that do 985 * appear in one of the other tables in the {@code UNION} query that we 986 * are constructing. 987 * @param columnsPresentInTable a Set of the names of the columns 988 * that appear in this table (i.e. in the table whose name is 989 * mTables). Since columns in unionColumns include columns that 990 * appear only in other tables, we use this array to distinguish 991 * which ones actually are present. Other columns will have 992 * NULL values for results from this subquery. 993 * @param computedColumnsOffset all columns in unionColumns before 994 * this index are included under the assumption that they're 995 * computed and therefore won't appear in columnsPresentInTable, 996 * e.g. "date * 1000 as normalized_date" 997 * @param typeDiscriminatorValue the value used for the 998 * type-discriminator column in this subquery 999 * @param selection A filter declaring which rows to return, 1000 * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE} 1001 * itself). Passing null will return all rows for the given 1002 * URL. 1003 * @param groupBy A filter declaring how to group rows, formatted 1004 * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} itself). 1005 * Passing null will cause the rows to not be grouped. 1006 * @param having A filter declare which row groups to include in 1007 * the cursor, if row grouping is being used, formatted as an 1008 * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing 1009 * null will cause all row groups to be included, and is 1010 * required when row grouping is not being used. 1011 * @return the resulting SQL {@code SELECT} statement 1012 */ buildUnionSubQuery( String typeDiscriminatorColumn, String[] unionColumns, Set<String> columnsPresentInTable, int computedColumnsOffset, String typeDiscriminatorValue, String selection, String groupBy, String having)1013 public String buildUnionSubQuery( 1014 String typeDiscriminatorColumn, 1015 String[] unionColumns, 1016 Set<String> columnsPresentInTable, 1017 int computedColumnsOffset, 1018 String typeDiscriminatorValue, 1019 String selection, 1020 String groupBy, 1021 String having) { 1022 int unionColumnsCount = unionColumns.length; 1023 String[] projectionIn = new String[unionColumnsCount]; 1024 1025 for (int i = 0; i < unionColumnsCount; i++) { 1026 String unionColumn = unionColumns[i]; 1027 1028 if (unionColumn.equals(typeDiscriminatorColumn)) { 1029 projectionIn[i] = "'" + typeDiscriminatorValue + "' AS " 1030 + typeDiscriminatorColumn; 1031 } else if (i <= computedColumnsOffset 1032 || columnsPresentInTable.contains(unionColumn)) { 1033 projectionIn[i] = unionColumn; 1034 } else { 1035 projectionIn[i] = "NULL AS " + unionColumn; 1036 } 1037 } 1038 return buildQuery( 1039 projectionIn, selection, groupBy, having, 1040 null /* sortOrder */, 1041 null /* limit */); 1042 } 1043 1044 /** 1045 * @deprecated This method's signature is misleading since no SQL parameter 1046 * substitution is carried out. The selection arguments parameter does not get 1047 * used at all. To avoid confusion, call 1048 * {@link #buildUnionSubQuery} 1049 * instead. 1050 */ 1051 @Deprecated buildUnionSubQuery( String typeDiscriminatorColumn, String[] unionColumns, Set<String> columnsPresentInTable, int computedColumnsOffset, String typeDiscriminatorValue, String selection, String[] selectionArgs, String groupBy, String having)1052 public String buildUnionSubQuery( 1053 String typeDiscriminatorColumn, 1054 String[] unionColumns, 1055 Set<String> columnsPresentInTable, 1056 int computedColumnsOffset, 1057 String typeDiscriminatorValue, 1058 String selection, 1059 String[] selectionArgs, 1060 String groupBy, 1061 String having) { 1062 return buildUnionSubQuery( 1063 typeDiscriminatorColumn, unionColumns, columnsPresentInTable, 1064 computedColumnsOffset, typeDiscriminatorValue, selection, 1065 groupBy, having); 1066 } 1067 1068 /** 1069 * Given a set of subqueries, all of which are {@code SELECT} statements, 1070 * construct a query that returns the union of what those 1071 * subqueries return. 1072 * @param subQueries an array of SQL {@code SELECT} statements, all of 1073 * which must have the same columns as the same positions in 1074 * their results 1075 * @param sortOrder How to order the rows, formatted as an SQL 1076 * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing 1077 * null will use the default sort order, which may be unordered. 1078 * @param limit The limit clause, which applies to the entire union result set 1079 * 1080 * @return the resulting SQL {@code SELECT} statement 1081 */ buildUnionQuery(String[] subQueries, String sortOrder, String limit)1082 public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) { 1083 StringBuilder query = new StringBuilder(128); 1084 int subQueryCount = subQueries.length; 1085 String unionOperator = mDistinct ? " UNION " : " UNION ALL "; 1086 1087 for (int i = 0; i < subQueryCount; i++) { 1088 if (i > 0) { 1089 query.append(unionOperator); 1090 } 1091 query.append(subQueries[i]); 1092 } 1093 appendClause(query, " ORDER BY ", sortOrder); 1094 appendClause(query, " LIMIT ", limit); 1095 return query.toString(); 1096 } 1097 maybeWithOperator(@ullable String operator, @NonNull String column)1098 private static @NonNull String maybeWithOperator(@Nullable String operator, 1099 @NonNull String column) { 1100 if (operator != null) { 1101 return operator + "(" + column + ")"; 1102 } else { 1103 return column; 1104 } 1105 } 1106 1107 /** {@hide} */ 1108 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) computeProjection(@ullable String[] projectionIn)1109 public @Nullable String[] computeProjection(@Nullable String[] projectionIn) { 1110 if (!ArrayUtils.isEmpty(projectionIn)) { 1111 String[] projectionOut = new String[projectionIn.length]; 1112 for (int i = 0; i < projectionIn.length; i++) { 1113 projectionOut[i] = computeSingleProjectionOrThrow(projectionIn[i]); 1114 } 1115 return projectionOut; 1116 } else if (mProjectionMap != null) { 1117 // Return all columns in projection map. 1118 Set<Entry<String, String>> entrySet = mProjectionMap.entrySet(); 1119 String[] projection = new String[entrySet.size()]; 1120 Iterator<Entry<String, String>> entryIter = entrySet.iterator(); 1121 int i = 0; 1122 1123 while (entryIter.hasNext()) { 1124 Entry<String, String> entry = entryIter.next(); 1125 1126 // Don't include the _count column when people ask for no projection. 1127 if (entry.getKey().equals(BaseColumns._COUNT)) { 1128 continue; 1129 } 1130 projection[i++] = entry.getValue(); 1131 } 1132 return projection; 1133 } 1134 return null; 1135 } 1136 computeSingleProjectionOrThrow(@onNull String userColumn)1137 private @NonNull String computeSingleProjectionOrThrow(@NonNull String userColumn) { 1138 final String column = computeSingleProjection(userColumn); 1139 if (column != null) { 1140 return column; 1141 } else { 1142 throw new IllegalArgumentException("Invalid column " + userColumn); 1143 } 1144 } 1145 computeSingleProjection(@onNull String userColumn)1146 private @Nullable String computeSingleProjection(@NonNull String userColumn) { 1147 // When no mapping provided, anything goes 1148 if (mProjectionMap == null) { 1149 return userColumn; 1150 } 1151 1152 String operator = null; 1153 String column = mProjectionMap.get(userColumn); 1154 1155 // When no direct match found, look for aggregation 1156 if (column == null) { 1157 final Matcher matcher = sAggregationPattern.matcher(userColumn); 1158 if (matcher.matches()) { 1159 operator = matcher.group(1); 1160 userColumn = matcher.group(2); 1161 column = mProjectionMap.get(userColumn); 1162 } 1163 } 1164 1165 if (column != null) { 1166 return maybeWithOperator(operator, column); 1167 } 1168 1169 if (mStrictFlags == 0 1170 && (userColumn.contains(" AS ") || userColumn.contains(" as "))) { 1171 /* A column alias already exist */ 1172 return maybeWithOperator(operator, userColumn); 1173 } 1174 1175 // If greylist is configured, we might be willing to let 1176 // this custom column bypass our strict checks. 1177 if (mProjectionGreylist != null) { 1178 boolean match = false; 1179 for (Pattern p : mProjectionGreylist) { 1180 if (p.matcher(userColumn).matches()) { 1181 match = true; 1182 break; 1183 } 1184 } 1185 1186 if (match) { 1187 Log.w(TAG, "Allowing abusive custom column: " + userColumn); 1188 return maybeWithOperator(operator, userColumn); 1189 } 1190 } 1191 1192 return null; 1193 } 1194 isTableOrColumn(String token)1195 private boolean isTableOrColumn(String token) { 1196 if (mTables.equals(token)) return true; 1197 return computeSingleProjection(token) != null; 1198 } 1199 1200 /** {@hide} */ computeWhere(@ullable String selection)1201 public @Nullable String computeWhere(@Nullable String selection) { 1202 final boolean hasInternal = !TextUtils.isEmpty(mWhereClause); 1203 final boolean hasExternal = !TextUtils.isEmpty(selection); 1204 1205 if (hasInternal || hasExternal) { 1206 final StringBuilder where = new StringBuilder(); 1207 if (hasInternal) { 1208 where.append('(').append(mWhereClause).append(')'); 1209 } 1210 if (hasInternal && hasExternal) { 1211 where.append(" AND "); 1212 } 1213 if (hasExternal) { 1214 where.append('(').append(selection).append(')'); 1215 } 1216 return where.toString(); 1217 } else { 1218 return null; 1219 } 1220 } 1221 1222 /** 1223 * Wrap given argument in parenthesis, unless it's {@code null} or 1224 * {@code ()}, in which case return it verbatim. 1225 */ wrap(@ullable String arg)1226 private @Nullable String wrap(@Nullable String arg) { 1227 if (TextUtils.isEmpty(arg)) { 1228 return arg; 1229 } else { 1230 return "(" + arg + ")"; 1231 } 1232 } 1233 } 1234