• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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