| /* |
| * 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 android.provider; |
| |
| import static android.provider.VerificationLogsHelper.createIsNotNullLog; |
| import static android.provider.VerificationLogsHelper.createIsNotValidLog; |
| import static android.provider.VerificationLogsHelper.createIsNullLog; |
| import static android.provider.VerificationLogsHelper.logVerifications; |
| import static android.provider.VerificationLogsHelper.verifyCursorNotNullAndMediaCollectionIdPresent; |
| import static android.provider.VerificationLogsHelper.verifyMediaCollectionId; |
| import static android.provider.VerificationLogsHelper.verifyProjectionForCursor; |
| import static android.provider.VerificationLogsHelper.verifyTotalTimeForExecution; |
| |
| import android.annotation.StringDef; |
| import android.content.Intent; |
| import android.content.res.AssetFileDescriptor; |
| import android.database.Cursor; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Point; |
| import android.os.Bundle; |
| import android.os.ParcelFileDescriptor; |
| import android.os.SystemProperties; |
| import android.util.Log; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Provides helper methods that help verify that the received results from cloud provider |
| * implementations are staying true to contract by returning non null outputs and setting required |
| * extras/states in the result. |
| * |
| * Note: logs for local provider and not printed. |
| */ |
| final class CmpApiVerifier { |
| private static final String LOCAL_PROVIDER_AUTHORITY = |
| "com.android.providers.media.photopicker"; |
| |
| private static boolean isCloudMediaProviderLoggingEnabled() { |
| return (SystemProperties.getInt("ro.debuggable", 0) == 1) && Log.isLoggable( |
| "CloudMediaProvider", Log.VERBOSE); |
| } |
| |
| /** |
| * Verifies and logs results received by CloudMediaProvider Apis. |
| * |
| * <p><b>Note:</b> It only logs the errors and does not throw any exceptions. |
| */ |
| static void verifyApiResult(CmpApiResult result, long totalTimeTakenForExecution, |
| String authority) { |
| // Do not perform any operation if the authority is of the local provider or when the |
| // logging is not enabled. |
| if (!LOCAL_PROVIDER_AUTHORITY.equals(authority) |
| && isCloudMediaProviderLoggingEnabled()) { |
| try { |
| ArrayList<String> verificationResult = new ArrayList<>(); |
| ArrayList<String> errors = new ArrayList<>(); |
| verifyTotalTimeForExecution(totalTimeTakenForExecution, |
| CMP_API_TO_THRESHOLD_MAP.get(result.getApi()), errors); |
| |
| switch (result.getApi()) { |
| case CloudMediaProviderApis.OnGetMediaCollectionInfo: { |
| verifyOnGetMediaCollectionInfo(result.getBundle(), verificationResult, |
| errors); |
| break; |
| } |
| case CloudMediaProviderApis.OnQueryMedia: { |
| verifyOnQueryMedia(result.getCursor(), verificationResult, errors); |
| break; |
| } |
| case CloudMediaProviderApis.OnQueryDeletedMedia: { |
| verifyOnQueryDeletedMedia(result.getCursor(), verificationResult, errors); |
| break; |
| } |
| case CloudMediaProviderApis.OnQueryAlbums: { |
| verifyOnQueryAlbums(result.getCursor(), verificationResult, errors); |
| break; |
| } |
| case CloudMediaProviderApis.OnOpenPreview: { |
| verifyOnOpenPreview(result.getAssetFileDescriptor(), result.getDimensions(), |
| verificationResult, errors); |
| break; |
| } |
| case CloudMediaProviderApis.OnOpenMedia: { |
| verifyOnOpenMedia(result.getParcelFileDescriptor(), verificationResult, |
| errors); |
| break; |
| } |
| default: |
| throw new UnsupportedOperationException( |
| "The verification for requested API is not supported."); |
| } |
| logVerifications(authority, result.getApi(), totalTimeTakenForExecution, |
| verificationResult, errors); |
| } catch (Exception e) { |
| VerificationLogsHelper.logException(e.getMessage()); |
| } |
| } |
| } |
| |
| /** |
| * Verifies OnGetMediaCollectionInfo API by performing and logging the following checks: |
| * |
| * <ul> |
| * <li>Received Bundle is not null.</li> |
| * <li>Bundle contains media collection ID: |
| * {@link CloudMediaProviderContract.MediaCollectionInfo#MEDIA_COLLECTION_ID}</li> |
| * <li>Bundle contains last sync generation: |
| * {@link CloudMediaProviderContract.MediaCollectionInfo#LAST_MEDIA_SYNC_GENERATION}</li> |
| * <li>Bundle contains account name: |
| * {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_NAME}</li> |
| * <li>Bundle contains account configuration intent: |
| * {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_CONFIGURATION_INTENT}</li> |
| * </ul> |
| */ |
| static void verifyOnGetMediaCollectionInfo( |
| Bundle outputBundle, List<String> verificationResult, List<String> errors |
| ) { |
| if (outputBundle != null) { |
| verificationResult.add(createIsNotNullLog("Received bundle")); |
| |
| String mediaCollectionId = outputBundle.getString( |
| CloudMediaProviderContract.MediaCollectionInfo.MEDIA_COLLECTION_ID |
| ); |
| // verifies media collection id. |
| verifyMediaCollectionId( |
| mediaCollectionId, |
| verificationResult, |
| errors |
| ); |
| |
| long syncGeneration = outputBundle.getLong( |
| CloudMediaProviderContract.MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION, |
| -1L |
| ); |
| |
| // verified last sync generation. |
| if (syncGeneration != -1L) { |
| if (syncGeneration >= 0) { |
| verificationResult.add( |
| CloudMediaProviderContract.MediaCollectionInfo |
| .LAST_MEDIA_SYNC_GENERATION + " : " + syncGeneration |
| ); |
| } else { |
| errors.add( |
| CloudMediaProviderContract.MediaCollectionInfo |
| .LAST_MEDIA_SYNC_GENERATION + " is < 0" |
| ); |
| } |
| } else { |
| errors.add( |
| createIsNotValidLog( |
| CloudMediaProviderContract.MediaCollectionInfo |
| .LAST_MEDIA_SYNC_GENERATION |
| ) |
| ); |
| } |
| |
| String accountName = outputBundle.getString( |
| CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME |
| ); |
| |
| // verifies account name. |
| if (accountName != null) { |
| if (!accountName.isEmpty()) { |
| // In future if the cloud media provider is extended to have multiple |
| // accounts then logging account name itself might be a useful |
| // information to log but for now only logging its presence. |
| verificationResult.add( |
| CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME |
| + " is present " |
| ); |
| } else { |
| errors.add( |
| CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME |
| + " is empty" |
| ); |
| } |
| } else { |
| errors.add(createIsNullLog( |
| CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME |
| ) |
| ); |
| } |
| |
| Intent intent = outputBundle.getParcelable( |
| CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT |
| ); |
| // verified the presence of account configuration intent. |
| if (intent != null) { |
| verificationResult.add( |
| CloudMediaProviderContract.MediaCollectionInfo |
| .ACCOUNT_CONFIGURATION_INTENT |
| + " is present." |
| ); |
| } else { |
| errors.add(createIsNullLog( |
| CloudMediaProviderContract.MediaCollectionInfo |
| .ACCOUNT_CONFIGURATION_INTENT |
| ) |
| ); |
| } |
| |
| } else { |
| errors.add(createIsNullLog("Received output bundle")); |
| } |
| } |
| |
| /** |
| * Verifies OnQueryMedia API by performing and logging the following checks: |
| * |
| * <ul> |
| * <li>Received Cursor is not null.</li> |
| * <li>Cursor contains non empty media collection ID: |
| * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li> |
| * <li>Projection for cursor is as expected: |
| * {@link CloudMediaProviderContract.MediaColumns#ALL_PROJECTION}</li> |
| * <li>Logs count of rows in the cursor, if cursor is non null.</li> |
| * </ul> |
| */ |
| static void verifyOnQueryMedia( |
| Cursor c, List<String> verificationResult, List<String> errors |
| ) { |
| if (c != null) { |
| verifyCursorNotNullAndMediaCollectionIdPresent( |
| c, |
| verificationResult, |
| errors |
| ); |
| // verify that all columns are present per CloudMediaProviderContract.AlbumColumns |
| verifyProjectionForCursor( |
| c, |
| Arrays.asList(CloudMediaProviderContract.MediaColumns.ALL_PROJECTION), |
| errors |
| ); |
| } else { |
| errors.add(createIsNullLog("Received cursor")); |
| } |
| } |
| |
| /** |
| * Verifies OnQueryDeletedMedia API by performing and logging the following checks: |
| * |
| * <ul> |
| * <li>Received Cursor is not null.</li> |
| * <li>Cursor contains non empty media collection ID: |
| * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li> |
| * <li>Logs count of rows in the cursor, if cursor is non null.</li> |
| * </ul> |
| */ |
| static void verifyOnQueryDeletedMedia( |
| Cursor c, List<String> verificationResult, List<String> errors |
| ) { |
| verifyCursorNotNullAndMediaCollectionIdPresent(c, verificationResult, errors); |
| } |
| |
| /** |
| * Verifies OnQueryAlbums API by performing and logging the following checks: |
| * |
| * <ul> |
| * <li>Received Cursor is not null.</li> |
| * <li>Cursor contains non empty media collection ID: |
| * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}</li> |
| * <li>Projection for cursor is as expected: |
| * {@link CloudMediaProviderContract.AlbumColumns#ALL_PROJECTION}</li> |
| * <li>Logs count of rows in the cursor and the album names, if cursor is non null.</li> |
| * </ul> |
| */ |
| static void verifyOnQueryAlbums( |
| Cursor c, List<String> verificationResult, List<String> errors |
| ) { |
| if (c != null) { |
| verifyCursorNotNullAndMediaCollectionIdPresent(c, verificationResult, errors); |
| |
| // verify that all columns are present per CloudMediaProviderContract.AlbumColumns |
| verifyProjectionForCursor( |
| c, |
| Arrays.asList(CloudMediaProviderContract.AlbumColumns.ALL_PROJECTION), |
| errors |
| ); |
| if (c.getCount() > 0) { |
| // Only log album data if projection and other checks have returned positive |
| // results. |
| StringBuilder strBuilder = new StringBuilder("Albums present and their count: "); |
| int columnIndexForId = c.getColumnIndex(CloudMediaProviderContract.AlbumColumns.ID); |
| int columnIndexForItemCount = c.getColumnIndex( |
| CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT); |
| c.moveToPosition(-1); |
| while (c.moveToNext()) { |
| strBuilder.append("\n\t\t\t" + c.getString(columnIndexForId) + ", " + c.getLong( |
| columnIndexForItemCount)); |
| } |
| c.moveToPosition(-1); |
| verificationResult.add(strBuilder.toString()); |
| } |
| } |
| } |
| |
| |
| /** |
| * Verifies OnOpenPreview API by performing and logging the following checks: |
| * |
| * <ul> |
| * <li>Received AssetFileDescriptor is not null.</li> |
| * <li>Logs size of the thumbnail.</li> |
| * </ul> |
| */ |
| static void verifyOnOpenPreview( |
| AssetFileDescriptor assetFileDescriptor, |
| Point expectedSize, List<String> verificationResult, List<String> errors |
| ) { |
| if (assetFileDescriptor == null) { |
| errors.add(createIsNullLog("Received AssetFileDescriptor")); |
| } else { |
| verificationResult.add(createIsNotNullLog("Received AssetFileDescriptor")); |
| BitmapFactory.Options options = new BitmapFactory.Options(); |
| options.inJustDecodeBounds = true; // Only decode the bounds |
| BitmapFactory.decodeFileDescriptor(assetFileDescriptor.getFileDescriptor(), null, |
| options); |
| |
| int width = options.outWidth; |
| int height = options.outHeight; |
| |
| verificationResult.add("Dimensions of file received: " |
| + "Width: " + width + ", Height: " + height + ", expected: " + expectedSize.x |
| + ", " + expectedSize.y); |
| } |
| } |
| |
| /** |
| * Verifies OnOpenMedia API by performing and logging the following checks: |
| * |
| * <ul> |
| * <li>Received ParcelFileDescriptor is not null.</li> |
| * </ul> |
| */ |
| static void verifyOnOpenMedia( |
| ParcelFileDescriptor fd, |
| List<String> verificationResult, List<String> errors |
| ) { |
| if (fd == null) { |
| errors.add(createIsNullLog("Received FileDescriptor")); |
| } else { |
| verificationResult.add(createIsNotNullLog("Received FileDescriptor")); |
| } |
| } |
| |
| @StringDef({ |
| CloudMediaProviderApis.OnGetMediaCollectionInfo, |
| CloudMediaProviderApis.OnQueryMedia, |
| CloudMediaProviderApis.OnQueryDeletedMedia, |
| CloudMediaProviderApis.OnQueryAlbums, |
| CloudMediaProviderApis.OnOpenPreview, |
| CloudMediaProviderApis.OnOpenMedia |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| @interface CloudMediaProviderApis { |
| String OnGetMediaCollectionInfo = "onGetMediaCollectionInfo"; |
| String OnQueryMedia = "onQueryMedia"; |
| String OnQueryDeletedMedia = "onQueryDeletedMedia"; |
| String OnQueryAlbums = "onQueryAlbums"; |
| String OnOpenPreview = "onOpenPreview"; |
| String OnOpenMedia = "onOpenMedia"; |
| } |
| |
| private static final Map<String, Long> CMP_API_TO_THRESHOLD_MAP = Map.of( |
| CloudMediaProviderApis.OnGetMediaCollectionInfo, 200L, |
| CloudMediaProviderApis.OnQueryMedia, 500L, |
| CloudMediaProviderApis.OnQueryDeletedMedia, 500L, |
| CloudMediaProviderApis.OnQueryAlbums, 500L, |
| CloudMediaProviderApis.OnOpenPreview, 1000L, |
| CloudMediaProviderApis.OnOpenMedia, 1000L |
| ); |
| } |