1 /* 2 * Copyright (C) 2022 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; 18 19 import static android.photopicker.cts.PhotoPickerCloudUtils.addImage; 20 import static android.photopicker.cts.PhotoPickerCloudUtils.containsExcept; 21 import static android.photopicker.cts.PhotoPickerCloudUtils.disableDeviceConfigSync; 22 import static android.photopicker.cts.PhotoPickerCloudUtils.disablePickImagesPreload; 23 import static android.photopicker.cts.PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders; 24 import static android.photopicker.cts.PhotoPickerCloudUtils.enablePickImagesPreload; 25 import static android.photopicker.cts.PhotoPickerCloudUtils.extractMediaIds; 26 import static android.photopicker.cts.PhotoPickerCloudUtils.isPickImagesPreloadEnabled; 27 import static android.photopicker.cts.PickerProviderMediaGenerator.MediaGenerator; 28 import static android.photopicker.cts.PickerProviderMediaGenerator.syncCloudProvider; 29 import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris; 30 import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia; 31 import static android.photopicker.cts.util.ResultsAssertionsUtils.assertRedactedReadOnlyAccess; 32 import static android.provider.MediaStore.PickerMediaColumns; 33 34 import static com.google.common.truth.Truth.assertThat; 35 36 import static org.junit.Assert.assertThrows; 37 38 import android.content.ClipData; 39 import android.content.ContentResolver; 40 import android.content.Intent; 41 import android.database.Cursor; 42 import android.net.Uri; 43 import android.os.Build; 44 import android.os.storage.StorageManager; 45 import android.photopicker.cts.cloudproviders.CloudProviderNoIntentFilter; 46 import android.photopicker.cts.cloudproviders.CloudProviderNoPermission; 47 import android.photopicker.cts.cloudproviders.CloudProviderPrimary; 48 import android.photopicker.cts.cloudproviders.CloudProviderSecondary; 49 import android.provider.MediaStore; 50 import android.util.Pair; 51 52 import androidx.annotation.Nullable; 53 import androidx.test.filters.SdkSuppress; 54 import androidx.test.runner.AndroidJUnit4; 55 56 import org.junit.After; 57 import org.junit.AfterClass; 58 import org.junit.Before; 59 import org.junit.BeforeClass; 60 import org.junit.Test; 61 import org.junit.runner.RunWith; 62 63 import java.io.File; 64 import java.io.IOException; 65 import java.util.ArrayList; 66 import java.util.Collections; 67 import java.util.List; 68 69 70 /** 71 * Photo Picker Device only tests for common flows. 72 */ 73 @RunWith(AndroidJUnit4.class) 74 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S) 75 public class CloudPhotoPickerTest extends PhotoPickerBaseTest { 76 private static final String TAG = CloudPhotoPickerTest.class.getSimpleName(); 77 private final List<Uri> mUriList = new ArrayList<>(); 78 private MediaGenerator mCloudPrimaryMediaGenerator; 79 private MediaGenerator mCloudSecondaryMediaGenerator; 80 81 private static final long IMAGE_SIZE_BYTES = 107684; 82 83 private static final String COLLECTION_1 = "COLLECTION_1"; 84 private static final String COLLECTION_2 = "COLLECTION_2"; 85 86 private static final String CLOUD_ID1 = "CLOUD_ID1"; 87 private static final String CLOUD_ID2 = "CLOUD_ID2"; 88 @Nullable 89 private static DeviceStatePreserver sDeviceStatePreserver; 90 91 @BeforeClass setUpBeforeClass()92 public static void setUpBeforeClass() throws IOException { 93 sDeviceStatePreserver = new DeviceStatePreserver(sDevice); 94 sDeviceStatePreserver.saveCurrentCloudProviderState(); 95 disableDeviceConfigSync(); 96 97 // This is a self-instrumentation test, so both "target" package name and "own" package name 98 // should be the same (android.photopicker.cts). 99 enableCloudMediaAndSetAllowedCloudProviders(sTargetPackageName); 100 } 101 102 @AfterClass tearDownClass()103 public static void tearDownClass() throws Exception { 104 if (sDeviceStatePreserver != null) { 105 sDeviceStatePreserver.restoreCloudProviderState(); 106 } 107 } 108 @Before setUp()109 public void setUp() throws Exception { 110 super.setUp(); 111 112 mCloudPrimaryMediaGenerator = PickerProviderMediaGenerator.getMediaGenerator( 113 mContext, CloudProviderPrimary.AUTHORITY); 114 mCloudSecondaryMediaGenerator = PickerProviderMediaGenerator.getMediaGenerator( 115 mContext, CloudProviderSecondary.AUTHORITY); 116 117 mCloudPrimaryMediaGenerator.resetAll(); 118 mCloudSecondaryMediaGenerator.resetAll(); 119 120 mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_1); 121 mCloudSecondaryMediaGenerator.setMediaCollectionId(COLLECTION_1); 122 123 setCloudProvider(null); 124 } 125 126 @After tearDown()127 public void tearDown() throws Exception { 128 for (Uri uri : mUriList) { 129 deleteMedia(uri, mContext); 130 } 131 if (mActivity != null) { 132 mActivity.finish(); 133 } 134 mUriList.clear(); 135 if (mCloudPrimaryMediaGenerator != null) { 136 setCloudProvider(null); 137 } 138 } 139 140 @Test testCloudOnlySync()141 public void testCloudOnlySync() throws Exception { 142 initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1)); 143 144 final ClipData clipData = launchPickerAndFetchMedia(1); 145 final List<String> mediaIds = extractMediaIds(clipData, 1); 146 147 assertThat(mediaIds).containsExactly(CLOUD_ID1); 148 } 149 150 @Test testCloudPlusLocalSyncWithoutDedupe()151 public void testCloudPlusLocalSyncWithoutDedupe() throws Exception { 152 mUriList.addAll(createImagesAndGetUris(1, mContext.getUserId())); 153 initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1)); 154 155 final ClipData clipData = launchPickerAndFetchMedia(2); 156 final List<String> mediaIds = extractMediaIds(clipData, 2); 157 158 assertThat(mediaIds).containsExactly(CLOUD_ID1, mUriList.get(0).getLastPathSegment()); 159 } 160 161 @Test testCloudPlusLocalSyncWithDedupe()162 public void testCloudPlusLocalSyncWithDedupe() throws Exception { 163 mUriList.addAll(createImagesAndGetUris(1, mContext.getUserId())); 164 initPrimaryCloudProviderWithImage(Pair.create(mUriList.get(0).getLastPathSegment(), 165 CLOUD_ID1)); 166 167 final ClipData clipData = launchPickerAndFetchMedia(1); 168 final List<String> mediaIds = extractMediaIds(clipData, 1); 169 170 containsExcept(mediaIds, mUriList.get(0).getLastPathSegment(), CLOUD_ID1); 171 } 172 173 @Test testDeleteCloudMedia()174 public void testDeleteCloudMedia() throws Exception { 175 initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1), 176 Pair.create(null, CLOUD_ID2)); 177 178 ClipData clipData = launchPickerAndFetchMedia(2); 179 List<String> mediaIds = extractMediaIds(clipData, 2); 180 181 assertThat(mediaIds).containsExactly(CLOUD_ID1, CLOUD_ID2); 182 183 mCloudPrimaryMediaGenerator.deleteMedia(/* localId */ null, CLOUD_ID1, 184 /* trackDeleted */ true); 185 syncCloudProvider(mContext); 186 187 clipData = launchPickerAndFetchMedia(2); 188 mediaIds = extractMediaIds(clipData, 1); 189 190 containsExcept(mediaIds, CLOUD_ID2, CLOUD_ID1); 191 } 192 193 @Test testVersionChange()194 public void testVersionChange() throws Exception { 195 // Save current device config 196 final boolean isPickImagesPreloadEnabled = isPickImagesPreloadEnabled(); 197 disablePickImagesPreload(); 198 199 initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1), 200 Pair.create(null, CLOUD_ID2)); 201 202 ClipData clipData = launchPickerAndFetchMedia(2); 203 List<String> mediaIds = extractMediaIds(clipData, 2); 204 205 assertThat(mediaIds).containsExactly(CLOUD_ID1, CLOUD_ID2); 206 207 mCloudPrimaryMediaGenerator.deleteMedia(/* localId */ null, CLOUD_ID1, 208 /* trackDeleted */ false); 209 syncCloudProvider(mContext); 210 211 clipData = launchPickerAndFetchMedia(2); 212 mediaIds = extractMediaIds(clipData, 2); 213 214 assertThat(mediaIds).containsExactly(CLOUD_ID1, CLOUD_ID2); 215 216 mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_2); 217 syncCloudProvider(mContext); 218 219 clipData = launchPickerAndFetchMedia(2); 220 mediaIds = extractMediaIds(clipData, 1); 221 222 containsExcept(mediaIds, CLOUD_ID2, CLOUD_ID1); 223 224 // Restore device config 225 if (isPickImagesPreloadEnabled) { 226 enablePickImagesPreload(); 227 } 228 } 229 230 @Test testSupportedProviders()231 public void testSupportedProviders() throws Exception { 232 assertThat(MediaStore.isSupportedCloudMediaProviderAuthority(mContext.getContentResolver(), 233 CloudProviderPrimary.AUTHORITY)).isTrue(); 234 assertThat(MediaStore.isSupportedCloudMediaProviderAuthority(mContext.getContentResolver(), 235 CloudProviderSecondary.AUTHORITY)).isTrue(); 236 237 assertThat(MediaStore.isSupportedCloudMediaProviderAuthority(mContext.getContentResolver(), 238 CloudProviderNoPermission.AUTHORITY)).isFalse(); 239 assertThat(MediaStore.isSupportedCloudMediaProviderAuthority(mContext.getContentResolver(), 240 CloudProviderNoIntentFilter.AUTHORITY)).isFalse(); 241 } 242 243 @Test testProviderSwitchSuccess()244 public void testProviderSwitchSuccess() throws Exception { 245 setCloudProvider(CloudProviderPrimary.AUTHORITY); 246 assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(), 247 CloudProviderPrimary.AUTHORITY)).isTrue(); 248 249 addImage(mCloudPrimaryMediaGenerator, /* localId */ null, CLOUD_ID1); 250 addImage(mCloudSecondaryMediaGenerator, /* localId */ null, CLOUD_ID2); 251 252 syncCloudProvider(mContext); 253 254 ClipData clipData = launchPickerAndFetchMedia(2); 255 List<String> mediaIds = extractMediaIds(clipData, 1); 256 257 containsExcept(mediaIds, CLOUD_ID1, CLOUD_ID2); 258 259 setCloudProvider(CloudProviderSecondary.AUTHORITY); 260 assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(), 261 CloudProviderPrimary.AUTHORITY)).isFalse(); 262 263 clipData = launchPickerAndFetchMedia(2); 264 mediaIds = extractMediaIds(clipData, 1); 265 266 containsExcept(mediaIds, CLOUD_ID2, CLOUD_ID1); 267 } 268 269 @Test testProviderSwitchFailure()270 public void testProviderSwitchFailure() throws Exception { 271 setCloudProvider(CloudProviderNoIntentFilter.AUTHORITY); 272 assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(), 273 CloudProviderPrimary.AUTHORITY)).isFalse(); 274 275 setCloudProvider(CloudProviderNoPermission.AUTHORITY); 276 assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(), 277 CloudProviderPrimary.AUTHORITY)).isFalse(); 278 } 279 280 @Test testUriAccessWithValidProjection()281 public void testUriAccessWithValidProjection() throws Exception { 282 initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1)); 283 284 final ClipData clipData = launchPickerAndFetchMedia(1); 285 final List<String> mediaIds = extractMediaIds(clipData, 1); 286 287 assertThat(mediaIds).containsExactly(CLOUD_ID1); 288 289 final ContentResolver resolver = mContext.getContentResolver(); 290 String expectedDisplayName = CLOUD_ID1 + ".jpg"; 291 292 try (Cursor c = resolver.query(clipData.getItemAt(0).getUri(), null, null, null)) { 293 assertThat(c).isNotNull(); 294 assertThat(c.moveToFirst()).isTrue(); 295 296 assertThat(c.getString(c.getColumnIndex(PickerMediaColumns.MIME_TYPE))) 297 .isEqualTo("image/jpeg"); 298 assertThat(c.getString(c.getColumnIndex(PickerMediaColumns.DISPLAY_NAME))) 299 .isEqualTo(expectedDisplayName); 300 assertThat(c.getLong(c.getColumnIndex(PickerMediaColumns.SIZE))) 301 .isEqualTo(IMAGE_SIZE_BYTES); 302 assertThat(c.getLong(c.getColumnIndex(PickerMediaColumns.DURATION_MILLIS))) 303 .isEqualTo(0); 304 assertThat(c.getLong(c.getColumnIndex(PickerMediaColumns.DATE_TAKEN))) 305 .isGreaterThan(0); 306 307 final File file = new File(c.getString(c.getColumnIndex(PickerMediaColumns.DATA))); 308 assertThat(file.getPath().endsWith(expectedDisplayName)).isTrue(); 309 assertThat(file.length()).isEqualTo(IMAGE_SIZE_BYTES); 310 } 311 312 assertRedactedReadOnlyAccess(clipData.getItemAt(0).getUri()); 313 } 314 315 @Test testUriAccessWithInvalidProjection()316 public void testUriAccessWithInvalidProjection() throws Exception { 317 initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1)); 318 319 final ClipData clipData = launchPickerAndFetchMedia(1); 320 final List<String> mediaIds = extractMediaIds(clipData, 1); 321 322 assertThat(mediaIds).containsExactly(CLOUD_ID1); 323 324 final ContentResolver resolver = mContext.getContentResolver(); 325 try (Cursor c = resolver.query( 326 clipData.getItemAt(0).getUri(), 327 new String[] {MediaStore.MediaColumns.RELATIVE_PATH}, null, null)) { 328 assertThat(c).isNotNull(); 329 assertThat(c.moveToFirst()).isTrue(); 330 331 assertThat(c.getString(c.getColumnIndex(MediaStore.MediaColumns.RELATIVE_PATH))) 332 .isEqualTo(null); 333 } 334 } 335 336 @Test testCloudEventNotification()337 public void testCloudEventNotification() throws Exception { 338 // Create a placeholder local image to ensure that the picker UI is never empty. 339 // The PhotoPickerUiUtils#findItemList needs to select an item and it times out if the 340 // Picker UI is empty. 341 mUriList.addAll(createImagesAndGetUris(1, mContext.getUserId())); 342 343 // Cloud provider isn't set 344 assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(), 345 CloudProviderPrimary.AUTHORITY)).isFalse(); 346 addImage(mCloudPrimaryMediaGenerator, /* localId */ null, CLOUD_ID1); 347 348 // Notification fails because the calling cloud provider isn't enabled 349 assertThrows("Unauthorized cloud media notification", SecurityException.class, 350 () -> MediaStore.notifyCloudMediaChangedEvent(mContext.getContentResolver(), 351 CloudProviderPrimary.AUTHORITY, COLLECTION_1)); 352 353 // Sleep because the notification API throttles requests with a 1s delay 354 Thread.sleep(1500); 355 356 ClipData clipData = launchPickerAndFetchMedia(1); 357 List<String> mediaIds = extractMediaIds(clipData, 1); 358 359 assertThat(mediaIds).containsNoneIn(Collections.singletonList(CLOUD_ID1)); 360 361 // Now set the cloud provider and verify that notification succeeds 362 setCloudProvider(CloudProviderPrimary.AUTHORITY); 363 assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(), 364 CloudProviderPrimary.AUTHORITY)).isTrue(); 365 366 MediaStore.notifyCloudMediaChangedEvent(mContext.getContentResolver(), 367 CloudProviderPrimary.AUTHORITY, COLLECTION_1); 368 369 assertThrows("Unauthorized cloud media notification", SecurityException.class, 370 () -> MediaStore.notifyCloudMediaChangedEvent(mContext.getContentResolver(), 371 CloudProviderSecondary.AUTHORITY, COLLECTION_1)); 372 373 // Sleep because the notification API throttles requests with a 1s delay 374 Thread.sleep(1500); 375 376 clipData = launchPickerAndFetchMedia(1); 377 mediaIds = extractMediaIds(clipData, 1); 378 379 assertThat(mediaIds).containsExactly(CLOUD_ID1); 380 } 381 382 @Test 383 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU) testStorageManagerKnowsCloudProvider()384 public void testStorageManagerKnowsCloudProvider() throws Exception { 385 final StorageManager storageManager = mContext.getSystemService(StorageManager.class); 386 387 setCloudProvider(CloudProviderPrimary.AUTHORITY); 388 assertThat(storageManager.getCloudMediaProvider()) 389 .isEqualTo(CloudProviderPrimary.AUTHORITY); 390 391 setCloudProvider(CloudProviderSecondary.AUTHORITY); 392 assertThat(storageManager.getCloudMediaProvider()) 393 .isEqualTo(CloudProviderSecondary.AUTHORITY); 394 395 setCloudProvider(null); 396 assertThat(storageManager.getCloudMediaProvider()).isNull(); 397 } 398 launchPickerAndFetchMedia(int maxCount)399 private ClipData launchPickerAndFetchMedia(int maxCount) throws Exception { 400 final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); 401 intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit()); 402 mActivity.startActivityForResult(intent, REQUEST_CODE); 403 404 return PhotoPickerCloudUtils.fetchPickerMedia(mActivity, sDevice, maxCount); 405 } 406 initPrimaryCloudProviderWithImage(Pair<String, String>.... mediaPairs)407 private void initPrimaryCloudProviderWithImage(Pair<String, String>... mediaPairs) 408 throws Exception { 409 PhotoPickerCloudUtils.initCloudProviderWithImage(mContext, mCloudPrimaryMediaGenerator, 410 CloudProviderPrimary.AUTHORITY, mediaPairs); 411 } 412 } 413