1 /* 2 * Copyright (C) 2024 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 com.android.providers.media.photopicker.v2.sqlite; 18 19 import static android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE; 20 21 import static java.util.Objects.requireNonNull; 22 23 import android.content.ContentValues; 24 import android.database.Cursor; 25 import android.database.sqlite.SQLiteDatabase; 26 import android.util.Log; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.Nullable; 30 31 import com.android.providers.media.photopicker.v2.model.SearchRequest; 32 import com.android.providers.media.photopicker.v2.model.SearchSuggestionRequest; 33 import com.android.providers.media.photopicker.v2.model.SearchTextRequest; 34 35 import java.util.ArrayList; 36 import java.util.List; 37 import java.util.Locale; 38 import java.util.stream.Collectors; 39 40 /** 41 * Convenience class for running Picker Search Request related sql queries. 42 */ 43 public class SearchRequestDatabaseUtil { 44 private static final String TAG = "SearchDatabaseUtil"; 45 46 // Note that SQLite treats all null values as different. So, if you apply a 47 // UNIQUE(...) constraint on some columns and if any of those columns holds a null value, 48 // the unique constraint will not be applied. This is why in the search request table, 49 // a placeholder value will be used instead of null so that the unique constraint gets 50 // applied to all search requests saved in the table. 51 // The placeholder values should not be a valid value to any of the columns in the unique 52 // constraint. 53 public static final String PLACEHOLDER_FOR_NULL = ""; 54 55 /** 56 * Tries to insert the given search request in the DB with the REPLACE constraint conflict 57 * resolution strategy. 58 * 59 * @param database The database you need to run the query on. 60 * @param searchRequest An object that contains search request details. 61 * @return The row id of the inserted row or -1 in case of a SQLite constraint conflict. 62 * @throws RuntimeException if an error occurs in running the sql command. 63 */ saveSearchRequest( @onNull SQLiteDatabase database, @NonNull SearchRequest searchRequest)64 public static long saveSearchRequest( 65 @NonNull SQLiteDatabase database, 66 @NonNull SearchRequest searchRequest) { 67 final String table = PickerSQLConstants.Table.SEARCH_REQUEST.name(); 68 69 try { 70 final long result = database.insertWithOnConflict( 71 table, 72 /* nullColumnHack */ null, 73 searchRequestToContentValues(searchRequest), 74 CONFLICT_IGNORE 75 ); 76 77 if (result == -1) { 78 Log.e(TAG, "Could not save request due to a conflict constraint"); 79 } 80 return result; 81 } catch (RuntimeException e) { 82 throw new RuntimeException("Could not save search request ", e); 83 } 84 } 85 86 /** 87 * Update resume key for the given search request ID. 88 * 89 * @param database The database you need to run the query on. 90 * @param searchRequestId Identifier for a search request. 91 * @param resumeKey The resume key that can be used to fetch the next page of results, 92 * or indicate that the sync is complete. 93 * @param isLocal True if the sync resume key of local sync should be updated, else false if the 94 * sync resume key of cloud sync should be updated. 95 * @throws RuntimeException if an error occurs in running the sql command. 96 */ updateResumeKey( @onNull SQLiteDatabase database, int searchRequestId, @Nullable String resumeKey, @NonNull String authority, boolean isLocal)97 public static void updateResumeKey( 98 @NonNull SQLiteDatabase database, 99 int searchRequestId, 100 @Nullable String resumeKey, 101 @NonNull String authority, 102 boolean isLocal) { 103 final String table = PickerSQLConstants.Table.SEARCH_REQUEST.name(); 104 105 ContentValues contentValues = new ContentValues(); 106 if (isLocal) { 107 contentValues.put( 108 PickerSQLConstants.SearchRequestTableColumns 109 .LOCAL_SYNC_RESUME_KEY.getColumnName(), 110 resumeKey); 111 contentValues.put( 112 PickerSQLConstants.SearchRequestTableColumns 113 .LOCAL_AUTHORITY.getColumnName(), 114 authority); 115 } else { 116 contentValues.put( 117 PickerSQLConstants.SearchRequestTableColumns 118 .CLOUD_SYNC_RESUME_KEY.getColumnName(), 119 resumeKey); 120 contentValues.put( 121 PickerSQLConstants.SearchRequestTableColumns 122 .CLOUD_AUTHORITY.getColumnName(), 123 authority); 124 } 125 126 database.update( 127 table, 128 contentValues, 129 String.format( 130 Locale.ROOT, 131 "%s.%s = %d", 132 table, 133 PickerSQLConstants.SearchRequestTableColumns 134 .SEARCH_REQUEST_ID.getColumnName(), 135 searchRequestId 136 ), 137 /* whereArgs */ null 138 ); 139 } 140 141 /** 142 * Queries the database to try and fetch a unique search request ID for the given search 143 * request. 144 * 145 * @param database The database you need to run the query on. 146 * @param searchRequest Object that contains search request details. 147 * @return the ID of the given search request or -1 if it can't find the search request in the 148 * database. In case multiple search requests are a match, the first one is returned. 149 */ getSearchRequestID( @onNull SQLiteDatabase database, @NonNull SearchRequest searchRequest)150 public static int getSearchRequestID( 151 @NonNull SQLiteDatabase database, 152 @NonNull SearchRequest searchRequest) { 153 final SelectSQLiteQueryBuilder queryBuilder = new SelectSQLiteQueryBuilder(database) 154 .setTables(PickerSQLConstants.Table.SEARCH_REQUEST.name()) 155 .setProjection(List.of( 156 PickerSQLConstants.SearchRequestTableColumns 157 .SEARCH_REQUEST_ID.getColumnName())); 158 159 addSearchRequestIDWhereClause(queryBuilder, searchRequest); 160 161 try (Cursor cursor = database.rawQuery( 162 queryBuilder.buildQuery(), /* selectionArgs */ null)) { 163 if (cursor.moveToFirst()) { 164 if (cursor.getCount() > 1) { 165 Log.e(TAG, "Cursor cannot have more than one search request match " 166 + "- returning the first match"); 167 } 168 return cursor.getInt( 169 cursor.getColumnIndexOrThrow( 170 PickerSQLConstants.SearchRequestTableColumns.SEARCH_REQUEST_ID 171 .getColumnName() 172 ) 173 ); 174 } 175 176 // If the cursor is empty, return -1; 177 Log.w(TAG, "Search request does not exist in the DB."); 178 return -1; 179 } catch (RuntimeException e) { 180 Log.e(TAG, "Could not fetch search request ID.", e); 181 return -1; 182 } 183 } 184 185 /** 186 * Queries the database to try and fetch search request details for the given search request ID. 187 * 188 * @param database The database you need to run the query on. 189 * @param searchRequestID ID of the search request. 190 * @return the search request object corresponding to the given search request id, 191 * or null if it can't find the search request in the database. In case multiple search 192 * requests are a match, the first one is returned. 193 */ 194 @Nullable getSearchRequestDetails( @onNull SQLiteDatabase database, @NonNull int searchRequestID )195 public static SearchRequest getSearchRequestDetails( 196 @NonNull SQLiteDatabase database, 197 @NonNull int searchRequestID 198 ) { 199 final List<String> projection = List.of( 200 PickerSQLConstants.SearchRequestTableColumns.LOCAL_SYNC_RESUME_KEY.getColumnName(), 201 PickerSQLConstants.SearchRequestTableColumns.LOCAL_AUTHORITY.getColumnName(), 202 PickerSQLConstants.SearchRequestTableColumns.CLOUD_SYNC_RESUME_KEY.getColumnName(), 203 PickerSQLConstants.SearchRequestTableColumns.CLOUD_AUTHORITY.getColumnName(), 204 PickerSQLConstants.SearchRequestTableColumns.SEARCH_TEXT.getColumnName(), 205 PickerSQLConstants.SearchRequestTableColumns.MEDIA_SET_ID.getColumnName(), 206 PickerSQLConstants.SearchRequestTableColumns.SUGGESTION_AUTHORITY.getColumnName(), 207 PickerSQLConstants.SearchRequestTableColumns.SUGGESTION_TYPE.getColumnName(), 208 PickerSQLConstants.SearchRequestTableColumns.MIME_TYPES.getColumnName() 209 ); 210 final SelectSQLiteQueryBuilder queryBuilder = new SelectSQLiteQueryBuilder(database) 211 .setTables(PickerSQLConstants.Table.SEARCH_REQUEST.name()) 212 .setProjection(projection); 213 214 addSearchRequestDetailsWhereClause(queryBuilder, searchRequestID); 215 216 try (Cursor cursor = database.rawQuery( 217 queryBuilder.buildQuery(), /* selectionArgs */ null)) { 218 if (cursor.moveToFirst()) { 219 if (cursor.getCount() > 1) { 220 Log.e(TAG, "Cursor cannot have more than one search request match " 221 + "- returning the first match"); 222 } 223 224 final String suggestionAuthority = getColumnValueOrNull( 225 cursor, 226 PickerSQLConstants.SearchRequestTableColumns 227 .SUGGESTION_AUTHORITY.getColumnName() 228 ); 229 final String mimeTypes = getColumnValueOrNull( 230 cursor, 231 PickerSQLConstants.SearchRequestTableColumns 232 .MIME_TYPES.getColumnName() 233 ); 234 final String searchText = getColumnValueOrNull( 235 cursor, 236 PickerSQLConstants.SearchRequestTableColumns 237 .SEARCH_TEXT.getColumnName() 238 ); 239 final String localSyncResumeKey = getColumnValueOrNull( 240 cursor, 241 PickerSQLConstants.SearchRequestTableColumns 242 .LOCAL_SYNC_RESUME_KEY.getColumnName() 243 ); 244 final String localAuthority = getColumnValueOrNull( 245 cursor, 246 PickerSQLConstants.SearchRequestTableColumns 247 .LOCAL_AUTHORITY.getColumnName() 248 ); 249 final String cloudSyncResumeKey = getColumnValueOrNull( 250 cursor, 251 PickerSQLConstants.SearchRequestTableColumns 252 .CLOUD_SYNC_RESUME_KEY.getColumnName() 253 ); 254 final String cloudAuthority = getColumnValueOrNull( 255 cursor, 256 PickerSQLConstants.SearchRequestTableColumns 257 .CLOUD_AUTHORITY.getColumnName() 258 ); 259 260 final SearchRequest searchRequest; 261 if (suggestionAuthority == null) { 262 // This is a search text request 263 searchRequest = new SearchTextRequest( 264 SearchRequest.getMimeTypesAsList(mimeTypes), 265 searchText, 266 localSyncResumeKey, 267 localAuthority, 268 cloudSyncResumeKey, 269 cloudAuthority 270 ); 271 } else { 272 // This is a search suggestion request 273 final String mediaSetID = requireNonNull( 274 getColumnValueOrNull( 275 cursor, 276 PickerSQLConstants.SearchRequestTableColumns 277 .MEDIA_SET_ID.getColumnName() 278 ) 279 ); 280 final String suggestionType = requireNonNull( 281 getColumnValueOrNull( 282 cursor, 283 PickerSQLConstants.SearchRequestTableColumns 284 .SUGGESTION_TYPE.getColumnName() 285 ) 286 ); 287 288 searchRequest = new SearchSuggestionRequest( 289 SearchRequest.getMimeTypesAsList(mimeTypes), 290 searchText, 291 mediaSetID, 292 suggestionAuthority, 293 suggestionType, 294 localSyncResumeKey, 295 localAuthority, 296 cloudSyncResumeKey, 297 cloudAuthority 298 ); 299 } 300 return searchRequest; 301 } 302 303 // If the cursor is empty, return null; 304 Log.w(TAG, "Search request does not exist in the DB."); 305 return null; 306 } catch (RuntimeException e) { 307 Log.e(TAG, "Could not fetch search request details.", e); 308 return null; 309 } 310 } 311 312 /** 313 * @param database The database you need to run the query on. 314 * @param isLocal True if the search results synced with the local provider need to be reset. 315 * Else if the search results synced with cloud provider need to be reset, 316 * this is false. 317 * @return a list of search request IDs of the search requests that are either fully or 318 * partially synced with the provider. 319 */ getSyncedRequestIds( @onNull SQLiteDatabase database, boolean isLocal)320 public static List<Integer> getSyncedRequestIds( 321 @NonNull SQLiteDatabase database, 322 boolean isLocal) { 323 SelectSQLiteQueryBuilder queryBuilder = new SelectSQLiteQueryBuilder(database); 324 queryBuilder.setTables(PickerSQLConstants.Table.SEARCH_REQUEST.name()) 325 .setProjection(List.of( 326 PickerSQLConstants.SearchRequestTableColumns 327 .SEARCH_REQUEST_ID.getColumnName() 328 )); 329 330 if (isLocal) { 331 queryBuilder.appendWhereStandalone( 332 String.format( 333 Locale.ROOT, 334 "%s IS NOT NULL OR %s IS NOT NULL", 335 PickerSQLConstants.SearchRequestTableColumns 336 .LOCAL_AUTHORITY.getColumnName(), 337 PickerSQLConstants.SearchRequestTableColumns 338 .LOCAL_SYNC_RESUME_KEY.getColumnName() 339 ) 340 ); 341 } else { 342 queryBuilder.appendWhereStandalone( 343 String.format( 344 Locale.ROOT, 345 "%s IS NOT NULL OR %s IS NOT NULL", 346 PickerSQLConstants.SearchRequestTableColumns 347 .CLOUD_AUTHORITY.getColumnName(), 348 PickerSQLConstants.SearchRequestTableColumns 349 .CLOUD_SYNC_RESUME_KEY.getColumnName() 350 ) 351 ); 352 } 353 354 final List<Integer> searchRequestIds = new ArrayList<>(); 355 try (Cursor cursor = database.rawQuery(queryBuilder.buildQuery(), null)) { 356 if (cursor.moveToFirst()) { 357 do { 358 searchRequestIds.add(cursor.getInt( 359 cursor.getColumnIndexOrThrow( 360 PickerSQLConstants.SearchRequestTableColumns 361 .SEARCH_REQUEST_ID.getColumnName() 362 ) 363 )); 364 } while (cursor.moveToNext()); 365 } 366 } 367 return searchRequestIds; 368 } 369 370 /** 371 * Clear sync resume info from the database. 372 * 373 * @param database SQLiteDatabase object that contains the database connection. 374 * @param searchRequestIds List of search request ids that identify the rows that need to be 375 * updated. 376 * @param isLocal This is true when the local sync resume info needs to clear, 377 * otherwise it is false. 378 * @return The number of items that were updated. 379 */ clearSyncResumeInfo( @onNull SQLiteDatabase database, @NonNull List<Integer> searchRequestIds, boolean isLocal)380 public static int clearSyncResumeInfo( 381 @NonNull SQLiteDatabase database, 382 @NonNull List<Integer> searchRequestIds, 383 boolean isLocal) { 384 requireNonNull(database); 385 requireNonNull(searchRequestIds); 386 if (searchRequestIds.isEmpty()) { 387 Log.d(TAG, "No search request ids received for clearing resume info"); 388 return 0; 389 } 390 391 final String whereClause = String.format( 392 Locale.ROOT, 393 "%s IN ('%s')", 394 PickerSQLConstants.SearchRequestTableColumns.SEARCH_REQUEST_ID.getColumnName(), 395 searchRequestIds 396 .stream() 397 .map(Object::toString) 398 .collect(Collectors.joining("','"))); 399 400 final ContentValues updatedValues = new ContentValues(); 401 if (isLocal) { 402 updatedValues.put( 403 PickerSQLConstants.SearchRequestTableColumns 404 .LOCAL_SYNC_RESUME_KEY.getColumnName(), 405 (String) null 406 ); 407 updatedValues.put( 408 PickerSQLConstants.SearchRequestTableColumns.LOCAL_AUTHORITY.getColumnName(), 409 (String) null 410 ); 411 } else { 412 updatedValues.put( 413 PickerSQLConstants.SearchRequestTableColumns 414 .CLOUD_SYNC_RESUME_KEY.getColumnName(), 415 (String) null 416 ); 417 updatedValues.put( 418 PickerSQLConstants.SearchRequestTableColumns.CLOUD_AUTHORITY.getColumnName(), 419 (String) null 420 ); 421 } 422 423 final int updatedSearchRequestsCount = database.update( 424 PickerSQLConstants.Table.SEARCH_REQUEST.name(), 425 updatedValues, 426 whereClause, 427 /* whereArgs */ null); 428 Log.d(TAG, "Updated number of search results: " + updatedSearchRequestsCount); 429 return updatedSearchRequestsCount; 430 } 431 432 /** 433 * Clears all search requests from the database. 434 * 435 * @param database SQLiteDatabase object that contains the database connection. 436 * @return The number of items that were updated. 437 */ clearAllSearchRequests(@onNull SQLiteDatabase database)438 public static int clearAllSearchRequests(@NonNull SQLiteDatabase database) { 439 requireNonNull(database); 440 441 int searchRequestsDeletionCount = 442 database.delete( 443 PickerSQLConstants.Table.SEARCH_REQUEST.name(), 444 /* whereClause */ null, 445 /* whereArgs */ null); 446 447 Log.d(TAG, String.format( 448 Locale.ROOT, 449 "Deleted %s rows in search request table", 450 searchRequestsDeletionCount)); 451 452 return searchRequestsDeletionCount; 453 } 454 455 456 /** 457 * @return ContentValues that contains a mapping of column names of search_request table as key 458 * and search request data as values. This is intended to be used in SQLite insert queries. 459 */ 460 @NonNull searchRequestToContentValues( @onNull SearchRequest searchRequest)461 private static ContentValues searchRequestToContentValues( 462 @NonNull SearchRequest searchRequest) { 463 requireNonNull(searchRequest); 464 465 final ContentValues values = new ContentValues(); 466 467 // Insert value or placeholder for null for unique column. 468 values.put( 469 PickerSQLConstants.SearchRequestTableColumns.MIME_TYPES.getColumnName(), 470 getValueOrPlaceholder( 471 SearchRequest.getMimeTypesAsString(searchRequest.getMimeTypes()))); 472 473 // Insert value as it is for non-unique columns. 474 values.put( 475 PickerSQLConstants.SearchRequestTableColumns.LOCAL_SYNC_RESUME_KEY.getColumnName(), 476 searchRequest.getLocalSyncResumeKey()); 477 478 values.put( 479 PickerSQLConstants.SearchRequestTableColumns.LOCAL_AUTHORITY.getColumnName(), 480 searchRequest.getLocalAuthority()); 481 482 values.put( 483 PickerSQLConstants.SearchRequestTableColumns.CLOUD_SYNC_RESUME_KEY.getColumnName(), 484 searchRequest.getCloudSyncResumeKey()); 485 486 values.put( 487 PickerSQLConstants.SearchRequestTableColumns.CLOUD_AUTHORITY.getColumnName(), 488 searchRequest.getCloudAuthority()); 489 490 if (searchRequest instanceof SearchTextRequest searchTextRequest) { 491 // Insert placeholder for null for unique column. 492 values.put( 493 PickerSQLConstants.SearchRequestTableColumns.SEARCH_TEXT.getColumnName(), 494 getValueOrPlaceholder(searchTextRequest.getSearchText())); 495 values.put( 496 PickerSQLConstants.SearchRequestTableColumns.MEDIA_SET_ID.getColumnName(), 497 PLACEHOLDER_FOR_NULL); 498 values.put( 499 PickerSQLConstants.SearchRequestTableColumns 500 .SUGGESTION_AUTHORITY.getColumnName(), 501 PLACEHOLDER_FOR_NULL); 502 values.put( 503 PickerSQLConstants.SearchRequestTableColumns.SUGGESTION_TYPE.getColumnName(), 504 PLACEHOLDER_FOR_NULL); 505 } else if (searchRequest instanceof SearchSuggestionRequest searchSuggestionRequest) { 506 // Insert value or placeholder for null for unique column. 507 values.put( 508 PickerSQLConstants.SearchRequestTableColumns.SEARCH_TEXT.getColumnName(), 509 getValueOrPlaceholder( 510 searchSuggestionRequest.getSearchSuggestion().getSearchText())); 511 values.put( 512 PickerSQLConstants.SearchRequestTableColumns.MEDIA_SET_ID.getColumnName(), 513 getValueOrPlaceholder( 514 searchSuggestionRequest.getSearchSuggestion().getMediaSetId())); 515 values.put( 516 PickerSQLConstants.SearchRequestTableColumns 517 .SUGGESTION_AUTHORITY.getColumnName(), 518 getValueOrPlaceholder(searchSuggestionRequest 519 .getSearchSuggestion().getAuthority())); 520 values.put( 521 PickerSQLConstants.SearchRequestTableColumns.SUGGESTION_TYPE.getColumnName(), 522 getValueOrPlaceholder(searchSuggestionRequest.getSearchSuggestion() 523 .getSearchSuggestionType())); 524 } else { 525 throw new IllegalStateException( 526 "Could not identify search request type " + searchRequest); 527 } 528 529 return values; 530 } 531 532 /** 533 * @param queryBuilder Adds where clauses based on the given searchRequest. 534 * @param searchRequest Object that contains search request details. 535 */ addSearchRequestIDWhereClause( @onNull SelectSQLiteQueryBuilder queryBuilder, @NonNull SearchRequest searchRequest)536 private static void addSearchRequestIDWhereClause( 537 @NonNull SelectSQLiteQueryBuilder queryBuilder, 538 @NonNull SearchRequest searchRequest) { 539 String searchText; 540 String mediaSetId = null; 541 String authority = null; 542 String suggestionType = null; 543 if (searchRequest instanceof SearchTextRequest searchTextRequest) { 544 searchText = getValueOrPlaceholder(searchTextRequest.getSearchText()); 545 } else if (searchRequest instanceof SearchSuggestionRequest searchSuggestionRequest) { 546 searchText = getValueOrPlaceholder( 547 searchSuggestionRequest.getSearchSuggestion().getSearchText()); 548 mediaSetId = getValueOrPlaceholder(searchSuggestionRequest 549 .getSearchSuggestion().getMediaSetId()); 550 authority = getValueOrPlaceholder(searchSuggestionRequest 551 .getSearchSuggestion().getAuthority()); 552 suggestionType = getValueOrPlaceholder(searchSuggestionRequest 553 .getSearchSuggestion().getSearchSuggestionType()); 554 } else { 555 throw new IllegalStateException( 556 "Could not identify search request type " + searchRequest); 557 } 558 559 addWhereClause( 560 queryBuilder, 561 PickerSQLConstants.SearchRequestTableColumns.MIME_TYPES.getColumnName(), 562 SearchRequest.getMimeTypesAsString(searchRequest.getMimeTypes())); 563 addWhereClause( 564 queryBuilder, 565 PickerSQLConstants.SearchRequestTableColumns.SEARCH_TEXT.getColumnName(), 566 searchText); 567 addWhereClause( 568 queryBuilder, 569 PickerSQLConstants.SearchRequestTableColumns.MEDIA_SET_ID.getColumnName(), 570 mediaSetId); 571 addWhereClause( 572 queryBuilder, 573 PickerSQLConstants.SearchRequestTableColumns.SUGGESTION_AUTHORITY.getColumnName(), 574 authority); 575 addWhereClause( 576 queryBuilder, 577 PickerSQLConstants.SearchRequestTableColumns.SUGGESTION_TYPE.getColumnName(), 578 suggestionType); 579 } 580 addSearchRequestDetailsWhereClause( @onNull SelectSQLiteQueryBuilder queryBuilder, @NonNull int searchRequestID )581 private static void addSearchRequestDetailsWhereClause( 582 @NonNull SelectSQLiteQueryBuilder queryBuilder, 583 @NonNull int searchRequestID 584 ) { 585 queryBuilder.appendWhereStandalone( 586 String.format(Locale.ROOT, 587 " %s = '%s' ", 588 PickerSQLConstants.SearchRequestTableColumns 589 .SEARCH_REQUEST_ID.getColumnName(), 590 searchRequestID)); 591 } 592 593 /** 594 * @param queryBuilder Adds an equality where clauses based on the given column name and value. 595 * @param columnName Column name on which an equals check needs to be added. 596 * @param value The desired value that needs to be added to the where clause equality check. 597 * If the value is null, it will be replaced by a non-null placeholder used in the 598 * table for empty/null values. 599 */ addWhereClause( @onNull SelectSQLiteQueryBuilder queryBuilder, @NonNull String columnName, @Nullable String value)600 private static void addWhereClause( 601 @NonNull SelectSQLiteQueryBuilder queryBuilder, 602 @NonNull String columnName, 603 @Nullable String value) { 604 value = getValueOrPlaceholder(value); 605 queryBuilder.appendWhereStandalone(String.format(Locale.ROOT, 606 " %s = '%s' ", columnName, value)); 607 } 608 609 /** 610 * @param value Input value that can be nullable. 611 * @return If the input value is null, returns it as it is , otherwise returns a non-null 612 * placeholder for empty/null values. 613 */ 614 @NonNull getValueOrPlaceholder(@ullable String value)615 private static String getValueOrPlaceholder(@Nullable String value) { 616 if (value == null) { 617 return PLACEHOLDER_FOR_NULL; 618 } 619 return value; 620 } 621 622 @Nullable getColumnValueOrNull(@onNull Cursor cursor, @NonNull String columnName)623 private static String getColumnValueOrNull(@NonNull Cursor cursor, @NonNull String columnName) { 624 return getValueOrNull(cursor.getString(cursor.getColumnIndexOrThrow(columnName))); 625 } 626 627 @Nullable getValueOrNull(@onNull String value)628 private static String getValueOrNull(@NonNull String value) { 629 if (PLACEHOLDER_FOR_NULL.equals(value)) { 630 return null; 631 } 632 return value; 633 } 634 } 635