• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.android.providers.media.photopicker;
18 
19 import static android.provider.CloudMediaProviderContract.AlbumColumns;
20 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA;
21 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS;
22 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES;
23 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS;
24 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS;
25 import static android.provider.CloudMediaProviderContract.MediaColumns;
26 import static android.provider.MediaStore.VOLUME_EXTERNAL;
27 
28 import static com.android.providers.media.util.MimeUtils.isImageMimeType;
29 import static com.android.providers.media.util.MimeUtils.isVideoMimeType;
30 
31 import static com.google.common.truth.Truth.assertThat;
32 import static com.google.common.truth.Truth.assertWithMessage;
33 
34 import android.Manifest;
35 import android.app.UiAutomation;
36 import android.content.ContentResolver;
37 import android.content.ContentValues;
38 import android.content.Context;
39 import android.content.res.AssetFileDescriptor;
40 import android.database.Cursor;
41 import android.net.Uri;
42 import android.os.Environment;
43 import android.os.ParcelFileDescriptor;
44 import android.provider.MediaStore;
45 
46 import androidx.test.InstrumentationRegistry;
47 
48 import com.android.providers.media.photopicker.data.ItemsProvider;
49 import com.android.providers.media.photopicker.data.model.Category;
50 import com.android.providers.media.photopicker.data.model.UserId;
51 import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
52 
53 import org.junit.Before;
54 import org.junit.Test;
55 
56 import java.io.File;
57 import java.io.FileOutputStream;
58 import java.io.IOException;
59 import java.util.ArrayList;
60 import java.util.List;
61 
62 public class ItemsProviderTest {
63 
64     /**
65      * To help avoid flaky tests, give ourselves a unique nonce to be used for
66      * all filesystem paths, so that we don't risk conflicting with previous
67      * test runs.
68      */
69     private static final String NONCE = String.valueOf(System.nanoTime());
70     private static final String TAG = "ItemsProviderTest";
71     private static final String VIDEO_FILE_NAME = TAG + "_file_" + NONCE + ".mp4";
72     private static final String IMAGE_FILE_NAME = TAG + "_file_" + NONCE + ".jpg";
73     private static final String HIDDEN_DIR_NAME = TAG + "_hidden_dir_" + NONCE;
74 
75     private ContentResolver mIsolatedResolver;
76     private ItemsProvider mItemsProvider;
77 
78     @Before
setUp()79     public void setUp() {
80         final UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation()
81                 .getUiAutomation();
82 
83         uiAutomation.adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
84                         Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
85                         Manifest.permission.READ_DEVICE_CONFIG,
86                         Manifest.permission.INTERACT_ACROSS_USERS);
87 
88         // Remove sync delay to avoid flaky tests
89         final String setSyncDelayCommand =
90                 "device_config put storage pickerdb.default_sync_delay_ms 0";
91         uiAutomation.executeShellCommand(setSyncDelayCommand);
92 
93         final Context context = InstrumentationRegistry.getTargetContext();
94         final Context isolatedContext
95                 = new IsolatedContext(context, "databases", /*asFuseThread*/ false);
96         mIsolatedResolver = isolatedContext.getContentResolver();
97         mItemsProvider = new ItemsProvider(isolatedContext);
98 
99         // Wait for MediaStore to be Idle to reduce flakes caused by database updates
100         MediaStore.waitForIdle(mIsolatedResolver);
101     }
102 
103     /**
104      * Tests {@link ItemsProvider#getCategories(String, UserId)} to return correct info about
105      * {@link #ALBUM_ID_CAMERA}.
106      */
107     @Test
testGetCategories_camera()108     public void testGetCategories_camera() throws Exception {
109         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
110         assertThat(c.getCount()).isEqualTo(0);
111 
112         // Create 1 image file in Camera dir to test
113         // {@link ItemsProvider#getCategories(String, UserId)}.
114         final File cameraDir = getCameraDir();
115         File imageFile = assertCreateNewImage(cameraDir);
116         try {
117             assertGetCategoriesMatchSingle(ALBUM_ID_CAMERA, /* numberOfItems */ 1);
118         } finally {
119             imageFile.delete();
120         }
121     }
122 
123     /**
124      * Tests {@link ItemsProvider#getCategories(String, UserId)} to return correct info about
125      * {@link #ALBUM_ID_CAMERA}.
126      */
127     @Test
testGetCategories_not_camera()128     public void testGetCategories_not_camera() throws Exception {
129         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
130         assertThat(c.getCount()).isEqualTo(0);
131 
132         // negative test case: image file which should not be returned in Camera category
133         final File picturesDir = getPicturesDir();
134         File nonCameraImageFile = assertCreateNewImage(picturesDir);
135         try {
136             assertGetCategoriesMatchSingle(ALBUM_ID_CAMERA, /* numberOfItems */ 0);
137         } finally {
138             nonCameraImageFile.delete();
139         }
140     }
141 
142     /**
143      * Tests {@link ItemsProvider#getCategories(String, UserId)} to return correct info about
144      * {@link #ALBUM_ID_VIDEOS}.
145      */
146     @Test
testGetCategories_videos()147     public void testGetCategories_videos() throws Exception {
148         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
149         assertThat(c.getCount()).isEqualTo(0);
150 
151         // Create 1 video file in Movies dir to test
152         // {@link ItemsProvider#getCategories(String, UserId)}.
153         final File moviesDir = getMoviesDir();
154         File videoFile = assertCreateNewVideo(moviesDir);
155         try {
156             assertGetCategoriesMatchSingle(ALBUM_ID_VIDEOS, /* numberOfItems */ 1);
157         } finally {
158             videoFile.delete();
159         }
160     }
161 
162     /**
163      * Tests {@link ItemsProvider#getCategories(String, UserId)} to return correct info about
164      * {@link #ALBUM_ID_VIDEOS}.
165      */
166     @Test
testGetCategories_not_videos()167     public void testGetCategories_not_videos() throws Exception {
168         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
169         assertThat(c.getCount()).isEqualTo(0);
170 
171         // negative test case: image file which should not be returned in Videos category
172         final File picturesDir = getPicturesDir();
173         File imageFile = assertCreateNewImage(picturesDir);
174         try {
175             assertGetCategoriesMatchSingle(ALBUM_ID_VIDEOS, /* numberOfItems */ 0);
176         } finally {
177             imageFile.delete();
178         }
179     }
180 
181     /**
182      * Tests {@link ItemsProvider#getCategories(String, UserId)} to return correct info about
183      * {@link #ALBUM_ID_SCREENSHOTS}.
184      */
185     @Test
testGetCategories_screenshots()186     public void testGetCategories_screenshots() throws Exception {
187         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
188         assertThat(c.getCount()).isEqualTo(0);
189 
190         // Create 1 image file in Screenshots dir to test
191         // {@link ItemsProvider#getCategories(String, UserId)}
192         final File screenshotsDir = getScreenshotsDir();
193         File imageFile = assertCreateNewImage(screenshotsDir);
194         // Create 1 image file in Screenshots dir of Downloads dir
195         // {@link ItemsProvider#getCategories(String, UserId)}
196         final File screenshotsDirInDownloadsDir = getScreenshotsDirFromDownloadsDir();
197         File imageFileInScreenshotDirInDownloads =
198                 assertCreateNewImage(screenshotsDirInDownloadsDir);
199         try {
200             assertGetCategoriesMatchMultiple(ALBUM_ID_SCREENSHOTS,
201                     ALBUM_ID_DOWNLOADS, /* numberOfItemsInScreenshots */ 2,
202                                              /* numberOfItemsInDownloads */ 1);
203         } finally {
204             imageFile.delete();
205             imageFileInScreenshotDirInDownloads.delete();
206         }
207     }
208 
209     /**
210      * Tests {@link ItemsProvider#getCategories(String, UserId)} to return correct info about
211      * {@link #ALBUM_ID_SCREENSHOTS}.
212      */
213     @Test
testGetCategories_not_screenshots()214     public void testGetCategories_not_screenshots() throws Exception {
215         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
216         assertThat(c.getCount()).isEqualTo(0);
217 
218         // negative test case: image file which should not be returned in Screenshots category
219         final File cameraDir = getCameraDir();
220         File imageFile = assertCreateNewImage(cameraDir);
221         try {
222             assertGetCategoriesMatchSingle(ALBUM_ID_SCREENSHOTS, /* numberOfItems */ 0);
223         } finally {
224             imageFile.delete();
225         }
226     }
227 
228     /**
229      * Tests {@link ItemsProvider#getCategories(String, UserId)} to return correct info about
230      * {@link AlbumColumns#ALBUM_ID_FAVORITES}.
231      */
232     @Test
testGetCategories_favorites()233     public void testGetCategories_favorites() throws Exception {
234         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
235         assertThat(c.getCount()).isEqualTo(0);
236 
237         // positive test case: image file which should be returned in favorites category
238         final File picturesDir = getPicturesDir();
239         final File imageFile = assertCreateNewImage(picturesDir);
240         setIsFavorite(imageFile);
241         try {
242             assertGetCategoriesMatchSingle(ALBUM_ID_FAVORITES, /* numberOfItems */1);
243         } finally {
244             imageFile.delete();
245         }
246     }
247 
248     /**
249      * Tests {@link ItemsProvider#getCategories(String, UserId)} to return correct info about
250      * {@link AlbumColumns#ALBUM_ID_FAVORITES}.
251      */
252     @Test
testGetCategories_not_favorites()253     public void testGetCategories_not_favorites() throws Exception {
254         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
255         assertThat(c.getCount()).isEqualTo(0);
256 
257         // negative test case: image file which should not be returned in favorites category
258         final File picturesDir = getPicturesDir();
259         final File nonFavImageFile = assertCreateNewImage(picturesDir);
260         try {
261             assertGetCategoriesMatchSingle(ALBUM_ID_FAVORITES, /* numberOfItems */ 0);
262         } finally {
263             nonFavImageFile.delete();
264         }
265     }
266 
267     /**
268      * Tests {@link ItemsProvider#getCategories(String, UserId)} to return correct info about
269      * {@link #ALBUM_ID_DOWNLOADS}.
270      */
271     @Test
testGetCategories_downloads()272     public void testGetCategories_downloads() throws Exception {
273         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
274         assertThat(c.getCount()).isEqualTo(0);
275 
276         // Create 1 image file in Downloads dir to test
277         // {@link ItemsProvider#getCategories(String, UserId)}.
278         final File downloadsDir = getDownloadsDir();
279         final File imageFile = assertCreateNewImage(downloadsDir);
280         try {
281             assertGetCategoriesMatchSingle(ALBUM_ID_DOWNLOADS, /* numberOfItems */ 1);
282         } finally {
283             imageFile.delete();
284         }
285     }
286 
287     /**
288      * Tests {@link ItemsProvider#getCategories(String, UserId)} to return correct info about
289      * {@link #ALBUM_ID_DOWNLOADS}.
290      */
291     @Test
testGetCategories_not_downloads()292     public void testGetCategories_not_downloads() throws Exception {
293         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
294         assertThat(c.getCount()).isEqualTo(0);
295 
296         // negative test case: image file which should not be returned in Downloads category
297         final File picturesDir = getPicturesDir();
298         final File nonDownloadsImageFile = assertCreateNewImage(picturesDir);
299         try {
300             assertGetCategoriesMatchSingle(ALBUM_ID_DOWNLOADS, /* numberOfItems */ 0);
301         } finally {
302             nonDownloadsImageFile.delete();
303         }
304     }
305 
306     /**
307      * Tests {@link ItemsProvider#getCategories(String, UserId)} to return correct info about
308      * {@link #ALBUM_ID_CAMERA} and {@link #ALBUM_ID_VIDEOS}.
309      */
310     @Test
testGetCategories_camera_and_videos()311     public void testGetCategories_camera_and_videos() throws Exception {
312         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
313         assertThat(c.getCount()).isEqualTo(0);
314 
315         // Create 1 video file in Camera dir to test
316         // {@link ItemsProvider#getCategories(String, UserId)}.
317         final File cameraDir = getCameraDir();
318         File videoFile = assertCreateNewVideo(cameraDir);
319         try {
320             assertGetCategoriesMatchMultiple(ALBUM_ID_CAMERA, ALBUM_ID_VIDEOS,
321                     /* numberOfItemsInCamera */ 1,
322                     /* numberOfItemsInVideos */ 1);
323         } finally {
324             videoFile.delete();
325         }
326     }
327 
328     /**
329      * Tests {@link ItemsProvider#getCategories(String, UserId)} to return correct info about
330      * {@link AlbumColumns#ALBUM_ID_SCREENSHOTS} and {@link AlbumColumns#ALBUM_ID_FAVORITES}.
331      */
332     @Test
testGetCategories_screenshots_and_favorites()333     public void testGetCategories_screenshots_and_favorites() throws Exception {
334         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
335         assertThat(c.getCount()).isEqualTo(0);
336 
337         // Create 1 image file in Screenshots dir to test
338         // {@link ItemsProvider#getCategories(String, UserId)}
339         final File screenshotsDir = getScreenshotsDir();
340         File imageFile = assertCreateNewImage(screenshotsDir);
341         setIsFavorite(imageFile);
342         try {
343             assertGetCategoriesMatchMultiple(ALBUM_ID_SCREENSHOTS,
344                     ALBUM_ID_FAVORITES,
345                     /* numberOfItemsInScreenshots */ 1,
346                     /* numberOfItemsInFavorites */ 1);
347         } finally {
348             imageFile.delete();
349         }
350     }
351 
352     /**
353      * Tests {@link ItemsProvider#getCategories(String, UserId)} to return correct info about
354      * {@link AlbumColumns#ALBUM_ID_DOWNLOADS} and {@link AlbumColumns#ALBUM_ID_FAVORITES}.
355      */
356     @Test
testGetCategories_downloads_and_favorites()357     public void testGetCategories_downloads_and_favorites() throws Exception {
358         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
359         assertThat(c.getCount()).isEqualTo(0);
360 
361         // Create 1 image file in Screenshots dir to test
362         // {@link ItemsProvider#getCategories(String, UserId)}
363         final File downloadsDir = getDownloadsDir();
364         File imageFile = assertCreateNewImage(downloadsDir);
365         setIsFavorite(imageFile);
366         try {
367             assertGetCategoriesMatchMultiple(ALBUM_ID_DOWNLOADS,
368                     ALBUM_ID_FAVORITES,
369                     /* numberOfItemsInScreenshots */ 1,
370                     /* numberOfItemsInFavorites */ 1);
371         } finally {
372             imageFile.delete();
373         }
374     }
375 
376     /**
377      * Tests {@link ItemsProvider#getItems(String, int, int, String, UserId)} to return all
378      * images and videos.
379      */
380     @Test
testGetItems()381     public void testGetItems() throws Exception {
382         // Create 1 image and 1 video file to test
383         // {@link ItemsProvider#getItems(String, int, int, String, UserId)}.
384         // Both files should be returned.
385         File imageFile = assertCreateNewImage();
386         File videoFile = assertCreateNewVideo();
387         try {
388             final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
389                     /* limit */ -1, /* mimeType */ null, /* userId */ null);
390             assertThat(res).isNotNull();
391             assertThat(res.getCount()).isEqualTo(2);
392 
393             assertThatOnlyImagesVideos(res);
394             // Reset the cursor back. Cursor#moveToPosition(-1) will reset the position to -1,
395             // but since there is no such valid cursor position, it returns false.
396             assertThat(res.moveToPosition(-1)).isFalse();
397             assertThatAllImagesVideos(res.getCount());
398         } finally {
399             imageFile.delete();
400             videoFile.delete();
401         }
402     }
403 
404     @Test
testGetItems_sortOrder()405     public void testGetItems_sortOrder() throws Exception {
406         try {
407             final long timeNow = System.nanoTime() / 1000;
408             final Uri imageFileDateNowPlus1Uri = prepareFileAndGetUri(
409                     new File(getDownloadsDir(),  "latest_" + IMAGE_FILE_NAME), timeNow + 1000);
410             final Uri imageFileDateNowUri
411                     = prepareFileAndGetUri(new File(getDcimDir(), IMAGE_FILE_NAME), timeNow);
412             final Uri videoFileDateNowUri
413                     = prepareFileAndGetUri(new File(getCameraDir(), VIDEO_FILE_NAME), timeNow);
414 
415             // This is the list of uris based on the expected sort order of items returned by
416             // ItemsProvider#getItems
417             List<Uri> uris = new ArrayList<>();
418             // This is the latest image file
419             uris.add(imageFileDateNowPlus1Uri);
420             // Video file was scanned after image file, hence has higher _id than image file
421             uris.add(videoFileDateNowUri);
422             uris.add(imageFileDateNowUri);
423 
424             try (Cursor cursor = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
425                     /* limit */ -1, /* mimeType */ null, /* userId */ null)) {
426                 assertThat(cursor).isNotNull();
427 
428                 final int expectedCount = uris.size();
429                 assertThat(cursor.getCount()).isEqualTo(expectedCount);
430 
431                 int rowNum = 0;
432                 assertThat(cursor.moveToFirst()).isTrue();
433                 final int idColumnIndex = cursor.getColumnIndexOrThrow(MediaColumns.ID);
434                 while (rowNum < expectedCount) {
435                     assertWithMessage("id at row:" + rowNum + " is expected to be"
436                             + " same as id in " + uris.get(rowNum))
437                             .that(String.valueOf(cursor.getLong(idColumnIndex)))
438                             .isEqualTo(uris.get(rowNum).getLastPathSegment());
439                     cursor.moveToNext();
440                     rowNum++;
441                 }
442             }
443         } finally {
444             deleteAllFilesNoThrow();
445         }
446     }
447 
448     /**
449      * Tests {@link {@link ItemsProvider#getItems(String, int, int, String, UserId)}} does not
450      * return hidden images/videos.
451      */
452     @Test
testGetItems_nonMedia()453     public void testGetItems_nonMedia() throws Exception {
454         // Create 1 image and 1 video file in a hidden dir to test
455         // {@link ItemsProvider#getItems(String, int, int, String, UserId)}.
456         // Both should not be returned.
457         File hiddenDir = createHiddenDir();
458         File imageFileHidden = assertCreateNewImage(hiddenDir);
459         File videoFileHidden = assertCreateNewVideo(hiddenDir);
460         try {
461             final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
462                     /* limit */ -1, /* mimeType */ null, /* userId */ null);
463             assertThat(res).isNotNull();
464             assertThat(res.getCount()).isEqualTo(0);
465         } finally {
466             imageFileHidden.delete();
467             videoFileHidden.delete();
468             hiddenDir.delete();
469         }
470     }
471 
472     /**
473      * Tests {@link ItemsProvider#getItems(String, int, int, String, UserId)} to return all
474      * images and videos based on the mimeType. Image mimeType should only return images.
475      */
476     @Test
testGetItemsImages()477     public void testGetItemsImages() throws Exception {
478         // Create 1 image and 1 video file to test
479         // {@link ItemsProvider#getItems(String, int, int, String, UserId)}.
480         // Only 1 should be returned.
481         File imageFile = assertCreateNewImage();
482         File videoFile = assertCreateNewVideo();
483         try {
484             final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
485                     /* limit */ -1, /* mimeType */ "image/*", /* userId */ null);
486             assertThat(res).isNotNull();
487             assertThat(res.getCount()).isEqualTo(1);
488 
489             assertThatOnlyImages(res);
490             assertThatAllImages(res.getCount());
491         } finally {
492             imageFile.delete();
493             videoFile.delete();
494         }
495     }
496 
497     /**
498      * Tests {@link ItemsProvider#getItems(String, int, int, String, UserId)} to return all
499      * images and videos based on the mimeType. Image mimeType should only return images.
500      */
501     @Test
testGetItemsImages_png()502     public void testGetItemsImages_png() throws Exception {
503         // Create a jpg file image. Tests negative use case, this should not be returned below.
504         File imageFile = assertCreateNewImage();
505         try {
506             final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
507                     /* limit */ -1, /* mimeType */ "image/png", /* userId */ null);
508             assertThat(res).isNotNull();
509             assertThat(res.getCount()).isEqualTo(0);
510         } finally {
511             imageFile.delete();
512         }
513     }
514 
515     /**
516      * Tests {@link ItemsProvider#getItems(String, int, int, String, UserId)} does not return
517      * hidden images/videos.
518      */
519     @Test
testGetItemsImages_nonMedia()520     public void testGetItemsImages_nonMedia() throws Exception {
521         // Create 1 image and 1 video file in a hidden dir to test
522         // {@link ItemsProvider#getItems(String, int, int, String)}.
523         // Both should not be returned.
524         File hiddenDir = createHiddenDir();
525         File imageFileHidden = assertCreateNewImage(hiddenDir);
526         File videoFileHidden = assertCreateNewVideo(hiddenDir);
527         try {
528             final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
529                     /* limit */ -1, /* mimeType */ "image/*", /* userId */ null);
530             assertThat(res).isNotNull();
531             assertThat(res.getCount()).isEqualTo(0);
532         } finally {
533             imageFileHidden.delete();
534             videoFileHidden.delete();
535             hiddenDir.delete();
536         }
537     }
538 
539     /**
540      * Tests {@link ItemsProvider#getItems(String, int, int, String, UserId)} to return all
541      * images and videos based on the mimeType. Video mimeType should only return videos.
542      */
543     @Test
testGetItemsVideos()544     public void testGetItemsVideos() throws Exception {
545         // Create 1 image and 1 video file to test
546         // {@link ItemsProvider#getItems(String, int, int, String)}.
547         // Only 1 should be returned.
548         File imageFile = assertCreateNewImage();
549         File videoFile = assertCreateNewVideo();
550         try {
551             final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
552                     /* limit */ -1, /* mimeType */ "video/*", /* userId */ null);
553             assertThat(res).isNotNull();
554             assertThat(res.getCount()).isEqualTo(1);
555 
556             assertThatOnlyVideos(res);
557             assertThatAllVideos(res.getCount());
558         } finally {
559             imageFile.delete();
560             videoFile.delete();
561         }
562     }
563 
564     /**
565      * Tests {@link ItemsProvider#getItems(String, int, int, String, UserId)} to return all
566      * images and videos based on the mimeType. Image mimeType should only return images.
567      */
568     @Test
testGetItemsVideos_mp4()569     public void testGetItemsVideos_mp4() throws Exception {
570         // Create a mp4 video file. Tests positive use case, this should be returned below.
571         File videoFile = assertCreateNewVideo();
572         try {
573             final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
574                     /* limit */ -1, /* mimeType */ "video/mp4", /* userId */ null);
575             assertThat(res).isNotNull();
576             assertThat(res.getCount()).isEqualTo(1);
577         } finally {
578             videoFile.delete();
579         }
580     }
581 
582     /**
583      * Tests {@link ItemsProvider#getItems(String, int, int, String, UserId)} does not return
584      * hidden images/videos.
585      */
586     @Test
testGetItemsVideos_nonMedia()587     public void testGetItemsVideos_nonMedia() throws Exception {
588         // Create 1 image and 1 video file in a hidden dir to test the API.
589         // Both should not be returned.
590         File hiddenDir = createHiddenDir();
591         File imageFileHidden = assertCreateNewImage(hiddenDir);
592         File videoFileHidden = assertCreateNewVideo(hiddenDir);
593         try {
594             final Cursor res = mItemsProvider.getItems(Category.DEFAULT, /* offset */ 0,
595                     /* limit */ -1, /* mimeType */ "video/*", /* userId */ null);
596             assertThat(res).isNotNull();
597             assertThat(res.getCount()).isEqualTo(0);
598         } finally {
599             imageFileHidden.delete();
600             videoFileHidden.delete();
601             hiddenDir.delete();
602         }
603     }
604 
assertGetCategoriesMatchSingle(String expectedCategoryName, int expectedNumberOfItems)605     private void assertGetCategoriesMatchSingle(String expectedCategoryName,
606             int expectedNumberOfItems) throws Exception {
607         if (expectedNumberOfItems == 0) {
608             assertCategoriesNoMatch(expectedCategoryName);
609             return;
610         }
611 
612         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
613         assertThat(c).isNotNull();
614         assertThat(c.getCount()).isEqualTo(1);
615 
616         // Assert that only expected category is returned and has expectedNumberOfItems items in it
617         assertThat(c.moveToFirst()).isTrue();
618         final int nameColumnIndex = c.getColumnIndexOrThrow(AlbumColumns.DISPLAY_NAME);
619         final int numOfItemsColumnIndex = c.getColumnIndexOrThrow(AlbumColumns.MEDIA_COUNT);
620         final int coverIdIndex = c.getColumnIndexOrThrow(AlbumColumns.MEDIA_COVER_ID);
621 
622         final String categoryName = c.getString(nameColumnIndex);
623         final int numOfItems = c.getInt(numOfItemsColumnIndex);
624         final Uri coverUri = ItemsProvider.getItemsUri(c.getString(coverIdIndex),
625                 PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, UserId.CURRENT_USER);
626 
627         assertThat(categoryName).isEqualTo(expectedCategoryName);
628         assertThat(numOfItems).isEqualTo(expectedNumberOfItems);
629         assertCategoryUriIsValid(coverUri);
630     }
631 
assertCategoryUriIsValid(Uri uri)632     private void assertCategoryUriIsValid(Uri uri) throws Exception {
633         try (AssetFileDescriptor fd1 = mIsolatedResolver.openTypedAssetFile(uri, "image/*",
634                 null, null)) {
635             assertThat(fd1).isNotNull();
636         }
637         try (ParcelFileDescriptor fd2 = mIsolatedResolver.openFileDescriptor(uri, "r")) {
638             assertThat(fd2).isNotNull();
639         }
640     }
641 
assertCategoriesNoMatch(String expectedCategoryName)642     private void assertCategoriesNoMatch(String expectedCategoryName) {
643         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
644         while (c != null && c.moveToNext()) {
645             final int nameColumnIndex = c.getColumnIndexOrThrow(AlbumColumns.DISPLAY_NAME);
646             final String categoryName = c.getString(nameColumnIndex);
647             assertThat(categoryName).isNotEqualTo(expectedCategoryName);
648         }
649     }
650 
assertGetCategoriesMatchMultiple(String category1, String category2, int numberOfItems1, int numberOfItems2)651     private void assertGetCategoriesMatchMultiple(String category1, String category2,
652             int numberOfItems1, int numberOfItems2) {
653         Cursor c = mItemsProvider.getCategories(/* mimeType */ null, /* userId */ null);
654         assertThat(c).isNotNull();
655         assertThat(c.getCount()).isEqualTo(2);
656 
657         // Assert that category1 and category2 is returned and has numberOfItems1 and
658         // numberOfItems2 items in them respectively.
659         boolean isCategory1Returned = false;
660         boolean isCategory2Returned = false;
661         while (c.moveToNext()) {
662             final int nameColumnIndex = c.getColumnIndexOrThrow(AlbumColumns.DISPLAY_NAME);
663             final int numOfItemsColumnIndex = c.getColumnIndexOrThrow(
664                     AlbumColumns.MEDIA_COUNT);
665 
666             final String categoryName = c.getString(nameColumnIndex);
667             final int numOfItems = c.getInt(numOfItemsColumnIndex);
668 
669 
670             if (categoryName.equals(category1)) {
671                 isCategory1Returned = true;
672                 assertThat(numOfItems).isEqualTo(numberOfItems1);
673             } else if (categoryName.equals(category2)) {
674                 isCategory2Returned = true;
675                 assertThat(numOfItems).isEqualTo(numberOfItems2);
676             }
677         }
678 
679         assertThat(isCategory1Returned).isTrue();
680         assertThat(isCategory2Returned).isTrue();
681     }
682 
setIsFavorite(File file)683     private void setIsFavorite(File file) {
684         final Uri uri = MediaStore.scanFile(mIsolatedResolver, file);
685         final ContentValues values = new ContentValues();
686         values.put(MediaStore.MediaColumns.IS_FAVORITE, 1);
687         // Assert that 1 row corresponding to this file is updated.
688         assertThat(mIsolatedResolver.update(uri, values, null)).isEqualTo(1);
689         // Wait for MediaStore to be Idle to reduce flakes caused by database updates
690         MediaStore.waitForIdle(mIsolatedResolver);
691     }
692 
assertThatOnlyImagesVideos(Cursor c)693     private void assertThatOnlyImagesVideos(Cursor c) throws Exception {
694         while (c.moveToNext()) {
695             int mimeTypeColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE);
696             String mimeType = c.getString(mimeTypeColumn);
697             assertThat(isImageMimeType(mimeType) || isVideoMimeType(mimeType)).isTrue();
698         }
699     }
700 
assertThatOnlyImages(Cursor c)701     private void assertThatOnlyImages(Cursor c) throws Exception {
702         while (c.moveToNext()) {
703             int mimeTypeColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE);
704             String mimeType = c.getString(mimeTypeColumn);
705             assertThat(isImageMimeType(mimeType)).isTrue();
706         }
707     }
708 
assertThatOnlyVideos(Cursor c)709     private void assertThatOnlyVideos(Cursor c) throws Exception {
710         while (c.moveToNext()) {
711             int mimeTypeColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE);
712             String mimeType = c.getString(mimeTypeColumn);
713             assertThat(isVideoMimeType(mimeType)).isTrue();
714         }
715     }
716 
assertThatAllImagesVideos(int count)717     private void assertThatAllImagesVideos(int count) {
718         int countOfImages = getCountOfMediaStoreImages();
719         int countOfVideos = getCountOfMediaStoreVideos();
720         assertThat(count).isEqualTo(countOfImages + countOfVideos);
721     }
722 
assertThatAllImages(int count)723     private void assertThatAllImages(int count) {
724         int countOfImages = getCountOfMediaStoreImages();
725         assertThat(count).isEqualTo(countOfImages);
726     }
727 
assertThatAllVideos(int count)728     private void assertThatAllVideos(int count) {
729         int countOfVideos = getCountOfMediaStoreVideos();
730         assertThat(count).isEqualTo(countOfVideos);
731     }
732 
getCountOfMediaStoreImages()733     private int getCountOfMediaStoreImages() {
734         try (Cursor c = mIsolatedResolver.query(
735                 MediaStore.Images.Media.getContentUri(VOLUME_EXTERNAL), null, null, null)) {
736             assertThat(c.moveToFirst()).isTrue();
737             return c.getCount();
738         }
739     }
740 
getCountOfMediaStoreVideos()741     private int getCountOfMediaStoreVideos() {
742         try (Cursor c = mIsolatedResolver.query(
743                 MediaStore.Video.Media.getContentUri(VOLUME_EXTERNAL), null, null, null)) {
744             assertThat(c.moveToFirst()).isTrue();
745             return c.getCount();
746         }
747     }
748 
assertCreateNewVideo(File dir)749     private File assertCreateNewVideo(File dir) throws Exception {
750         return assertCreateNewFile(dir, VIDEO_FILE_NAME);
751     }
752 
assertCreateNewImage(File dir)753     private File assertCreateNewImage(File dir) throws Exception {
754         return assertCreateNewFile(dir, IMAGE_FILE_NAME);
755     }
756 
assertCreateNewVideo()757     private File assertCreateNewVideo() throws Exception {
758         return assertCreateNewFile(getDownloadsDir(), VIDEO_FILE_NAME);
759     }
760 
assertCreateNewImage()761     private File assertCreateNewImage() throws Exception {
762         return assertCreateNewFile(getDownloadsDir(), IMAGE_FILE_NAME);
763     }
764 
assertCreateNewFile(File parentDir, String fileName)765     private File assertCreateNewFile(File parentDir, String fileName) throws Exception {
766         final File file = new File(parentDir, fileName);
767         prepareFileAndGetUri(file, /* lastModifiedTime */ -1);
768 
769         return file;
770     }
771 
prepareFileAndGetUri(File file, long lastModifiedTime)772     private Uri prepareFileAndGetUri(File file, long lastModifiedTime) throws IOException {
773         ensureParentExists(file.getParentFile());
774 
775         assertThat(file.createNewFile()).isTrue();
776 
777         // Write 1 byte because 0byte files are not valid in the picker db
778         try (FileOutputStream fos = new FileOutputStream(file)) {
779             fos.write(1);
780         }
781 
782         if (lastModifiedTime != -1) {
783             file.setLastModified(lastModifiedTime);
784         }
785 
786         final Uri uri = MediaStore.scanFile(mIsolatedResolver, file);
787         assertWithMessage("Uri obtained by scanning file " + file)
788                 .that(uri).isNotNull();
789         // Wait for picker db sync
790         MediaStore.waitForIdle(mIsolatedResolver);
791 
792         return uri;
793     }
794 
ensureParentExists(File parent)795     private void ensureParentExists(File parent) {
796         if (!parent.exists()) {
797             parent.mkdirs();
798         }
799         assertThat(parent.exists()).isTrue();
800     }
801 
getDownloadsDir()802     private File getDownloadsDir() {
803         return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS);
804     }
805 
getDcimDir()806     private File getDcimDir() {
807         return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DCIM);
808     }
809 
getPicturesDir()810     private File getPicturesDir() {
811         return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_PICTURES);
812     }
813 
getMoviesDir()814     private File getMoviesDir() {
815         return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_MOVIES);
816     }
817 
getCameraDir()818     private File getCameraDir() {
819         return new File(getDcimDir(), "Camera");
820     }
821 
getScreenshotsDir()822     private File getScreenshotsDir() {
823         return new File(getPicturesDir(), Environment.DIRECTORY_SCREENSHOTS);
824     }
825 
getScreenshotsDirFromDownloadsDir()826     private File getScreenshotsDirFromDownloadsDir() {
827         return new File(getDownloadsDir(), Environment.DIRECTORY_SCREENSHOTS);
828     }
829 
createHiddenDir()830     private File createHiddenDir() throws Exception {
831         File parentDir = new File(Environment.getExternalStorageDirectory(),
832                 Environment.DIRECTORY_DOWNLOADS);
833         File dir = new File(parentDir, HIDDEN_DIR_NAME);
834         dir.mkdirs();
835         File nomedia = new File(dir, ".nomedia");
836         nomedia.createNewFile();
837 
838         MediaStore.scanFile(mIsolatedResolver, nomedia);
839 
840         return dir;
841     }
842 
deleteAllFilesNoThrow()843     private void deleteAllFilesNoThrow() {
844         try (Cursor c = mIsolatedResolver.query(
845                 MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
846                 new String[] {MediaStore.MediaColumns.DATA}, null, null)) {
847             while(c.moveToNext()) {
848                 (new File(c.getString(
849                         c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)))).delete();
850             }
851         }
852     }
853 }
854