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 android.provider; 18 19 import static android.provider.VerificationLogsHelper.createIsNotNullLog; 20 import static android.provider.VerificationLogsHelper.createIsNotValidLog; 21 import static android.provider.VerificationLogsHelper.createIsNullLog; 22 import static android.provider.VerificationLogsHelper.logVerifications; 23 import static android.provider.VerificationLogsHelper.verifyCursorNotNullAndMediaCollectionIdPresent; 24 import static android.provider.VerificationLogsHelper.verifyMediaCollectionId; 25 import static android.provider.VerificationLogsHelper.verifyProjectionForCursor; 26 import static android.provider.VerificationLogsHelper.verifyTotalTimeForExecution; 27 28 import android.annotation.StringDef; 29 import android.content.Intent; 30 import android.content.res.AssetFileDescriptor; 31 import android.database.Cursor; 32 import android.graphics.BitmapFactory; 33 import android.graphics.Point; 34 import android.os.Bundle; 35 import android.os.ParcelFileDescriptor; 36 import android.os.SystemProperties; 37 import android.util.Log; 38 39 import java.lang.annotation.Retention; 40 import java.lang.annotation.RetentionPolicy; 41 import java.util.ArrayList; 42 import java.util.Arrays; 43 import java.util.List; 44 import java.util.Map; 45 46 /** 47 * Provides helper methods that help verify that the received results from cloud provider 48 * implementations are staying true to contract by returning non null outputs and setting required 49 * extras/states in the result. 50 * 51 * Note: logs for local provider and not printed. 52 */ 53 final class CmpApiVerifier { 54 private static final String LOCAL_PROVIDER_AUTHORITY = 55 "com.android.providers.media.photopicker"; 56 isCloudMediaProviderLoggingEnabled()57 private static boolean isCloudMediaProviderLoggingEnabled() { 58 return (SystemProperties.getInt("ro.debuggable", 0) == 1) && Log.isLoggable( 59 "CloudMediaProvider", Log.VERBOSE); 60 } 61 62 /** 63 * Verifies and logs results received by CloudMediaProvider Apis. 64 * 65 * <p><b>Note:</b> It only logs the errors and does not throw any exceptions. 66 */ verifyApiResult(CmpApiResult result, long totalTimeTakenForExecution, String authority)67 static void verifyApiResult(CmpApiResult result, long totalTimeTakenForExecution, 68 String authority) { 69 // Do not perform any operation if the authority is of the local provider or when the 70 // logging is not enabled. 71 if (!LOCAL_PROVIDER_AUTHORITY.equals(authority) 72 && isCloudMediaProviderLoggingEnabled()) { 73 try { 74 ArrayList<String> verificationResult = new ArrayList<>(); 75 ArrayList<String> errors = new ArrayList<>(); 76 verifyTotalTimeForExecution(totalTimeTakenForExecution, 77 CMP_API_TO_THRESHOLD_MAP.get(result.getApi()), errors); 78 79 switch (result.getApi()) { 80 case CloudMediaProviderApis.OnGetMediaCollectionInfo: { 81 verifyOnGetMediaCollectionInfo(result.getBundle(), verificationResult, 82 errors); 83 break; 84 } 85 case CloudMediaProviderApis.OnQueryMedia: { 86 verifyOnQueryMedia(result.getCursor(), verificationResult, errors); 87 break; 88 } 89 case CloudMediaProviderApis.OnQueryDeletedMedia: { 90 verifyOnQueryDeletedMedia(result.getCursor(), verificationResult, errors); 91 break; 92 } 93 case CloudMediaProviderApis.OnQueryAlbums: { 94 verifyOnQueryAlbums(result.getCursor(), verificationResult, errors); 95 break; 96 } 97 case CloudMediaProviderApis.OnOpenPreview: { 98 verifyOnOpenPreview(result.getAssetFileDescriptor(), result.getDimensions(), 99 verificationResult, errors); 100 break; 101 } 102 case CloudMediaProviderApis.OnOpenMedia: { 103 verifyOnOpenMedia(result.getParcelFileDescriptor(), verificationResult, 104 errors); 105 break; 106 } 107 default: 108 throw new UnsupportedOperationException( 109 "The verification for requested API is not supported."); 110 } 111 logVerifications(authority, result.getApi(), totalTimeTakenForExecution, 112 verificationResult, errors); 113 } catch (Exception e) { 114 VerificationLogsHelper.logException(e.getMessage()); 115 } 116 } 117 } 118 119 /** 120 * Verifies OnGetMediaCollectionInfo API by performing and logging the following checks: 121 * 122 * <ul> 123 * <li>Received Bundle is not null.</li> 124 * <li>Bundle contains media collection ID: 125 * {@link CloudMediaProviderContract.MediaCollectionInfo#MEDIA_COLLECTION_ID}</li> 126 * <li>Bundle contains last sync generation: 127 * {@link CloudMediaProviderContract.MediaCollectionInfo#LAST_MEDIA_SYNC_GENERATION}</li> 128 * <li>Bundle contains account name: 129 * {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_NAME}</li> 130 * <li>Bundle contains account configuration intent: 131 * {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_CONFIGURATION_INTENT}</li> 132 * </ul> 133 */ verifyOnGetMediaCollectionInfo( Bundle outputBundle, List<String> verificationResult, List<String> errors )134 static void verifyOnGetMediaCollectionInfo( 135 Bundle outputBundle, List<String> verificationResult, List<String> errors 136 ) { 137 if (outputBundle != null) { 138 verificationResult.add(createIsNotNullLog("Received bundle")); 139 140 String mediaCollectionId = outputBundle.getString( 141 CloudMediaProviderContract.MediaCollectionInfo.MEDIA_COLLECTION_ID 142 ); 143 // verifies media collection id. 144 verifyMediaCollectionId( 145 mediaCollectionId, 146 verificationResult, 147 errors 148 ); 149 150 long syncGeneration = outputBundle.getLong( 151 CloudMediaProviderContract.MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION, 152 -1L 153 ); 154 155 // verified last sync generation. 156 if (syncGeneration != -1L) { 157 if (syncGeneration >= 0) { 158 verificationResult.add( 159 CloudMediaProviderContract.MediaCollectionInfo 160 .LAST_MEDIA_SYNC_GENERATION + " : " + syncGeneration 161 ); 162 } else { 163 errors.add( 164 CloudMediaProviderContract.MediaCollectionInfo 165 .LAST_MEDIA_SYNC_GENERATION + " is < 0" 166 ); 167 } 168 } else { 169 errors.add( 170 createIsNotValidLog( 171 CloudMediaProviderContract.MediaCollectionInfo 172 .LAST_MEDIA_SYNC_GENERATION 173 ) 174 ); 175 } 176 177 String accountName = outputBundle.getString( 178 CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME 179 ); 180 181 // verifies account name. 182 if (accountName != null) { 183 if (!accountName.isEmpty()) { 184 // In future if the cloud media provider is extended to have multiple 185 // accounts then logging account name itself might be a useful 186 // information to log but for now only logging its presence. 187 verificationResult.add( 188 CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME 189 + " is present " 190 ); 191 } else { 192 errors.add( 193 CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME 194 + " is empty" 195 ); 196 } 197 } else { 198 errors.add(createIsNullLog( 199 CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME 200 ) 201 ); 202 } 203 204 Intent intent = outputBundle.getParcelable( 205 CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT 206 ); 207 // verified the presence of account configuration intent. 208 if (intent != null) { 209 verificationResult.add( 210 CloudMediaProviderContract.MediaCollectionInfo 211 .ACCOUNT_CONFIGURATION_INTENT 212 + " is present." 213 ); 214 } else { 215 errors.add(createIsNullLog( 216 CloudMediaProviderContract.MediaCollectionInfo 217 .ACCOUNT_CONFIGURATION_INTENT 218 ) 219 ); 220 } 221 222 } else { 223 errors.add(createIsNullLog("Received output bundle")); 224 } 225 } 226 227 /** 228 * Verifies OnQueryMedia API by performing and logging the following checks: 229 * 230 * <ul> 231 * <li>Received Cursor is not null.</li> 232 * <li>Cursor contains non empty media collection ID: 233 * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li> 234 * <li>Projection for cursor is as expected: 235 * {@link CloudMediaProviderContract.MediaColumns#ALL_PROJECTION}</li> 236 * <li>Logs count of rows in the cursor, if cursor is non null.</li> 237 * </ul> 238 */ verifyOnQueryMedia( Cursor c, List<String> verificationResult, List<String> errors )239 static void verifyOnQueryMedia( 240 Cursor c, List<String> verificationResult, List<String> errors 241 ) { 242 if (c != null) { 243 verifyCursorNotNullAndMediaCollectionIdPresent( 244 c, 245 verificationResult, 246 errors 247 ); 248 // verify that all columns are present per CloudMediaProviderContract.AlbumColumns 249 verifyProjectionForCursor( 250 c, 251 Arrays.asList(CloudMediaProviderContract.MediaColumns.ALL_PROJECTION), 252 errors 253 ); 254 } else { 255 errors.add(createIsNullLog("Received cursor")); 256 } 257 } 258 259 /** 260 * Verifies OnQueryDeletedMedia API by performing and logging the following checks: 261 * 262 * <ul> 263 * <li>Received Cursor is not null.</li> 264 * <li>Cursor contains non empty media collection ID: 265 * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li> 266 * <li>Logs count of rows in the cursor, if cursor is non null.</li> 267 * </ul> 268 */ verifyOnQueryDeletedMedia( Cursor c, List<String> verificationResult, List<String> errors )269 static void verifyOnQueryDeletedMedia( 270 Cursor c, List<String> verificationResult, List<String> errors 271 ) { 272 verifyCursorNotNullAndMediaCollectionIdPresent(c, verificationResult, errors); 273 } 274 275 /** 276 * Verifies OnQueryAlbums API by performing and logging the following checks: 277 * 278 * <ul> 279 * <li>Received Cursor is not null.</li> 280 * <li>Cursor contains non empty media collection ID: 281 * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li> 282 * <li>Projection for cursor is as expected: 283 * {@link CloudMediaProviderContract.AlbumColumns#ALL_PROJECTION}</li> 284 * <li>Logs count of rows in the cursor and the album names, if cursor is non null.</li> 285 * </ul> 286 */ verifyOnQueryAlbums( Cursor c, List<String> verificationResult, List<String> errors )287 static void verifyOnQueryAlbums( 288 Cursor c, List<String> verificationResult, List<String> errors 289 ) { 290 if (c != null) { 291 verifyCursorNotNullAndMediaCollectionIdPresent(c, verificationResult, errors); 292 293 // verify that all columns are present per CloudMediaProviderContract.AlbumColumns 294 verifyProjectionForCursor( 295 c, 296 Arrays.asList(CloudMediaProviderContract.AlbumColumns.ALL_PROJECTION), 297 errors 298 ); 299 if (c.getCount() > 0) { 300 // Only log album data if projection and other checks have returned positive 301 // results. 302 StringBuilder strBuilder = new StringBuilder("Albums present and their count: "); 303 int columnIndexForId = c.getColumnIndex(CloudMediaProviderContract.AlbumColumns.ID); 304 int columnIndexForItemCount = c.getColumnIndex( 305 CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT); 306 c.moveToPosition(-1); 307 while (c.moveToNext()) { 308 strBuilder.append("\n\t\t\t" + c.getString(columnIndexForId) + ", " + c.getLong( 309 columnIndexForItemCount)); 310 } 311 c.moveToPosition(-1); 312 verificationResult.add(strBuilder.toString()); 313 } 314 } 315 } 316 317 318 /** 319 * Verifies OnOpenPreview API by performing and logging the following checks: 320 * 321 * <ul> 322 * <li>Received AssetFileDescriptor is not null.</li> 323 * <li>Logs size of the thumbnail.</li> 324 * </ul> 325 */ verifyOnOpenPreview( AssetFileDescriptor assetFileDescriptor, Point expectedSize, List<String> verificationResult, List<String> errors )326 static void verifyOnOpenPreview( 327 AssetFileDescriptor assetFileDescriptor, 328 Point expectedSize, List<String> verificationResult, List<String> errors 329 ) { 330 if (assetFileDescriptor == null) { 331 errors.add(createIsNullLog("Received AssetFileDescriptor")); 332 } else { 333 verificationResult.add(createIsNotNullLog("Received AssetFileDescriptor")); 334 BitmapFactory.Options options = new BitmapFactory.Options(); 335 options.inJustDecodeBounds = true; // Only decode the bounds 336 BitmapFactory.decodeFileDescriptor(assetFileDescriptor.getFileDescriptor(), null, 337 options); 338 339 int width = options.outWidth; 340 int height = options.outHeight; 341 342 verificationResult.add("Dimensions of file received: " 343 + "Width: " + width + ", Height: " + height + ", expected: " + expectedSize.x 344 + ", " + expectedSize.y); 345 } 346 } 347 348 /** 349 * Verifies OnOpenMedia API by performing and logging the following checks: 350 * 351 * <ul> 352 * <li>Received ParcelFileDescriptor is not null.</li> 353 * </ul> 354 */ verifyOnOpenMedia( ParcelFileDescriptor fd, List<String> verificationResult, List<String> errors )355 static void verifyOnOpenMedia( 356 ParcelFileDescriptor fd, 357 List<String> verificationResult, List<String> errors 358 ) { 359 if (fd == null) { 360 errors.add(createIsNullLog("Received FileDescriptor")); 361 } else { 362 verificationResult.add(createIsNotNullLog("Received FileDescriptor")); 363 } 364 } 365 366 @StringDef({ 367 CloudMediaProviderApis.OnGetMediaCollectionInfo, 368 CloudMediaProviderApis.OnQueryMedia, 369 CloudMediaProviderApis.OnQueryDeletedMedia, 370 CloudMediaProviderApis.OnQueryAlbums, 371 CloudMediaProviderApis.OnOpenPreview, 372 CloudMediaProviderApis.OnOpenMedia 373 }) 374 @Retention(RetentionPolicy.SOURCE) 375 @interface CloudMediaProviderApis { 376 String OnGetMediaCollectionInfo = "onGetMediaCollectionInfo"; 377 String OnQueryMedia = "onQueryMedia"; 378 String OnQueryDeletedMedia = "onQueryDeletedMedia"; 379 String OnQueryAlbums = "onQueryAlbums"; 380 String OnOpenPreview = "onOpenPreview"; 381 String OnOpenMedia = "onOpenMedia"; 382 } 383 384 private static final Map<String, Long> CMP_API_TO_THRESHOLD_MAP = Map.of( 385 CloudMediaProviderApis.OnGetMediaCollectionInfo, 200L, 386 CloudMediaProviderApis.OnQueryMedia, 500L, 387 CloudMediaProviderApis.OnQueryDeletedMedia, 500L, 388 CloudMediaProviderApis.OnQueryAlbums, 500L, 389 CloudMediaProviderApis.OnOpenPreview, 1000L, 390 CloudMediaProviderApis.OnOpenMedia, 1000L 391 ); 392 } 393