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