/* * 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. * *

Note: 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 verificationResult = new ArrayList<>(); ArrayList 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: * *

*/ static void verifyOnGetMediaCollectionInfo( Bundle outputBundle, List verificationResult, List 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: * * */ static void verifyOnQueryMedia( Cursor c, List verificationResult, List 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: * *
    *
  • Received Cursor is not null.
  • *
  • Cursor contains non empty media collection ID: * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}
  • *
  • Logs count of rows in the cursor, if cursor is non null.
  • *
*/ static void verifyOnQueryDeletedMedia( Cursor c, List verificationResult, List errors ) { verifyCursorNotNullAndMediaCollectionIdPresent(c, verificationResult, errors); } /** * Verifies OnQueryAlbums API by performing and logging the following checks: * *
    *
  • Received Cursor is not null.
  • *
  • Cursor contains non empty media collection ID: * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID}
  • *
  • Projection for cursor is as expected: * {@link CloudMediaProviderContract.AlbumColumns#ALL_PROJECTION}
  • *
  • Logs count of rows in the cursor and the album names, if cursor is non null.
  • *
*/ static void verifyOnQueryAlbums( Cursor c, List verificationResult, List 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: * *
    *
  • Received AssetFileDescriptor is not null.
  • *
  • Logs size of the thumbnail.
  • *
*/ static void verifyOnOpenPreview( AssetFileDescriptor assetFileDescriptor, Point expectedSize, List verificationResult, List 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: * *
    *
  • Received ParcelFileDescriptor is not null.
  • *
*/ static void verifyOnOpenMedia( ParcelFileDescriptor fd, List verificationResult, List 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 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 ); }