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