/*
* 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:
*
*
* - Received Bundle is not null.
* - Bundle contains media collection ID:
* {@link CloudMediaProviderContract.MediaCollectionInfo#MEDIA_COLLECTION_ID}
* - Bundle contains last sync generation:
* {@link CloudMediaProviderContract.MediaCollectionInfo#LAST_MEDIA_SYNC_GENERATION}
* - Bundle contains account name:
* {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_NAME}
* - Bundle contains account configuration intent:
* {@link CloudMediaProviderContract.MediaCollectionInfo#ACCOUNT_CONFIGURATION_INTENT}
*
*/
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:
*
*
* - 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.MediaColumns#ALL_PROJECTION}
* - Logs count of rows in the cursor, if cursor is non null.
*
*/
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
);
}