blob: c89716f8d2769f3952996225f7c4de19f4cccab5 [file] [log] [blame]
/*
* Copyright (C) 2022 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 android.photopicker.cts;
import static android.os.SystemProperties.getBoolean;
import static android.photopicker.cts.PickerProviderMediaGenerator.MediaGenerator;
import static android.photopicker.cts.PickerProviderMediaGenerator.setCloudProvider;
import static android.photopicker.cts.PickerProviderMediaGenerator.syncCloudProvider;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
import static android.photopicker.cts.util.PhotoPickerUiUtils.findItemList;
import static android.photopicker.cts.util.ResultsAssertionsUtils.assertRedactedReadOnlyAccess;
import static android.provider.MediaStore.PickerMediaColumns;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.content.ClipData;
import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.storage.StorageManager;
import android.photopicker.cts.cloudproviders.CloudProviderNoIntentFilter;
import android.photopicker.cts.cloudproviders.CloudProviderNoPermission;
import android.photopicker.cts.cloudproviders.CloudProviderPrimary;
import android.photopicker.cts.cloudproviders.CloudProviderSecondary;
import android.provider.MediaStore;
import android.util.Pair;
import androidx.test.filters.SdkSuppress;
import androidx.test.runner.AndroidJUnit4;
import androidx.test.uiautomator.UiObject;
import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Photo Picker Device only tests for common flows.
*/
@RunWith(AndroidJUnit4.class)
public class CloudPhotoPickerTest extends PhotoPickerBaseTest {
private final List<Uri> mUriList = new ArrayList<>();
private MediaGenerator mCloudPrimaryMediaGenerator;
private MediaGenerator mCloudSecondaryMediaGenerator;
private static final long IMAGE_SIZE_BYTES = 107684;
private static final String COLLECTION_1 = "COLLECTION_1";
private static final String COLLECTION_2 = "COLLECTION_2";
private static final String CLOUD_ID1 = "CLOUD_ID1";
private static final String CLOUD_ID2 = "CLOUD_ID2";
@Before
public void setUp() throws Exception {
super.setUp();
mCloudPrimaryMediaGenerator = PickerProviderMediaGenerator.getMediaGenerator(
mContext, CloudProviderPrimary.AUTHORITY);
mCloudSecondaryMediaGenerator = PickerProviderMediaGenerator.getMediaGenerator(
mContext, CloudProviderSecondary.AUTHORITY);
mCloudPrimaryMediaGenerator.resetAll();
mCloudSecondaryMediaGenerator.resetAll();
mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
mCloudSecondaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
setCloudProvider(mContext, null);
Assume.assumeTrue(getBoolean("sys.photopicker.pickerdb.enabled", true));
}
@After
public void tearDown() throws Exception {
for (Uri uri : mUriList) {
deleteMedia(uri, mContext);
}
mActivity.finish();
mUriList.clear();
setCloudProvider(mContext, null);
}
@Test
public void testCloudOnlySync() throws Exception {
initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1));
final ClipData clipData = fetchPickerMedia(1);
final List<String> mediaIds = extractMediaIds(clipData, 1);
assertThat(mediaIds).containsExactly(CLOUD_ID1);
}
@Test
public void testCloudPlusLocalSyncWithoutDedupe() throws Exception {
mUriList.addAll(createImagesAndGetUris(1, mContext.getUserId()));
initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1));
final ClipData clipData = fetchPickerMedia(2);
final List<String> mediaIds = extractMediaIds(clipData, 2);
assertThat(mediaIds).containsExactly(CLOUD_ID1, mUriList.get(0).getLastPathSegment());
}
@Test
public void testCloudPlusLocalSyncWithDedupe() throws Exception {
mUriList.addAll(createImagesAndGetUris(1, mContext.getUserId()));
initPrimaryCloudProviderWithImage(Pair.create(mUriList.get(0).getLastPathSegment(),
CLOUD_ID1));
final ClipData clipData = fetchPickerMedia(1);
final List<String> mediaIds = extractMediaIds(clipData, 1);
containsExcept(mediaIds, mUriList.get(0).getLastPathSegment(), CLOUD_ID1);
}
@Test
public void testDeleteCloudMedia() throws Exception {
initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1),
Pair.create(null, CLOUD_ID2));
ClipData clipData = fetchPickerMedia(2);
List<String> mediaIds = extractMediaIds(clipData, 2);
assertThat(mediaIds).containsExactly(CLOUD_ID1, CLOUD_ID2);
mCloudPrimaryMediaGenerator.deleteMedia(/* localId */ null, CLOUD_ID1,
/* trackDeleted */ true);
syncCloudProvider(mContext);
clipData = fetchPickerMedia(2);
mediaIds = extractMediaIds(clipData, 1);
containsExcept(mediaIds, CLOUD_ID2, CLOUD_ID1);
}
@Test
public void testVersionChange() throws Exception {
initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1),
Pair.create(null, CLOUD_ID2));
ClipData clipData = fetchPickerMedia(2);
List<String> mediaIds = extractMediaIds(clipData, 2);
assertThat(mediaIds).containsExactly(CLOUD_ID1, CLOUD_ID2);
mCloudPrimaryMediaGenerator.deleteMedia(/* localId */ null, CLOUD_ID1,
/* trackDeleted */ false);
syncCloudProvider(mContext);
clipData = fetchPickerMedia(2);
mediaIds = extractMediaIds(clipData, 2);
assertThat(mediaIds).containsExactly(CLOUD_ID1, CLOUD_ID2);
mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_2);
syncCloudProvider(mContext);
clipData = fetchPickerMedia(2);
mediaIds = extractMediaIds(clipData, 1);
containsExcept(mediaIds, CLOUD_ID2, CLOUD_ID1);
}
@Test
public void testSupportedProviders() throws Exception {
assertThat(MediaStore.isSupportedCloudMediaProviderAuthority(mContext.getContentResolver(),
CloudProviderPrimary.AUTHORITY)).isTrue();
assertThat(MediaStore.isSupportedCloudMediaProviderAuthority(mContext.getContentResolver(),
CloudProviderSecondary.AUTHORITY)).isTrue();
assertThat(MediaStore.isSupportedCloudMediaProviderAuthority(mContext.getContentResolver(),
CloudProviderNoPermission.AUTHORITY)).isFalse();
assertThat(MediaStore.isSupportedCloudMediaProviderAuthority(mContext.getContentResolver(),
CloudProviderNoIntentFilter.AUTHORITY)).isFalse();
}
@Test
public void testProviderSwitchSuccess() throws Exception {
setCloudProvider(mContext, CloudProviderPrimary.AUTHORITY);
assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
CloudProviderPrimary.AUTHORITY)).isTrue();
addImage(mCloudPrimaryMediaGenerator, /* localId */ null, CLOUD_ID1);
addImage(mCloudSecondaryMediaGenerator, /* localId */ null, CLOUD_ID2);
syncCloudProvider(mContext);
ClipData clipData = fetchPickerMedia(2);
List<String> mediaIds = extractMediaIds(clipData, 1);
containsExcept(mediaIds, CLOUD_ID1, CLOUD_ID2);
setCloudProvider(mContext, CloudProviderSecondary.AUTHORITY);
assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
CloudProviderPrimary.AUTHORITY)).isFalse();
clipData = fetchPickerMedia(2);
mediaIds = extractMediaIds(clipData, 1);
containsExcept(mediaIds, CLOUD_ID2, CLOUD_ID1);
}
@Test
public void testProviderSwitchFailure() throws Exception {
setCloudProvider(mContext, CloudProviderNoIntentFilter.AUTHORITY);
assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
CloudProviderPrimary.AUTHORITY)).isFalse();
setCloudProvider(mContext, CloudProviderNoPermission.AUTHORITY);
assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
CloudProviderPrimary.AUTHORITY)).isFalse();
}
@Test
public void testUriAccessWithValidProjection() throws Exception {
initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1));
final ClipData clipData = fetchPickerMedia(1);
final List<String> mediaIds = extractMediaIds(clipData, 1);
assertThat(mediaIds).containsExactly(CLOUD_ID1);
final ContentResolver resolver = mContext.getContentResolver();
String expectedDisplayName = CLOUD_ID1 + ".jpg";
try (Cursor c = resolver.query(clipData.getItemAt(0).getUri(), null, null, null)) {
assertThat(c).isNotNull();
assertThat(c.moveToFirst()).isTrue();
assertThat(c.getString(c.getColumnIndex(PickerMediaColumns.MIME_TYPE)))
.isEqualTo("image/jpeg");
assertThat(c.getString(c.getColumnIndex(PickerMediaColumns.DISPLAY_NAME)))
.isEqualTo(expectedDisplayName);
assertThat(c.getLong(c.getColumnIndex(PickerMediaColumns.SIZE)))
.isEqualTo(IMAGE_SIZE_BYTES);
assertThat(c.getLong(c.getColumnIndex(PickerMediaColumns.DURATION_MILLIS)))
.isEqualTo(0);
assertThat(c.getLong(c.getColumnIndex(PickerMediaColumns.DATE_TAKEN)))
.isGreaterThan(0);
final File file = new File(c.getString(c.getColumnIndex(PickerMediaColumns.DATA)));
assertThat(file.getPath().endsWith(expectedDisplayName)).isTrue();
assertThat(file.length()).isEqualTo(IMAGE_SIZE_BYTES);
}
assertRedactedReadOnlyAccess(clipData.getItemAt(0).getUri());
}
@Test
public void testUriAccessWithInvalidProjection() throws Exception {
initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1));
final ClipData clipData = fetchPickerMedia(1);
final List<String> mediaIds = extractMediaIds(clipData, 1);
assertThat(mediaIds).containsExactly(CLOUD_ID1);
final ContentResolver resolver = mContext.getContentResolver();
assertThrows(IllegalArgumentException.class, () -> resolver.query(
clipData.getItemAt(0).getUri(),
new String[] {MediaStore.MediaColumns.RELATIVE_PATH}, null, null));
}
@Test
public void testCloudEventNotification() throws Exception {
// Create a placeholder local image to ensure that the picker UI is never empty.
// The PhotoPickerUiUtils#findItemList needs to select an item and it times out if the
// Picker UI is empty.
mUriList.addAll(createImagesAndGetUris(1, mContext.getUserId()));
// Cloud provider isn't set
assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
CloudProviderPrimary.AUTHORITY)).isFalse();
addImage(mCloudPrimaryMediaGenerator, /* localId */ null, CLOUD_ID1);
// Notification fails because the calling cloud provider isn't enabled
assertThrows("Unauthorized cloud media notification", SecurityException.class,
() -> MediaStore.notifyCloudMediaChangedEvent(mContext.getContentResolver(),
CloudProviderPrimary.AUTHORITY, COLLECTION_1));
// Sleep because the notification API throttles requests with a 1s delay
Thread.sleep(1500);
ClipData clipData = fetchPickerMedia(1);
List<String> mediaIds = extractMediaIds(clipData, 1);
assertThat(mediaIds).containsNoneIn(Collections.singletonList(CLOUD_ID1));
// Now set the cloud provider and verify that notification succeeds
setCloudProvider(mContext, CloudProviderPrimary.AUTHORITY);
assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
CloudProviderPrimary.AUTHORITY)).isTrue();
MediaStore.notifyCloudMediaChangedEvent(mContext.getContentResolver(),
CloudProviderPrimary.AUTHORITY, COLLECTION_1);
assertThrows("Unauthorized cloud media notification", SecurityException.class,
() -> MediaStore.notifyCloudMediaChangedEvent(mContext.getContentResolver(),
CloudProviderSecondary.AUTHORITY, COLLECTION_1));
// Sleep because the notification API throttles requests with a 1s delay
Thread.sleep(1500);
clipData = fetchPickerMedia(1);
mediaIds = extractMediaIds(clipData, 1);
assertThat(mediaIds).containsExactly(CLOUD_ID1);
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
public void testStorageManagerKnowsCloudProvider() {
final StorageManager storageManager = mContext.getSystemService(StorageManager.class);
setCloudProvider(mContext, CloudProviderPrimary.AUTHORITY);
assertThat(storageManager.getCloudMediaProvider())
.isEqualTo(CloudProviderPrimary.AUTHORITY);
setCloudProvider(mContext, CloudProviderSecondary.AUTHORITY);
assertThat(storageManager.getCloudMediaProvider())
.isEqualTo(CloudProviderSecondary.AUTHORITY);
setCloudProvider(mContext, null);
assertThat(storageManager.getCloudMediaProvider()).isNull();
}
private List<String> extractMediaIds(ClipData clipData, int minCount) {
final int count = clipData.getItemCount();
assertThat(count).isAtLeast(minCount);
final List<String> mediaIds = new ArrayList<>();
for (int i = 0; i < count; i++) {
mediaIds.add(clipData.getItemAt(i).getUri().getLastPathSegment());
}
return mediaIds;
}
private ClipData fetchPickerMedia(int maxCount) throws Exception {
final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
mActivity.startActivityForResult(intent, REQUEST_CODE);
final List<UiObject> itemList = findItemList(maxCount);
for (int i = 0; i < itemList.size(); i++) {
final UiObject item = itemList.get(i);
item.click();
sDevice.waitForIdle();
}
final UiObject addButton = findAddButton();
addButton.click();
sDevice.waitForIdle();
return mActivity.getResult().data.getClipData();
}
private void initPrimaryCloudProviderWithImage(Pair<String, String>... mediaPairs)
throws Exception {
setCloudProvider(mContext, CloudProviderPrimary.AUTHORITY);
assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
CloudProviderPrimary.AUTHORITY)).isTrue();
for (Pair<String, String> pair: mediaPairs) {
addImage(mCloudPrimaryMediaGenerator, pair.first, pair.second);
}
syncCloudProvider(mContext);
}
private void addImage(MediaGenerator generator, String localId, String cloudId)
throws Exception {
generator.addMedia(localId, cloudId, /* albumId */ null, "image/jpeg",
/* mimeTypeExtension */ 0, IMAGE_SIZE_BYTES, /* isFavorite */ false,
R.raw.lg_g4_iso_800_jpg);
}
private static void containsExcept(List<String> mediaIds, String contained,
String notContained) {
assertThat(mediaIds).contains(contained);
assertThat(mediaIds).containsNoneIn(Collections.singletonList(notContained));
}
}