Snap for 11296156 from 89c99dea0f83b620e332d719847fcfe803d6a2a1 to mainline-tzdata5-release
Change-Id: I3b5449c4f52850d6f33dda5f717c3bc8a2732373
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 8884bd9..6bf7182 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -24,7 +24,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.USE_RESERVED_DISK" />
+ <uses-permission android:name="android.permission.USE_RESERVED_DISK" android:maxSdkVersion="34" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
diff --git a/apex/framework/api/current.txt b/apex/framework/api/current.txt
index cddfcfa..d760d14 100644
--- a/apex/framework/api/current.txt
+++ b/apex/framework/api/current.txt
@@ -151,6 +151,7 @@
field public static final String EXTRA_MEDIA_TITLE = "android.intent.extra.title";
field public static final String EXTRA_OUTPUT = "output";
field @FlaggedApi("com.android.providers.media.flags.pick_ordered_images") public static final String EXTRA_PICK_IMAGES_IN_ORDER = "android.provider.extra.PICK_IMAGES_IN_ORDER";
+ field @FlaggedApi("com.android.providers.media.flags.picker_default_tab") public static final String EXTRA_PICK_IMAGES_LAUNCH_TAB = "android.provider.extra.PICK_IMAGES_LAUNCH_TAB";
field public static final String EXTRA_PICK_IMAGES_MAX = "android.provider.extra.PICK_IMAGES_MAX";
field public static final String EXTRA_SCREEN_ORIENTATION = "android.intent.extra.screenOrientation";
field public static final String EXTRA_SHOW_ACTION_ICONS = "android.intent.extra.showActionIcons";
@@ -172,6 +173,8 @@
field public static final String MEDIA_SCANNER_VOLUME = "volume";
field public static final String META_DATA_REVIEW_GALLERY_PREWARM_SERVICE = "android.media.review_gallery_prewarm_service";
field public static final String META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE = "android.media.still_image_camera_preview_service";
+ field @FlaggedApi("com.android.providers.media.flags.picker_default_tab") public static final int PICK_IMAGES_TAB_ALBUMS = 0; // 0x0
+ field @FlaggedApi("com.android.providers.media.flags.picker_default_tab") public static final int PICK_IMAGES_TAB_IMAGES = 1; // 0x1
field public static final String QUERY_ARG_INCLUDE_RECENTLY_UNMOUNTED_VOLUMES = "android:query-arg-recently-unmounted-volumes";
field public static final String QUERY_ARG_MATCH_FAVORITE = "android:query-arg-match-favorite";
field public static final String QUERY_ARG_MATCH_PENDING = "android:query-arg-match-pending";
diff --git a/apex/framework/java/android/provider/AsyncContentProvider.java b/apex/framework/java/android/provider/AsyncContentProvider.java
index 25d5609..e12a0f6 100644
--- a/apex/framework/java/android/provider/AsyncContentProvider.java
+++ b/apex/framework/java/android/provider/AsyncContentProvider.java
@@ -37,7 +37,7 @@
*/
public final class AsyncContentProvider {
- private static final long TIMEOUT_IN_SECONDS = 5L;
+ private static final long TIMEOUT_IN_MINUTES = 3L;
private final IAsyncContentProvider mAsyncContentProvider;
@@ -53,7 +53,7 @@
CompletableFuture<ParcelFileDescriptor> future = new CompletableFuture<>();
RemoteCallback callback = new RemoteCallback(result -> setResult(result, future));
mAsyncContentProvider.openMedia(mediaId, callback);
- return future.get(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ return future.get(TIMEOUT_IN_MINUTES, TimeUnit.MINUTES);
}
private void setResult(Bundle result, CompletableFuture<ParcelFileDescriptor> future) {
@@ -71,4 +71,4 @@
+ "CloudMediaProvider"));
}
}
-}
\ No newline at end of file
+}
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index b1172af..d232fd3 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -872,6 +872,41 @@
}
/**
+ * The name of an optional intent-extra used to allow apps to specify the tab the picker should
+ * open with. The extra can only be specified in {@link MediaStore#ACTION_PICK_IMAGES}.
+ * <p>
+ * The value of this intent-extra must be one of: {@link MediaStore#PICK_IMAGES_TAB_ALBUMS}
+ * for the albums tab and {@link MediaStore#PICK_IMAGES_TAB_IMAGES} for the photos tab.
+ * The system will decide which tab to open by default and in most cases,
+ * it is {@link MediaStore#PICK_IMAGES_TAB_IMAGES} i.e. the photos tab.
+ */
+ @FlaggedApi("com.android.providers.media.flags.picker_default_tab")
+ public static final String EXTRA_PICK_IMAGES_LAUNCH_TAB =
+ "android.provider.extra.PICK_IMAGES_LAUNCH_TAB";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "PICK_IMAGES_TAB_" }, value = {
+ PICK_IMAGES_TAB_ALBUMS,
+ PICK_IMAGES_TAB_IMAGES
+ })
+ public @interface PickImagesTab { }
+
+ /**
+ * One of the permitted values for {@link MediaStore#EXTRA_PICK_IMAGES_LAUNCH_TAB} to open the
+ * picker with albums tab.
+ */
+ @FlaggedApi("com.android.providers.media.flags.picker_default_tab")
+ public static final int PICK_IMAGES_TAB_ALBUMS = 0;
+
+ /**
+ * One of the permitted values for {@link MediaStore#EXTRA_PICK_IMAGES_LAUNCH_TAB} to open the
+ * picker with photos tab.
+ */
+ @FlaggedApi("com.android.providers.media.flags.picker_default_tab")
+ public static final int PICK_IMAGES_TAB_IMAGES = 1;
+
+ /**
* Specify that the caller wants to receive the original media format without transcoding.
*
* <b>Caution: using this flag can cause app
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 131c795..6398796 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -72,7 +72,7 @@
<string name="picker_profile_work_paused_title" msgid="382212880704235925">"Applis professionnelles en pause"</string>
<string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"Pour ouvrir des photos professionnelles, activez vos applis professionnelles, puis réessayez"</string>
<string name="picker_privacy_message" msgid="9132700451027116817">"Cette appli peut uniquement accéder aux photos que vous sélectionnez"</string>
- <string name="picker_header_permissions" msgid="675872774407768495">"Sélectionnez les photos et videos auxquelles cette appli peut accéder"</string>
+ <string name="picker_header_permissions" msgid="675872774407768495">"Sélectionnez les photos et vidéos auxquelles cette appli peut accéder"</string>
<string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> élément}one{<xliff:g id="COUNT_1">^1</xliff:g> élément}many{<xliff:g id="COUNT_1">^1</xliff:g> éléments}other{<xliff:g id="COUNT_1">^1</xliff:g> éléments}}"</string>
<string name="picker_add_button_multi_select" msgid="4005164092275518399">"Ajouter (<xliff:g id="COUNT">^1</xliff:g>)"</string>
<string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Autoriser (<xliff:g id="COUNT">^1</xliff:g>)"</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 55a1f9d..351fd0b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -148,6 +148,7 @@
<style name="SelectedMediaPreloaderDialogTheme"
parent="@style/ThemeOverlay.MaterialComponents.MaterialAlertDialog.Centered">
+ <item name="android:background">@color/picker_background_color</item>
<item name="android:textColor">?attr/colorOnSurfaceVariant</item>
<item name="materialAlertDialogTitleTextStyle">@style/AlertDialogTitleStyle</item>
</style>
diff --git a/src/com/android/providers/media/AccessChecker.java b/src/com/android/providers/media/AccessChecker.java
index adde626..986b444 100644
--- a/src/com/android/providers/media/AccessChecker.java
+++ b/src/com/android/providers/media/AccessChecker.java
@@ -363,6 +363,16 @@
return PACKAGE_USER_ID_COLUMN + "=" + callingIdentity.uid / MediaStore.PER_USER_RANGE;
}
+ /**
+ * Returns true if redaction is needed for openFile calls on picker uri by checking calling
+ * package permission
+ *
+ * @param callingIdentity - the current caller
+ */
+ public static boolean isRedactionNeededForPickerUri(LocalCallingIdentity callingIdentity) {
+ return callingIdentity.hasPermission(LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED);
+ }
+
@VisibleForTesting
static String getWhereForMediaTypeMatch(int mediaType) {
return bindSelection("media_type=?", mediaType);
diff --git a/src/com/android/providers/media/DatabaseBackupAndRecovery.java b/src/com/android/providers/media/DatabaseBackupAndRecovery.java
index 2a99e7d..7a601eb 100644
--- a/src/com/android/providers/media/DatabaseBackupAndRecovery.java
+++ b/src/com/android/providers/media/DatabaseBackupAndRecovery.java
@@ -155,6 +155,21 @@
private static Map<String, String> sOwnerIdRelationMap;
+ public static final String STABLE_URI_INTERNAL_PROPERTY =
+ "persist.sys.fuse.backup.internal_db_backup";
+
+ private static boolean STABLE_URI_INTERNAL_PROPERTY_VALUE = true;
+
+ public static final String STABLE_URI_EXTERNAL_PROPERTY =
+ "persist.sys.fuse.backup.external_volume_backup";
+
+ private static boolean STABLE_URI_EXTERNAL_PROPERTY_VALUE = false;
+
+ public static final String STABLE_URI_PUBLIC_PROPERTY =
+ "persist.sys.fuse.backup.public_db_backup";
+
+ private static boolean STABLE_URI_PUBLIC_PROPERTY_VALUE = false;
+
protected DatabaseBackupAndRecovery(ConfigStore configStore, VolumeCache volumeCache) {
mConfigStore = configStore;
mVolumeCache = volumeCache;
@@ -167,19 +182,18 @@
switch (volumeName) {
case MediaStore.VOLUME_INTERNAL:
return mConfigStore.isStableUrisForInternalVolumeEnabled()
- || SystemProperties.getBoolean("persist.sys.fuse.backup.internal_db_backup",
- /* defaultValue */ false);
+ || SystemProperties.getBoolean(STABLE_URI_INTERNAL_PROPERTY,
+ /* defaultValue */ STABLE_URI_INTERNAL_PROPERTY_VALUE);
case MediaStore.VOLUME_EXTERNAL_PRIMARY:
return mConfigStore.isStableUrisForExternalVolumeEnabled()
- || SystemProperties.getBoolean(
- "persist.sys.fuse.backup.external_volume_backup",
- /* defaultValue */ false);
+ || SystemProperties.getBoolean(STABLE_URI_EXTERNAL_PROPERTY,
+ /* defaultValue */ STABLE_URI_EXTERNAL_PROPERTY_VALUE);
default:
// public volume
return isStableUrisEnabled(MediaStore.VOLUME_EXTERNAL_PRIMARY)
&& mConfigStore.isStableUrisForPublicVolumeEnabled()
- || SystemProperties.getBoolean("persist.sys.fuse.backup.public_db_backup",
- /* defaultValue */ false);
+ || SystemProperties.getBoolean(STABLE_URI_PUBLIC_PROPERTY,
+ /* defaultValue */ STABLE_URI_PUBLIC_PROPERTY_VALUE);
}
}
@@ -215,15 +229,18 @@
FuseDaemon fuseDaemon = getFuseDaemonForFileWithWait(new File(
DatabaseBackupAndRecovery.EXTERNAL_PRIMARY_ROOT_PATH));
Log.d(TAG, "Received db backup Fuse Daemon for: " + volumeName);
- if (isStableUrisEnabled(volumeName)) {
- if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
- // Setup internal and external volumes
- fuseDaemon.setupVolumeDbBackup();
- } else {
- // Setup public volume
- fuseDaemon.setupPublicVolumeDbBackup(volumeName);
- }
+ if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName) && (
+ isStableUrisEnabled(MediaStore.VOLUME_INTERNAL) || isStableUrisEnabled(
+ MediaStore.VOLUME_EXTERNAL_PRIMARY))) {
+ // Setup internal and external volumes
+ fuseDaemon.setupVolumeDbBackup();
mSetupCompletePublicVolumes.add(volumeName);
+ } else if (isStableUrisEnabled(volumeName)) {
+ // Setup public volume
+ fuseDaemon.setupPublicVolumeDbBackup(volumeName);
+ mSetupCompletePublicVolumes.add(volumeName);
+ } else {
+ return;
}
} catch (IOException e) {
Log.e(TAG, "Failure in setting up backup and recovery for volume: " + volumeName, e);
@@ -241,7 +258,7 @@
public void backupDatabases(DatabaseHelper internalDatabaseHelper,
DatabaseHelper externalDatabaseHelper, CancellationSignal signal) {
setupVolumeDbBackupAndRecovery(MediaStore.VOLUME_EXTERNAL_PRIMARY,
- new File(EXTERNAL_PRIMARY_ROOT_PATH));
+ new File(EXTERNAL_PRIMARY_ROOT_PATH));
Log.i(TAG, "Triggering database backup");
backupInternalDatabase(internalDatabaseHelper, signal);
backupExternalDatabase(externalDatabaseHelper, MediaStore.VOLUME_EXTERNAL_PRIMARY, signal);
@@ -283,6 +300,8 @@
}
if (!mSetupCompletePublicVolumes.contains(MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
+ Log.w(TAG,
+ "Setup is not present for backup of internal and external primary volume.");
return;
}
diff --git a/src/com/android/providers/media/LocalUriMatcher.java b/src/com/android/providers/media/LocalUriMatcher.java
index 888a619..9beebdb 100644
--- a/src/com/android/providers/media/LocalUriMatcher.java
+++ b/src/com/android/providers/media/LocalUriMatcher.java
@@ -72,11 +72,12 @@
static final int DOWNLOADS_ID = 801;
static final int PICKER = 900;
- static final int PICKER_ID = 901;
+ public static final int PICKER_ID = 901;
static final int PICKER_INTERNAL_MEDIA_ALL = 902;
static final int PICKER_INTERNAL_MEDIA_LOCAL = 903;
static final int PICKER_INTERNAL_ALBUMS_ALL = 904;
static final int PICKER_INTERNAL_ALBUMS_LOCAL = 905;
+ public static final int PICKER_GET_CONTENT_ID = 906;
public static final int MEDIA_GRANTS = 1000;
@@ -121,6 +122,11 @@
// content://media/picker/<user-id>/<authority>/media/<media-id>
mPublic.addURI(auth, "picker/#/*/media/*", PICKER_ID);
+ // content://media/picker_get_content/<user-id>/<media-id>
+ mPublic.addURI(auth, "picker_get_content/#/#", PICKER_GET_CONTENT_ID);
+ // content://media/picker_get_content/<user-id>/<authority>/media/<media-id>
+ mPublic.addURI(auth, "picker_get_content/#/*/media/*", PICKER_GET_CONTENT_ID);
+
mPublic.addURI(auth, "cli", CLI);
mPublic.addURI(auth, "*/images/media", IMAGES_MEDIA);
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 32a8e70..ec2d8c2 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -61,6 +61,7 @@
import static com.android.providers.media.AccessChecker.getWhereForUserSelectedAccess;
import static com.android.providers.media.AccessChecker.hasAccessToCollection;
import static com.android.providers.media.AccessChecker.hasUserSelectedAccess;
+import static com.android.providers.media.AccessChecker.isRedactionNeededForPickerUri;
import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME;
import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME;
import static com.android.providers.media.LocalCallingIdentity.APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID;
@@ -109,6 +110,7 @@
import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS_ID;
import static com.android.providers.media.LocalUriMatcher.MEDIA_GRANTS;
import static com.android.providers.media.LocalUriMatcher.MEDIA_SCANNER;
+import static com.android.providers.media.LocalUriMatcher.PICKER_GET_CONTENT_ID;
import static com.android.providers.media.LocalUriMatcher.PICKER_ID;
import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_ALBUMS_ALL;
import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_ALBUMS_LOCAL;
@@ -122,6 +124,8 @@
import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS_ID;
import static com.android.providers.media.LocalUriMatcher.VOLUMES;
import static com.android.providers.media.LocalUriMatcher.VOLUMES_ID;
+import static com.android.providers.media.PickerUriResolver.PICKER_GET_CONTENT_SEGMENT;
+import static com.android.providers.media.PickerUriResolver.PICKER_SEGMENT;
import static com.android.providers.media.PickerUriResolver.getMediaUri;
import static com.android.providers.media.photopicker.data.ItemsProvider.EXTRA_MIME_TYPE_SELECTION;
import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND;
@@ -1340,7 +1344,8 @@
mConfigStore, pickerSyncLockManager);
mPickerDataLayer = PickerDataLayer.create(context, mPickerDbFacade, mPickerSyncController,
mConfigStore);
- mPickerUriResolver = new PickerUriResolver(context, mPickerDbFacade, mProjectionHelper);
+ mPickerUriResolver = new PickerUriResolver(context, mPickerDbFacade, mProjectionHelper,
+ mUriMatcher);
if (SdkLevel.isAtLeastS()) {
mTranscodeHelper = new TranscodeHelperImpl(context, this, mConfigStore);
@@ -2275,7 +2280,8 @@
@Keep
public FileLookupResult onFileLookupForFuse(String path, int uid, int tid) {
uid = getBinderUidForFuse(uid, tid);
- final int userId = uidToUserId(uid);
+ // Use MediaProviders UserId as the caller might be calling cross profile.
+ final int userId = UserHandle.myUserId();
if (isSyntheticPath(path, userId)) {
if (isRedactedPath(path, userId)) {
@@ -2362,13 +2368,14 @@
boolean result = false;
switch (segmentCount) {
case 1:
- // .../picker
- if (lastSegment.equals("picker")) {
+ // .../picker or .../picker_get_content
+ if (lastSegment.equals(PICKER_SEGMENT) || lastSegment.equals(
+ PICKER_GET_CONTENT_SEGMENT)) {
result = file.exists() || file.mkdir();
}
break;
case 2:
- // .../picker/<user-id>
+ // .../picker/<user-id> or .../picker_get_content/<user-id>
try {
Integer.parseInt(lastSegment);
result = file.exists() || file.mkdir();
@@ -2378,21 +2385,24 @@
}
break;
case 3:
- // .../picker/<user-id>/<authority>
+ // .../picker/<user-id>/<authority> or .../picker_get_content/<user-id>/<authority>
result = preparePickerAuthorityPathSegment(file, lastSegment, uid);
break;
case 4:
- // .../picker/<user-id>/<authority>/media
+ // .../picker/<user-id>/<authority>/media or
+ // .../picker_get_content/<user-id>/<authority>/media
if (lastSegment.equals("media")) {
result = file.exists() || file.mkdir();
}
break;
case 5:
- // .../picker/<user-id>/<authority>/media/<media-id.extension>
+ // .../picker/<user-id>/<authority>/media/<media-id.extension> or
+ // .../picker_get_content/<user-id>/<authority>/media/<media-id.extension>
+ final String pickerSegmentType = syntheticRelativePathSegments.get(0);
final String fileUserId = syntheticRelativePathSegments.get(1);
final String authority = syntheticRelativePathSegments.get(2);
- result = preparePickerMediaIdPathSegment(file, authority, lastSegment, fileUserId,
- uid);
+ result = preparePickerMediaIdPathSegment(file, pickerSegmentType, authority,
+ lastSegment, fileUserId, uid);
break;
}
@@ -2410,8 +2420,9 @@
new long[0]);
}
- // ['', 'storage', 'emulated', '0', 'transforms', 'synthetic', 'picker', '<user-id>',
- // '<host>', 'media', '<fileName>']
+ // ['', 'storage', 'emulated', '0', 'transforms', 'synthetic',
+ // 'picker' or 'picker_get_content', '<user-id>', '<host>', 'media', '<fileName>']
+ final String pickerSegmentType = segments[6];
final String userId = segments[7];
final String fileName = segments[10];
final String host = segments[8];
@@ -2447,7 +2458,19 @@
try (FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) {
final String mimeType = MimeUtils.resolveMimeType(new File(path));
- final long[] redactionRanges = getRedactionRanges(fis, mimeType).redactionRanges;
+ // Picker segment indicates we need to force redact location metadata.
+ // Picker_get_content indicates that we need to check A_M_L permission to decide if the
+ // metadata needs to be redacted
+ LocalCallingIdentity callingIdentityForOriginalUid = getCachedCallingIdentityForFuse(
+ uid);
+ final boolean isRedactionNeeded = pickerSegmentType.equalsIgnoreCase(PICKER_SEGMENT)
+ || callingIdentityForOriginalUid == null
+ || isRedactionNeededForPickerUri(callingIdentityForOriginalUid);
+ Log.v(TAG, "Redaction needed for file open: " + isRedactionNeeded);
+ long[] redactionRanges = new long[0];
+ if (isRedactionNeeded) {
+ redactionRanges = getRedactionRanges(fis, mimeType).redactionRanges;
+ }
return new FileOpenResult(0 /* status */, uid, /* transformsUid */ 0,
/* nativeFd */ pfd.detachFd(), redactionRanges);
} catch (IOException e) {
@@ -2464,13 +2487,14 @@
return false;
}
- private boolean preparePickerMediaIdPathSegment(File file, String authority, String fileName,
- String userId, int uid) {
+ private boolean preparePickerMediaIdPathSegment(File file, String pickerSegmentType,
+ String authority, String fileName, String userId, int uid) {
final String mediaId = extractFileName(fileName);
- final String[] projection = new String[] { MediaStore.PickerMediaColumns.SIZE };
+ final String[] projection = new String[]{MediaStore.PickerMediaColumns.SIZE};
- final Uri uri = Uri.parse("content://media/picker/" + userId + "/" + authority + "/media/"
- + mediaId);
+ final Uri uri = Uri.parse(
+ "content://media/" + pickerSegmentType + "/" + userId + "/" + authority + "/media/"
+ + mediaId);
try (Cursor cursor = mPickerUriResolver.query(uri, projection, /* callingPid */0, uid,
mCallingIdentity.get().getPackageName())) {
if (cursor != null && cursor.moveToFirst()) {
@@ -4055,6 +4079,7 @@
return Downloads.CONTENT_TYPE;
case PICKER_ID:
+ case PICKER_GET_CONTENT_ID:
return mPickerUriResolver.getType(url, Binder.getCallingPid(),
Binder.getCallingUid());
}
@@ -8651,7 +8676,7 @@
private boolean isPickerUri(Uri uri) {
final int match = matchUri(uri, /* allowHidden */ isCallingPackageAllowedHidden());
- return match == PICKER_ID;
+ return match == PICKER_ID || match == PICKER_GET_CONTENT_ID;
}
@Override
@@ -8678,9 +8703,20 @@
uri = safeUncanonicalize(uri);
if (isPickerUri(uri)) {
- final int callingPid = mCallingIdentity.get().pid;
- final int callingUid = mCallingIdentity.get().uid;
- return mPickerUriResolver.openFile(uri, mode, signal, callingPid, callingUid);
+ int tid = Process.myTid();
+ synchronized (mPendingOpenInfo) {
+ mPendingOpenInfo.put(tid, new PendingOpenInfo(
+ Binder.getCallingUid(), /* mediaCapabilitiesUid */ 0, /* shouldRedact */
+ false, /* transcodeReason */ 0));
+ }
+
+ try {
+ return mPickerUriResolver.openFile(uri, mode, signal, mCallingIdentity.get());
+ } finally {
+ synchronized (mPendingOpenInfo) {
+ mPendingOpenInfo.remove(tid);
+ }
+ }
}
final boolean allowHidden = isCallingPackageAllowedHidden();
@@ -8813,10 +8849,21 @@
// This is needed for thumbnail resolution as it doesn't go through openFileCommon
if (isPickerUri(uri)) {
- final int callingPid = mCallingIdentity.get().pid;
- final int callingUid = mCallingIdentity.get().uid;
- return mPickerUriResolver.openTypedAssetFile(uri, mimeTypeFilter, opts, signal,
- callingPid, callingUid);
+ int tid = Process.myTid();
+ synchronized (mPendingOpenInfo) {
+ mPendingOpenInfo.put(tid, new PendingOpenInfo(
+ Binder.getCallingUid(), /* mediaCapabilitiesUid */ 0, /* shouldRedact */
+ false, /* transcodeReason */ 0));
+ }
+
+ try {
+ return mPickerUriResolver.openTypedAssetFile(uri, mimeTypeFilter, opts, signal,
+ mCallingIdentity.get());
+ } finally {
+ synchronized (mPendingOpenInfo) {
+ mPendingOpenInfo.remove(tid);
+ }
+ }
}
// TODO: enforce that caller has access to this uri
@@ -9862,7 +9909,8 @@
boolean isSuccess = false;
final int originalUid = getBinderUidForFuse(uid, tid);
- final int callingUserId = uidToUserId(uid);
+ // Use MediaProvider's own ID here since the caller may be cross profile.
+ final int userId = UserHandle.myUserId();
int mediaCapabilitiesUid = 0;
final PendingOpenInfo pendingOpenInfo;
synchronized (mPendingOpenInfo) {
@@ -9876,14 +9924,14 @@
try {
boolean forceRedaction = false;
String redactedUriId = null;
- if (isSyntheticPath(path, callingUserId)) {
+ if (isSyntheticPath(path, userId)) {
if (forWrite) {
// Synthetic URIs are not allowed to update EXIF headers.
return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
mediaCapabilitiesUid, new long[0]);
}
- if (isRedactedPath(path, callingUserId)) {
+ if (isRedactedPath(path, userId)) {
redactedUriId = extractFileName(path);
// If path is redacted Uris' path, ioPath must be the real path, ioPath must
@@ -9893,7 +9941,7 @@
// Irrespective of the permissions we want to redact in this case.
redact = true;
forceRedaction = true;
- } else if (isPickerPath(path, callingUserId)) {
+ } else if (isPickerPath(path, userId)) {
return handlePickerFileOpen(path, originalUid);
} else {
// we don't support any other transformations under .transforms/synthetic dir
diff --git a/src/com/android/providers/media/PickerUriResolver.java b/src/com/android/providers/media/PickerUriResolver.java
index b56b694..0b4fabf 100644
--- a/src/com/android/providers/media/PickerUriResolver.java
+++ b/src/com/android/providers/media/PickerUriResolver.java
@@ -19,6 +19,9 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Process.SYSTEM_UID;
+import static com.android.providers.media.AccessChecker.isRedactionNeededForPickerUri;
+import static com.android.providers.media.LocalUriMatcher.PICKER_GET_CONTENT_ID;
+import static com.android.providers.media.LocalUriMatcher.PICKER_ID;
import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
import static com.android.providers.media.util.FileUtils.toFuseFile;
@@ -30,6 +33,7 @@
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
+import android.os.Binder;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
@@ -58,7 +62,9 @@
public class PickerUriResolver {
private static final String TAG = "PickerUriResolver";
- private static final String PICKER_SEGMENT = "picker";
+ public static final String PICKER_SEGMENT = "picker";
+
+ public static final String PICKER_GET_CONTENT_SEGMENT = "picker_get_content";
private static final String PICKER_INTERNAL_SEGMENT = "picker_internal";
/** A uri with prefix "content://media/picker" is considered as a picker uri */
public static final Uri PICKER_URI = MediaStore.AUTHORITY_URI.buildUpon().
@@ -73,6 +79,7 @@
public static final String REFRESH_PICKER_UI_PATH = "refresh_ui";
public static final Uri REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI =
PICKER_INTERNAL_URI.buildUpon().appendPath(REFRESH_PICKER_UI_PATH).build();
+ public static final String INIT_PATH = "init";
public static final String MEDIA_PATH = "media";
public static final String ALBUM_PATH = "albums";
@@ -84,23 +91,28 @@
private final PickerDbFacade mDbFacade;
private final Set<String> mAllValidProjectionColumns;
private final String[] mAllValidProjectionColumnsArray;
+ private final LocalUriMatcher mLocalUriMatcher;
- PickerUriResolver(Context context, PickerDbFacade dbFacade, ProjectionHelper projectionHelper) {
+ PickerUriResolver(Context context, PickerDbFacade dbFacade, ProjectionHelper projectionHelper,
+ LocalUriMatcher localUriMatcher) {
mContext = context;
mDbFacade = dbFacade;
mAllValidProjectionColumns = projectionHelper.getProjectionMap(
MediaStore.PickerMediaColumns.class).keySet();
mAllValidProjectionColumnsArray = mAllValidProjectionColumns.toArray(new String[0]);
+ mLocalUriMatcher = localUriMatcher;
}
public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal,
- int callingPid, int callingUid) throws FileNotFoundException {
+ LocalCallingIdentity localCallingIdentity)
+ throws FileNotFoundException {
if (ParcelFileDescriptor.parseMode(mode) != ParcelFileDescriptor.MODE_READ_ONLY) {
throw new SecurityException("PhotoPicker Uris can only be accessed to read."
+ " Uri: " + uri);
}
- checkUriPermission(uri, callingPid, callingUid);
+ checkPermissionForRequireOriginalQueryParam(uri, localCallingIdentity);
+ checkUriPermission(uri, localCallingIdentity.pid, localCallingIdentity.uid);
final ContentResolver resolver;
try {
@@ -117,9 +129,10 @@
}
public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts,
- CancellationSignal signal, int callingPid, int callingUid)
+ CancellationSignal signal, LocalCallingIdentity localCallingIdentity)
throws FileNotFoundException {
- checkUriPermission(uri, callingPid, callingUid);
+ checkPermissionForRequireOriginalQueryParam(uri, localCallingIdentity);
+ checkUriPermission(uri, localCallingIdentity.pid, localCallingIdentity.uid);
final ContentResolver resolver;
try {
@@ -210,7 +223,8 @@
+ CloudMediaProviderContract.URI_PATH_SURFACE_CONTROLLER);
}
- private ParcelFileDescriptor openPickerFile(Uri uri) throws FileNotFoundException {
+ private ParcelFileDescriptor openPickerFile(Uri uri)
+ throws FileNotFoundException {
final File file = getPickerFileFromUri(uri);
if (file == null) {
throw new FileNotFoundException("File not found for uri: " + uri);
@@ -235,19 +249,38 @@
@VisibleForTesting
Cursor queryPickerUri(Uri uri, String[] projection) {
+ String pickerSegmentType = getPickerSegmentType(uri);
uri = unwrapProviderUri(uri);
- return mDbFacade.queryMediaIdForApps(uri.getHost(), uri.getLastPathSegment(),
- projection);
+ return mDbFacade.queryMediaIdForApps(pickerSegmentType, uri.getHost(),
+ uri.getLastPathSegment(), projection);
}
- public static Uri wrapProviderUri(Uri uri, int userId) {
+ private String getPickerSegmentType(Uri uri) {
+ switch (mLocalUriMatcher.matchUri(uri, /* allowHidden */ false)) {
+ case PICKER_ID:
+ return PICKER_SEGMENT;
+ case PICKER_GET_CONTENT_ID:
+ return PICKER_GET_CONTENT_SEGMENT;
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates a picker uri incorporating authority, user id and cloud provider.
+ */
+ public static Uri wrapProviderUri(Uri uri, String action, int userId) {
final List<String> segments = uri.getPathSegments();
if (segments.size() != 2) {
throw new IllegalArgumentException("Unexpected provider URI: " + uri);
}
Uri.Builder builder = initializeUriBuilder(MediaStore.AUTHORITY);
- builder.appendPath(PICKER_SEGMENT);
+ if (action.equalsIgnoreCase(Intent.ACTION_GET_CONTENT)) {
+ builder.appendPath(PICKER_GET_CONTENT_SEGMENT);
+ } else {
+ builder.appendPath(PICKER_SEGMENT);
+ }
builder.appendPath(String.valueOf(userId));
builder.appendPath(uri.getHost());
@@ -293,10 +326,36 @@
}
private void checkUriPermission(Uri uri, int pid, int uid) {
- if (!isSelf(uid) && mContext.checkUriPermission(uri, pid, uid,
+ // Clear query parameters to check for URI permissions, apps can add requireOriginal
+ // query parameter to URI, URI grants will not be present in that case.
+ Uri uriWithoutQueryParams = uri.buildUpon().clearQuery().build();
+ if (!isSelf(uid) && mContext.checkUriPermission(uriWithoutQueryParams, pid, uid,
Intent.FLAG_GRANT_READ_URI_PERMISSION) != PERMISSION_GRANTED) {
throw new SecurityException("Calling uid ( " + uid + " ) does not have permission to " +
- "access picker uri: " + uri);
+ "access picker uri: " + uriWithoutQueryParams);
+ }
+ }
+
+ private void checkPermissionForRequireOriginalQueryParam(Uri uri,
+ LocalCallingIdentity localCallingIdentity) {
+ String value = uri.getQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL);
+ if (value == null || value.isEmpty()) {
+ return;
+ }
+
+ // Check if requireOriginal is set
+ if (Integer.parseInt(value) == 1) {
+ if (mLocalUriMatcher.matchUri(uri, /* allowHidden */ false) == PICKER_ID) {
+ throw new UnsupportedOperationException(
+ "Require Original is not supported for Picker URI " + uri);
+ }
+
+ if (mLocalUriMatcher.matchUri(uri, /* allowHidden */ false) == PICKER_GET_CONTENT_ID
+ && isRedactionNeededForPickerUri(localCallingIdentity)) {
+ throw new UnsupportedOperationException("Calling uid ( " + Binder.getCallingUid()
+ + " ) does not have ACCESS_MEDIA_LOCATION permission for requesting "
+ + "original file");
+ }
}
}
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
index 48ee7fb..be7f3b1 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
@@ -132,6 +132,9 @@
private int mToolbarHeight = 0;
private boolean mShouldLogCancelledResult = true;
+ private AccessibilityManager mAccessibilityManager;
+ private boolean mIsAccessibilityEnabled;
+
@Override
public void onCreate(Bundle savedInstanceState) {
// This is required as GET_CONTENT with type "*/*" is also received by PhotoPicker due
@@ -185,6 +188,9 @@
mTabLayout = findViewById(R.id.tab_layout);
+ mAccessibilityManager = getSystemService(AccessibilityManager.class);
+ mIsAccessibilityEnabled = mAccessibilityManager.isEnabled();
+
initBottomSheetBehavior();
// Save the fragment container layout so that we can adjust the padding based on preview or
@@ -201,6 +207,7 @@
observeRefreshUiNotificationLiveData();
// Restore state operation should always be kept at the end of this method.
restoreState(savedInstanceState);
+
// Call this after state is restored, to use the correct LOGGER_INSTANCE_ID_ARG
if (savedInstanceState == null) {
final String intentAction = intent != null ? intent.getAction() : null;
@@ -501,7 +508,7 @@
*/
@VisibleForTesting
protected boolean isAccessibilityEnabled() {
- return getSystemService(AccessibilityManager.class).isEnabled();
+ return mIsAccessibilityEnabled;
}
private static int getBottomSheetPeekHeight(Context context) {
@@ -547,7 +554,7 @@
logPickerSelectionConfirmed(mSelection.getSelectedItems().size());
if (shouldPreloadSelectedItems()) {
final var uris = PickerResult.getPickerUrisForItems(
- mSelection.getSelectedItems());
+ getIntent().getAction(), mSelection.getSelectedItems());
mPickerViewModel.logPreloadingStarted(uris.size());
mPreloaderInstanceHolder.preloader =
SelectedMediaPreloader.preload(/* activity */ this, uris);
@@ -576,7 +583,8 @@
// The permission controller will pass the requesting package's UID here
final Bundle extras = getIntent().getExtras();
final int uid = extras.getInt(Intent.EXTRA_UID);
- final List<Uri> uris = getPickerUrisForItems(mSelection.getSelectedItemsWithoutGrants());
+ final List<Uri> uris = getPickerUrisForItems(getIntent().getAction(),
+ mSelection.getSelectedItemsWithoutGrants());
if (!uris.isEmpty()) {
ForegroundThread.getExecutor().execute(() -> {
// Handle grants in another thread to not block the UI.
@@ -589,7 +597,7 @@
// deselected them.
if (mPickerViewModel.isManagedSelectionEnabled()) {
final List<Uri> urisForItemsWhoseGrantsNeedsToBeRevoked = getPickerUrisForItems(
- mSelection.getPreGrantedItemsToBeRevoked());
+ getIntent().getAction(), mSelection.getPreGrantedItemsToBeRevoked());
if (!urisForItemsWhoseGrantsNeedsToBeRevoked.isEmpty()) {
ForegroundThread.getExecutor().execute(() -> {
// Handle grants in another thread to not block the UI.
@@ -603,9 +611,8 @@
}
private void setResultForPickImagesOrGetContentAction() {
- final Intent resultData = getPickerResponseIntent(
- mSelection.canSelectMultiple(),
- mSelection.getSelectedItems());
+ final Intent resultData = getPickerResponseIntent(getIntent().getAction(),
+ mSelection.canSelectMultiple(), mSelection.getSelectedItems());
setResult(RESULT_OK, resultData);
}
@@ -924,14 +931,14 @@
/**
* Reset to Photo Picker initial launch state (Photos grid tab) in the current profile mode.
*/
- private void resetInCurrentProfile() {
+ private void resetInCurrentProfile(boolean shouldSendInitRequest) {
// Clear all the fragments in the FragmentManager
final FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.popBackStackImmediate(/* name */ null,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
// Reset all content in the current profile
- mPickerViewModel.resetAllContentInCurrentProfile();
+ mPickerViewModel.resetAllContentInCurrentProfile(shouldSendInitRequest);
// Set up the fragments same as the initial launch state
setupInitialLaunchState();
@@ -1085,10 +1092,11 @@
* refresh the UI.
*/
private void observeRefreshUiNotificationLiveData() {
- mPickerViewModel.shouldRefreshUiLiveData()
- .observe(this, shouldRefresh -> {
- if (shouldRefresh && !mPickerViewModel.shouldShowOnlyLocalFeatures()) {
- resetInCurrentProfile();
+ mPickerViewModel.refreshUiLiveData()
+ .observe(this, refreshRequest -> {
+ if (refreshRequest.shouldRefreshPicker()
+ && !mPickerViewModel.shouldShowOnlyLocalFeatures()) {
+ resetInCurrentProfile(refreshRequest.shouldInitPicker());
}
});
}
diff --git a/src/com/android/providers/media/photopicker/PickerSyncController.java b/src/com/android/providers/media/photopicker/PickerSyncController.java
index eb7df4f..a5fba5c 100644
--- a/src/com/android/providers/media/photopicker/PickerSyncController.java
+++ b/src/com/android/providers/media/photopicker/PickerSyncController.java
@@ -26,6 +26,7 @@
import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.MEDIA_COLLECTION_ID;
import static android.provider.MediaStore.MY_UID;
+import static com.android.providers.media.PickerUriResolver.INIT_PATH;
import static com.android.providers.media.PickerUriResolver.PICKER_INTERNAL_URI;
import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI;
import static com.android.providers.media.PickerUriResolver.getDeletedMediaUri;
@@ -723,7 +724,7 @@
// Send UI refresh notification for any active picker sessions, as the
// UI data might be stale if a full sync needs to be run.
- sendPickerUiRefreshNotification();
+ sendPickerUiRefreshNotification(/* isInitPending */ false);
final Bundle fullSyncQueryArgs = new Bundle();
if (enablePagedSync) {
@@ -1050,14 +1051,27 @@
Log.wtf(TAG, "CLOUD_PROVIDER_LOCK is already held by this thread.");
}
- sendPickerUiRefreshNotification();
+ sendPickerUiRefreshNotification(/* isInitPending */ true);
}
}
- private void sendPickerUiRefreshNotification() {
- ContentResolver contentResolver = mContext.getContentResolver();
+ /**
+ * Send Picker UI content observers a notification that a refresh is required.
+ * @param isInitPending when true, appends the URI path segment
+ * {@link com.android.providers.media.PickerUriResolver.INIT_PATH} to the notification URI
+ * to indicate that the UI that the cached picker data might be stale.
+ * When a request notification is being sent from the sync path, set isInitPending as false to
+ * prevent sending refresh notification in a loop.
+ */
+ private void sendPickerUiRefreshNotification(boolean isInitPending) {
+ final ContentResolver contentResolver = mContext.getContentResolver();
if (contentResolver != null) {
- contentResolver.notifyChange(REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI, null);
+ final Uri.Builder builder = REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI.buildUpon();
+ if (isInitPending) {
+ builder.appendPath(INIT_PATH);
+ }
+ final Uri refreshUri = builder.build();
+ contentResolver.notifyChange(refreshUri, null);
} else {
Log.d(TAG, "Couldn't notify the Picker UI to refresh");
}
diff --git a/src/com/android/providers/media/photopicker/SelectedMediaPreloader.java b/src/com/android/providers/media/photopicker/SelectedMediaPreloader.java
index deefc1b..f6eb626 100644
--- a/src/com/android/providers/media/photopicker/SelectedMediaPreloader.java
+++ b/src/com/android/providers/media/photopicker/SelectedMediaPreloader.java
@@ -47,13 +47,9 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -262,23 +258,14 @@
Trace.beginSection("Preloader.openFd");
- CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
- try {
- mContentResolver.openAssetFileDescriptor(uri, "r").close();
- } catch (FileNotFoundException e) {
- isOpenedSuccessfully.set(false);
- Log.w(TAG, "Could not open FileDescriptor for " + uri, e);
- } catch (IOException e) {
- Log.w(TAG, "Failed to preload media file ", e);
- }
- });
-
try {
- future.get(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
- } catch (TimeoutException e) {
- return isOpenedSuccessfully.get();
- } catch (InterruptedException | ExecutionException e) {
- Log.w(TAG, "Could not preload the media item ", e);
+ mContentResolver.openAssetFileDescriptor(uri, "r").close();
+ } catch (FileNotFoundException e) {
+ isOpenedSuccessfully.set(false);
+ Log.w(TAG, "Could not open FileDescriptor for " + uri, e);
+ } catch (IOException e) {
+ isOpenedSuccessfully.set(false);
+ Log.w(TAG, "Failed to preload media file ", e);
} finally {
Trace.endSection();
diff --git a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
index 3fcdad9..50789b8 100644
--- a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
+++ b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
@@ -48,6 +48,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.providers.media.PickerUriResolver;
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.model.Item;
import com.android.providers.media.photopicker.sync.CloseableReentrantLock;
@@ -104,10 +105,7 @@
private static final int FAIL = -1;
private static final String TABLE_MEDIA = "media";
- // Intentionally use /sdcard path so that the receiving app resolves it to it's per-user
- // external storage path, e.g. /storage/emulated/<userid>. That way FUSE cross-user access is
- // not required for picker paths sent across users
- private static final String PICKER_PATH = "/sdcard/" + getPickerRelativePath();
+
private static final String TABLE_ALBUM_MEDIA = "album_media";
@VisibleForTesting
@@ -962,7 +960,7 @@
* Returns a {@link Cursor} containing picker db media rows with columns as {@code projection},
* a subset of {@link PickerMediaColumns}.
*/
- public Cursor queryMediaIdForApps(String authority, String mediaId,
+ public Cursor queryMediaIdForApps(String pickerSegmentType, String authority, String mediaId,
@NonNull String[] projection) {
final String[] selectionArgs = new String[] { mediaId };
final SQLiteQueryBuilder qb = createVisibleMediaQueryBuilder();
@@ -973,13 +971,13 @@
}
if (authority.equals(mLocalProvider)) {
- return queryMediaIdForAppsLocked(qb, projection, selectionArgs);
+ return queryMediaIdForAppsLocked(qb, projection, selectionArgs, pickerSegmentType);
}
try (CloseableReentrantLock ignored = mPickerSyncLockManager
.lock(PickerSyncLockManager.DB_CLOUD_LOCK)) {
if (authority.equals(mCloudProvider)) {
- return queryMediaIdForAppsLocked(qb, projection, selectionArgs);
+ return queryMediaIdForAppsLocked(qb, projection, selectionArgs, pickerSegmentType);
}
}
@@ -987,8 +985,9 @@
}
private Cursor queryMediaIdForAppsLocked(@NonNull SQLiteQueryBuilder qb,
- @NonNull String[] projection, @NonNull String[] selectionArgs) {
- return qb.query(mDatabase, getMediaStoreProjectionLocked(projection),
+ @NonNull String[] projection, @NonNull String[] selectionArgs,
+ String pickerSegmentType) {
+ return qb.query(mDatabase, getMediaStoreProjectionLocked(projection, pickerSegmentType),
/* selection */ null, selectionArgs, /* groupBy */ null, /* having */ null,
/* orderBy */ null, /* limitStr */ null);
}
@@ -1127,7 +1126,7 @@
private String[] getCloudMediaProjectionLocked() {
return new String[] {
getProjectionAuthorityLocked(),
- getProjectionDataLocked(MediaColumns.DATA),
+ getProjectionDataLocked(MediaColumns.DATA, PickerUriResolver.PICKER_SEGMENT),
getProjectionId(MediaColumns.ID),
// The id in the picker.db table represents the row id. This is used in UI pagination.
getProjectionSimple(KEY_ID, Item.ROW_ID),
@@ -1141,13 +1140,14 @@
};
}
- private String[] getMediaStoreProjectionLocked(String[] columns) {
+ private String[] getMediaStoreProjectionLocked(String[] columns, String pickerSegmentType) {
final String[] projection = new String[columns.length];
for (int i = 0; i < projection.length; i++) {
switch (columns[i]) {
case PickerMediaColumns.DATA:
- projection[i] = getProjectionDataLocked(PickerMediaColumns.DATA);
+ projection[i] = getProjectionDataLocked(PickerMediaColumns.DATA,
+ pickerSegmentType);
break;
case PickerMediaColumns.DISPLAY_NAME:
projection[i] =
@@ -1202,13 +1202,13 @@
KEY_CLOUD_ID, mLocalProvider, mCloudProvider, MediaColumns.AUTHORITY);
}
- private String getProjectionDataLocked(String asColumn) {
+ private String getProjectionDataLocked(String asColumn, String pickerSegmentType) {
// _data format:
// /sdcard/.transforms/synthetic/picker/<user-id>/<authority>/media/<display-name>
// See PickerUriResolver#getMediaUri
final String authority = String.format("CASE WHEN %s IS NULL THEN '%s' ELSE '%s' END",
KEY_CLOUD_ID, mLocalProvider, mCloudProvider);
- final String fullPath = "'" + PICKER_PATH + "/'"
+ final String fullPath = "'" + getPickerPath(pickerSegmentType) + "/'"
+ "||" + "'" + MediaStore.MY_USER_ID + "/'"
+ "||" + authority
+ "||" + "'/" + CloudMediaProviderContract.URI_PATH_MEDIA + "/'"
@@ -1216,6 +1216,13 @@
return String.format("%s AS %s", fullPath, asColumn);
}
+ private String getPickerPath(String pickerSegmentType) {
+ // Intentionally use /sdcard path so that the receiving app resolves it to its per-user
+ // external storage path, e.g. /storage/emulated/<userid>. That way FUSE cross-user
+ // access is not required for picker paths sent across users
+ return "/sdcard/" + getPickerRelativePath(pickerSegmentType);
+ }
+
private String getProjectionId(String asColumn) {
// We prefer cloud_id first and it only matters for cloud+local items. For those, the row
// will already be associated with a cloud authority, see #getProjectionAuthorityLocked.
diff --git a/src/com/android/providers/media/photopicker/data/PickerResult.java b/src/com/android/providers/media/photopicker/data/PickerResult.java
index 7bea27c..8108afa 100644
--- a/src/com/android/providers/media/photopicker/data/PickerResult.java
+++ b/src/com/android/providers/media/photopicker/data/PickerResult.java
@@ -40,10 +40,10 @@
* @return {@code Intent} which contains Uri that has been granted access on.
*/
@NonNull
- public static Intent getPickerResponseIntent(boolean canSelectMultiple,
+ public static Intent getPickerResponseIntent(String action, boolean canSelectMultiple,
@NonNull List<Item> selectedItems) {
// 1. Get Picker Uris corresponding to the selected items
- List<Uri> selectedUris = getPickerUrisForItems(selectedItems);
+ List<Uri> selectedUris = getPickerUrisForItems(action, selectedItems);
// 2. Grant read access to picker Uris and return
Intent intent = new Intent();
@@ -71,22 +71,23 @@
}
@VisibleForTesting
- static Uri getPickerUri(Uri uri) {
+ static Uri getPickerUri(String action, Uri uri) {
final String userInfo = uri.getUserInfo();
final String userId = userInfo == null ? UserId.CURRENT_USER.toString() : userInfo;
- return PickerUriResolver.wrapProviderUri(uri, Integer.parseInt(userId));
+ return PickerUriResolver.wrapProviderUri(uri, action, Integer.parseInt(userId));
}
/**
* Returns list of PhotoPicker Uris corresponding to each {@link Item}
*
+ * @param action action name which opened PhotoPicker
* @param items list of Item for which we return uri list.
*/
@NonNull
- public static List<Uri> getPickerUrisForItems(@NonNull List<Item> items) {
+ public static List<Uri> getPickerUrisForItems(String action, @NonNull List<Item> items) {
List<Uri> uris = new ArrayList<>();
for (Item item : items) {
- uris.add(getPickerUri(item.getContentUri()));
+ uris.add(getPickerUri(action, item.getContentUri()));
}
return uris;
diff --git a/src/com/android/providers/media/photopicker/data/model/RefreshRequest.java b/src/com/android/providers/media/photopicker/data/model/RefreshRequest.java
new file mode 100644
index 0000000..5b4c3c6
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/data/model/RefreshRequest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 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.data.model;
+
+public class RefreshRequest {
+ public static RefreshRequest DEFAULT = new RefreshRequest(false, false);
+ private boolean mShouldRefresh;
+ private boolean mShouldInit;
+
+ public RefreshRequest(boolean shouldRefresh, boolean shouldInit) {
+ mShouldRefresh = shouldRefresh;
+ mShouldInit = shouldInit;
+ }
+
+ /**
+ * Returns true if Photo Picker UI should be refreshed, otherwise returns false.
+ */
+ public boolean shouldRefreshPicker() {
+ return mShouldRefresh;
+ }
+
+ /**
+ * Returns true if Photo Picker data might be stale and should be initialized, otherwise
+ * returns false.
+ */
+ public boolean shouldInitPicker() {
+ return mShouldInit;
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java b/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java
index 8e070b5..8f06fb5 100644
--- a/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java
@@ -18,6 +18,7 @@
import static com.android.providers.media.util.MimeUtils.isVideoMimeType;
import android.os.Bundle;
+import android.provider.MediaStore;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -68,11 +69,17 @@
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
- mTabContainerAdapter = new TabContainerAdapter(/* fragment */ this);
mViewPager = view.findViewById(R.id.picker_tab_viewpager);
- mViewPager.setAdapter(mTabContainerAdapter);
final ViewModelProvider viewModelProvider = new ViewModelProvider(requireActivity());
mPickerViewModel = viewModelProvider.get(PickerViewModel.class);
+ mTabContainerAdapter = new TabContainerAdapter(/* fragment */ this);
+ mViewPager.setAdapter(mTabContainerAdapter);
+
+ // Launch in albums tab if the app requests so
+ if (mPickerViewModel.getPickerLaunchTab() == MediaStore.PICK_IMAGES_TAB_ALBUMS) {
+ // Launch the picker in Albums tab without any switch animation
+ mViewPager.setCurrentItem(ALBUMS_TAB_POSITION, /* smoothScroll */ false);
+ }
// If the ViewPager2 has more than one page with BottomSheetBehavior, the scrolled view
// (e.g. RecyclerView) on the second page can't be scrolled. The workaround is to update
diff --git a/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaSelectFragment.java b/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaSelectFragment.java
index f08bd75..7d05864 100644
--- a/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaSelectFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaSelectFragment.java
@@ -139,7 +139,8 @@
providerMediaCollectionInfo.getAccountConfigurationIntent();
selectedPref.setExtraWidgetOnClickListener(
accountConfigurationIntent == null ? null : v ->
- requireActivity().startActivity(accountConfigurationIntent));
+ requireActivity().startActivityAsUser(
+ accountConfigurationIntent, mUserId.getUserHandle()));
});
}
diff --git a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
index ff5e5c0..69f138f 100644
--- a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
+++ b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
@@ -24,6 +24,7 @@
import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS;
import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS;
+import static com.android.providers.media.PickerUriResolver.INIT_PATH;
import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI;
import static com.android.providers.media.photopicker.DataLoaderThread.TOKEN;
import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
@@ -81,6 +82,7 @@
import com.android.providers.media.photopicker.data.UserIdManager;
import com.android.providers.media.photopicker.data.model.Category;
import com.android.providers.media.photopicker.data.model.Item;
+import com.android.providers.media.photopicker.data.model.RefreshRequest;
import com.android.providers.media.photopicker.data.model.UserId;
import com.android.providers.media.photopicker.metrics.NonUiEventLogger;
import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger;
@@ -103,9 +105,6 @@
*/
public class PickerViewModel extends AndroidViewModel {
public static final String TAG = "PhotoPicker";
-
- private static final int RECENT_MINIMUM_COUNT = 12;
-
private static final int INSTANCE_ID_MAX = 1 << 15;
private static final int DELAY_MILLIS = 0;
@@ -122,6 +121,8 @@
private final MuteStatus mMuteStatus;
public boolean mEmptyPageDisplayed = false;
+ @MediaStore.PickImagesTab
+ private int mPickerLaunchTab = MediaStore.PICK_IMAGES_TAB_IMAGES;
// TODO(b/193857982): We keep these four data sets now, we may need to find a way to reduce the
// data set to reduce memories.
@@ -138,11 +139,13 @@
private MutableLiveData<List<Category>> mCategoryList;
private MutableLiveData<Boolean> mIsAllPreGrantedMediaLoaded = new MutableLiveData<>(false);
- private final MutableLiveData<Boolean> mShouldRefreshUiLiveData = new MutableLiveData<>(false);
+ private final MutableLiveData<RefreshRequest> mRefreshUiLiveData =
+ new MutableLiveData<>(RefreshRequest.DEFAULT);
private final ContentObserver mRefreshUiNotificationObserver = new ContentObserver(null) {
@Override
- public void onChange(boolean selfChange) {
- mShouldRefreshUiLiveData.postValue(true);
+ public void onChange(boolean selfChange, Uri uri) {
+ boolean shouldInit = uri.getLastPathSegment().equals(INIT_PATH);
+ mRefreshUiLiveData.postValue(new RefreshRequest(true, shouldInit));
}
};
@@ -227,6 +230,14 @@
}
}
+ public int getPickerLaunchTab() {
+ return mPickerLaunchTab;
+ }
+
+ public void setPickerLaunchTab(int launchTab) {
+ mPickerLaunchTab = launchTab;
+ }
+
@VisibleForTesting
protected void initConfigStore() {
mConfigStore = MediaApplication.getConfigStore();
@@ -363,22 +374,24 @@
@UiThread
public void onSwitchedProfile() {
resetRefreshUiNotificationObserver();
- resetAllContentInCurrentProfile();
+ resetAllContentInCurrentProfile(/* shouldSendInitRequest */ true);
}
/**
* Reset all the content (items, categories & banners) in the current profile.
*/
@UiThread
- public void resetAllContentInCurrentProfile() {
+ public void resetAllContentInCurrentProfile(boolean shouldSendInitRequest) {
Log.d(TAG, "Reset all content in current profile");
// Post 'should refresh UI live data' value as false to avoid unnecessary repetitive resets
- mShouldRefreshUiLiveData.postValue(false);
+ mRefreshUiLiveData.postValue(RefreshRequest.DEFAULT);
clearQueuedTasksInDataLoaderThread();
- initPhotoPickerData();
+ if (shouldSendInitRequest) {
+ initPhotoPickerData();
+ }
// Clear the existing content - selection, photos grid, albums grid, banners
mSelection.clearSelectedItems();
@@ -863,6 +876,23 @@
* Parse values from {@code intent} and set corresponding fields
*/
public void parseValuesFromIntent(Intent intent) throws IllegalArgumentException {
+ final Bundle extras = intent.getExtras();
+ if (extras != null && extras.containsKey(MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB)) {
+ if (intent.getAction().equals(ACTION_GET_CONTENT)) {
+ Log.e(TAG, "EXTRA_PICKER_LAUNCH_TAB cannot be passed as an extra in "
+ + "ACTION_GET_CONTENT");
+ } else if (intent.getAction().equals(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)) {
+ throw new IllegalArgumentException("EXTRA_PICKER_LAUNCH_TAB cannot be passed as an "
+ + "extra in ACTION_USER_SELECT_IMAGES_FOR_APP");
+ } else {
+ mPickerLaunchTab = extras.getInt(MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB);
+ if (!checkPickerLaunchOptionValidity(mPickerLaunchTab)) {
+ throw new IllegalArgumentException("Incorrect value " + mPickerLaunchTab
+ + " received for the intent extra: "
+ + MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB);
+ }
+ }
+ }
mUserIdManager.setIntentAndCheckRestrictions(intent);
mMimeTypeFilters = MimeFilterUtils.getMimeTypeFilters(intent);
@@ -899,6 +929,11 @@
}
}
+ private boolean checkPickerLaunchOptionValidity(int launchOption) {
+ return launchOption == MediaStore.PICK_IMAGES_TAB_IMAGES
+ || launchOption == MediaStore.PICK_IMAGES_TAB_ALBUMS;
+ }
+
private void initBannerManager() {
mBannerManager = shouldShowOnlyLocalFeatures()
? new BannerManager(mAppContext, mUserIdManager, mConfigStore)
@@ -1398,14 +1433,14 @@
* @return a {@link LiveData} that posts Should Refresh Picker UI as {@code true} when notified.
*/
@NonNull
- public LiveData<Boolean> shouldRefreshUiLiveData() {
- return mShouldRefreshUiLiveData;
+ public LiveData<RefreshRequest> refreshUiLiveData() {
+ return mRefreshUiLiveData;
}
private void registerRefreshUiNotificationObserver() {
mContentResolver = getContentResolverForSelectedUser();
mContentResolver.registerContentObserver(REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI,
- /* notifyForDescendants */ false, mRefreshUiNotificationObserver);
+ /* notifyForDescendants */ true, mRefreshUiNotificationObserver);
}
private void unregisterRefreshUiNotificationObserver() {
diff --git a/src/com/android/providers/media/util/IsoInterface.java b/src/com/android/providers/media/util/IsoInterface.java
index 8da64b9..5fb5130 100644
--- a/src/com/android/providers/media/util/IsoInterface.java
+++ b/src/com/android/providers/media/util/IsoInterface.java
@@ -261,6 +261,9 @@
}
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
+ } catch (OutOfMemoryError e) {
+ Log.e(TAG, "Too many boxes in file. This might imply a corrupted file.", e);
+ throw new IOException(e.getMessage());
}
// Also create a flattened structure to speed up searching
@@ -295,8 +298,8 @@
public @NonNull long[] getBoxRanges(int type) {
LongArray res = new LongArray();
for (Box box : mFlattened) {
- for (int i = 0; i < box.range.length; i += 2) {
- if (box.type == type) {
+ if (box.type == type) {
+ for (int i = 0; i < box.range.length; i += 2) {
res.add(box.range[i] + box.headerSize);
res.add(box.range[i] + box.range[i + 1]);
}
@@ -308,8 +311,8 @@
public @NonNull long[] getBoxRanges(@NonNull UUID uuid) {
LongArray res = new LongArray();
for (Box box : mFlattened) {
- for (int i = 0; i < box.range.length; i += 2) {
- if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
+ if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
+ for (int i = 0; i < box.range.length; i += 2) {
res.add(box.range[i] + box.headerSize);
res.add(box.range[i] + box.range[i + 1]);
}
diff --git a/src/com/android/providers/media/util/SyntheticPathUtils.java b/src/com/android/providers/media/util/SyntheticPathUtils.java
index aa0db93..6d73802 100644
--- a/src/com/android/providers/media/util/SyntheticPathUtils.java
+++ b/src/com/android/providers/media/util/SyntheticPathUtils.java
@@ -16,14 +16,17 @@
package com.android.providers.media.util;
+import static com.android.providers.media.PickerUriResolver.PICKER_GET_CONTENT_SEGMENT;
+import static com.android.providers.media.PickerUriResolver.PICKER_SEGMENT;
import static com.android.providers.media.util.FileUtils.buildPath;
import static com.android.providers.media.util.FileUtils.buildPrimaryVolumeFile;
import static com.android.providers.media.util.FileUtils.extractFileName;
-import androidx.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
@@ -37,7 +40,6 @@
private static final String TRANSFORMS_DIR = ".transforms";
private static final String SYNTHETIC_DIR = "synthetic";
private static final String REDACTED_DIR = "redacted";
- private static final String PICKER_DIR = "picker";
public static final String REDACTED_URI_ID_PREFIX = "RUID";
public static final int REDACTED_URI_ID_SIZE = 36;
@@ -48,8 +50,12 @@
return buildPath(/* base */ null, TRANSFORMS_DIR, SYNTHETIC_DIR, REDACTED_DIR).getPath();
}
- public static String getPickerRelativePath() {
- return buildPath(/* base */ null, TRANSFORMS_DIR, SYNTHETIC_DIR, PICKER_DIR).getPath();
+ /**
+ * Returns picker synthetic path directory.
+ */
+ public static String getPickerRelativePath(String pickerSegmentType) {
+ return buildPath(/* base */ null, TRANSFORMS_DIR, SYNTHETIC_DIR,
+ pickerSegmentType).getPath();
}
public static boolean isRedactedPath(String path, int userId) {
@@ -66,10 +72,13 @@
}
public static boolean isPickerPath(String path, int userId) {
- final String pickerDir = buildPrimaryVolumeFile(userId, getPickerRelativePath())
- .getAbsolutePath();
+ final String pickerDir = buildPrimaryVolumeFile(userId, getPickerRelativePath(
+ PICKER_SEGMENT)).getAbsolutePath();
+ final String pickerGetContentDir = buildPrimaryVolumeFile(userId,
+ getPickerRelativePath(PICKER_GET_CONTENT_SEGMENT)).getAbsolutePath();
- return path != null && startsWith(path, pickerDir);
+ return path != null && (startsWith(path, pickerDir) || startsWith(path,
+ pickerGetContentDir));
}
public static boolean isSyntheticPath(String path, int userId) {
diff --git a/tests/src/com/android/providers/media/AccessCheckerTest.java b/tests/src/com/android/providers/media/AccessCheckerTest.java
index 49a870e..ef6c963 100644
--- a/tests/src/com/android/providers/media/AccessCheckerTest.java
+++ b/tests/src/com/android/providers/media/AccessCheckerTest.java
@@ -29,6 +29,7 @@
import static com.android.providers.media.AccessChecker.getWhereForUserSelectedAccess;
import static com.android.providers.media.AccessChecker.hasAccessToCollection;
import static com.android.providers.media.AccessChecker.hasUserSelectedAccess;
+import static com.android.providers.media.AccessChecker.isRedactionNeededForPickerUri;
import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA;
import static com.android.providers.media.LocalUriMatcher.DOWNLOADS;
import static com.android.providers.media.LocalUriMatcher.DOWNLOADS_ID;
@@ -45,7 +46,9 @@
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import android.os.Bundle;
import android.system.Os;
@@ -398,6 +401,28 @@
}
@Test
+ public void testIsRedactionNeededForPickerUri_returnsFalse_withNoRedactPerms() {
+ LocalCallingIdentity callingIdentityWithRedactionNotNeededPermission =
+ LocalCallingIdentity.forTest(
+ InstrumentationRegistry.getTargetContext(), Os.getuid(),
+ ~LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED);
+
+ assertFalse("App with write perms should get non redacted data",
+ isRedactionNeededForPickerUri(callingIdentityWithRedactionNotNeededPermission));
+ }
+
+ @Test
+ public void testIsRedactionNeededForPickerUri_returnsTrue_withRedactPerms() {
+ LocalCallingIdentity callingIdentityWithRedactionNeededPermission =
+ LocalCallingIdentity.forTest(
+ InstrumentationRegistry.getTargetContext(), Os.getuid(),
+ LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED);
+
+ assertTrue("App with no perms should get redacted data",
+ isRedactionNeededForPickerUri(callingIdentityWithRedactionNeededPermission));
+ }
+
+ @Test
public void testGetWhereForConstrainedAccess_forWrite_hasLegacyWrite() {
LocalCallingIdentity hasLegacyWrite = LocalCallingIdentity.forTest(
InstrumentationRegistry.getTargetContext(), Os.getuid(),
diff --git a/tests/src/com/android/providers/media/LocalUriMatcherTest.java b/tests/src/com/android/providers/media/LocalUriMatcherTest.java
index 32721ed..01ce3ec 100644
--- a/tests/src/com/android/providers/media/LocalUriMatcherTest.java
+++ b/tests/src/com/android/providers/media/LocalUriMatcherTest.java
@@ -43,6 +43,15 @@
LocalUriMatcher.PICKER_ID,
assembleTestUri(new String[] {"picker", "0", "anything", "media", "anything"}));
+ assertMatchesPublic(
+ LocalUriMatcher.PICKER_GET_CONTENT_ID,
+ assembleTestUri(new String[]{"picker_get_content", Integer.toString(1),
+ Integer.toString(1)}));
+ assertMatchesPublic(
+ LocalUriMatcher.PICKER_GET_CONTENT_ID,
+ assembleTestUri(
+ new String[]{"picker_get_content", "0", "anything", "media", "anything"}));
+
assertMatchesPublic(LocalUriMatcher.CLI, assembleTestUri(new String[] {"cli"}));
assertMatchesPublic(
@@ -204,6 +213,15 @@
LocalUriMatcher.PICKER_ID,
assembleTestUri(new String[] {"picker", "0", "anything", "media", "anything"}));
+ assertMatchesHidden(
+ LocalUriMatcher.PICKER_GET_CONTENT_ID,
+ assembleTestUri(new String[]{"picker_get_content", Integer.toString(1),
+ Integer.toString(1)}));
+ assertMatchesHidden(
+ LocalUriMatcher.PICKER_GET_CONTENT_ID,
+ assembleTestUri(
+ new String[]{"picker_get_content", "0", "anything", "media", "anything"}));
+
assertMatchesHidden(LocalUriMatcher.CLI, assembleTestUri(new String[] {"cli"}));
assertMatchesHidden(
diff --git a/tests/src/com/android/providers/media/PickerUriResolverTest.java b/tests/src/com/android/providers/media/PickerUriResolverTest.java
index 3d8c260..e1d1016 100644
--- a/tests/src/com/android/providers/media/PickerUriResolverTest.java
+++ b/tests/src/com/android/providers/media/PickerUriResolverTest.java
@@ -16,8 +16,10 @@
package com.android.providers.media;
+import static android.content.Intent.ACTION_GET_CONTENT;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.provider.MediaStore.ACTION_PICK_IMAGES;
import static androidx.test.InstrumentationRegistry.getContext;
import static androidx.test.InstrumentationRegistry.getTargetContext;
@@ -26,6 +28,7 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -78,10 +81,13 @@
private static Uri sTestPickerUri;
private static String TEST_ID;
+ private static Uri sMediaStoreUriInOtherContext;
+
private static class TestPickerUriResolver extends PickerUriResolver {
TestPickerUriResolver(Context context) {
super(context, new PickerDbFacade(getTargetContext(), new PickerSyncLockManager()),
- new ProjectionHelper(Column.class, ExportedSince.class));
+ new ProjectionHelper(Column.class, ExportedSince.class),
+ new LocalUriMatcher(MediaStore.AUTHORITY));
}
@Override
@@ -119,14 +125,16 @@
Manifest.permission.INTERACT_ACROSS_USERS);
sCurrentContext = mock(Context.class);
when(sCurrentContext.getUser()).thenReturn(UserHandle.of(UserHandle.myUserId()));
+ PackageManager packageManager = mock(PackageManager.class);
+ when(sCurrentContext.getPackageManager()).thenReturn(packageManager);
+ when(packageManager.getPackagesForUid(anyInt())).thenReturn(
+ new String[]{getContext().getPackageName()});
final Context otherUserContext = createOtherUserContext(TEST_USER);
sTestPickerUriResolver = new TestPickerUriResolver(sCurrentContext);
- final Uri mediaStoreUriInOtherContext = createTestFileInContext(otherUserContext);
- TEST_ID = mediaStoreUriInOtherContext.getLastPathSegment();
- sTestPickerUri = getPickerUriForId(ContentUris.parseId(mediaStoreUriInOtherContext),
- TEST_USER);
+ sMediaStoreUriInOtherContext = createTestFileInContext(otherUserContext);
+ TEST_ID = sMediaStoreUriInOtherContext.getLastPathSegment();
}
@AfterClass
@@ -136,24 +144,37 @@
@Test
public void wrapProviderUriValid() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
final String providerSuffix = "authority/media/media_id";
final Uri providerUriUserImplicit = Uri.parse("content://" + providerSuffix);
final Uri providerUriUser0 = Uri.parse("content://0@" + providerSuffix);
final Uri mediaUriUser0 = Uri.parse("content://media/picker/0/" + providerSuffix);
+ final Uri mediaUriUser0PickerGetContent = Uri.parse(
+ "content://media/picker_get_content/0/" + providerSuffix);
final Uri providerUriUser10 = Uri.parse("content://10@" + providerSuffix);
final Uri mediaUriUser10 = Uri.parse("content://media/picker/10/" + providerSuffix);
- assertThat(PickerUriResolver.wrapProviderUri(providerUriUserImplicit, 0))
+ assertThat(PickerUriResolver.wrapProviderUri(providerUriUserImplicit,
+ ACTION_PICK_IMAGES, 0))
.isEqualTo(mediaUriUser0);
- assertThat(PickerUriResolver.wrapProviderUri(providerUriUser0, 0)).isEqualTo(mediaUriUser0);
+ assertThat(PickerUriResolver.wrapProviderUri(providerUriUserImplicit,
+ ACTION_GET_CONTENT, 0))
+ .isEqualTo(mediaUriUser0PickerGetContent);
+ assertThat(
+ PickerUriResolver.wrapProviderUri(providerUriUser0, ACTION_PICK_IMAGES,
+ 0)).isEqualTo(mediaUriUser0);
assertThat(PickerUriResolver.unwrapProviderUri(mediaUriUser0)).isEqualTo(providerUriUser0);
- assertThat(PickerUriResolver.wrapProviderUri(providerUriUserImplicit, 10))
+ assertThat(PickerUriResolver.wrapProviderUri(providerUriUserImplicit,
+ ACTION_PICK_IMAGES, 10))
.isEqualTo(mediaUriUser10);
- assertThat(PickerUriResolver.wrapProviderUri(providerUriUser10, 10))
+ assertThat(
+ PickerUriResolver.wrapProviderUri(providerUriUser10, ACTION_PICK_IMAGES,
+ 10))
.isEqualTo(mediaUriUser10);
assertThat(PickerUriResolver.unwrapProviderUri(mediaUriUser10))
.isEqualTo(providerUriUser10);
@@ -161,6 +182,8 @@
@Test
public void wrapProviderUriInvalid() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
final String providerSuffixLong = "authority/media/media_id/another_media_id";
final String providerSuffixShort = "authority/media";
@@ -171,18 +194,22 @@
final Uri mediaUriUserShort = Uri.parse("content://media/picker/0/" + providerSuffixShort);
assertThrows(IllegalArgumentException.class,
- () -> PickerUriResolver.wrapProviderUri(providerUriUserLong, 0));
+ () -> PickerUriResolver.wrapProviderUri(providerUriUserLong, ACTION_PICK_IMAGES,
+ 0));
assertThrows(IllegalArgumentException.class,
() -> PickerUriResolver.unwrapProviderUri(mediaUriUserLong));
assertThrows(IllegalArgumentException.class,
() -> PickerUriResolver.unwrapProviderUri(mediaUriUserShort));
assertThrows(IllegalArgumentException.class,
- () -> PickerUriResolver.wrapProviderUri(providerUriUserShort, 0));
+ () -> PickerUriResolver.wrapProviderUri(providerUriUserShort, ACTION_PICK_IMAGES,
+ 0));
}
@Test
public void testGetAlbumUri() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
final String authority = "foo";
final Uri uri = Uri.parse("content://foo/album");
assertThat(PickerUriResolver.getAlbumUri(authority)).isEqualTo(uri);
@@ -190,6 +217,8 @@
@Test
public void testGetMediaUri() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
final String authority = "foo";
final Uri uri = Uri.parse("content://foo/media");
assertThat(PickerUriResolver.getMediaUri(authority)).isEqualTo(uri);
@@ -197,6 +226,8 @@
@Test
public void testGetDeletedMediaUri() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
final String authority = "foo";
final Uri uri = Uri.parse("content://foo/deleted_media");
assertThat(PickerUriResolver.getDeletedMediaUri(authority)).isEqualTo(uri);
@@ -204,6 +235,8 @@
@Test
public void testCreateSurfaceControllerUri() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
final String authority = "foo";
final Uri uri = Uri.parse("content://foo/surface_controller");
assertThat(PickerUriResolver.createSurfaceControllerUri(authority)).isEqualTo(uri);
@@ -211,10 +244,13 @@
@Test
public void testOpenFile_mode_w() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
updateReadUriPermission(sTestPickerUri, /* grant */ true);
try {
sTestPickerUriResolver.openFile(sTestPickerUri, "w", /* signal */ null,
- /* callingPid */ -1, /* callingUid */ -1);
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ 0));
fail("Write is not supported for Picker Uris. uri: " + sTestPickerUri);
} catch (SecurityException expected) {
// expected
@@ -225,10 +261,13 @@
@Test
public void testOpenFile_mode_rw() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
updateReadUriPermission(sTestPickerUri, /* grant */ true);
try {
sTestPickerUriResolver.openFile(sTestPickerUri, "rw", /* signal */ null,
- /* callingPid */ -1, /* callingUid */ -1);
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ 0));
fail("Read-Write is not supported for Picker Uris. uri: " + sTestPickerUri);
} catch (SecurityException expected) {
// expected
@@ -239,10 +278,13 @@
@Test
public void testOpenFile_mode_invalid() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
updateReadUriPermission(sTestPickerUri, /* grant */ true);
try {
sTestPickerUriResolver.openFile(sTestPickerUri, "foo", /* signal */ null,
- /* callingPid */ -1, /* callingUid */ -1);
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ 0));
fail("Invalid mode should not be supported for openFile. uri: " + sTestPickerUri);
} catch (IllegalArgumentException expected) {
// expected
@@ -252,6 +294,8 @@
@Test
public void testPickerUriResolver_permissionDenied() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
updateReadUriPermission(sTestPickerUri, /* grant */ false);
testOpenFile_permissionDenied(sTestPickerUri);
@@ -262,13 +306,15 @@
@Test
public void testPermissionGrantedOnOtherUserUri() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
// This test requires the uri to be valid in 2 different users, but the permission is
// granted in one user only.
final int otherUserId = 50;
final Context otherUserContext = createOtherUserContext(otherUserId);
final Uri mediaStoreUserInAnotherValidUser = createTestFileInContext(otherUserContext);
final Uri grantedUri = getPickerUriForId(ContentUris.parseId(
- mediaStoreUserInAnotherValidUser), otherUserId);
+ mediaStoreUserInAnotherValidUser), otherUserId, ACTION_PICK_IMAGES);
updateReadUriPermission(grantedUri, /* grant */ true);
final Uri deniedUri = sTestPickerUri;
@@ -282,9 +328,12 @@
@Test
public void testPickerUriResolver_userInvalid() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
final int invalidUserId = 40;
- final Uri inValidUserPickerUri = getPickerUriForId(/* id */ 1, invalidUserId);
+ final Uri inValidUserPickerUri = getPickerUriForId(/* id */ 1, invalidUserId,
+ ACTION_PICK_IMAGES);
updateReadUriPermission(inValidUserPickerUri, /* grant */ true);
// This method is called on current context when pickerUriResolver wants to get the content
@@ -302,6 +351,8 @@
@Test
public void testPickerUriResolver_userValid() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
updateReadUriPermission(sTestPickerUri, /* grant */ true);
assertThat(PickerUriResolver.getUserId(sTestPickerUri)).isEqualTo(TEST_USER);
@@ -312,7 +363,102 @@
}
@Test
+ public void testPickerUriResolver_pickerUri_fileOpenWithRequireOriginal() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
+ // Grants given on original uri
+ updateReadUriPermission(sTestPickerUri, /* grant */ true);
+ sTestPickerUri = MediaStore.setRequireOriginal(sTestPickerUri);
+
+ assertThat(PickerUriResolver.getUserId(sTestPickerUri)).isEqualTo(TEST_USER);
+ try (ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(sTestPickerUri,
+ "r", /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ 0))) {
+ fail("Require original should not be supported for picker uri:" + sTestPickerUri);
+ } catch (UnsupportedOperationException expected) {
+ // expected
+ }
+ try (ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(sTestPickerUri,
+ "r", /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ 0))) {
+ fail("Require original should not be supported for picker uri:" + sTestPickerUri);
+ } catch (UnsupportedOperationException expected) {
+ // expected
+ }
+
+ try (AssetFileDescriptor afd = sTestPickerUriResolver.openTypedAssetFile(sTestPickerUri,
+ "image/*", /* opts */ null, /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ 0))) {
+ fail("Require original should not be supported for picker uri:" + sTestPickerUri);
+ } catch (UnsupportedOperationException expected) {
+ // expected
+ }
+ try (AssetFileDescriptor afd = sTestPickerUriResolver.openTypedAssetFile(sTestPickerUri,
+ "image/*", /* opts */ null, /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ 0))) {
+ fail("Require original should not be supported for picker uri:" + sTestPickerUri);
+ } catch (UnsupportedOperationException expected) {
+ // expected
+ }
+
+ testQuery(sTestPickerUri);
+ testGetType(sTestPickerUri, "image/jpeg");
+ }
+
+ @Test
+ public void testPickerUriResolver_pickerGetContentUri_fileOpenWithRequireOriginal()
+ throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_GET_CONTENT);
+ // Grants given on original uri
+ updateReadUriPermission(sTestPickerUri, /* grant */ true);
+ sTestPickerUri = MediaStore.setRequireOriginal(sTestPickerUri);
+
+ assertThat(PickerUriResolver.getUserId(sTestPickerUri)).isEqualTo(TEST_USER);
+ try (ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(sTestPickerUri,
+ "r", /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ ~LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED))) {
+ assertThat(pfd).isNotNull();
+ }
+ try (ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(sTestPickerUri,
+ "r", /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED))) {
+ fail("Require original should not be supported when calling package does not have "
+ + "required permission");
+ } catch (UnsupportedOperationException expected) {
+ // expected
+ }
+
+ try (AssetFileDescriptor afd = sTestPickerUriResolver.openTypedAssetFile(sTestPickerUri,
+ "image/*", /* opts */ null, /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ ~LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED))) {
+ assertThat(afd).isNotNull();
+ }
+ try (AssetFileDescriptor afd = sTestPickerUriResolver.openTypedAssetFile(sTestPickerUri,
+ "image/*", /* opts */ null, /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED))) {
+ fail("Require original should not be supported when calling package does not have "
+ + "required permission");
+ } catch (UnsupportedOperationException expected) {
+ // expected
+ }
+
+ testQuery(sTestPickerUri);
+ testGetType(sTestPickerUri, "image/jpeg");
+ }
+
+ @Test
public void testQueryUnknownColumn() throws Exception {
+ sTestPickerUri = getPickerUriForId(ContentUris.parseId(sMediaStoreUriInOtherContext),
+ TEST_USER, ACTION_PICK_IMAGES);
final int myUid = Process.myUid();
final int myPid = Process.myPid();
final String myPackageName = getContext().getPackageName();
@@ -367,25 +513,28 @@
Intent.FLAG_GRANT_READ_URI_PERMISSION)).thenReturn(permission);
}
- private static Uri getPickerUriForId(long id, int user) {
+ private static Uri getPickerUriForId(long id, int user, String action) {
final Uri providerUri = PickerUriResolver
.getMediaUri(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)
.buildUpon()
.appendPath(String.valueOf(id))
.build();
- return PickerUriResolver.wrapProviderUri(providerUri, user);
+ return PickerUriResolver.wrapProviderUri(providerUri, action, user);
}
private void testOpenFile(Uri uri) throws Exception {
try (ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(uri, "r", /* signal */ null,
- /* callingPid */ -1, /* callingUid */ -1)) {
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ 0))) {
assertThat(pfd).isNotNull();
}
}
private void testOpenTypedAssetFile(Uri uri) throws Exception {
try (AssetFileDescriptor afd = sTestPickerUriResolver.openTypedAssetFile(uri, "image/*",
- /* opts */ null, /* signal */ null, /* callingPid */ -1, /* callingUid */ -1)) {
+ /* opts */ null, /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ 0))) {
assertThat(afd).isNotNull();
}
}
@@ -409,8 +558,9 @@
private void testOpenFileInvalidUser(Uri uri) {
try {
- sTestPickerUriResolver.openFile(uri, "r", /* signal */ null, /* callingPid */ -1,
- /* callingUid */ -1);
+ sTestPickerUriResolver.openFile(uri, "r", /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ 0));
fail("Invalid user specified in the picker uri: " + uri);
} catch (FileNotFoundException expected) {
// expected
@@ -421,7 +571,9 @@
private void testOpenTypedAssetFileInvalidUser(Uri uri) throws Exception {
try {
sTestPickerUriResolver.openTypedAssetFile(uri, "image/*", /* opts */ null,
- /* signal */ null, /* callingPid */ -1, /* callingUid */ -1);
+ /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ 0));
fail("Invalid user specified in the picker uri: " + uri);
} catch (FileNotFoundException expected) {
// expected
@@ -449,8 +601,9 @@
private void testOpenFile_permissionDenied(Uri uri) throws Exception {
try {
- sTestPickerUriResolver.openFile(uri, "r", /* signal */ null, /* callingPid */ -1,
- /* callingUid */ -1);
+ sTestPickerUriResolver.openFile(uri, "r", /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ 0));
fail("openFile should fail if the caller does not have permission grant on the picker"
+ " uri: " + uri);
} catch (SecurityException expected) {
@@ -463,7 +616,9 @@
private void testOpenTypedAssetFile_permissionDenied(Uri uri) throws Exception {
try {
sTestPickerUriResolver.openTypedAssetFile(uri, "image/*", /* opts */ null,
- /* signal */ null, /* callingPid */ -1, /* callingUid */ -1);
+ /* signal */ null,
+ LocalCallingIdentity.forTest(sCurrentContext, /* uid */ -1, /* permission */
+ 0));
fail("openTypedAssetFile should fail if the caller does not have permission grant on"
+ " the picker uri: " + uri);
} catch (SecurityException expected) {
diff --git a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
index 7408e4b..f9ff002 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
@@ -17,6 +17,7 @@
package com.android.providers.media.photopicker;
import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
+import static com.android.providers.media.PickerUriResolver.INIT_PATH;
import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI;
import static com.android.providers.media.photopicker.NotificationContentObserver.MEDIA;
@@ -36,6 +37,7 @@
import android.content.res.Resources;
import android.database.ContentObserver;
import android.database.Cursor;
+import android.net.Uri;
import android.os.Handler;
import android.os.Process;
import android.os.storage.StorageManager;
@@ -1725,7 +1727,7 @@
// Simulate a UI session begins listening.
contentResolver.registerContentObserver(REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI,
- /* notifyForDescendants */ false, refreshUiNotificationObserver);
+ /* notifyForDescendants */ true, refreshUiNotificationObserver);
mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_2);
@@ -1733,6 +1735,11 @@
assertWithMessage("Refresh ui notification should have been received.")
.that(refreshUiNotificationObserver.mNotificationReceived).isTrue();
+
+ assertWithMessage("Refresh ui notification uri should not include init path.")
+ .that(refreshUiNotificationObserver.mNotificationUri.getLastPathSegment()
+ .equals(INIT_PATH))
+ .isFalse();
} finally {
contentResolver.unregisterContentObserver(refreshUiNotificationObserver);
}
@@ -1744,7 +1751,7 @@
final TestContentObserver refreshUiNotificationObserver = new TestContentObserver(null);
try {
contentResolver.registerContentObserver(REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI,
- /* notifyForDescendants */ false, refreshUiNotificationObserver);
+ /* notifyForDescendants */ true, refreshUiNotificationObserver);
assertWithMessage("Refresh ui notification should have not been received.")
.that(refreshUiNotificationObserver.mNotificationReceived).isFalse();
@@ -1760,7 +1767,12 @@
"Failed to receive refresh ui notification on change in cloud provider.")
.that(refreshUiNotificationObserver.mNotificationReceived).isTrue();
- refreshUiNotificationObserver.mNotificationReceived = false;
+ assertWithMessage("Refresh ui notification uri should not include init path.")
+ .that(refreshUiNotificationObserver.mNotificationUri.getLastPathSegment()
+ .equals(INIT_PATH))
+ .isTrue();
+
+ refreshUiNotificationObserver.clear();
// The SET_CLOUD_PROVIDER is called using a different cloud provider from before
mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
@@ -1769,7 +1781,12 @@
"Failed to receive refresh ui notification on change in cloud provider.")
.that(refreshUiNotificationObserver.mNotificationReceived).isTrue();
- refreshUiNotificationObserver.mNotificationReceived = false;
+ assertWithMessage("Refresh ui notification uri should not include init path.")
+ .that(refreshUiNotificationObserver.mNotificationUri.getLastPathSegment()
+ .equals(INIT_PATH))
+ .isTrue();
+
+ refreshUiNotificationObserver.clear();
// The cloud provider remains unchanged on PickerSyncController construction
mController = PickerSyncController
@@ -1879,14 +1896,21 @@
private static class TestContentObserver extends ContentObserver {
boolean mNotificationReceived;
+ Uri mNotificationUri;
TestContentObserver(Handler handler) {
super(handler);
}
@Override
- public void onChange(boolean selfChange) {
+ public void onChange(boolean selfChange, Uri uri) {
mNotificationReceived = true;
+ mNotificationUri = uri;
+ }
+
+ public void clear() {
+ mNotificationReceived = false;
+ mNotificationUri = null;
}
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
index d7838af..769223d 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
@@ -21,7 +21,6 @@
import static com.android.providers.media.util.MimeUtils.getExtensionFromMimeType;
-import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
@@ -41,6 +40,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.providers.media.PickerUriResolver;
import com.android.providers.media.ProjectionHelper;
import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
import com.android.providers.media.photopicker.sync.SyncTracker;
@@ -1351,20 +1351,38 @@
// Assert all projection columns
final String[] allProjection = mProjectionHelper.getProjectionMap(
PickerMediaColumns.class).keySet().toArray(new String[0]);
- try (Cursor cr = mFacade.queryMediaIdForApps(LOCAL_PROVIDER, LOCAL_ID,
- allProjection)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ try (Cursor cr = mFacade.queryMediaIdForApps(PickerUriResolver.PICKER_SEGMENT,
+ LOCAL_PROVIDER, LOCAL_ID, allProjection)) {
+ assertWithMessage(
+ "Unexpected number of rows when asserting all projection columns with "
+ + "PickerUriResolver as PICKER_SEGMENT on local provider.")
+ .that(cr.getCount()).isEqualTo(1);
cr.moveToFirst();
- assertMediaStoreCursor(cr, LOCAL_ID, DATE_TAKEN_MS);
+ assertMediaStoreCursor(cr, LOCAL_ID, DATE_TAKEN_MS, PickerUriResolver.PICKER_SEGMENT);
+ }
+
+ try (Cursor cr = mFacade.queryMediaIdForApps(PickerUriResolver.PICKER_GET_CONTENT_SEGMENT,
+ LOCAL_PROVIDER, LOCAL_ID, allProjection)) {
+ assertWithMessage(
+ "Unexpected number of rows when asserting all projection columns with "
+ + "PickerUriResolver as PICKER_GET_CONTENT_SEGMENT on local provider.")
+ .that(cr.getCount()).isEqualTo(1);
+
+ cr.moveToFirst();
+ assertMediaStoreCursor(cr, LOCAL_ID, DATE_TAKEN_MS,
+ PickerUriResolver.PICKER_GET_CONTENT_SEGMENT);
}
// Assert one projection column
final String[] oneProjection = new String[]{PickerMediaColumns.DATE_TAKEN};
- try (Cursor cr = mFacade.queryMediaIdForApps(CLOUD_PROVIDER, CLOUD_ID,
- oneProjection)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ try (Cursor cr = mFacade.queryMediaIdForApps(PickerUriResolver.PICKER_SEGMENT,
+ CLOUD_PROVIDER, CLOUD_ID, oneProjection)) {
+ assertWithMessage(
+ "Unexpected number of rows when asserting one projection column with cloud "
+ + "provider.")
+ .that(cr.getCount()).isEqualTo(1);
cr.moveToFirst();
assertWithMessage(
@@ -1380,9 +1398,12 @@
invalidColumn
};
- try (Cursor cr = mFacade.queryMediaIdForApps(CLOUD_PROVIDER, CLOUD_ID,
- invalidProjection)) {
- assertThat(cr.getCount()).isEqualTo(1);
+ try (Cursor cr = mFacade.queryMediaIdForApps(PickerUriResolver.PICKER_SEGMENT,
+ CLOUD_PROVIDER, CLOUD_ID, invalidProjection)) {
+ assertWithMessage(
+ "Unexpected number of rows when asserting invalid projection column with "
+ + "cloud provider.")
+ .that(cr.getCount()).isEqualTo(1);
cr.moveToFirst();
assertWithMessage(
@@ -2243,8 +2264,8 @@
return mediaId + getExtensionFromMimeType(mimeType);
}
- private static String getData(String authority, String displayName) {
- return "/sdcard/.transforms/synthetic/picker/0/" + authority + "/media/"
+ private static String getData(String authority, String displayName, String pickerSegmentType) {
+ return "/sdcard/.transforms/synthetic/" + pickerSegmentType + "/0/" + authority + "/media/"
+ displayName;
}
@@ -2270,8 +2291,10 @@
private static void assertCloudMediaCursor(Cursor cursor, String id, String mimeType) {
final String displayName = getDisplayName(id, mimeType);
- final String localData = getData(LOCAL_PROVIDER, displayName);
- final String cloudData = getData(CLOUD_PROVIDER, displayName);
+ final String localData = getData(LOCAL_PROVIDER, displayName,
+ PickerUriResolver.PICKER_SEGMENT);
+ final String cloudData = getData(CLOUD_PROVIDER, displayName,
+ PickerUriResolver.PICKER_SEGMENT);
assertWithMessage("Unexpected value of MediaColumns.ID for the cloud media cursor.")
.that(cursor.getString(cursor.getColumnIndex(MediaColumns.ID)))
@@ -2358,10 +2381,11 @@
}
}
- private static void assertMediaStoreCursor(Cursor cursor, String id, long dateTakenMs) {
+ private static void assertMediaStoreCursor(Cursor cursor, String id, long dateTakenMs,
+ String pickerSegmentType) {
final String displayName = getDisplayName(id, MP4_VIDEO_MIME_TYPE);
- final String localData = getData(LOCAL_PROVIDER, displayName);
- final String cloudData = getData(CLOUD_PROVIDER, displayName);
+ final String localData = getData(LOCAL_PROVIDER, displayName, pickerSegmentType);
+ final String cloudData = getData(CLOUD_PROVIDER, displayName, pickerSegmentType);
assertWithMessage(
"Unexpected value for PickerMediaColumns.DISPLAY_NAME for the media store cursor.")
diff --git a/tests/src/com/android/providers/media/photopicker/data/PickerResultTest.java b/tests/src/com/android/providers/media/photopicker/data/PickerResultTest.java
index e4ded70..3fb365d 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerResultTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerResultTest.java
@@ -16,9 +16,12 @@
package com.android.providers.media.photopicker.data;
+import static android.content.Intent.ACTION_GET_CONTENT;
+import static android.provider.MediaStore.ACTION_PICK_IMAGES;
import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_NONE;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.providers.media.PickerUriResolver.PICKER_GET_CONTENT_SEGMENT;
import static com.google.common.truth.Truth.assertThat;
@@ -58,7 +61,7 @@
}
/**
- * Tests {@link PickerResult#getPickerResponseIntent(boolean, List)} with single item
+ * Tests {@link PickerResult#getPickerResponseIntent(String, boolean, List)} with single item
* @throws Exception
*/
@Test
@@ -66,9 +69,10 @@
List<Item> items = null;
try {
items = createItemSelection(1);
- final Uri expectedPickerUri = PickerResult.getPickerUri(items.get(0).getContentUri());
+ final Uri expectedPickerUri = PickerResult.getPickerUri(ACTION_PICK_IMAGES,
+ items.get(0).getContentUri());
final Intent intent = PickerResult.getPickerResponseIntent(
- /* canSelectMultiple */ false, items);
+ ACTION_PICK_IMAGES, /* canSelectMultiple */ false, items);
final Uri result = intent.getData();
assertPickerUriFormat(result);
@@ -84,8 +88,32 @@
}
}
+ @Test
+ public void testGetResultSingleForActionGetContent() throws Exception {
+ List<Item> items = null;
+ try {
+ items = createItemSelection(1);
+ final Uri expectedPickerUri = PickerResult.getPickerUri(ACTION_GET_CONTENT,
+ items.get(0).getContentUri());
+ final Intent intent = PickerResult.getPickerResponseIntent(
+ ACTION_GET_CONTENT, /* canSelectMultiple */ false, items);
+
+ final Uri result = intent.getData();
+ assertGetContentPickerUriFormat(result);
+ assertThat(result).isEqualTo(expectedPickerUri);
+
+ final ClipData clipData = intent.getClipData();
+ assertThat(clipData).isNotNull();
+ final int count = clipData.getItemCount();
+ assertThat(count).isEqualTo(1);
+ assertThat(clipData.getItemAt(0).getUri()).isEqualTo(expectedPickerUri);
+ } finally {
+ deleteFiles(items);
+ }
+ }
+
/**
- * Tests {@link PickerResult#getPickerResponseIntent(boolean, List)} with multiple items
+ * Tests {@link PickerResult#getPickerResponseIntent(String, boolean, List)} with multiple items
* @throws Exception
*/
@Test
@@ -95,11 +123,12 @@
final int itemCount = 3;
items = createItemSelection(itemCount);
List<Uri> expectedPickerUris = new ArrayList<>();
- for (Item item: items) {
- expectedPickerUris.add(PickerResult.getPickerUri(item.getContentUri()));
+ for (Item item : items) {
+ expectedPickerUris.add(PickerResult.getPickerUri(ACTION_PICK_IMAGES,
+ item.getContentUri()));
}
- final Intent intent = PickerResult.getPickerResponseIntent(/* canSelectMultiple */ true,
- items);
+ final Intent intent = PickerResult.getPickerResponseIntent(
+ ACTION_PICK_IMAGES, /* canSelectMultiple */ true, items);
final ClipData clipData = intent.getClipData();
final int count = clipData.getItemCount();
@@ -114,10 +143,36 @@
}
}
+ @Test
+ public void testGetResultMultipleForActionGetContent() throws Exception {
+ ArrayList<Item> items = null;
+ try {
+ final int itemCount = 3;
+ items = createItemSelection(itemCount);
+ List<Uri> expectedPickerUris = new ArrayList<>();
+ for (Item item : items) {
+ expectedPickerUris.add(PickerResult.getPickerUri(ACTION_GET_CONTENT,
+ item.getContentUri()));
+ }
+ final Intent intent = PickerResult.getPickerResponseIntent(
+ ACTION_GET_CONTENT, /* canSelectMultiple */ true, items);
+
+ final ClipData clipData = intent.getClipData();
+ final int count = clipData.getItemCount();
+ assertThat(count).isEqualTo(itemCount);
+ for (int i = 0; i < count; i++) {
+ Uri uri = clipData.getItemAt(i).getUri();
+ assertGetContentPickerUriFormat(uri);
+ assertThat(uri).isEqualTo(expectedPickerUris.get(i));
+ }
+ } finally {
+ deleteFiles(items);
+ }
+ }
+
/**
- * Tests {@link PickerResult#getPickerResponseIntent(boolean, List)} when the user selected
- * only one item in multi-select mode
- * @throws Exception
+ * Tests {@link PickerResult#getPickerResponseIntent(String, boolean, List)} when the user
+ * selected only one item in multi-select mode
*/
@Test
public void testGetResultMultiple_onlyOneItemSelected() throws Exception {
@@ -125,9 +180,10 @@
try {
final int itemCount = 1;
items = createItemSelection(itemCount);
- final Uri expectedPickerUri = PickerResult.getPickerUri(items.get(0).getContentUri());
- final Intent intent = PickerResult.getPickerResponseIntent(/* canSelectMultiple */ true,
- items);
+ final Uri expectedPickerUri = PickerResult.getPickerUri(ACTION_PICK_IMAGES,
+ items.get(0).getContentUri());
+ final Intent intent = PickerResult.getPickerResponseIntent(
+ ACTION_PICK_IMAGES, /* canSelectMultiple */ true, items);
final ClipData clipData = intent.getClipData();
final int count = clipData.getItemCount();
@@ -144,6 +200,12 @@
assertThat(uri.toString().startsWith(pickerUriPrefix)).isTrue();
}
+ private void assertGetContentPickerUriFormat(Uri uri) {
+ final String pickerNonRedactedUriPrefix = MediaStore.AUTHORITY_URI.buildUpon().appendPath(
+ PICKER_GET_CONTENT_SEGMENT).build().toString();
+ assertThat(uri.toString().startsWith(pickerNonRedactedUriPrefix)).isTrue();
+ }
+
/**
* Returns a PhotoSelection on which the test app does not have access to.
*/
diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
index 4ca8dd9..4ca0e7b 100644
--- a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
@@ -76,6 +76,7 @@
import com.android.providers.media.photopicker.data.model.Category;
import com.android.providers.media.photopicker.data.model.Item;
import com.android.providers.media.photopicker.data.model.ModelTestUtils;
+import com.android.providers.media.photopicker.data.model.RefreshRequest;
import com.android.providers.media.photopicker.data.model.UserId;
import org.junit.Before;
@@ -598,6 +599,53 @@
}
@Test
+ public void testParseValuesFromPickImagesIntent_launchPickerInPhotosTab() {
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB, MediaStore.PICK_IMAGES_TAB_IMAGES);
+
+ mPickerViewModel.parseValuesFromIntent(intent);
+
+ assertThat(mPickerViewModel.getPickerLaunchTab()).isEqualTo(
+ MediaStore.PICK_IMAGES_TAB_IMAGES);
+ }
+
+ @Test
+ public void testParseValuesFromPickImagesIntent_launchPickerInAlbumsTab() {
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB, MediaStore.PICK_IMAGES_TAB_ALBUMS);
+
+ mPickerViewModel.parseValuesFromIntent(intent);
+
+ assertThat(mPickerViewModel.getPickerLaunchTab()).isEqualTo(
+ MediaStore.PICK_IMAGES_TAB_ALBUMS);
+ }
+
+ @Test
+ public void testParseValuesFromPickImagesIntent_launchPickerWithIncorrectTabOption() {
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB, 2);
+
+ try {
+ mPickerViewModel.parseValuesFromIntent(intent);
+ fail("Incorrect value passed for the picker launch tab option in the intent");
+ } catch (IllegalArgumentException expected) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testParseValuesFromGetContentIntent_extraPickerLaunchTab() {
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB, MediaStore.PICK_IMAGES_TAB_ALBUMS);
+
+ mPickerViewModel.parseValuesFromIntent(intent);
+
+ // GET_CONTENT doesn't support this option. Launch tab will always default to photos
+ assertThat(mPickerViewModel.getPickerLaunchTab()).isEqualTo(
+ MediaStore.PICK_IMAGES_TAB_IMAGES);
+ }
+
+ @Test
public void testShouldShowOnlyLocalFeatures() {
mConfigStore.enableCloudMediaFeature();
@@ -618,17 +666,17 @@
@Test
public void testRefreshUiNotifications() throws InterruptedException {
- final LiveData<Boolean> shouldRefreshUi = mPickerViewModel.shouldRefreshUiLiveData();
- assertFalse(shouldRefreshUi.getValue());
+ final LiveData<RefreshRequest> shouldRefreshUi = mPickerViewModel.refreshUiLiveData();
+ assertFalse(shouldRefreshUi.getValue().shouldRefreshPicker());
final ContentResolver contentResolver = sTargetContext.getContentResolver();
contentResolver.notifyChange(REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI, null);
TimeUnit.MILLISECONDS.sleep(100);
- assertTrue(shouldRefreshUi.getValue());
+ assertTrue(shouldRefreshUi.getValue().shouldRefreshPicker());
- mPickerViewModel.resetAllContentInCurrentProfile();
- assertFalse(shouldRefreshUi.getValue());
+ mPickerViewModel.resetAllContentInCurrentProfile(false);
+ assertFalse(shouldRefreshUi.getValue().shouldRefreshPicker());
}
@Test
diff --git a/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java b/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java
index 3405ff0..0aee9df 100644
--- a/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java
+++ b/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java
@@ -18,6 +18,7 @@
import static com.android.providers.media.tests.utils.PublicVolumeSetupHelper.createNewPublicVolume;
import static com.android.providers.media.tests.utils.PublicVolumeSetupHelper.deletePublicVolumes;
+import static com.android.providers.media.tests.utils.PublicVolumeSetupHelper.executeShellCommand;
import static com.android.providers.media.util.FileUtils.getVolumePath;
import static org.junit.Assert.assertEquals;
@@ -33,6 +34,7 @@
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Build;
import android.os.Environment;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -45,6 +47,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.providers.media.ConfigStore;
+import com.android.providers.media.DatabaseBackupAndRecovery;
import com.android.providers.media.stableuris.dao.BackupIdRow;
import org.junit.AfterClass;
@@ -63,7 +66,6 @@
import java.util.Set;
@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = 31, codeName = "S")
public class StableUriIdleMaintenanceServiceTest {
private static final String TAG = "StableUriIdleMaintenanceServiceTest";
@@ -86,6 +88,9 @@
@BeforeClass
public static void setUpClass() throws Exception {
adoptShellPermission();
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
+ return;
+ }
// Read existing value of the flag
sInitialDeviceConfigValueForInternal = Boolean.parseBoolean(
@@ -110,6 +115,10 @@
@AfterClass
public static void tearDownClass() throws Exception {
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
+ dropShellPermission();
+ return;
+ }
// Restore previous value of the flag
DeviceConfig.setProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
@@ -126,6 +135,7 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 31, codeName = "S")
public void testDataMigrationForInternalVolume() throws Exception {
final Context context = InstrumentationRegistry.getTargetContext();
final ContentResolver resolver = context.getContentResolver();
@@ -163,6 +173,11 @@
public void testDataMigrationForExternalVolume() throws Exception {
final Context context = InstrumentationRegistry.getTargetContext();
final ContentResolver resolver = context.getContentResolver();
+ // Enable feature for Android R
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
+ executeShellCommand(
+ "setprop " + DatabaseBackupAndRecovery.STABLE_URI_EXTERNAL_PROPERTY + " true");
+ }
Set<String> newFilePaths = new HashSet<String>();
Map<String, Long> pathToIdMap = new HashMap<>();
MediaStore.waitForIdle(resolver);
@@ -207,6 +222,12 @@
for (String path : newFilePaths) {
new File(path).delete();
}
+
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
+ executeShellCommand(
+ "setprop " + DatabaseBackupAndRecovery.STABLE_URI_EXTERNAL_PROPERTY
+ + " false");
+ }
}
}
diff --git a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
index 37dd48f..a105bbd 100644
--- a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
+++ b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import android.content.Context;
@@ -27,11 +28,14 @@
import com.android.providers.media.R;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -97,6 +101,18 @@
assertEquals("3F9DD7A46B26513A7C35272F0D623A06", xmp.getOriginalDocumentId());
}
+
+ @Test
+ @Ignore // This test creates a file that causes MediaProvider to OOM with our current
+ // IsoInterface implementation.
+ // While MediaProvider should now be resistant to that, we cannot leave this test safely enabled
+ // in a test suite as for b/316578793
+ // Leaving its implementation here to test further improvement to IsoInterface implementation.
+ public void testFileWithTooManyBoxesDoesNotRunOutOfMemory() throws Exception {
+ final File file = createFileWithLotsOfBoxes("too-many-boxes");
+ assertThrows(IOException.class, () -> IsoInterface.fromFile(file));
+ }
+
@Test
public void testIsoMeta() throws Exception {
final IsoInterface isoMeta = IsoInterface.fromFile(stageFile(R.raw.test_video_xmp));
@@ -128,4 +144,19 @@
}
return file;
}
+
+ private static File createFileWithLotsOfBoxes(String filename) throws Exception {
+ File file = File.createTempFile(filename, ".mp4");
+ try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file))) {
+ byte[] sizeHeader = new byte[]{0x00, 0x00, 0x00, 0x08};
+ out.write(sizeHeader);
+ out.write("ftyp".getBytes());
+ byte[] freeBlock = "free".getBytes();
+ for (int i = 0; i < 5000000; i++) {
+ out.write(sizeHeader);
+ out.write(freeBlock);
+ }
+ }
+ return file;
+ }
}
diff --git a/tests/src/com/android/providers/media/util/SyntheticPathUtilsTest.java b/tests/src/com/android/providers/media/util/SyntheticPathUtilsTest.java
index b913114..0c8c5df 100644
--- a/tests/src/com/android/providers/media/util/SyntheticPathUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/SyntheticPathUtilsTest.java
@@ -23,9 +23,13 @@
import static com.android.providers.media.util.SyntheticPathUtils.isPickerPath;
import static com.android.providers.media.util.SyntheticPathUtils.isRedactedPath;
import static com.android.providers.media.util.SyntheticPathUtils.isSyntheticPath;
+
import static com.google.common.truth.Truth.assertThat;
import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.PickerUriResolver;
+
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -46,7 +50,10 @@
@Test
public void testGetPickerRelativePath() throws Exception {
- assertThat(getPickerRelativePath()).isEqualTo(".transforms/synthetic/picker");
+ assertThat(getPickerRelativePath(PickerUriResolver.PICKER_SEGMENT)).isEqualTo(
+ ".transforms/synthetic/picker");
+ assertThat(getPickerRelativePath(PickerUriResolver.PICKER_GET_CONTENT_SEGMENT)).isEqualTo(
+ ".transforms/synthetic/picker_get_content");
}
@Test
diff --git a/tools/photopicker/res/layout/activity_main.xml b/tools/photopicker/res/layout/activity_main.xml
index 6348a4e..d5db47d 100644
--- a/tools/photopicker/res/layout/activity_main.xml
+++ b/tools/photopicker/res/layout/activity_main.xml
@@ -100,6 +100,7 @@
android:textSize="16sp" />
</LinearLayout>
+
<CheckBox
android:id="@+id/cbx_ordered_selection"
android:layout_width="wrap_content"
@@ -107,6 +108,37 @@
android:text="ORDERED SELECTION"
android:textSize="16sp" />
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <CheckBox
+ android:id="@+id/cbx_set_picker_launch_tab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/picker_launch_tab_option"
+ android:textSize="16sp" />
+
+ <RadioGroup
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <RadioButton android:id="@+id/rb_albums"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Albums"
+ android:enabled="false"/>
+ <RadioButton android:id="@+id/rb_photos"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Photos"
+ android:enabled="false"/>
+ </RadioGroup>
+
+ </LinearLayout>
+
+
<Button
android:id="@+id/launch_button"
android:layout_width="match_parent"
diff --git a/tools/photopicker/res/values/strings.xml b/tools/photopicker/res/values/strings.xml
new file mode 100644
index 0000000..b6b6131
--- /dev/null
+++ b/tools/photopicker/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources>
+ <!-- Picker launch tab checkbox label -->
+ <string name="picker_launch_tab_option">SET PICKER LAUNCH TAB</string>
+</resources>
\ No newline at end of file
diff --git a/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java b/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java
index 1d549f3..98e58c0 100644
--- a/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java
+++ b/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java
@@ -34,6 +34,7 @@
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
+import android.widget.RadioButton;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.VideoView;
@@ -48,6 +49,8 @@
private static final String TAG = "PhotoPickerToolActivity";
private static final String EXTRA_PICK_IMAGES_MAX = "android.provider.extra.PICK_IMAGES_MAX";
private static final String ACTION_PICK_IMAGES = "android.provider.action.PICK_IMAGES";
+ private static final String EXTRA_PICK_IMAGES_LAUNCH_TAB =
+ "android.provider.extra.PICK_IMAGES_LAUNCH_TAB";
private static final int PICK_IMAGES_MAX_LIMIT = 100;
private static final int REQUEST_CODE = 42;
@@ -63,10 +66,17 @@
private CheckBox mSetSelectionCountCheckBox;
private CheckBox mAllowMultipleCheckBox;
private CheckBox mGetContentCheckBox;
+
private CheckBox mOrderedSelectionCheckBox;
+
+ private CheckBox mPickerLaunchTabCheckBox;
+
private EditText mMaxCountText;
private EditText mMimeTypeText;
+ private RadioButton mAlbumsRadioButton;
+ private RadioButton mPhotosRadioButton;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -82,12 +92,17 @@
mMaxCountText = findViewById(R.id.edittext_max_count);
mMimeTypeText = findViewById(R.id.edittext_mime_type);
mScrollView = findViewById(R.id.scrollview);
+ mPickerLaunchTabCheckBox = findViewById(R.id.cbx_set_picker_launch_tab);
+ mAlbumsRadioButton = findViewById(R.id.rb_albums);
+ mPhotosRadioButton = findViewById(R.id.rb_photos);
mSetImageOnlyCheckBox.setOnCheckedChangeListener(this::onShowImageOnlyCheckedChanged);
mSetVideoOnlyCheckBox.setOnCheckedChangeListener(this::onShowVideoOnlyCheckedChanged);
mSetMimeTypeCheckBox.setOnCheckedChangeListener(this::onSetMimeTypeCheckedChanged);
mSetSelectionCountCheckBox.setOnCheckedChangeListener(
this::onSetSelectionCountCheckedChanged);
+ mPickerLaunchTabCheckBox.setOnCheckedChangeListener(
+ this::onSetPickerLaunchTabCheckedChanged);
mMaxCountText.addTextChangedListener(new TextWatcher() {
@Override
@@ -157,6 +172,11 @@
mMaxCountText.setEnabled(isChecked);
}
+ private void onSetPickerLaunchTabCheckedChanged(View view, boolean isChecked) {
+ mAlbumsRadioButton.setEnabled(isChecked);
+ mPhotosRadioButton.setEnabled(isChecked);
+ }
+
private void onLaunchButtonClicked(View view) {
final Intent intent;
if (mGetContentCheckBox.isChecked()) {
@@ -164,6 +184,16 @@
intent.setType("*/*");
} else {
intent = new Intent(ACTION_PICK_IMAGES);
+ // This extra is not permitted in GET_CONTENT
+ if (mPickerLaunchTabCheckBox.isChecked()) {
+ int launchTab;
+ if (mAlbumsRadioButton.isChecked()) {
+ launchTab = 0;
+ } else {
+ launchTab = 1;
+ }
+ intent.putExtra(EXTRA_PICK_IMAGES_LAUNCH_TAB, launchTab);
+ }
}
if (mAllowMultipleCheckBox.isChecked()) {