1 /*
2  * Copyright (C) 2021 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.photopicker.cts.util;
18 
19 import static android.provider.MediaStore.PickerMediaColumns;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 import static com.google.common.truth.Truth.assertWithMessage;
23 
24 import static org.junit.Assert.fail;
25 
26 import android.content.ContentResolver;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.UriPermission;
30 import android.database.Cursor;
31 import android.media.ExifInterface;
32 import android.net.Uri;
33 import android.os.FileUtils;
34 import android.os.ParcelFileDescriptor;
35 import android.provider.MediaStore;
36 
37 import androidx.annotation.NonNull;
38 import androidx.test.InstrumentationRegistry;
39 
40 import java.io.ByteArrayOutputStream;
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileNotFoundException;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.List;
49 import java.util.Map;
50 
51 /**
52  * Photo Picker Utility methods for PhotoPicker result assertions.
53  */
54 public class ResultsAssertionsUtils {
55     private static final String TAG = "PhotoPickerTestAssertions";
56 
assertPickerUriFormat(String action, Uri uri, int expectedUserId)57     public static void assertPickerUriFormat(String action, Uri uri, int expectedUserId) {
58         // content://media/picker/<user-id>/<media-id>
59         final int userId = Integer.parseInt(uri.getPathSegments().get(1));
60         assertThat(userId).isEqualTo(expectedUserId);
61 
62         final String auth = uri.getPathSegments().get(0);
63         if (action.equalsIgnoreCase(MediaStore.ACTION_PICK_IMAGES)) {
64             assertThat(auth).isEqualTo("picker");
65         } else {
66             assertThat(auth).contains("picker");
67         }
68     }
69 
assertPersistedGrant(Uri uri, ContentResolver resolver)70     public static void assertPersistedGrant(Uri uri, ContentResolver resolver) {
71         resolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
72 
73         final List<UriPermission> uriPermissions = resolver.getPersistedUriPermissions();
74         final List<Uri> uris = new ArrayList<>();
75         for (UriPermission perm : uriPermissions) {
76             if (perm.isReadPermission()) {
77                 uris.add(perm.getUri());
78             }
79         }
80 
81         assertThat(uris).contains(uri);
82     }
83 
assertMimeType(Uri uri, String expectedMimeType)84     public static void assertMimeType(Uri uri, String expectedMimeType) throws Exception {
85         final Context context = InstrumentationRegistry.getTargetContext();
86         final String resultMimeType = context.getContentResolver().getType(uri);
87         assertThat(resultMimeType).isEqualTo(expectedMimeType);
88     }
89 
assertContainsMimeType(Uri uri, String[] expectedMimeTypes)90     public static void assertContainsMimeType(Uri uri, String[] expectedMimeTypes) {
91         final Context context = InstrumentationRegistry.getTargetContext();
92         final String resultMimeType = context.getContentResolver().getType(uri);
93         assertThat(Arrays.asList(expectedMimeTypes).contains(resultMimeType)).isTrue();
94     }
95 
assertRedactedReadOnlyAccess(Uri uri)96     public static void assertRedactedReadOnlyAccess(Uri uri) throws Exception {
97         assertThat(uri).isNotNull();
98         final String[] projection = new String[]{ PickerMediaColumns.MIME_TYPE };
99         final Context context = InstrumentationRegistry.getTargetContext();
100         final ContentResolver resolver = context.getContentResolver();
101         try (Cursor c = resolver.query(uri, projection, null, null)) {
102             assertThat(c).isNotNull();
103             assertThat(c.moveToFirst()).isTrue();
104 
105             final String mimeType = c.getString(c.getColumnIndex(PickerMediaColumns.MIME_TYPE));
106 
107             if (mimeType.startsWith("image")) {
108                 assertImageRedactedReadOnlyAccess(uri, resolver);
109             } else if (mimeType.startsWith("video")) {
110                 assertVideoRedactedReadOnlyAccess(uri, resolver);
111             } else {
112                 fail("The mime type is not as expected: " + mimeType);
113             }
114         }
115     }
116 
assertExtension(@onNull Uri uri, @NonNull Map<String, String> mimeTypeToExpectedExtensionMap)117     public static void assertExtension(@NonNull Uri uri,
118             @NonNull Map<String, String> mimeTypeToExpectedExtensionMap) {
119         assertThat(uri).isNotNull();
120 
121         final ContentResolver resolver =
122                 InstrumentationRegistry.getTargetContext().getContentResolver();
123         final String[] projection =
124                 new String[]{ PickerMediaColumns.MIME_TYPE, PickerMediaColumns.DISPLAY_NAME };
125 
126         try (Cursor c = resolver.query(
127                 uri, projection, /* queryArgs */ null, /* cancellationSignal */ null)) {
128             assertThat(c).isNotNull();
129             assertThat(c.moveToFirst()).isTrue();
130 
131             final String mimeType = c.getString(c.getColumnIndex(PickerMediaColumns.MIME_TYPE));
132             final String expectedExtension = mimeTypeToExpectedExtensionMap.get(mimeType);
133 
134             final String displayName =
135                     c.getString(c.getColumnIndex(PickerMediaColumns.DISPLAY_NAME));
136             final String[] displayNameParts = displayName.split("\\.");
137             final String resultExtension = displayNameParts[displayNameParts.length - 1];
138 
139             assertWithMessage("Unexpected picker file extension")
140                     .that(resultExtension)
141                     .isEqualTo(expectedExtension);
142         }
143     }
144 
assertVideoRedactedReadOnlyAccess(Uri uri, ContentResolver resolver)145     private static void assertVideoRedactedReadOnlyAccess(Uri uri, ContentResolver resolver)
146             throws Exception {
147         // The location is redacted
148         // TODO(b/201505595): Make this method work for test_video.mp4. Currently it works only for
149         //  test_video_mj2.mp4
150         try (InputStream in = resolver.openInputStream(uri);
151                 ByteArrayOutputStream out = new ByteArrayOutputStream()) {
152             FileUtils.copy(in, out);
153             byte[] bytes = out.toByteArray();
154             byte[] xmpBytes = Arrays.copyOfRange(bytes, 3269, 3269 + 13197);
155             String xmp = new String(xmpBytes);
156             assertWithMessage("Failed to redact XMP longitude")
157                     .that(xmp.contains("10,41.751000E")).isFalse();
158             assertWithMessage("Failed to redact XMP latitude")
159                     .that(xmp.contains("53,50.070500N")).isFalse();
160             assertWithMessage("Redacted non-location XMP")
161                     .that(xmp.contains("13166/7763")).isTrue();
162         }
163 
164         assertNoWriteAccess(uri, resolver);
165     }
166 
assertImageRedactedReadOnlyAccess(Uri uri, ContentResolver resolver)167     private static void assertImageRedactedReadOnlyAccess(Uri uri, ContentResolver resolver)
168             throws Exception {
169         // Assert URI access
170         // The location is redacted
171         try (InputStream is = resolver.openInputStream(uri)) {
172             assertImageExifRedacted(is);
173         }
174 
175         // Assert no write access
176         try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "w")) {
177             fail("Does not grant write access to uri " + uri.toString());
178         } catch (SecurityException | FileNotFoundException expected) {
179         }
180 
181         // Assert file path access
182         try (Cursor c = resolver.query(uri, null, null, null)) {
183             assertThat(c).isNotNull();
184             assertThat(c.moveToFirst()).isTrue();
185 
186             File file = new File(c.getString(c.getColumnIndex(PickerMediaColumns.DATA)));
187 
188             // The location is redacted
189             try (InputStream is = new FileInputStream(file)) {
190                 assertImageExifRedacted(is);
191             }
192 
193             assertNoWriteAccess(uri, resolver);
194         }
195     }
196 
assertImageExifRedacted(InputStream is)197     private static void assertImageExifRedacted(InputStream is) throws IOException {
198         final ExifInterface exif = new ExifInterface(is);
199         final float[] latLong = new float[2];
200         exif.getLatLong(latLong);
201         assertWithMessage("Failed to redact latitude")
202                 .that(latLong[0]).isWithin(0.001f).of(0);
203         assertWithMessage("Failed to redact longitude")
204                 .that(latLong[1]).isWithin(0.001f).of(0);
205 
206         String xmp = exif.getAttribute(ExifInterface.TAG_XMP);
207         assertWithMessage("Failed to redact XMP longitude")
208                 .that(xmp.contains("10,41.751000E")).isFalse();
209         assertWithMessage("Failed to redact XMP latitude")
210                 .that(xmp.contains("53,50.070500N")).isFalse();
211         assertWithMessage("Redacted non-location XMP")
212                 .that(xmp.contains("LensDefaults")).isTrue();
213     }
214 
assertReadOnlyAccess(Uri uri, ContentResolver resolver)215     public static void assertReadOnlyAccess(Uri uri, ContentResolver resolver) throws Exception {
216         try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r")) {
217             assertThat(pfd).isNotNull();
218         }
219 
220         assertNoWriteAccess(uri, resolver);
221     }
222 
assertNoWriteAccess(Uri uri, ContentResolver resolver)223     private static void assertNoWriteAccess(Uri uri, ContentResolver resolver) throws Exception {
224         try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "w")) {
225             fail("Does not grant write access to uri " + uri.toString());
226         } catch (SecurityException | FileNotFoundException expected) {
227         }
228     }
229 }
230