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