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; 18 19 import static com.android.providers.media.PickerUriResolver.getPickerSegmentFromIntentAction; 20 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_CLOUD_ID; 21 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_DATE_TAKEN_MS; 22 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_DURATION_MS; 23 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_ID; 24 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_LOCAL_ID; 25 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_MIME_TYPE; 26 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_SIZE_BYTES; 27 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION; 28 29 import static java.util.Objects.requireNonNull; 30 31 import android.provider.CloudMediaProviderContract; 32 import android.provider.MediaStore; 33 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 37 import com.android.providers.media.photopicker.v2.model.MediaSource; 38 39 import java.util.Arrays; 40 41 /** 42 * Helper class that keeps track of Picker related Constants. 43 */ 44 public class PickerSQLConstants { 45 /** 46 * An enum that holds the table names in Picker DB 47 */ 48 enum Table { 49 MEDIA, 50 ALBUM_MEDIA, 51 } 52 53 /** 54 * An enum that holds the columns names for the Available Providers query response. 55 */ 56 enum AvailableProviderResponse { 57 AUTHORITY("authority"), 58 MEDIA_SOURCE("media_source"), 59 UID("uid"); 60 61 private final String mColumnName; 62 AvailableProviderResponse(String columnName)63 AvailableProviderResponse(String columnName) { 64 this.mColumnName = columnName; 65 } 66 getColumnName()67 public String getColumnName() { 68 return mColumnName; 69 } 70 } 71 72 /** 73 * An enum that holds the DB columns names and projections for the Album SQL query response. 74 */ 75 public enum AlbumResponse { 76 ALBUM_ID(CloudMediaProviderContract.AlbumColumns.ID), 77 PICKER_ID("picker_id"), 78 AUTHORITY("authority"), 79 DATE_TAKEN(CloudMediaProviderContract.AlbumColumns.DATE_TAKEN_MILLIS), 80 ALBUM_NAME(CloudMediaProviderContract.AlbumColumns.DISPLAY_NAME), 81 UNWRAPPED_COVER_URI("unwrapped_cover_uri"), 82 COVER_MEDIA_SOURCE("media_source"); 83 84 private final String mColumnName; 85 AlbumResponse(@onNull String columnName)86 AlbumResponse(@NonNull String columnName) { 87 requireNonNull(columnName); 88 this.mColumnName = columnName; 89 } 90 getColumnName()91 public String getColumnName() { 92 return mColumnName; 93 } 94 } 95 96 /** 97 * @param columnName Input album column name. 98 * @return Corresponding enum AlbumResponse to the given column name. 99 * @throws IllegalArgumentException if the column name does not correspond to a AlbumResponse 100 * enum. 101 */ mapColumnNameToAlbumResponseColumn(String columnName)102 public static AlbumResponse mapColumnNameToAlbumResponseColumn(String columnName) 103 throws IllegalArgumentException { 104 for (AlbumResponse albumResponseColumn : AlbumResponse.values()) { 105 if (albumResponseColumn.getColumnName().equalsIgnoreCase(columnName)) { 106 return albumResponseColumn; 107 } 108 } 109 throw new IllegalArgumentException(columnName + " does not exist. Available data: " 110 + Arrays.toString(PickerSQLConstants.AlbumResponse.values())); 111 } 112 113 /** 114 * An enum that holds the DB columns names and projections for the Media SQL query response. 115 */ 116 enum MediaResponse { 117 MEDIA_ID(CloudMediaProviderContract.MediaColumns.ID), 118 AUTHORITY(CloudMediaProviderContract.MediaColumns.AUTHORITY), 119 MEDIA_SOURCE("media_source"), 120 WRAPPED_URI("wrapped_uri"), 121 UNWRAPPED_URI("unwrapped_uri"), 122 PICKER_ID(KEY_ID, "picker_id"), 123 DATE_TAKEN_MS(KEY_DATE_TAKEN_MS, CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS), 124 SIZE_IN_BYTES(KEY_SIZE_BYTES, CloudMediaProviderContract.MediaColumns.SIZE_BYTES), 125 MIME_TYPE(KEY_MIME_TYPE, CloudMediaProviderContract.MediaColumns.MIME_TYPE), 126 STANDARD_MIME_TYPE(KEY_STANDARD_MIME_TYPE_EXTENSION, 127 CloudMediaProviderContract.MediaColumns.STANDARD_MIME_TYPE_EXTENSION), 128 DURATION_MS(KEY_DURATION_MS, CloudMediaProviderContract.MediaColumns.DURATION_MILLIS); 129 130 private static final String DEFAULT_PROJECTION = "%s AS %s"; 131 @Nullable 132 private final String mColumnName; 133 @NonNull 134 private final String mProjectedName; 135 MediaResponse(@onNull String dbColumnName, @NonNull String projectedName)136 MediaResponse(@NonNull String dbColumnName, @NonNull String projectedName) { 137 this.mColumnName = dbColumnName; 138 this.mProjectedName = projectedName; 139 } 140 MediaResponse(@onNull String projectedName)141 MediaResponse(@NonNull String projectedName) { 142 this.mColumnName = null; 143 this.mProjectedName = projectedName; 144 } 145 146 @Nullable getColumnName()147 public String getColumnName() { 148 return mColumnName; 149 } 150 151 @NonNull getProjectedName()152 public String getProjectedName() { 153 return mProjectedName; 154 } 155 156 @NonNull getProjection( @ullable String localAuthority, @Nullable String cloudAuthority, @NonNull String intentAction )157 public String getProjection( 158 @Nullable String localAuthority, 159 @Nullable String cloudAuthority, 160 @NonNull String intentAction 161 ) { 162 switch (this) { 163 case WRAPPED_URI: 164 return String.format( 165 DEFAULT_PROJECTION, 166 getWrappedUri(localAuthority, cloudAuthority, intentAction), 167 mProjectedName 168 ); 169 default: 170 return getProjection(localAuthority, cloudAuthority); 171 } 172 } 173 174 @NonNull getProjection( @ullable String localAuthority, @Nullable String cloudAuthority )175 public String getProjection( 176 @Nullable String localAuthority, 177 @Nullable String cloudAuthority 178 ) { 179 switch (this) { 180 case AUTHORITY: 181 return String.format( 182 DEFAULT_PROJECTION, 183 getAuthority(localAuthority, cloudAuthority), 184 mProjectedName 185 ); 186 case UNWRAPPED_URI: 187 return String.format( 188 DEFAULT_PROJECTION, 189 getUnwrappedUri(localAuthority, cloudAuthority), 190 mProjectedName 191 ); 192 default: 193 return getProjection(); 194 } 195 } 196 197 @NonNull getProjection()198 public String getProjection() { 199 switch (this) { 200 case MEDIA_ID: 201 return String.format( 202 DEFAULT_PROJECTION, 203 getMediaId(), 204 mProjectedName 205 ); 206 case MEDIA_SOURCE: 207 return String.format( 208 DEFAULT_PROJECTION, 209 getMediaSource(), 210 mProjectedName 211 ); 212 default: 213 if (mColumnName == null) { 214 throw new IllegalArgumentException( 215 "Could not get projection for " + this.name() 216 ); 217 } 218 return String.format(DEFAULT_PROJECTION, mColumnName, mProjectedName); 219 } 220 } 221 getMediaId()222 private String getMediaId() { 223 return String.format( 224 "IFNULL(%s, %s)", 225 KEY_CLOUD_ID, 226 KEY_LOCAL_ID 227 ); 228 } 229 getMediaSource()230 private String getMediaSource() { 231 return String.format( 232 "CASE WHEN %s IS NULL THEN '%s' ELSE '%s' END", 233 KEY_CLOUD_ID, 234 MediaSource.LOCAL, 235 MediaSource.REMOTE 236 ); 237 } 238 getAuthority( @ullable String localAuthority, @Nullable String cloudAuthority )239 private String getAuthority( 240 @Nullable String localAuthority, 241 @Nullable String cloudAuthority 242 ) { 243 return String.format( 244 "CASE WHEN %s IS NULL THEN '%s' ELSE '%s' END", 245 KEY_CLOUD_ID, 246 localAuthority, 247 cloudAuthority 248 ); 249 } 250 getWrappedUri( @ullable String localAuthority, @Nullable String cloudAuthority, @NonNull String intentAction )251 private String getWrappedUri( 252 @Nullable String localAuthority, 253 @Nullable String cloudAuthority, 254 @NonNull String intentAction 255 ) { 256 // The format is: 257 // content://media/picker/<user-id>/<cloud-provider-authority>/media/<media-id> 258 return String.format( 259 "'content://%s/%s/%s/' || %s || '/media/' || %s", 260 MediaStore.AUTHORITY, 261 getPickerSegmentFromIntentAction(intentAction), 262 MediaStore.MY_USER_ID, 263 getAuthority(localAuthority, cloudAuthority), 264 getMediaId() 265 ); 266 } 267 getUnwrappedUri( @ullable String localAuthority, @Nullable String cloudAuthority )268 private String getUnwrappedUri( 269 @Nullable String localAuthority, 270 @Nullable String cloudAuthority 271 ) { 272 // The format is: 273 // content://<cloud-provider-authority>/media/<media-id> 274 return String.format( 275 "'content://%s@' || %s || '/media/' || %s", 276 MediaStore.MY_USER_ID, 277 getAuthority(localAuthority, cloudAuthority), 278 getMediaId() 279 ); 280 } 281 } 282 283 enum MediaResponseExtras { 284 PREV_PAGE_ID("prev_page_picker_id"), 285 PREV_PAGE_DATE_TAKEN("prev_page_date_taken"), 286 NEXT_PAGE_ID("next_page_picker_id"), 287 NEXT_PAGE_DATE_TAKEN("next_page_date_taken"); 288 289 private final String mKey; 290 MediaResponseExtras(String key)291 MediaResponseExtras(String key) { 292 mKey = key; 293 } 294 getKey()295 public String getKey() { 296 return mKey; 297 } 298 } 299 } 300