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