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