• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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