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.sync; 18 19 import static android.provider.CloudMediaProviderContract.EXTRA_PROVIDER_CAPABILITIES; 20 import static android.provider.CloudMediaProviderContract.METHOD_GET_CAPABILITIES; 21 22 import static java.util.Objects.requireNonNull; 23 24 import android.content.Context; 25 import android.content.Intent; 26 import android.database.Cursor; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.os.CancellationSignal; 30 import android.os.OperationCanceledException; 31 import android.provider.CloudMediaProviderContract; 32 import android.provider.CloudMediaProviderContract.SortOrder; 33 import android.util.Log; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 38 import java.util.List; 39 40 /** 41 * A client class responsible for fetching search results from 42 * cloud media provider and local search provider. 43 */ 44 public class PickerSearchProviderClient { 45 private static final String TAG = "PickerSearchProviderClient"; 46 47 @NonNull 48 private final Context mContext; 49 50 @NonNull 51 private final String mCloudProviderAuthority; 52 PickerSearchProviderClient(@onNull Context context, @NonNull String cloudProviderAuthority)53 private PickerSearchProviderClient(@NonNull Context context, 54 @NonNull String cloudProviderAuthority) { 55 mContext = requireNonNull(context); 56 mCloudProviderAuthority = requireNonNull(cloudProviderAuthority); 57 } 58 59 /** 60 * Create instance of a picker search client. 61 */ create(@onNull Context context, @NonNull String cloudProviderAuthority)62 public static PickerSearchProviderClient create(@NonNull Context context, 63 @NonNull String cloudProviderAuthority) { 64 return new PickerSearchProviderClient(context, cloudProviderAuthority); 65 } 66 67 /** 68 * Method for querying CloudMediaProvider for media search result. 69 * Note: This functions does not expect pagination args. 70 */ 71 @Nullable fetchSearchResultsFromCmp( @ullable String suggestedMediaSetId, @Nullable String searchText, @SortOrder int sortOrder, @Nullable List<String> mimeTypes, int pageSize, @Nullable String resumePageToken, @Nullable CancellationSignal cancellationSignal)72 public Cursor fetchSearchResultsFromCmp( 73 @Nullable String suggestedMediaSetId, 74 @Nullable String searchText, 75 @SortOrder int sortOrder, 76 @Nullable List<String> mimeTypes, 77 int pageSize, 78 @Nullable String resumePageToken, 79 @Nullable CancellationSignal cancellationSignal) { 80 if (suggestedMediaSetId == null && searchText == null) { 81 throw new IllegalArgumentException( 82 "both suggestedMediaSet and searchText can not be null at once"); 83 } 84 final Bundle queryArgs = new Bundle(); 85 queryArgs.putString(CloudMediaProviderContract.KEY_SEARCH_TEXT, searchText); 86 queryArgs.putString(CloudMediaProviderContract.KEY_MEDIA_SET_ID, suggestedMediaSetId); 87 queryArgs.putInt(CloudMediaProviderContract.EXTRA_PAGE_SIZE, pageSize); 88 queryArgs.putString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN, resumePageToken); 89 queryArgs.putInt(CloudMediaProviderContract.EXTRA_SORT_ORDER, sortOrder); 90 if (mimeTypes != null) { 91 queryArgs.putStringArray( 92 Intent.EXTRA_MIME_TYPES, 93 mimeTypes.toArray(new String[mimeTypes.size()])); 94 } 95 96 Log.d(TAG, "Search results query sent to CMP: " + queryArgs); 97 98 final Cursor cursor = mContext.getContentResolver().query( 99 getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_SEARCH_MEDIA), 100 null, queryArgs, null); 101 102 if (cursor == null) { 103 Log.d(TAG, "Search results response from the CMP is null."); 104 105 } else { 106 Log.d(TAG, "Search results received from the CMP: " + cursor.getCount() 107 + " extras: " + cursor.getExtras()); 108 } 109 return cursor; 110 } 111 112 /** 113 * Method for querying CloudMediaProvider for search suggestions 114 */ 115 @Nullable fetchSearchSuggestionsFromCmp(@onNull String prefixText, int limit, @Nullable CancellationSignal cancellationSignal)116 public Cursor fetchSearchSuggestionsFromCmp(@NonNull String prefixText, 117 int limit, 118 @Nullable CancellationSignal cancellationSignal) { 119 final Bundle queryArgs = new Bundle(); 120 queryArgs.putString(CloudMediaProviderContract.KEY_PREFIX_TEXT, requireNonNull(prefixText)); 121 queryArgs.putInt(CloudMediaProviderContract.EXTRA_PAGE_SIZE, limit); 122 123 Log.d(TAG, "Search suggestions query sent to CMP: " + queryArgs); 124 125 final Cursor cursor = mContext.getContentResolver().query( 126 getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_SEARCH_SUGGESTION), 127 null, queryArgs, null); 128 129 if (cursor == null) { 130 Log.d(TAG, "Search suggestions response from the CMP is null."); 131 132 } else { 133 Log.d(TAG, "Search suggestions received from the CMP: " + cursor.getCount() 134 + " extras: " + cursor.getExtras()); 135 } 136 return cursor; 137 } 138 139 /** 140 * Method for querying CloudMediaProvider for MediaCategories 141 */ 142 @Nullable fetchMediaCategoriesFromCmp( @ullable String parentCategoryId, @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal)143 public Cursor fetchMediaCategoriesFromCmp( 144 @Nullable String parentCategoryId, 145 @Nullable Bundle queryArgs, 146 @Nullable CancellationSignal cancellationSignal) { 147 if (queryArgs == null) { 148 queryArgs = new Bundle(); 149 } 150 queryArgs.putString(CloudMediaProviderContract.KEY_PARENT_CATEGORY_ID, parentCategoryId); 151 152 Log.d(TAG, "Categories query sent to CMP: " + queryArgs); 153 154 final Cursor cursor = mContext.getContentResolver().query( 155 getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_MEDIA_CATEGORY), 156 null, queryArgs, cancellationSignal); 157 158 if (cursor == null) { 159 Log.d(TAG, "Categories response from the CMP is null."); 160 161 } else { 162 Log.d(TAG, "Categories received from the CMP: " + cursor.getCount() 163 + " extras: " + cursor.getExtras()); 164 } 165 return cursor; 166 } 167 168 /** 169 * Method for querying CloudMediaProvider for MediaSets 170 */ 171 @Nullable fetchMediaSetsFromCmp( @onNull String mediaCategoryId, @Nullable String nextPageToken, int pageSize, @Nullable String[] mimeTypes, @Nullable CancellationSignal cancellationSignal)172 public Cursor fetchMediaSetsFromCmp( 173 @NonNull String mediaCategoryId, @Nullable String nextPageToken, int pageSize, 174 @Nullable String[] mimeTypes, @Nullable CancellationSignal cancellationSignal) 175 throws OperationCanceledException { 176 final Bundle queryArgs = new Bundle(); 177 queryArgs.putString(CloudMediaProviderContract.KEY_MEDIA_CATEGORY_ID, 178 requireNonNull(mediaCategoryId)); 179 queryArgs.putString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN, nextPageToken); 180 queryArgs.putInt(CloudMediaProviderContract.EXTRA_PAGE_SIZE, pageSize); 181 queryArgs.putStringArray(Intent.EXTRA_MIME_TYPES, mimeTypes); 182 183 Log.d(TAG, "Media sets query sent to CMP: " + queryArgs); 184 185 final Cursor cursor = mContext.getContentResolver().query( 186 getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_MEDIA_SET), 187 null, queryArgs, cancellationSignal); 188 189 if (cursor == null) { 190 Log.d(TAG, "Media sets response from the CMP is null."); 191 192 } else { 193 Log.d(TAG, "Media sets received from the CMP: " + cursor.getCount() 194 + " extras: " + cursor.getExtras()); 195 } 196 return cursor; 197 } 198 199 /** 200 * Method for querying Medias inside a MediaSet 201 */ 202 @Nullable fetchMediasInMediaSetFromCmp( @onNull String mediaSetId, @Nullable String pageToken, int pageSize, int sortOrder, @Nullable String[] mimeTypes, @Nullable CancellationSignal cancellationSignal)203 public Cursor fetchMediasInMediaSetFromCmp( 204 @NonNull String mediaSetId, 205 @Nullable String pageToken, 206 int pageSize, 207 int sortOrder, 208 @Nullable String[] mimeTypes, 209 @Nullable CancellationSignal cancellationSignal) throws OperationCanceledException { 210 final Bundle queryArgs = new Bundle(); 211 queryArgs.putString(CloudMediaProviderContract.KEY_MEDIA_SET_ID, 212 requireNonNull(mediaSetId)); 213 queryArgs.putInt(CloudMediaProviderContract.EXTRA_PAGE_SIZE, pageSize); 214 queryArgs.putString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN, pageToken); 215 queryArgs.putInt(CloudMediaProviderContract.EXTRA_SORT_ORDER, sortOrder); 216 queryArgs.putStringArray(Intent.EXTRA_MIME_TYPES, mimeTypes); 217 218 Log.d(TAG, "Media set content query sent to CMP: " + queryArgs); 219 220 final Cursor cursor = mContext.getContentResolver().query( 221 getCloudUriFromPath(CloudMediaProviderContract.URI_PATH_MEDIA_IN_MEDIA_SET), 222 null, queryArgs, cancellationSignal); 223 224 if (cursor == null) { 225 Log.d(TAG, "Media set contents response from the CMP is null."); 226 227 } else { 228 Log.d(TAG, "Media set contents received from the CMP: " + cursor.getCount() 229 + " extras: " + cursor.getExtras()); 230 } 231 return cursor; 232 } 233 getCloudUriFromPath(String uriPath)234 private Uri getCloudUriFromPath(String uriPath) { 235 return Uri.parse("content://" + mCloudProviderAuthority + "/" + uriPath); 236 } 237 238 /** 239 * Fetches the {@link android.provider.CloudMediaProviderContract.Capabilities} from the 240 * cloud media provider and returns them. In case there is an issue in fetching the 241 * capabilities, this method returns the default capabilities. 242 */ 243 @NonNull fetchCapabilities()244 public CloudMediaProviderContract.Capabilities fetchCapabilities() { 245 try { 246 final Bundle response = mContext.getContentResolver().call( 247 mCloudProviderAuthority, 248 METHOD_GET_CAPABILITIES, 249 /* arg */ null, 250 /* extras */ null); 251 requireNonNull(response); 252 253 final CloudMediaProviderContract.Capabilities capabilities = 254 response.getParcelable(EXTRA_PROVIDER_CAPABILITIES); 255 requireNonNull(capabilities); 256 Log.d(TAG, "Capabilities received from CMP: " + capabilities); 257 258 return capabilities; 259 } catch (RuntimeException e) { 260 Log.e(TAG, "Could not fetch capabilities from " + mCloudProviderAuthority); 261 262 // Return default capabilities. 263 return new CloudMediaProviderContract.Capabilities.Builder().build(); 264 } 265 } 266 } 267