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