summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Shubhi <shubhisaxena@google.com> 2024-10-25 17:00:00 +0000
committer Shubhi <shubhisaxena@google.com> 2024-10-30 14:13:12 +0000
commit726d3dff6b4b9861ff6e850564b2012bbedcd68b (patch)
treee840aafdacf4f2000238e8711bc717401e54b006
parent91f2d2e2cbd5736c89fdbd23da2d0d44c7e85654 (diff)
Add worker for search results sync
Bug: 361042632 Test: atest SearchResultsSyncWorkerTest Test: atest SearchRequestDatabaseUtilTest Change-Id: Ic1aee325a0a3f0073b0c00d503d2117fd2103ae1 Flag: com.android.providers.media.flags.enable_photopicker_search
-rw-r--r--src/com/android/providers/media/photopicker/sync/PickerSearchProviderClient.java (renamed from src/com/android/providers/media/photopicker/v2/PickerSearchProviderClient.java)12
-rw-r--r--src/com/android/providers/media/photopicker/sync/PickerSyncManager.java1
-rw-r--r--src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java338
-rw-r--r--src/com/android/providers/media/photopicker/v2/model/SearchRequest.java9
-rw-r--r--src/com/android/providers/media/photopicker/v2/sqlite/SearchLocalMediaSubQuery.java3
-rw-r--r--src/com/android/providers/media/photopicker/v2/sqlite/SearchRequestDatabaseUtil.java45
-rw-r--r--src/com/android/providers/media/photopicker/v2/sqlite/SearchResultsDatabaseUtil.java61
7 files changed, 461 insertions, 8 deletions
diff --git a/src/com/android/providers/media/photopicker/v2/PickerSearchProviderClient.java b/src/com/android/providers/media/photopicker/sync/PickerSearchProviderClient.java
index 0857d2c9a..7b37865d9 100644
--- a/src/com/android/providers/media/photopicker/v2/PickerSearchProviderClient.java
+++ b/src/com/android/providers/media/photopicker/sync/PickerSearchProviderClient.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.providers.media.photopicker.v2;
+package com.android.providers.media.photopicker.sync;
import static java.util.Objects.requireNonNull;
@@ -60,8 +60,12 @@ public class PickerSearchProviderClient {
* Note: This functions does not expect pagination args.
*/
@Nullable
- public Cursor fetchSearchResultsFromCmp(@Nullable String suggestedMediaSetId,
- @Nullable String searchText, @NonNull @SortOrder int sortOrder,
+ public Cursor fetchSearchResultsFromCmp(
+ @Nullable String suggestedMediaSetId,
+ @Nullable String searchText,
+ @SortOrder int sortOrder,
+ int pageSize,
+ @Nullable String resumePageToken,
@Nullable CancellationSignal cancellationSignal) {
if (suggestedMediaSetId == null && searchText == null) {
throw new IllegalArgumentException(
@@ -70,6 +74,8 @@ public class PickerSearchProviderClient {
final Bundle queryArgs = new Bundle();
queryArgs.putString(CloudMediaProviderContract.KEY_SEARCH_TEXT, searchText);
queryArgs.putString(CloudMediaProviderContract.KEY_MEDIA_SET_ID, suggestedMediaSetId);
+ queryArgs.putInt(CloudMediaProviderContract.EXTRA_PAGE_SIZE, pageSize);
+ queryArgs.putString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN, resumePageToken);
queryArgs.putInt(CloudMediaProviderContract.EXTRA_SORT_ORDER, sortOrder);
return mContext.getContentResolver().query(
diff --git a/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java b/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
index 88f52e08e..f39517f08 100644
--- a/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
+++ b/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
@@ -86,6 +86,7 @@ public class PickerSyncManager {
static final String SYNC_WORKER_INPUT_SYNC_SOURCE = "INPUT_SYNC_TYPE";
static final String SYNC_WORKER_INPUT_RESET_TYPE = "INPUT_RESET_TYPE";
static final String SYNC_WORKER_INPUT_ALBUM_ID = "INPUT_ALBUM_ID";
+ static final String SYNC_WORKER_INPUT_SEARCH_REQUEST_ID = "INPUT_SEARCH_REQUEST_ID";
static final String SYNC_WORKER_TAG_IS_PERIODIC = "PERIODIC";
static final long PROACTIVE_SYNC_DELAY_MS = 1500;
private static final int SYNC_MEDIA_PERIODIC_WORK_INTERVAL = 4; // Time unit is hours.
diff --git a/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java b/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java
new file mode 100644
index 000000000..76511ba2a
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/SearchResultsSyncWorker.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.photopicker.sync;
+
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SEARCH_REQUEST_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.provider.CloudMediaProviderContract;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.work.ListenableWorker;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
+import com.android.providers.media.photopicker.v2.model.SearchRequest;
+import com.android.providers.media.photopicker.v2.model.SearchSuggestionRequest;
+import com.android.providers.media.photopicker.v2.model.SearchSuggestionType;
+import com.android.providers.media.photopicker.v2.model.SearchTextRequest;
+import com.android.providers.media.photopicker.v2.sqlite.SearchRequestDatabaseUtil;
+import com.android.providers.media.photopicker.v2.sqlite.SearchResultsDatabaseUtil;
+
+import java.util.List;
+
+/**
+ * This is a {@link Worker} class responsible for syncing search results media with the
+ * correct sync source.
+ */
+public class SearchResultsSyncWorker extends Worker {
+ private static final String TAG = "SearchSyncWorker";
+ private static final int SYNC_PAGE_COUNT = 3;
+ private static final int PAGE_SIZE = 500;
+ private static final int INVALID_SYNC_SOURCE = -1;
+ private static final int INVALID_SEARCH_REQUEST_ID = -1;
+ @VisibleForTesting
+ public static final String SYNC_COMPLETE_RESUME_KEY = "SYNCED";
+ private final Context mContext;
+ private final CancellationSignal mCancellationSignal;
+
+ /**
+ * Creates an instance of the {@link Worker}.
+ *
+ * @param context the application {@link Context}
+ * @param workerParams the set of {@link WorkerParameters}
+ */
+ public SearchResultsSyncWorker(
+ @NonNull Context context,
+ @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+
+ mContext = context;
+ mCancellationSignal = new CancellationSignal();
+ }
+
+ @NonNull
+ @Override
+ public ListenableWorker.Result doWork() {
+ // Do not allow endless re-runs of this worker, if this isn't the original run,
+ // just succeed and wait until the next scheduled run.
+ if (getRunAttemptCount() > 0) {
+ Log.w(TAG, "Worker retry was detected, ending this run in failure.");
+ return ListenableWorker.Result.failure();
+ }
+
+ final int syncSource = getInputData().getInt(SYNC_WORKER_INPUT_SYNC_SOURCE,
+ /* defaultValue */ INVALID_SYNC_SOURCE);
+ final int searchRequestId = getInputData().getInt(SYNC_WORKER_INPUT_SEARCH_REQUEST_ID,
+ /* defaultValue */ INVALID_SEARCH_REQUEST_ID);
+
+ Log.i(TAG, String.format(
+ "Starting search results sync from sync source: %s search request id: %s",
+ syncSource, searchRequestId));
+
+ try {
+ throwIfWorkerStopped();
+
+ final SearchRequest searchRequest = SearchRequestDatabaseUtil
+ .getSearchRequestDetails(getDatabase(), searchRequestId);
+ validateWorkInput(syncSource, searchRequestId, searchRequest);
+
+ syncWithSource(syncSource, searchRequestId, searchRequest);
+
+ Log.i(TAG, String.format(
+ "Completed search results sync from sync source: %s search request id: %s",
+ syncSource, searchRequestId));
+ return ListenableWorker.Result.success();
+ } catch (RuntimeException | RequestObsoleteException e) {
+ Log.e(TAG, String.format("Could not complete search results sync sync from "
+ + "sync source: %s search request id: %s",
+ syncSource, searchRequestId), e);
+ return ListenableWorker.Result.failure();
+ }
+ }
+
+ /**
+ * Sync search results with the given sync source.
+ *
+ * @param syncSource Identifies if we need to sync with local provider or cloud provider.
+ * @param searchRequestId Identifier for the search request.
+ * @param searchRequest Details of the search request.
+ * @throws IllegalArgumentException If the search request could not be identified.
+ * @throws RequestObsoleteException If the search request has become obsolete.
+ */
+ private void syncWithSource(
+ int syncSource,
+ int searchRequestId,
+ @Nullable SearchRequest searchRequest)
+ throws IllegalArgumentException, RequestObsoleteException {
+ final String authority = getProviderAuthority(syncSource, searchRequest);
+ final PickerSearchProviderClient searchClient =
+ PickerSearchProviderClient.create(mContext, authority);
+
+ String resumePageToken = searchRequest.getResumeKey();
+
+ if (SYNC_COMPLETE_RESUME_KEY.equals(resumePageToken)) {
+ Log.i(TAG, "Sync has already been completed.");
+ return;
+ }
+
+ try {
+ for (int iteration = 0; iteration < SYNC_PAGE_COUNT; iteration++) {
+ throwIfWorkerStopped();
+ throwIfCloudProviderHasChanged(authority);
+
+ try (Cursor cursor = fetchSearchResultsFromCmp(
+ searchClient, searchRequest, resumePageToken)) {
+
+ List<ContentValues> contentValues =
+ SearchResultsDatabaseUtil.extractContentValuesList(
+ searchRequestId, cursor, isLocal(authority));
+
+ SearchResultsDatabaseUtil
+ .cacheSearchResults(getDatabase(), authority, contentValues);
+
+ resumePageToken = getResumePageToken(cursor.getExtras());
+ if (SYNC_COMPLETE_RESUME_KEY.equals(resumePageToken)) {
+ // Stop syncing if there are no more pages to sync.
+ break;
+ }
+ }
+ }
+ } finally {
+ // Save sync resume key till the point it was performed successfully
+ searchRequest.setResumeKey(resumePageToken);
+ SearchRequestDatabaseUtil
+ .updateResumeKey(getDatabase(), searchRequestId, resumePageToken);
+ }
+ }
+
+ /**
+ * @param extras Bundle received from the CloudMediaProvider with the search results cursor.
+ * @return Extracts the rsume page token from the extras and returns it. If it is not present
+ * in the extras, returns {@link SearchResultsSyncWorker#SYNC_COMPLETE_RESUME_KEY}
+ */
+ @NonNull
+ private String getResumePageToken(@Nullable Bundle extras) {
+ if (extras == null
+ || extras.getString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN) == null) {
+ return SYNC_COMPLETE_RESUME_KEY;
+ }
+
+ return extras.getString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN);
+ }
+
+ /**
+ * Get search results from the CloudMediaProvider.
+ */
+ @NonNull
+ private Cursor fetchSearchResultsFromCmp(
+ @NonNull PickerSearchProviderClient searchClient,
+ @NonNull SearchRequest searchRequest,
+ @Nullable String resumePageToken) {
+ final String suggestedMediaSetId;
+ final String searchText;
+ if (searchRequest instanceof SearchSuggestionRequest searchSuggestionRequest) {
+ suggestedMediaSetId = searchSuggestionRequest.getMediaSetId();
+ searchText = searchSuggestionRequest.getSearchText();
+ } else if (searchRequest instanceof SearchTextRequest searchTextRequest) {
+ suggestedMediaSetId = null;
+ searchText = searchTextRequest.getSearchText();
+ } else {
+ throw new IllegalArgumentException("Could not recognize the type of SearchRequest");
+ }
+
+ final Cursor cursor = searchClient.fetchSearchResultsFromCmp(
+ suggestedMediaSetId,
+ searchText,
+ CloudMediaProviderContract.SORT_ORDER_DESC_DATE_TAKEN,
+ PAGE_SIZE,
+ resumePageToken,
+ mCancellationSignal
+ );
+
+ if (cursor == null) {
+ throw new IllegalStateException("Cursor returned from provider is null.");
+ }
+
+ return cursor;
+ }
+
+
+ /**
+ * Validates input data received by the Worker for an immediate album sync.
+ */
+ private void validateWorkInput(
+ int syncSource,
+ int searchRequestId,
+ @Nullable SearchRequest searchRequest) throws IllegalArgumentException {
+ // Search result sync can only happen with either local provider or cloud provider. This
+ // information needs to be provided in the {@code inputData}.
+ if (syncSource != SYNC_LOCAL_ONLY && syncSource != SYNC_CLOUD_ONLY) {
+ throw new IllegalArgumentException("Invalid search results sync source " + syncSource);
+ }
+ if (searchRequestId == INVALID_SEARCH_REQUEST_ID) {
+ throw new IllegalArgumentException("Invalid search request id " + searchRequestId);
+ }
+ if (searchRequest == null) {
+ throw new IllegalArgumentException(
+ "Could not get search request details for search request id "
+ + searchRequestId);
+ }
+ if (searchRequest instanceof SearchSuggestionRequest searchSuggestionRequest) {
+ if (searchSuggestionRequest.getSearchSuggestionType() == SearchSuggestionType.ALBUM) {
+ final boolean isLocal = isLocal(searchSuggestionRequest.getAuthority());
+
+ if (isLocal && syncSource == SYNC_CLOUD_ONLY) {
+ throw new IllegalArgumentException(
+ "Cannot sync with cloud provider for local album suggestion. "
+ + "Search request id: " + searchRequestId);
+ } else if (!isLocal && syncSource == SYNC_LOCAL_ONLY) {
+ throw new IllegalArgumentException(
+ "Cannot sync with local provider for cloud album suggestion. "
+ + "Search request id: " + searchRequestId);
+ }
+ }
+ }
+ }
+
+ private String getProviderAuthority(
+ int syncSource,
+ @NonNull SearchRequest searchRequest) {
+ final String authority;
+ if (syncSource == SYNC_LOCAL_ONLY) {
+ authority = getLocalProviderAuthority();
+ } else if (syncSource == SYNC_CLOUD_ONLY) {
+ authority = getCurrentCloudProviderAuthority();
+ } else {
+ throw new IllegalArgumentException("Invalid search results sync source " + syncSource);
+ }
+
+ if (authority == null) {
+ throw new IllegalArgumentException("Authority of the provider to sync search results "
+ + "with cannot be null");
+ }
+
+ // Only in case of ALBUM type search suggestion, we want to explicitly query the source
+ // suggestion authority. For the rest of the suggestion types, we can query both
+ // available providers - local and cloud.
+ if (searchRequest instanceof SearchSuggestionRequest searchSuggestionRequest) {
+ if (searchSuggestionRequest.getSearchSuggestionType() == SearchSuggestionType.ALBUM) {
+ if (!authority.equals(searchSuggestionRequest.getAuthority())) {
+ throw new IllegalArgumentException(String.format(
+ "Mismatch in the suggestion source authority %s and the "
+ + "current sync authority %s for album search results sync",
+ searchSuggestionRequest.getAuthority(),
+ authority));
+ }
+ }
+ }
+
+ return authority;
+ }
+
+ private void throwIfCloudProviderHasChanged(@NonNull String authority)
+ throws RequestObsoleteException {
+ // Local provider's authority cannot change.
+ if (isLocal(authority)) {
+ return;
+ }
+
+ final String currentCloudAuthority = getCurrentCloudProviderAuthority();
+ if (!authority.equals(currentCloudAuthority)) {
+ throw new RequestObsoleteException("Cloud provider authority has changed. "
+ + " Current cloud provider authority: " + currentCloudAuthority
+ + " Cloud provider authority to sync with: " + authority);
+ }
+ }
+
+ private void throwIfWorkerStopped() throws RequestObsoleteException {
+ if (isStopped()) {
+ throw new RequestObsoleteException("Work is stopped " + getId());
+ }
+ }
+
+ private boolean isLocal(@NonNull String authority) {
+ return getLocalProviderAuthority().equals(authority);
+ }
+
+ @Nullable
+ private String getLocalProviderAuthority() {
+ return PickerSyncController.getInstanceOrThrow().getLocalProvider();
+ }
+
+ @Nullable
+ private String getCurrentCloudProviderAuthority() {
+ return PickerSyncController.getInstanceOrThrow().getCloudProvider();
+ }
+
+ private SQLiteDatabase getDatabase() {
+ return PickerSyncController.getInstanceOrThrow().getDbFacade().getDatabase();
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/v2/model/SearchRequest.java b/src/com/android/providers/media/photopicker/v2/model/SearchRequest.java
index aac92aa99..7e0292e54 100644
--- a/src/com/android/providers/media/photopicker/v2/model/SearchRequest.java
+++ b/src/com/android/providers/media/photopicker/v2/model/SearchRequest.java
@@ -36,7 +36,7 @@ public abstract class SearchRequest {
@Nullable
protected final List<String> mMimeTypes;
@Nullable
- protected final String mResumeKey;
+ protected String mResumeKey;
protected SearchRequest(@Nullable List<String> rawMimeTypes) {
this (
@@ -106,5 +106,12 @@ public abstract class SearchRequest {
public String getResumeKey() {
return mResumeKey;
}
+
+ /**
+ * Set the resume key for a given search request.
+ */
+ public void setResumeKey(@Nullable String mResumeKey) {
+ this.mResumeKey = mResumeKey;
+ }
}
diff --git a/src/com/android/providers/media/photopicker/v2/sqlite/SearchLocalMediaSubQuery.java b/src/com/android/providers/media/photopicker/v2/sqlite/SearchLocalMediaSubQuery.java
index 836728019..4fbf855c3 100644
--- a/src/com/android/providers/media/photopicker/v2/sqlite/SearchLocalMediaSubQuery.java
+++ b/src/com/android/providers/media/photopicker/v2/sqlite/SearchLocalMediaSubQuery.java
@@ -58,6 +58,9 @@ public class SearchLocalMediaSubQuery extends SearchMediaSubQuery {
) {
super.addWhereClause(queryBuilder, table, localAuthority, cloudAuthority, reverseOrder);
+ // In order to identify if a row represents local media item and not a cloud media item,
+ // check if the cloud_id is null. We can't have a check on local_id because local_id can be
+ // populated for a cloud media item as well.
queryBuilder.appendWhereStandalone(
String.format(
Locale.ROOT,
diff --git a/src/com/android/providers/media/photopicker/v2/sqlite/SearchRequestDatabaseUtil.java b/src/com/android/providers/media/photopicker/v2/sqlite/SearchRequestDatabaseUtil.java
index 9f60ad688..2ad4f384b 100644
--- a/src/com/android/providers/media/photopicker/v2/sqlite/SearchRequestDatabaseUtil.java
+++ b/src/com/android/providers/media/photopicker/v2/sqlite/SearchRequestDatabaseUtil.java
@@ -34,6 +34,7 @@ import com.android.providers.media.photopicker.v2.model.SearchSuggestionType;
import com.android.providers.media.photopicker.v2.model.SearchTextRequest;
import java.util.List;
+import java.util.Locale;
/**
* Convenience class for running Picker Search related sql queries.
@@ -51,15 +52,15 @@ public class SearchRequestDatabaseUtil {
public static final String PLACEHOLDER_FOR_NULL = "";
/**
- * Tries to insert the given search request in the DB with the IGNORE constraint conflict
+ * Tries to insert the given search request in the DB with the REPLACE constraint conflict
* resolution strategy.
*
* @param database The database you need to run the query on.
* @param searchRequest An object that contains search request details.
- * @return The row id of the inserted row. If the insertion did not happen, return -1.
+ * @return The row id of the inserted row or -1 in case of a SQLite constraint conflict.
* @throws RuntimeException if an error occurs in running the sql command.
*/
- public static long saveSearchRequestIfRequired(
+ public static long saveSearchRequest(
@NonNull SQLiteDatabase database,
@NonNull SearchRequest searchRequest) {
final String table = PickerSQLConstants.Table.SEARCH_REQUEST.name();
@@ -73,7 +74,7 @@ public class SearchRequestDatabaseUtil {
);
if (result == -1) {
- Log.e(TAG, "Insertion ignored because the row already exists");
+ Log.e(TAG, "Could not save request due to a conflict constraint");
}
return result;
} catch (RuntimeException e) {
@@ -82,6 +83,41 @@ public class SearchRequestDatabaseUtil {
}
/**
+ * Update resume key for the given search request ID.
+ *
+ * @param database The database you need to run the query on.
+ * @param searchRequestId Identifier for a search request.
+ * @param resumeKey The resume key that can be used to fetch the next page of results,
+ * or indicate that the sync is complete.
+ * @throws RuntimeException if an error occurs in running the sql command.
+ */
+ public static void updateResumeKey(
+ @NonNull SQLiteDatabase database,
+ int searchRequestId,
+ @Nullable String resumeKey) {
+ final String table = PickerSQLConstants.Table.SEARCH_REQUEST.name();
+
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(
+ PickerSQLConstants.SearchRequestTableColumns.SYNC_RESUME_KEY.getColumnName(),
+ resumeKey);
+
+ database.update(
+ table,
+ contentValues,
+ String.format(
+ Locale.ROOT,
+ "%s.%s = %d",
+ table,
+ PickerSQLConstants.SearchRequestTableColumns
+ .SEARCH_REQUEST_ID.getColumnName(),
+ searchRequestId
+ ),
+ null
+ );
+ }
+
+ /**
* Queries the database to try and fetch a unique search request ID for the given search
* request.
*
@@ -134,6 +170,7 @@ public class SearchRequestDatabaseUtil {
* or null if it can't find the search request in the database. In case multiple search
* requests are a match, the first one is returned.
*/
+ @Nullable
public static SearchRequest getSearchRequestDetails(
@NonNull SQLiteDatabase database,
@NonNull int searchRequestID
diff --git a/src/com/android/providers/media/photopicker/v2/sqlite/SearchResultsDatabaseUtil.java b/src/com/android/providers/media/photopicker/v2/sqlite/SearchResultsDatabaseUtil.java
index 13a7fa43d..7ddb401e5 100644
--- a/src/com/android/providers/media/photopicker/v2/sqlite/SearchResultsDatabaseUtil.java
+++ b/src/com/android/providers/media/photopicker/v2/sqlite/SearchResultsDatabaseUtil.java
@@ -25,11 +25,14 @@ import static com.android.providers.media.photopicker.v2.sqlite.PickerMediaDatab
import static java.util.Objects.requireNonNull;
+import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
import android.os.Bundle;
+import android.provider.CloudMediaProviderContract;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -37,12 +40,70 @@ import androidx.annotation.Nullable;
import com.android.providers.media.photopicker.PickerSyncController;
+import java.util.ArrayList;
import java.util.List;
public class SearchResultsDatabaseUtil {
private static final String TAG = "SearchResultsDatabaseUtil";
/**
+ * Utility method that extracts ContentValues in a format that can be inserted in the
+ * search_result_table.
+ *
+ * @param searchRequestId Identifier for a search request.
+ * @param cursor Cursor received from a CloudMediaProvider with the projection
+ * {@link CloudMediaProviderContract.MediaColumns}
+ * @param isLocal true if the received cursor came from the local provider, otherwise false.
+ * @return a list of ContentValues that can be inserted in the search_result_media table.
+ */
+ @NonNull
+ public static List<ContentValues> extractContentValuesList(
+ int searchRequestId, @NonNull Cursor cursor, boolean isLocal
+ ) {
+ final List<ContentValues> contentValuesList = new ArrayList<>(cursor.getCount());
+ if (cursor.moveToFirst()) {
+ do {
+ contentValuesList.add(extractContentValues(searchRequestId, cursor, isLocal));
+ } while (cursor.moveToNext());
+ }
+ return contentValuesList;
+ }
+
+ @NonNull
+ private static ContentValues extractContentValues(
+ int searchRequestId,
+ @NonNull Cursor cursor,
+ boolean isLocal) {
+ final ContentValues contentValues = new ContentValues();
+
+ final String id = cursor.getString(cursor.getColumnIndexOrThrow(
+ CloudMediaProviderContract.MediaColumns.ID));
+ final String rawMediaStoreUri = cursor.getString(cursor.getColumnIndexOrThrow(
+ CloudMediaProviderContract.MediaColumns.MEDIA_STORE_URI));
+ final Uri mediaStoreUri = rawMediaStoreUri == null ? null : Uri.parse(rawMediaStoreUri);
+ final String extractedLocalId = mediaStoreUri == null ? null
+ : String.valueOf(ContentUris.parseId(mediaStoreUri));
+
+ final String localId = isLocal ? id : extractedLocalId;
+ final String cloudId = isLocal ? null : id;
+
+ contentValues.put(
+ PickerSQLConstants.SearchResultMediaTableColumns.SEARCH_REQUEST_ID.getColumnName(),
+ searchRequestId
+ );
+ contentValues.put(
+ PickerSQLConstants.SearchResultMediaTableColumns.LOCAL_ID.getColumnName(),
+ localId
+ );
+ contentValues.put(
+ PickerSQLConstants.SearchResultMediaTableColumns.CLOUD_ID.getColumnName(),
+ cloudId
+ );
+
+ return contentValues;
+ }
+
+ /**
* Saved the search results media items received from CMP in the database as a temporary cache.
*
* @param database SQLite database object that holds DB connection(s) and provides a wrapper