blob: 6d8c725ccce9a62392d94ddb6d7ef6b8d1b0707d [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.providers.media.photopicker;
import static android.provider.CloudMediaProviderContract.AlbumColumns;
import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA;
import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS;
import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES;
import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS;
import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS;
import static android.provider.CloudMediaProviderContract.MediaColumns;
import static android.provider.MediaStore.VOLUME_EXTERNAL;
import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
import static com.android.providers.media.util.MimeUtils.isImageMimeType;
import static com.android.providers.media.util.MimeUtils.isVideoMimeType;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import android.Manifest;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.CloudMediaProviderContract;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Pair;
import androidx.test.InstrumentationRegistry;
import com.android.providers.media.IsolatedContext;
import com.android.providers.media.PickerProviderMediaGenerator;
import com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
import com.android.providers.media.TestConfigStore;
import com.android.providers.media.cloudproviders.CloudProviderPrimary;
import com.android.providers.media.photopicker.data.ItemsProvider;
import com.android.providers.media.photopicker.data.model.Category;
import com.android.providers.media.photopicker.data.model.UserId;
import com.google.common.io.ByteStreams;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class ItemsProviderTest {
/**
* To help avoid flaky tests, give ourselves a unique nonce to be used for
* all filesystem paths, so that we don't risk conflicting with previous
* test runs.
*/
private static final String NONCE = String.valueOf(System.nanoTime());
private static final String TAG = "ItemsProviderTest";
private static final String VIDEO_FILE_NAME = TAG + "_file_" + NONCE + ".mp4";
private static final String IMAGE_FILE_NAME = TAG + "_file_" + NONCE + ".jpg";
private static final String HIDDEN_DIR_NAME = TAG + "_hidden_dir_" + NONCE;
private static final Instrumentation sInstrumentation =
InstrumentationRegistry.getInstrumentation();
private static final Context sTargetContext = sInstrumentation.getTargetContext();
// We are in a self-instrumentation test - MediaProviderTests - so "target" package name and
// "own" package are the same: com.android.providers.media.tests.
private static final String sTargetPackageName = sTargetContext.getPackageName();
private ContentResolver mIsolatedResolver;
private ItemsProvider mItemsProvider;
private TestConfigStore mConfigStore;
@Before
public void setUp() throws Exception {
final UiAutomation uiAutomation = sInstrumentation.getUiAutomation();
uiAutomation.adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
Manifest.permission.READ_DEVICE_CONFIG,
Manifest.permission.INTERACT_ACROSS_USERS);
mConfigStore = new TestConfigStore();
// Remove sync delay to avoid flaky tests
mConfigStore.setPickerSyncDelayMs(0);
final Context isolatedContext = new IsolatedContext(sTargetContext, /* tag */ "databases",
/* asFuseThread */ false, sTargetContext.getUser(), mConfigStore);
mIsolatedResolver = isolatedContext.getContentResolver();
mItemsProvider = new ItemsProvider(isolatedContext);
// Wait for MediaStore to be Idle to reduce flakes caused by database updates
MediaStore.waitForIdle(mIsolatedResolver);
}
@After
public void tearDown() throws Exception {
setCloudProvider(null);
}
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
* about {@link #ALBUM_ID_CAMERA}.
*/
@Test
public void testGetCategories_camera() throws Exception {
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c.getCount()).isEqualTo(0);
// Create 1 image file in Camera dir to test
final File cameraDir = getCameraDir();
File imageFile = assertCreateNewImage(cameraDir);
try {
assertGetCategoriesMatchSingle(ALBUM_ID_CAMERA, /* numberOfItems */ 1);
} finally {
imageFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
* about {@link #ALBUM_ID_CAMERA}.
*/
@Test
public void testGetCategories_not_camera() throws Exception {
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c.getCount()).isEqualTo(0);
// negative test case: image file which should not be returned in Camera category
final File picturesDir = getPicturesDir();
File nonCameraImageFile = assertCreateNewImage(picturesDir);
try {
assertGetCategoriesMatchSingle(ALBUM_ID_CAMERA, /* numberOfItems */ 0);
} finally {
nonCameraImageFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
* about {@link #ALBUM_ID_VIDEOS}.
*/
@Test
public void testGetCategories_videos() throws Exception {
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c.getCount()).isEqualTo(0);
// Create 1 video file in Movies dir to test
final File moviesDir = getMoviesDir();
File videoFile = assertCreateNewVideo(moviesDir);
try {
assertGetCategoriesMatchSingle(ALBUM_ID_VIDEOS, /* numberOfItems */ 1);
} finally {
videoFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
* about {@link #ALBUM_ID_VIDEOS}.
*/
@Test
public void testGetCategories_not_videos() throws Exception {
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c.getCount()).isEqualTo(0);
// negative test case: image file which should not be returned in Videos category
final File picturesDir = getPicturesDir();
File imageFile = assertCreateNewImage(picturesDir);
try {
assertGetCategoriesMatchSingle(ALBUM_ID_VIDEOS, /* numberOfItems */ 0);
} finally {
imageFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
* about {@link #ALBUM_ID_SCREENSHOTS}.
*/
@Test
public void testGetCategories_screenshots() throws Exception {
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c.getCount()).isEqualTo(0);
// Create 1 image file in Screenshots dir to test
final File screenshotsDir = getScreenshotsDir();
File imageFile = assertCreateNewImage(screenshotsDir);
// Create 1 image file in Screenshots dir of Downloads dir
final File screenshotsDirInDownloadsDir = getScreenshotsDirFromDownloadsDir();
File imageFileInScreenshotDirInDownloads =
assertCreateNewImage(screenshotsDirInDownloadsDir);
// Add a top level /Screenshots directory and add a test image inside of it.
createTestScreenshotImages();
// This file should not be included since it's not a valid screenshot directory, even though
// it looks like one.
final File myAlbumScreenshotsDir =
new File(getPicturesDir(), "MyAlbum" + Environment.DIRECTORY_SCREENSHOTS);
final File myAlbumScreenshotsImg = assertCreateNewImage(myAlbumScreenshotsDir);
try {
assertGetCategoriesMatchMultiple(Arrays.asList(
Pair.create(ALBUM_ID_SCREENSHOTS, 3),
Pair.create(ALBUM_ID_DOWNLOADS, 1)
));
} finally {
imageFile.delete();
imageFileInScreenshotDirInDownloads.delete();
myAlbumScreenshotsImg.delete();
myAlbumScreenshotsDir.delete();
deleteTopLevelScreenshotDir();
}
}
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
* about {@link #ALBUM_ID_SCREENSHOTS}.
*/
@Test
public void testGetCategories_not_screenshots() throws Exception {
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c.getCount()).isEqualTo(0);
// negative test case: image file which should not be returned in Screenshots category
final File cameraDir = getCameraDir();
File imageFile = assertCreateNewImage(cameraDir);
try {
assertGetCategoriesMatchSingle(ALBUM_ID_SCREENSHOTS, /* numberOfItems */ 0);
} finally {
imageFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
* about {@link AlbumColumns#ALBUM_ID_FAVORITES}.
*/
@Test
public void testGetCategories_favorites() throws Exception {
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c.getCount()).isEqualTo(0);
// positive test case: image file which should be returned in favorites category
final File picturesDir = getPicturesDir();
final File imageFile = assertCreateNewImage(picturesDir);
setIsFavorite(imageFile);
try {
assertGetCategoriesMatchSingle(ALBUM_ID_FAVORITES, /* numberOfItems */1);
} finally {
imageFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
* about {@link AlbumColumns#ALBUM_ID_FAVORITES}.
*/
@Test
public void testGetCategories_not_favorites() throws Exception {
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c.getCount()).isEqualTo(0);
// negative test case: image file which should not be returned in favorites category
final File picturesDir = getPicturesDir();
final File nonFavImageFile = assertCreateNewImage(picturesDir);
try {
assertGetCategoriesMatchSingle(ALBUM_ID_FAVORITES, /* numberOfItems */ 0);
} finally {
nonFavImageFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
* about {@link #ALBUM_ID_DOWNLOADS}.
*/
@Test
public void testGetCategories_downloads() throws Exception {
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c.getCount()).isEqualTo(0);
// Create 1 image file in Downloads dir to test
final File downloadsDir = getDownloadsDir();
final File imageFile = assertCreateNewImage(downloadsDir);
try {
assertGetCategoriesMatchSingle(ALBUM_ID_DOWNLOADS, /* numberOfItems */ 1);
} finally {
imageFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
* about {@link #ALBUM_ID_DOWNLOADS}.
*/
@Test
public void testGetCategories_not_downloads() throws Exception {
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c.getCount()).isEqualTo(0);
// negative test case: image file which should not be returned in Downloads category
final File picturesDir = getPicturesDir();
final File nonDownloadsImageFile = assertCreateNewImage(picturesDir);
try {
assertGetCategoriesMatchSingle(ALBUM_ID_DOWNLOADS, /* numberOfItems */ 0);
} finally {
nonDownloadsImageFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
* about {@link #ALBUM_ID_VIDEOS}.
*/
@Test
public void testGetCategories_camera_and_videos() throws Exception {
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c.getCount()).isEqualTo(0);
// Create 1 video file in Camera dir to test
final File cameraDir = getCameraDir();
File videoFile = assertCreateNewVideo(cameraDir);
try {
assertGetCategoriesMatchMultiple(Arrays.asList(
Pair.create(ALBUM_ID_VIDEOS, 1),
Pair.create(ALBUM_ID_CAMERA, 1)
));
} finally {
videoFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
* about {@link AlbumColumns#ALBUM_ID_FAVORITES}.
*/
@Test
public void testGetCategories_screenshots_and_favorites() throws Exception {
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c.getCount()).isEqualTo(0);
// Create 1 image file in Screenshots dir to test
final File screenshotsDir = getScreenshotsDir();
File imageFile = assertCreateNewImage(screenshotsDir);
setIsFavorite(imageFile);
try {
assertGetCategoriesMatchMultiple(Arrays.asList(
Pair.create(ALBUM_ID_FAVORITES, 1),
Pair.create(ALBUM_ID_SCREENSHOTS, 1)
));
} finally {
imageFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
* about {@link AlbumColumns#ALBUM_ID_DOWNLOADS} and {@link AlbumColumns#ALBUM_ID_FAVORITES}.
*/
@Test
public void testGetCategories_downloads_and_favorites() throws Exception {
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c.getCount()).isEqualTo(0);
// Create 1 image file in Screenshots dir to test
final File downloadsDir = getDownloadsDir();
File imageFile = assertCreateNewImage(downloadsDir);
setIsFavorite(imageFile);
try {
assertGetCategoriesMatchMultiple(Arrays.asList(
Pair.create(ALBUM_ID_FAVORITES, 1),
Pair.create(ALBUM_ID_DOWNLOADS, 1)
));
} finally {
imageFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all
* images and videos.
*/
@Test
public void testGetItems() throws Exception {
// Create 1 image and 1 video file to test
// Both files should be returned.
File imageFile = assertCreateNewImage();
File videoFile = assertCreateNewVideo();
try {
final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
/* mimeType */ null, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(2);
assertThatOnlyImagesVideos(res);
// Reset the cursor back. Cursor#moveToPosition(-1) will reset the position to -1,
// but since there is no such valid cursor position, it returns false.
assertThat(res.moveToPosition(-1)).isFalse();
assertThatAllImagesVideos(res.getCount());
} finally {
imageFile.delete();
videoFile.delete();
}
}
@Test
public void testGetItems_sortOrder() throws Exception {
try {
final long timeNow = System.nanoTime() / 1000;
final Uri imageFileDateNowPlus1Uri = prepareFileAndGetUri(
new File(getDownloadsDir(), "latest_" + IMAGE_FILE_NAME), timeNow + 1000);
final Uri imageFileDateNowUri
= prepareFileAndGetUri(new File(getDcimDir(), IMAGE_FILE_NAME), timeNow);
final Uri videoFileDateNowUri
= prepareFileAndGetUri(new File(getCameraDir(), VIDEO_FILE_NAME), timeNow);
// This is the list of uris based on the expected sort order of items returned by
// ItemsProvider#getAllItems
List<Uri> uris = new ArrayList<>();
// This is the latest image file
uris.add(imageFileDateNowPlus1Uri);
// Video file was scanned after image file, hence has higher _id than image file
uris.add(videoFileDateNowUri);
uris.add(imageFileDateNowUri);
try (Cursor cursor = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
/* mimeType */ null, /* userId */ null)) {
assertThat(cursor).isNotNull();
final int expectedCount = uris.size();
assertThat(cursor.getCount()).isEqualTo(expectedCount);
int rowNum = 0;
assertThat(cursor.moveToFirst()).isTrue();
final int idColumnIndex = cursor.getColumnIndexOrThrow(MediaColumns.ID);
while (rowNum < expectedCount) {
assertWithMessage("id at row:" + rowNum + " is expected to be"
+ " same as id in " + uris.get(rowNum))
.that(String.valueOf(cursor.getLong(idColumnIndex)))
.isEqualTo(uris.get(rowNum).getLastPathSegment());
cursor.moveToNext();
rowNum++;
}
}
} finally {
deleteAllFilesNoThrow();
}
}
/**
* Tests {@link {@link ItemsProvider#getAllItems(Category, int, String[], UserId)}} does not
* return hidden images/videos.
*/
@Test
public void testGetItems_nonMedia() throws Exception {
// Create 1 image and 1 video file in a hidden dir to test
// Both should not be returned.
File hiddenDir = createHiddenDir();
File imageFileHidden = assertCreateNewImage(hiddenDir);
File videoFileHidden = assertCreateNewVideo(hiddenDir);
try {
final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
/* mimeType */ null, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(0);
} finally {
imageFileHidden.delete();
videoFileHidden.delete();
hiddenDir.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all
* images and videos based on the mimeType. Image mimeType should only return images.
*/
@Test
public void testGetItemsImages() throws Exception {
// Create 1 image and 1 video file to test
// Only 1 should be returned.
File imageFile = assertCreateNewImage();
File videoFile = assertCreateNewVideo();
try {
final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
/* mimeType */ new String[]{ "image/*"}, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(1);
assertThatOnlyImages(res);
assertThatAllImages(res.getCount());
} finally {
imageFile.delete();
videoFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all
* images and videos based on the mimeType. Image mimeType should only return images.
*/
@Test
public void testGetItemsImages_png() throws Exception {
// Create a jpg file image. Tests negative use case, this should not be returned below.
File imageFile = assertCreateNewImage();
try {
final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
/* mimeType */ new String[]{"image/png"}, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(0);
} finally {
imageFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} does not return
* hidden images/videos.
*/
@Test
public void testGetItemsImages_nonMedia() throws Exception {
// Create 1 image and 1 video file in a hidden dir to test
// Both should not be returned.
File hiddenDir = createHiddenDir();
File imageFileHidden = assertCreateNewImage(hiddenDir);
File videoFileHidden = assertCreateNewVideo(hiddenDir);
try {
final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
/* mimeType */ new String[]{"image/*"}, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(0);
} finally {
imageFileHidden.delete();
videoFileHidden.delete();
hiddenDir.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all
* images and videos based on the mimeType. Video mimeType should only return videos.
*/
@Test
public void testGetItemsVideos() throws Exception {
// Create 1 image and 1 video file to test
// Only 1 should be returned.
File imageFile = assertCreateNewImage();
File videoFile = assertCreateNewVideo();
try {
final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
/* mimeType */ new String[]{"video/*"}, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(1);
assertThatOnlyVideos(res);
assertThatAllVideos(res.getCount());
} finally {
imageFile.delete();
videoFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all
* images and videos based on the mimeType. Image mimeType should only return images.
*/
@Test
public void testGetItemsVideos_mp4() throws Exception {
// Create a mp4 video file. Tests positive use case, this should be returned below.
File videoFile = assertCreateNewVideo();
try {
final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
/* mimeType */ new String[]{"video/mp4"}, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(1);
} finally {
videoFile.delete();
}
}
/**
* Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} does not return
* hidden images/videos.
*/
@Test
public void testGetItemsVideos_nonMedia() throws Exception {
// Create 1 image and 1 video file in a hidden dir to test the API.
// Both should not be returned.
File hiddenDir = createHiddenDir();
File imageFileHidden = assertCreateNewImage(hiddenDir);
File videoFileHidden = assertCreateNewVideo(hiddenDir);
try {
final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
/* mimeType */ new String[]{"video/*"}, /* userId */ null);
assertThat(res).isNotNull();
assertThat(res.getCount()).isEqualTo(0);
} finally {
imageFileHidden.delete();
videoFileHidden.delete();
hiddenDir.delete();
}
}
/**
* Tests {@link ItemsProvider#getLocalItems(Category, int, String[], UserId)} to returns only
* local content.
*/
@Test
public void testGetLocalItems_withCloud() throws Exception {
File videoFile = assertCreateNewVideo();
try {
mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(
sTargetPackageName);
// Init cloud provider and add one item
setupCloudProvider((cloudMediaGenerator) -> {
cloudMediaGenerator.addMedia(null, "cloud_id1");
});
// Verify that getLocalItems includes only local contents
try (Cursor c = mItemsProvider.getLocalItems(Category.DEFAULT, -1, new String[]{},
UserId.CURRENT_USER)) {
assertThat(c.getCount()).isEqualTo(1);
assertThat(c.moveToFirst()).isTrue();
assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY)))
.isEqualTo(LOCAL_PICKER_PROVIDER_AUTHORITY);
}
// Verify that getAllItems includes cloud items
try (Cursor c = mItemsProvider.getAllItems(Category.DEFAULT, -1, new String[]{},
UserId.CURRENT_USER)) {
assertThat(c.getCount()).isEqualTo(2);
// Verify that the first item is cloud item
assertThat(c.moveToFirst()).isTrue();
assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY)))
.isEqualTo(CloudProviderPrimary.AUTHORITY);
assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.ID))).isEqualTo(
"cloud_id1");
// Verify that the second item is local item
assertThat(c.moveToNext()).isTrue();
assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY)))
.isEqualTo(LOCAL_PICKER_PROVIDER_AUTHORITY);
}
} finally {
videoFile.delete();
setCloudProvider(null);
mConfigStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature();
}
}
@Test
public void testGetLocalItems_mergedAlbum_withCloud() throws Exception {
File videoFile = assertCreateNewVideo();
Category videoAlbum = new Category(CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS,
LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 10, true);
try {
mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(
sTargetPackageName);
// Init cloud provider and add one item
setupCloudProvider((cloudMediaGenerator) -> {
cloudMediaGenerator.addMedia(null, "cloud_id1", null, "video/mp4", 0, 1024, false);
});
// Verify that getLocalItems for merged album "Video" includes only local contents
try (Cursor c = mItemsProvider.getLocalItems(videoAlbum, -1, new String[]{},
UserId.CURRENT_USER)) {
assertThat(c.getCount()).isEqualTo(1);
assertThat(c.moveToFirst()).isTrue();
assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY)))
.isEqualTo(LOCAL_PICKER_PROVIDER_AUTHORITY);
}
// Verify that getAllItems for merged album "Video" also includes cloud contents
try (Cursor c = mItemsProvider.getAllItems(videoAlbum, -1, new String[]{},
UserId.CURRENT_USER)) {
assertThat(c.getCount()).isEqualTo(2);
// Verify that the first item is cloud item
assertThat(c.moveToFirst()).isTrue();
assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY)))
.isEqualTo(CloudProviderPrimary.AUTHORITY);
assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.ID)))
.isEqualTo("cloud_id1");
// Verify that the second item is local item
assertThat(c.moveToNext()).isTrue();
assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY)))
.isEqualTo(LOCAL_PICKER_PROVIDER_AUTHORITY);
}
} finally {
videoFile.delete();
setCloudProvider(null);
mConfigStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature();
}
}
@Test
public void testGetLocalCategories_withCloud() throws Exception {
File videoFile = assertCreateNewVideo(getMoviesDir());
File screenshotFile = assertCreateNewImage(getScreenshotsDir());
final String cloudAlbum = "testAlbum";
try {
mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(
sTargetPackageName);
// Init cloud provider with 2 items and one cloud album
setupCloudProvider((cloudMediaGenerator) -> {
cloudMediaGenerator.addMedia(null, "cloud_id1", null, "video/mp4", 0, 1024, false);
cloudMediaGenerator.addMedia(null, "cloud_id2", cloudAlbum, "image/jpeg", 0, 1024,
false);
cloudMediaGenerator.createAlbum(cloudAlbum);
});
// Verify that getLocalCategories only returns local albums
try (Cursor c = mItemsProvider.getLocalCategories(/* mimeType */ null,
/* userId */ null)) {
assertGetCategoriesMatchMultiple(c, Arrays.asList(
Pair.create(ALBUM_ID_VIDEOS, 1),
Pair.create(ALBUM_ID_SCREENSHOTS, 1)
));
}
// Verify that getAllCategories returns local + cloud albums
try (Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null,
/* userId */ null)) {
assertGetCategoriesMatchMultiple(c, Arrays.asList(
Pair.create(ALBUM_ID_VIDEOS, 2),
Pair.create(ALBUM_ID_SCREENSHOTS, 1),
Pair.create(cloudAlbum, 1)
));
}
} finally {
videoFile.delete();
screenshotFile.delete();
setCloudProvider(null);
mConfigStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature();
}
}
private void setupCloudProvider(Consumer<MediaGenerator> initMedia) {
MediaGenerator cloudPrimaryMediaGenerator =
PickerProviderMediaGenerator.getMediaGenerator(CloudProviderPrimary.AUTHORITY);
cloudPrimaryMediaGenerator.resetAll();
cloudPrimaryMediaGenerator.setMediaCollectionId("COLLECTION_1");
initMedia.accept(cloudPrimaryMediaGenerator);
// Set the newly initialized cloud provider.
setCloudProvider(CloudProviderPrimary.AUTHORITY);
mIsolatedResolver.call(MediaStore.AUTHORITY, MediaStore.SYNC_PROVIDERS_CALL, null,
Bundle.EMPTY);
}
private void setCloudProvider(String authority) {
Bundle in = new Bundle();
in.putString(MediaStore.EXTRA_CLOUD_PROVIDER, authority);
mIsolatedResolver.call(MediaStore.AUTHORITY, MediaStore.SET_CLOUD_PROVIDER_CALL, null, in);
if (authority != null) {
assertWithMessage("Expected " + authority + " to be current cloud provider.")
.that(MediaStore.isCurrentCloudMediaProviderAuthority(mIsolatedResolver,
authority)).isTrue();
}
}
private void assertGetCategoriesMatchSingle(String expectedCategoryName,
int expectedNumberOfItems) throws Exception {
if (expectedNumberOfItems == 0) {
assertCategoriesNoMatch(expectedCategoryName);
return;
}
Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
assertThat(c).isNotNull();
assertThat(c.getCount()).isEqualTo(1);
// Assert that only expected category is returned and has expectedNumberOfItems items in it
assertThat(c.moveToFirst()).isTrue();
final int nameColumnIndex = c.getColumnIndexOrThrow(AlbumColumns.DISPLAY_NAME);
final int numOfItemsColumnIndex = c.getColumnIndexOrThrow(AlbumColumns.MEDIA_COUNT);
final int coverIdIndex = c.getColumnIndexOrThrow(AlbumColumns.MEDIA_COVER_ID);
final String categoryName = c.getString(nameColumnIndex);
final int numOfItems = c.getInt(numOfItemsColumnIndex);
final Uri coverUri = ItemsProvider.getItemsUri(c.getString(coverIdIndex),
LOCAL_PICKER_PROVIDER_AUTHORITY, UserId.CURRENT_USER);
assertThat(categoryName).isEqualTo(expectedCategoryName);
assertThat(numOfItems).isEqualTo(expectedNumberOfItems);
assertCategoryUriIsValid(coverUri);
}
private void assertCategoryUriIsValid(Uri uri) throws Exception {
try (AssetFileDescriptor fd1 = mIsolatedResolver.openTypedAssetFile(uri, "image/*",
null, null)) {
assertThat(fd1).isNotNull();
}
try (ParcelFileDescriptor fd2 = mIsolatedResolver.openFileDescriptor(uri, "r")) {
assertThat(fd2).isNotNull();
}
}
private void assertCategoriesNoMatch(String expectedCategoryName) {
try (Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null)) {
while (c != null && c.moveToNext()) {
final int nameColumnIndex = c.getColumnIndexOrThrow(AlbumColumns.DISPLAY_NAME);
final String categoryName = c.getString(nameColumnIndex);
assertThat(categoryName).isNotEqualTo(expectedCategoryName);
}
}
}
private void assertGetCategoriesMatchMultiple(List<Pair<String, Integer>> categories) {
try (Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null)) {
assertGetCategoriesMatchMultiple(c, categories);
}
}
private void assertGetCategoriesMatchMultiple(Cursor c,
List<Pair<String, Integer>> categories) {
assertThat(c).isNotNull();
assertWithMessage("Expected number of albums")
.that(c.getCount()).isEqualTo(categories.size());
final int nameColumnIndex = c.getColumnIndexOrThrow(AlbumColumns.DISPLAY_NAME);
final int numOfItemsColumnIndex = c.getColumnIndexOrThrow(
AlbumColumns.MEDIA_COUNT);
for (Pair<String, Integer> category : categories) {
c.moveToNext();
assertThat(category.first).isEqualTo(c.getString(nameColumnIndex));
assertWithMessage("Expected item count for " + category.first).that(
c.getInt(numOfItemsColumnIndex)).isEqualTo(category.second);
}
}
private void setIsFavorite(File file) {
final Uri uri = MediaStore.scanFile(mIsolatedResolver, file);
final ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.IS_FAVORITE, 1);
// Assert that 1 row corresponding to this file is updated.
assertThat(mIsolatedResolver.update(uri, values, null)).isEqualTo(1);
// Wait for MediaStore to be Idle to reduce flakes caused by database updates
MediaStore.waitForIdle(mIsolatedResolver);
}
private void assertThatOnlyImagesVideos(Cursor c) throws Exception {
while (c.moveToNext()) {
int mimeTypeColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE);
String mimeType = c.getString(mimeTypeColumn);
assertThat(isImageMimeType(mimeType) || isVideoMimeType(mimeType)).isTrue();
}
}
private void assertThatOnlyImages(Cursor c) throws Exception {
while (c.moveToNext()) {
int mimeTypeColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE);
String mimeType = c.getString(mimeTypeColumn);
assertThat(isImageMimeType(mimeType)).isTrue();
}
}
private void assertThatOnlyVideos(Cursor c) throws Exception {
while (c.moveToNext()) {
int mimeTypeColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE);
String mimeType = c.getString(mimeTypeColumn);
assertThat(isVideoMimeType(mimeType)).isTrue();
}
}
private void assertThatAllImagesVideos(int count) {
int countOfImages = getCountOfMediaStoreImages();
int countOfVideos = getCountOfMediaStoreVideos();
assertThat(count).isEqualTo(countOfImages + countOfVideos);
}
private void assertThatAllImages(int count) {
int countOfImages = getCountOfMediaStoreImages();
assertThat(count).isEqualTo(countOfImages);
}
private void assertThatAllVideos(int count) {
int countOfVideos = getCountOfMediaStoreVideos();
assertThat(count).isEqualTo(countOfVideos);
}
private void createTestScreenshotImages() throws IOException {
// Top Level /Screenshots/ directory is not allowed by MediaProvider, so the directory
// and test files in it are created via shell commands.
final String createTopLevelScreenshotDirCommand =
"mkdir -p "
+ Environment.getExternalStorageDirectory().getPath()
+ "/"
+ Environment.DIRECTORY_SCREENSHOTS;
final String createTopLevelScreenshotImgCommand =
"touch " + getTopLevelScreenshotsDir().getPath() + "/" + IMAGE_FILE_NAME;
final String writeDataTopLevelScreenshotImgCommand =
"echo 1 > " + getTopLevelScreenshotsDir().getPath() + "/" + IMAGE_FILE_NAME;
executeShellCommand(createTopLevelScreenshotDirCommand);
executeShellCommand(createTopLevelScreenshotImgCommand);
// Writes 1 byte to the file.
executeShellCommand(writeDataTopLevelScreenshotImgCommand);
final File topLevelScreenshotsDirImage =
new File(getTopLevelScreenshotsDir(), IMAGE_FILE_NAME);
// Force the mock MediaProvider to scan.
final Uri uri = MediaStore.scanFile(mIsolatedResolver, topLevelScreenshotsDirImage);
assertWithMessage("Uri obtained by scanning file " + topLevelScreenshotsDirImage)
.that(uri)
.isNotNull();
// Wait for picker db sync
MediaStore.waitForIdle(mIsolatedResolver);
}
private int getCountOfMediaStoreImages() {
try (Cursor c = mIsolatedResolver.query(
MediaStore.Images.Media.getContentUri(VOLUME_EXTERNAL), null, null, null)) {
assertThat(c.moveToFirst()).isTrue();
return c.getCount();
}
}
private int getCountOfMediaStoreVideos() {
try (Cursor c = mIsolatedResolver.query(
MediaStore.Video.Media.getContentUri(VOLUME_EXTERNAL), null, null, null)) {
assertThat(c.moveToFirst()).isTrue();
return c.getCount();
}
}
private File assertCreateNewVideo(File dir) throws Exception {
return assertCreateNewFile(dir, VIDEO_FILE_NAME);
}
private File assertCreateNewImage(File dir) throws Exception {
return assertCreateNewFile(dir, IMAGE_FILE_NAME);
}
private File assertCreateNewVideo() throws Exception {
return assertCreateNewFile(getDownloadsDir(), VIDEO_FILE_NAME);
}
private File assertCreateNewImage() throws Exception {
return assertCreateNewFile(getDownloadsDir(), IMAGE_FILE_NAME);
}
private File assertCreateNewFile(File parentDir, String fileName) throws Exception {
final File file = new File(parentDir, fileName);
prepareFileAndGetUri(file, /* lastModifiedTime */ -1);
return file;
}
private Uri prepareFileAndGetUri(File file, long lastModifiedTime) throws IOException {
ensureParentExists(file.getParentFile());
assertThat(file.createNewFile()).isTrue();
// Write 1 byte because 0byte files are not valid in the picker db
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(1);
}
if (lastModifiedTime != -1) {
file.setLastModified(lastModifiedTime);
}
final Uri uri = MediaStore.scanFile(mIsolatedResolver, file);
assertWithMessage("Uri obtained by scanning file " + file)
.that(uri).isNotNull();
// Wait for picker db sync
MediaStore.waitForIdle(mIsolatedResolver);
return uri;
}
private void ensureParentExists(File parent) {
if (!parent.exists()) {
parent.mkdirs();
}
assertThat(parent.exists()).isTrue();
}
private File getDownloadsDir() {
return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS);
}
private File getDcimDir() {
return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DCIM);
}
private File getPicturesDir() {
return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_PICTURES);
}
private File getMoviesDir() {
return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_MOVIES);
}
private File getCameraDir() {
return new File(getDcimDir(), "Camera");
}
private File getScreenshotsDir() {
return new File(getPicturesDir(), Environment.DIRECTORY_SCREENSHOTS);
}
private File getTopLevelScreenshotsDir() {
return new File(
Environment.getExternalStorageDirectory(), Environment.DIRECTORY_SCREENSHOTS);
}
private File getScreenshotsDirFromDownloadsDir() {
return new File(getDownloadsDir(), Environment.DIRECTORY_SCREENSHOTS);
}
private File createHiddenDir() throws Exception {
File parentDir = new File(Environment.getExternalStorageDirectory(),
Environment.DIRECTORY_DOWNLOADS);
File dir = new File(parentDir, HIDDEN_DIR_NAME);
dir.mkdirs();
File nomedia = new File(dir, ".nomedia");
nomedia.createNewFile();
MediaStore.scanFile(mIsolatedResolver, nomedia);
return dir;
}
private void deleteAllFilesNoThrow() {
try (Cursor c = mIsolatedResolver.query(
MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
new String[] {MediaStore.MediaColumns.DATA}, null, null)) {
while(c.moveToNext()) {
(new File(c.getString(
c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)))).delete();
}
}
}
private void deleteTopLevelScreenshotDir() throws IOException {
final String removeTopLevelScreenshotDirCommand =
"rm -rf " + getTopLevelScreenshotsDir().getPath();
executeShellCommand(removeTopLevelScreenshotDirCommand);
}
/** Executes a shell command. */
private static String executeShellCommand(String command) throws IOException {
int attempt = 0;
while (attempt++ < 5) {
try {
return executeShellCommandInternal(command);
} catch (InterruptedIOException e) {
Log.v(TAG, "Trouble executing " + command + "; trying again", e);
}
}
throw new IOException("Failed to execute " + command);
}
private static String executeShellCommandInternal(String cmd) throws IOException {
UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
try (FileInputStream output = new FileInputStream(
uiAutomation.executeShellCommand(cmd).getFileDescriptor())) {
return new String(ByteStreams.toByteArray(output));
}
}
}