Merge "Ignore flaky test until fixed." into sc-dev
diff --git a/Android.bp b/Android.bp
index a809320..3cad281 100644
--- a/Android.bp
+++ b/Android.bp
@@ -30,8 +30,8 @@
libs: [
"unsupportedappusage",
"app-compat-annotations",
+ "framework-annotations-lib",
"framework-mediaprovider.impl",
- "framework_mediaprovider_annotation",
"framework-media.stubs.module_lib",
"framework-statsd",
],
diff --git a/apex/framework/Android.bp b/apex/framework/Android.bp
index 66d9a5b..f770105 100644
--- a/apex/framework/Android.bp
+++ b/apex/framework/Android.bp
@@ -36,7 +36,6 @@
installable: true,
libs: [
- "framework_mediaprovider_annotation",
"framework-media.stubs.module_lib",
"unsupportedappusage",
],
@@ -59,10 +58,3 @@
],
path: "java",
}
-
-java_library {
- name: "framework_mediaprovider_annotation",
- srcs: [":framework-mediaprovider-annotation-sources"],
- installable: false,
- sdk_version: "core_current",
-}
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 9fb2a23..084f9e0 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -1860,6 +1860,12 @@
public static final int MEDIA_TYPE_DOCUMENT = 6;
/**
+ * Constant indicating the count of {@link #MEDIA_TYPE} columns.
+ * @hide
+ */
+ public static final int MEDIA_TYPE_COUNT = 7;
+
+ /**
* Modifier of the database row
*
* Specifies the last modifying operation of the database row. This
@@ -4255,6 +4261,9 @@
/**
* Returns true if the given application is the current system gallery of the device.
+ * <p>
+ * The system gallery is one app chosen by the OEM that has read & write access to all photos
+ * and videos on the device and control over folders in media collections.
*
* @param resolver The {@link ContentResolver} used to connect with
* {@link MediaStore#AUTHORITY}. Typically this value is {@link Context#getContentResolver()}.
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 728983b..318fbed 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -19,7 +19,7 @@
<string name="uid_label" msgid="8421971615411294156">"מדיה"</string>
<string name="storage_description" msgid="4081716890357580107">"אחסון מקומי"</string>
<string name="app_label" msgid="9035307001052716210">"אחסון מדיה"</string>
- <string name="artist_label" msgid="8105600993099120273">"אמן"</string>
+ <string name="artist_label" msgid="8105600993099120273">"אומן"</string>
<string name="unknown" msgid="2059049215682829375">"לא ידוע"</string>
<string name="root_images" msgid="5861633549189045666">"תמונות"</string>
<string name="root_videos" msgid="8792703517064649453">"סרטונים"</string>
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 96d863c..416fee7 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -27,6 +27,7 @@
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.database.Cursor.FIELD_TYPE_BLOB;
import static android.provider.MediaStore.MATCH_DEFAULT;
import static android.provider.MediaStore.MATCH_EXCLUDE;
import static android.provider.MediaStore.MATCH_INCLUDE;
@@ -153,6 +154,7 @@
import android.provider.BaseColumns;
import android.provider.Column;
import android.provider.DeviceConfig;
+import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio;
import android.provider.MediaStore.Audio.AudioColumns;
@@ -262,7 +264,10 @@
"(?:image_id|video_id)\\s*=\\s*(\\d+)");
/** File access by uid requires the transcoding transform */
- private static final int FLAG_TRANSFORM_TRANSCODING = 1;
+ private static final int FLAG_TRANSFORM_TRANSCODING = 1 << 0;
+
+ /** File access by uid is a synthetic path corresponding to a redacted URI */
+ private static final int FLAG_TRANSFORM_REDACTION = 1 << 1;
/**
* These directory names aren't declared in Environment as final variables, and so we need to
@@ -289,6 +294,9 @@
private static final String DIRECTORY_THUMBNAILS = ".thumbnails";
private static final List<String> PRIVATE_SUBDIRECTORIES_ANDROID = Arrays.asList("data", "obb");
private static final String REDACTED_URI_ID_PREFIX = "RUID";
+ private static final String TRANSFORMS_SYNTHETIC_DIR = ".transforms/synthetic";
+ private static final String REDACTED_URI_DIR = TRANSFORMS_SYNTHETIC_DIR + "/redacted";
+ public static final int REDACTED_URI_ID_SIZE = 36;
/**
* Hard-coded filename where the current value of
@@ -379,7 +387,9 @@
private static final int sUserId = UserHandle.myUserId();
- // WARNING/TODO (b/173505864): This will be replaced by signature APIs in S
+ /**
+ * Please use {@link getDownloadsProviderAuthority()} instead of using this directly.
+ */
private static final String DOWNLOADS_PROVIDER_AUTHORITY = "downloads";
@GuardedBy("mPendingOpenInfo")
@@ -997,6 +1007,9 @@
mTranscodeHelper = new TranscodeHelper(context, this);
+ // Create dir for redacted URI's path.
+ new File(getStorageRootPathForUid(UserHandle.myUserId()), REDACTED_URI_DIR).mkdirs();
+
final IntentFilter packageFilter = new IntentFilter();
packageFilter.setPriority(10);
packageFilter.addDataScheme("package");
@@ -1054,15 +1067,14 @@
}
ProviderInfo provider = mPackageManager.resolveContentProvider(
- DOWNLOADS_PROVIDER_AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE
+ getDownloadsProviderAuthority(), PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
if (provider != null) {
mDownloadsAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
}
- provider = mPackageManager.resolveContentProvider(
- MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
+ provider = mPackageManager.resolveContentProvider(getExternalStorageProviderAuthority(),
+ PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
if (provider != null) {
mExternalStorageAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
}
@@ -1491,6 +1503,10 @@
@Keep
public FileLookupResult onFileLookupForFuse(String path, int uid, int tid) {
uid = getBinderUidForFuse(uid, tid);
+ if (isSyntheticFilePathForRedactedUri(path, uid)) {
+ return getFileLookupResultsForRedactedUriPath(uid, path);
+ }
+
String ioPath = "";
boolean transformsComplete = true;
boolean transformsSupported = mTranscodeHelper.supportsTranscode(path);
@@ -1520,6 +1536,50 @@
transformsSupported, ioPath);
}
+ private boolean isSyntheticFilePathForRedactedUri(String path, int uid) {
+ if (path == null) return false;
+
+ final String transformsSyntheticDir = getStorageRootPathForUid(uid) + "/"
+ + REDACTED_URI_DIR;
+ final String fileName = extractDisplayName(path);
+ return fileName != null && path.toLowerCase(Locale.ROOT).startsWith(
+ transformsSyntheticDir.toLowerCase(Locale.ROOT)) && fileName.startsWith(
+ REDACTED_URI_ID_PREFIX) && fileName.length() == REDACTED_URI_ID_SIZE;
+ }
+
+ private boolean isSyntheticDirPath(String path, int uid) {
+ final String transformsSyntheticDir = getStorageRootPathForUid(uid) + "/"
+ + TRANSFORMS_SYNTHETIC_DIR;
+ return path != null && path.toLowerCase(Locale.ROOT).startsWith(
+ transformsSyntheticDir.toLowerCase(Locale.ROOT));
+ }
+
+ private FileLookupResult getFileLookupResultsForRedactedUriPath(int uid, @NonNull String path) {
+ final LocalCallingIdentity token = clearLocalCallingIdentity();
+ final String fileName = extractDisplayName(path);
+
+ final DatabaseHelper helper;
+ try {
+ helper = getDatabaseForUri(FileUtils.getContentUriForPath(path));
+ } catch (VolumeNotFoundException e) {
+ throw new IllegalStateException("Volume not found for file: " + path);
+ }
+
+ try (final Cursor c = helper.runWithoutTransaction(
+ (db) -> db.query("files", new String[]{MediaColumns.DATA},
+ FileColumns.REDACTED_URI_ID + "=?", new String[]{fileName}, null, null,
+ null))) {
+ if (!c.moveToFirst()) {
+ return new FileLookupResult(FLAG_TRANSFORM_REDACTION, 0, uid, false, true, null);
+ }
+
+ return new FileLookupResult(FLAG_TRANSFORM_REDACTION, 0, uid, true, true,
+ c.getString(0));
+ } finally {
+ restoreLocalCallingIdentity(token);
+ }
+ }
+
public int getBinderUidForFuse(int uid, int tid) {
if (uid != MY_UID) {
return uid;
@@ -2572,6 +2632,15 @@
final LocalCallingIdentity token = clearLocalCallingIdentity(
LocalCallingIdentity.fromExternal(getContext(), uid));
+ if(isRedactedUri(uri)) {
+ if((modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
+ // we don't allow write grants on redacted uris.
+ return PackageManager.PERMISSION_DENIED;
+ }
+
+ uri = getUriForRedactedUri(uri);
+ }
+
try {
final boolean allowHidden = isCallingPackageAllowedHidden();
final int table = matchUri(uri, allowHidden);
@@ -2658,6 +2727,12 @@
final ArraySet<String> honoredArgs = new ArraySet<>();
DatabaseUtils.resolveQueryArgs(queryArgs, honoredArgs::add, this::ensureCustomCollator);
+ Uri redactedUri = null;
+ if (isRedactedUri(uri)) {
+ redactedUri = uri;
+ uri = getUriForRedactedUri(uri);
+ }
+
uri = safeUncanonicalize(uri);
final String volumeName = getVolumeName(uri);
@@ -2770,6 +2845,16 @@
honoredArgs.toArray(new String[honoredArgs.size()]));
c.setExtras(extras);
}
+
+ // Query was on a redacted URI, update the sensitive information such as the _ID, DATA etc.
+ if (redactedUri != null && c != null) {
+ try {
+ return getRedactedUriCursor(redactedUri, c);
+ } finally {
+ c.close();
+ }
+ }
+
return c;
}
@@ -2778,6 +2863,95 @@
return REDACTED_URI_SUPPORTED_TYPES.contains(match);
}
+ private Cursor getRedactedUriCursor(Uri redactedUri, @NonNull Cursor c) {
+ final HashSet<String> columnNames = new HashSet<>(Arrays.asList(c.getColumnNames()));
+ final MatrixCursor redactedUriCursor = new MatrixCursor(c.getColumnNames());
+ final String redactedUriId = redactedUri.getLastPathSegment();
+
+ if (!c.moveToFirst()) {
+ return redactedUriCursor;
+ }
+
+ // NOTE: It is safe to assume that there will only be one entry corresponding to a
+ // redacted URI as it corresponds to a unique DB entry.
+ if (c.getCount() != 1) {
+ throw new AssertionError("Two rows corresponding to " + redactedUri.toString()
+ + " found, when only one expected");
+ }
+
+ final MatrixCursor.RowBuilder row = redactedUriCursor.newRow();
+ for (String columnName : c.getColumnNames()) {
+ final int colIndex = c.getColumnIndex(columnName);
+ if (c.getType(colIndex) == FIELD_TYPE_BLOB) {
+ row.add(c.getBlob(colIndex));
+ } else {
+ row.add(c.getString(colIndex));
+ }
+ }
+
+ updateRow(columnNames, MediaColumns._ID, row, redactedUriId);
+ updateRow(columnNames, MediaColumns.DISPLAY_NAME, row, redactedUriId);
+ updateRow(columnNames, MediaColumns.RELATIVE_PATH, row, REDACTED_URI_DIR);
+ updateRow(columnNames, MediaColumns.BUCKET_DISPLAY_NAME, row, REDACTED_URI_DIR);
+ updateRow(columnNames, MediaColumns.DATA, row, getPathForRedactedUriId(redactedUriId));
+ updateRow(columnNames, MediaColumns.DOCUMENT_ID, row, null);
+ updateRow(columnNames, MediaColumns.INSTANCE_ID, row, null);
+ updateRow(columnNames, MediaColumns.BUCKET_ID, row, null);
+
+ return redactedUriCursor;
+ }
+
+ static private String getPathForRedactedUriId(String redactedUriId) {
+ return getStorageRootPathForUid(Binder.getCallingUid()) + "/" + REDACTED_URI_DIR + "/"
+ + redactedUriId;
+ }
+
+ static private String getStorageRootPathForUid(int uid) {
+ return "/storage/emulated/" + (uid / PER_USER_RANGE);
+ }
+
+ private void updateRow(HashSet<String> columnNames, String columnName,
+ MatrixCursor.RowBuilder row, Object val) {
+ if (columnNames.contains(columnName)) {
+ row.add(columnName, val);
+ }
+ }
+
+ private Uri getUriForRedactedUri(Uri redactedUri) {
+ final Uri.Builder builder = redactedUri.buildUpon();
+ builder.path(null);
+ final List<String> segments = redactedUri.getPathSegments();
+ for (int i = 0; i < segments.size() - 1; i++) {
+ builder.appendPath(segments.get(i));
+ }
+
+ DatabaseHelper helper;
+ try {
+ helper = getDatabaseForUri(redactedUri);
+ } catch (VolumeNotFoundException e) {
+ throw e.rethrowAsIllegalArgumentException();
+ }
+
+ try (final Cursor c = helper.runWithoutTransaction(
+ (db) -> db.query("files", new String[]{MediaColumns._ID},
+ FileColumns.REDACTED_URI_ID + "=?",
+ new String[]{redactedUri.getLastPathSegment()}, null, null, null))) {
+ if (!c.moveToFirst()) {
+ throw new IllegalArgumentException(
+ "Uri: " + redactedUri.toString() + " not found.");
+ }
+
+ builder.appendPath(c.getString(0));
+ return builder.build();
+ }
+ }
+
+ private boolean isRedactedUri(Uri uri) {
+ String id = uri.getLastPathSegment();
+ return id != null && id.startsWith(REDACTED_URI_ID_PREFIX)
+ && id.length() == REDACTED_URI_ID_SIZE;
+ }
+
@Override
public String getType(Uri url) {
final int match = matchUri(url, true);
@@ -3680,6 +3854,7 @@
}
final long rowId;
+ Uri newUri = uri;
{
if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
String name = values.getAsString(Audio.Playlists.NAME);
@@ -3706,6 +3881,9 @@
values.put(FileColumns.SIZE, file.length());
}
}
+ if (!isFuseThread() && shouldFileBeHidden(file)) {
+ newUri = MediaStore.Files.getContentUri(MediaStore.getVolumeName(uri));
+ }
}
rowId = insertAllowingUpsert(qb, helper, values, path);
@@ -3716,7 +3894,7 @@
}
}
- return ContentUris.withAppendedId(uri, rowId);
+ return ContentUris.withAppendedId(newUri, rowId);
}
/**
@@ -4961,6 +5139,11 @@
throws FallbackException {
extras = (extras != null) ? extras : new Bundle();
+ if (isRedactedUri(uri)) {
+ // we don't support deletion on redacted uris.
+ return 0;
+ }
+
// INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
@@ -4968,7 +5151,7 @@
final boolean allowHidden = isCallingPackageAllowedHidden();
final int match = matchUri(uri, allowHidden);
- switch(match) {
+ switch (match) {
case AUDIO_MEDIA_ID:
case AUDIO_PLAYLISTS_ID:
case VIDEO_MEDIA_ID:
@@ -5063,6 +5246,7 @@
};
final boolean isFilesTable = qb.getTables().equals("files");
final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>();
+ final int[] countPerMediaType = new int[FileColumns.MEDIA_TYPE_COUNT];
if (isFilesTable) {
String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA);
if (deleteparam == null || ! deleteparam.equals("false")) {
@@ -5080,7 +5264,13 @@
mCallingIdentity.get().setOwned(id, false);
deleteIfAllowed(uri, extras, data);
- count += qb.delete(helper, BaseColumns._ID + "=" + id, null);
+ int res = qb.delete(helper, BaseColumns._ID + "=" + id, null);
+ count += res;
+ // Avoid ArrayIndexOutOfBounds if more mediaTypes are added,
+ // but mediaTypeSize is not updated
+ if (res > 0 && mediaType < countPerMediaType.length) {
+ countPerMediaType[mediaType] += res;
+ }
if (isDownload == 1) {
deletedDownloadIds.put(id, mimeType);
@@ -5136,7 +5326,7 @@
if (isFilesTable && !isCallingPackageSelf()) {
Metrics.logDeletion(volumeName, mCallingIdentity.get().uid,
- getCallingPackageOrSelf(), count);
+ getCallingPackageOrSelf(), count, countPerMediaType);
}
}
@@ -5393,7 +5583,7 @@
try (ContentProviderClient client = getContext().getContentResolver()
.acquireUnstableContentProviderClient(
- MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
+ getExternalStorageProviderAuthority())) {
extras.putParcelable(MediaStore.EXTRA_URI, fileUri);
return client.call(method, null, extras);
} catch (RemoteException e) {
@@ -5408,7 +5598,7 @@
final Uri fileUri;
try (ContentProviderClient client = getContext().getContentResolver()
.acquireUnstableContentProviderClient(
- MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
+ getExternalStorageProviderAuthority())) {
final Bundle res = client.call(method, null, extras);
fileUri = res.getParcelable(MediaStore.EXTRA_URI);
} catch (RemoteException e) {
@@ -5470,8 +5660,12 @@
extras.getParcelable(MediaStore.EXTRA_FILE_DESCRIPTOR);
try {
File file = getFileFromFileDescriptor(inputPfd);
- FuseDaemon fuseDaemon = getFuseDaemonForFile(file);
+ boolean supportsTranscode = mTranscodeHelper.supportsTranscode(file.getPath());
+ if (!supportsTranscode) {
+ throw new IOException("Input file descriptor is already original");
+ }
+ FuseDaemon fuseDaemon = getFuseDaemonForFile(file);
String outputPath = fuseDaemon.getOriginalMediaFormatFilePath(inputPfd);
if (TextUtils.isEmpty(outputPath)) {
throw new IOException("Invalid path for original media format file");
@@ -5892,6 +6086,11 @@
@Nullable Bundle extras) throws FallbackException {
extras = (extras != null) ? extras : new Bundle();
+ if (isRedactedUri(uri)) {
+ // we don't support update on redacted uris.
+ return 0;
+ }
+
// Related items are only considered for new media creation, and they
// can't be leveraged to move existing content into blocked locations
extras.remove(QUERY_ARG_RELATED_URI);
@@ -6790,6 +6989,11 @@
private ParcelFileDescriptor openFileCommon(Uri uri, String mode, CancellationSignal signal,
@Nullable Bundle opts)
throws FileNotFoundException {
+ boolean isRedactedUri = false;
+ if (isRedactedUri(uri)) {
+ uri = getUriForRedactedUri(uri);
+ isRedactedUri = true;
+ }
uri = safeUncanonicalize(uri);
opts = opts == null ? new Bundle() : opts;
@@ -6825,7 +7029,8 @@
}
}
- return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal, opts);
+ return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal, opts,
+ isRedactedUri);
}
@Override
@@ -7138,11 +7343,14 @@
* a "/mnt/user" path.
*/
private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, int match,
- String mode, CancellationSignal signal, @NonNull Bundle opts)
+ String mode, CancellationSignal signal, @NonNull Bundle opts, boolean isRedactedUri)
throws FileNotFoundException {
int modeBits = ParcelFileDescriptor.parseMode(mode);
boolean forWrite = (modeBits & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0;
if (forWrite) {
+ if (isRedactedUri) {
+ throw new UnsupportedOperationException("Write is not supported on redacted URIs");
+ }
// Upgrade 'w' only to 'rw'. This allows us acquire a WR_LOCK when calling
// #shouldOpenWithFuse
modeBits |= ParcelFileDescriptor.MODE_READ_WRITE;
@@ -7183,7 +7391,7 @@
final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName);
// Figure out if we need to redact contents
- final boolean redactionNeeded = callerIsOwner ? false : isRedactionNeeded(uri);
+ final boolean redactionNeeded = isRedactedUri || (!callerIsOwner && isRedactionNeeded(uri));
final RedactionInfo redactionInfo;
try {
redactionInfo = redactionNeeded ? getRedactionRanges(file)
@@ -7571,13 +7779,17 @@
*/
@NonNull
private long[] getRedactionRangesForFuse(String path, String ioPath, int original_uid, int uid,
- int tid) throws IOException {
+ int tid, boolean forceRedaction) throws IOException {
// |ioPath| might refer to a transcoded file path (which is not indexed in the db)
// |path| will always refer to a valid _data column
// We use |ioPath| for the filesystem access because in the case of transcoding,
// we want to get redaction ranges from the transcoded file and *not* the original file
final File file = new File(ioPath);
+ if (forceRedaction) {
+ return getRedactionRanges(file).redactionRanges;
+ }
+
// When calculating redaction ranges initiated from MediaProvider, the redaction policy
// is slightly different from the FUSE initiated opens redaction policy. targetSdk=29 from
// MediaProvider requires redaction, but targetSdk=29 apps from FUSE don't require redaction
@@ -7608,7 +7820,7 @@
final String[] projection = new String[]{
MediaColumns.OWNER_PACKAGE_NAME, MediaColumns._ID };
final String selection = MediaColumns.DATA + "=?";
- final String[] selectionArgs = new String[] { path };
+ final String[] selectionArgs = new String[]{path};
final String ownerPackageName;
final Uri item;
try (final Cursor c = queryForSingleItem(contentUri, projection, selection,
@@ -7627,6 +7839,7 @@
final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(),
ownerPackageName);
+
if (callerIsOwner) {
return new long[0];
}
@@ -7741,6 +7954,27 @@
}
try {
+ boolean forceRedaction = false;
+ if (isSyntheticFilePathForRedactedUri(path, uid)) {
+ if (forWrite) {
+ // Redacted URIs are not allowed to update EXIF headers.
+ return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
+ mediaCapabilitiesUid, new long[0]);
+ }
+
+ // If path is redacted Uris' path, ioPath must be the real path, ioPath must
+ // haven been updated to the real path during onFileLookupForFuse.
+ path = ioPath;
+
+ // Irrespective of the permissions we want to redact in this case.
+ redact = true;
+ forceRedaction = true;
+ } else if (isSyntheticDirPath(path, uid)) {
+ // we don't support any other transformations under .transforms/synthetic dir
+ return new FileOpenResult(OsConstants.ENOENT /* status */, originalUid,
+ mediaCapabilitiesUid, new long[0]);
+ }
+
if (isPrivatePackagePathNotAccessibleByCaller(path)) {
Log.e(TAG, "Can't open a file in another app's external directory!");
return new FileOpenResult(OsConstants.ENOENT, originalUid, mediaCapabilitiesUid,
@@ -7750,8 +7984,8 @@
if (shouldBypassFuseRestrictions(forWrite, path)) {
isSuccess = true;
return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid,
- redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid) :
- new long[0]);
+ redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid,
+ forceRedaction) : new long[0]);
}
// Legacy apps that made is this far don't have the right storage permission and hence
// are not allowed to access anything other than their external app directory
@@ -7805,8 +8039,8 @@
}
isSuccess = true;
return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid,
- redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid) :
- new long[0]);
+ redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid,
+ forceRedaction) : new long[0]);
} catch (IOException e) {
// We are here because
// * There is no db row corresponding to the requested path, which is more unlikely.
@@ -8371,6 +8605,30 @@
return mCallingIdentity.get().hasPermission(APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID);
}
+ private String getExternalStorageProviderAuthority() {
+ if (SdkLevel.isAtLeastS()) {
+ return getExternalStorageProviderAuthorityFromDocumentsContract();
+ }
+ return MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
+ }
+
+ @RequiresApi(Build.VERSION_CODES.S)
+ private String getExternalStorageProviderAuthorityFromDocumentsContract() {
+ return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
+ }
+
+ private String getDownloadsProviderAuthority() {
+ if (SdkLevel.isAtLeastS()) {
+ return getDownloadsProviderAuthorityFromDocumentsContract();
+ }
+ return DOWNLOADS_PROVIDER_AUTHORITY;
+ }
+
+ @RequiresApi(Build.VERSION_CODES.S)
+ private String getDownloadsProviderAuthorityFromDocumentsContract() {
+ return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
+ }
+
private boolean isCallingIdentityDownloadProvider(int uid) {
return uid == mDownloadsAuthorityAppId;
}
@@ -8387,9 +8645,9 @@
* The following apps have access to all private-app directories on secondary volumes:
* * ExternalStorageProvider
* * DownloadProvider
- * * Signature/privileged apps with ACCESS_MTP permission granted
- * (TODO(b/175796984): Allow *only* signature apps with ACCESS_MTP to access all
- * private-app directories).
+ * * Signature apps with ACCESS_MTP permission granted
+ * (Note: For Android R we also allow privileged apps with ACCESS_MTP to access all
+ * private-app directories, this additional access is removed for Android S+).
*
* Installer apps can only access private-app directories on Android/obb.
*
@@ -8400,16 +8658,43 @@
final LocalCallingIdentity token =
clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
try {
- if (isCallingIdentityDownloadProvider(uid) ||
- isCallingIdentityExternalStorageProvider(uid) || isCallingIdentityMtp(uid)) {
- return true;
+ if (SdkLevel.isAtLeastS()) {
+ return isMountModeAllowedPrivatePathAccess(uid, getCallingPackage(), path);
+ } else {
+ if (isCallingIdentityDownloadProvider(uid) ||
+ isCallingIdentityExternalStorageProvider(uid) || isCallingIdentityMtp(
+ uid)) {
+ return true;
+ }
+ return (isObbOrChildPath(path) && isCallingIdentityAllowedInstallerAccess(uid));
}
- return (isObbOrChildPath(path) && isCallingIdentityAllowedInstallerAccess(uid));
} finally {
restoreLocalCallingIdentity(token);
}
}
+ @RequiresApi(Build.VERSION_CODES.S)
+ private boolean isMountModeAllowedPrivatePathAccess(int uid, String packageName, String path) {
+ // This is required as only MediaProvider (package with WRITE_MEDIA_STORAGE) can access
+ // mount modes.
+ final CallingIdentity token = clearCallingIdentity();
+ try {
+ final int mountMode = mStorageManager.getExternalStorageMountMode(uid, packageName);
+ switch (mountMode) {
+ case StorageManager.MOUNT_MODE_EXTERNAL_ANDROID_WRITABLE:
+ case StorageManager.MOUNT_MODE_EXTERNAL_PASS_THROUGH:
+ return true;
+ case StorageManager.MOUNT_MODE_EXTERNAL_INSTALLER:
+ return isObbOrChildPath(path);
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Caller does not have the permissions to access mount modes: ", e);
+ } finally {
+ restoreCallingIdentity(token);
+ }
+ return false;
+ }
+
private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) {
// System internals can work with all media
if (isCallingPackageSelf() || isCallingPackageShell()) {
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index f3598c8..ee37448 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -22,6 +22,10 @@
import static com.android.providers.media.MediaProvider.collectUris;
import static com.android.providers.media.util.DatabaseUtils.getAsBoolean;
import static com.android.providers.media.util.Logging.TAG;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMediaLocation;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionManageMedia;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
import android.app.Activity;
import android.app.AlertDialog;
@@ -64,6 +68,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.providers.media.MediaProvider.LocalUriMatcher;
import com.android.providers.media.util.Metrics;
@@ -105,14 +110,26 @@
private AsyncTask<Void, Void, Void> positiveActionTask;
private Dialog progressDialog;
private TextView titleView;
+ private Handler mHandler;
+ private Runnable mShowProgressDialogRunnable = () -> {
+ // We will show the progress dialog, add the dim effect back.
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ progressDialog.show();
+ };
private static final Long LEAST_SHOW_PROGRESS_TIME_MS = 300L;
+ private static final Long BEFORE_SHOW_PROGRESS_TIME_MS = 300L;
- private static final String VERB_WRITE = "write";
- private static final String VERB_TRASH = "trash";
+ @VisibleForTesting
+ static final String VERB_WRITE = "write";
+ @VisibleForTesting
+ static final String VERB_TRASH = "trash";
+ @VisibleForTesting
+ static final String VERB_FAVORITE = "favorite";
+ @VisibleForTesting
+ static final String VERB_UNFAVORITE = "unfavorite";
+
private static final String VERB_UNTRASH = "untrash";
- private static final String VERB_FAVORITE = "favorite";
- private static final String VERB_UNFAVORITE = "unfavorite";
private static final String VERB_DELETE = "delete";
private static final String DATA_AUDIO = "audio";
@@ -136,6 +153,12 @@
getWindow().addSystemFlags(
WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
setFinishOnTouchOutside(false);
+ // remove the dim effect
+ // We may not show the progress dialog, if we don't remove the dim effect,
+ // it may have flicker.
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ getWindow().setDimAmount(0.0f);
+
// All untrusted input values here were validated when generating the
// original PendingIntent
@@ -154,19 +177,16 @@
return;
}
+ mHandler = new Handler(getMainLooper());
// Create Progress dialog
createProgressDialog();
- // Favorite-related requests are automatically granted for now; we still
- // make developers go through this no-op dialog flow to preserve our
- // ability to start prompting in the future
- switch (verb) {
- case VERB_FAVORITE:
- case VERB_UNFAVORITE: {
- onPositiveAction(null, 0);
- return;
- }
+ if (!shouldShowActionDialog(this, -1 /* pid */, appInfo.uid, getCallingPackage(),
+ null /* attributionTag */, verb)) {
+ onPositiveAction(null, 0);
+ return;
}
+
// Kick off async loading of description to show in dialog
final View bodyView = getLayoutInflater().inflate(R.layout.permission_body, null, false);
handleImageViewVisibility(bodyView, uris);
@@ -220,6 +240,7 @@
@Override
public void onDestroy() {
super.onDestroy();
+ mHandler.removeCallbacks(mShowProgressDialogRunnable);
// Cancel and interrupt the AsyncTask of the positive action. This avoids
// calling the old activity during "onPostExecute", but the AsyncTask could
// still finish its background task. For now we are ok with:
@@ -245,8 +266,10 @@
((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false);
}
- progressDialog.show();
final long startTime = System.currentTimeMillis();
+
+ mHandler.postDelayed(mShowProgressDialogRunnable, BEFORE_SHOW_PROGRESS_TIME_MS);
+
positiveActionTask = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
@@ -297,17 +320,23 @@
@Override
protected void onPostExecute(Void result) {
setResult(Activity.RESULT_OK);
- // Don't dismiss the progress dialog too quick, it will cause bad UX.
- final long duration = System.currentTimeMillis() - startTime;
- if (duration > LEAST_SHOW_PROGRESS_TIME_MS) {
- progressDialog.dismiss();
+ mHandler.removeCallbacks(mShowProgressDialogRunnable);
+
+ if (!progressDialog.isShowing()) {
finish();
} else {
- Handler handler = new Handler(getMainLooper());
- handler.postDelayed(() -> {
+ // Don't dismiss the progress dialog too quick, it will cause bad UX.
+ final long duration =
+ System.currentTimeMillis() - startTime - BEFORE_SHOW_PROGRESS_TIME_MS;
+ if (duration > LEAST_SHOW_PROGRESS_TIME_MS) {
progressDialog.dismiss();
finish();
- }, LEAST_SHOW_PROGRESS_TIME_MS - duration);
+ } else {
+ mHandler.postDelayed(() -> {
+ progressDialog.dismiss();
+ finish();
+ }, LEAST_SHOW_PROGRESS_TIME_MS - duration);
+ }
}
}
}.execute();
@@ -343,6 +372,37 @@
return keyCode == KeyEvent.KEYCODE_BACK;
}
+ @VisibleForTesting
+ static boolean shouldShowActionDialog(@NonNull Context context, int pid, int uid,
+ @NonNull String packageName, @Nullable String attributionTag, @NonNull String verb) {
+ // Favorite-related requests are automatically granted for now; we still
+ // make developers go through this no-op dialog flow to preserve our
+ // ability to start prompting in the future
+ if (TextUtils.equals(VERB_FAVORITE, verb) || TextUtils.equals(VERB_UNFAVORITE, verb)) {
+ return false;
+ }
+
+ // check READ_EXTERNAL_STORAGE and MANAGE_EXTERNAL_STORAGE permissions
+ if (!checkPermissionReadStorage(context, pid, uid, packageName, attributionTag)
+ && !checkPermissionManager(context, pid, uid, packageName, attributionTag)) {
+ Log.d(TAG, "No permission READ_EXTERNAL_STORAGE or MANAGE_EXTERNAL_STORAGE");
+ return true;
+ }
+ // check MANAGE_MEDIA permission
+ if (!checkPermissionManageMedia(context, pid, uid, packageName, attributionTag)) {
+ Log.d(TAG, "No permission MANAGE_MEDIA");
+ return true;
+ }
+
+ // if verb is write, check ACCESS_MEDIA_LOCATION permission
+ if (TextUtils.equals(verb, VERB_WRITE) && !checkPermissionAccessMediaLocation(context, pid,
+ uid, packageName, attributionTag)) {
+ Log.d(TAG, "No permission ACCESS_MEDIA_LOCATION");
+ return true;
+ }
+ return false;
+ }
+
private void handleImageViewVisibility(View bodyView, List<Uri> uris) {
if (uris.isEmpty()) {
return;
diff --git a/src/com/android/providers/media/TranscodeHelper.java b/src/com/android/providers/media/TranscodeHelper.java
index 9fcae72..b19d321 100644
--- a/src/com/android/providers/media/TranscodeHelper.java
+++ b/src/com/android/providers/media/TranscodeHelper.java
@@ -279,8 +279,19 @@
}
public void freeCache(long bytes) {
- // TODO(b/181846007): Implement cache clearing policies.
- mTranscodeDirectory.delete();
+ File[] files = mTranscodeDirectory.listFiles();
+ for (File file : files) {
+ if (bytes <= 0) {
+ return;
+ }
+ if (file.exists() && file.isFile()) {
+ long size = file.length();
+ boolean deleted = file.delete();
+ if (deleted) {
+ bytes -= size;
+ }
+ }
+ }
}
private UUID getTranscodeVolumeUuid() {
@@ -438,9 +449,9 @@
failureReason = waitTranscodingResult(uid, src, transcodingSession, latch);
errorCode = transcodingSession.getErrorCode();
- boolean success = failureReason == TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN;
+ result = failureReason == TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN;
- if (success) {
+ if (result) {
updateTranscodeStatus(src, TRANSCODE_COMPLETE);
} else {
logEvent("Transcoding failed for " + src + ". session: ", transcodingSession);
@@ -1451,7 +1462,6 @@
private static class StorageTranscodingSession {
public final TranscodingSession session;
public final CountDownLatch latch;
- private final Set<Integer> mBlockedUids = new ArraySet<>();
private boolean hasAnr;
public StorageTranscodingSession(TranscodingSession session, CountDownLatch latch) {
@@ -1460,15 +1470,11 @@
}
public void addBlockedUid(int uid) {
- synchronized (latch) {
- mBlockedUids.add(uid);
- }
+ session.addClientUid(uid);
}
public boolean isUidBlocked(int uid) {
- synchronized (latch) {
- return mBlockedUids.contains(uid);
- }
+ return session.getClientUids().contains(uid);
}
public void setAnr() {
@@ -1485,7 +1491,7 @@
@Override
public String toString() {
- return session.toString() + ". BlockedUids: " + mBlockedUids;
+ return session.toString() + ". BlockedUids: " + session.getClientUids();
}
}
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index c88fc20..72c313c 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -48,6 +48,8 @@
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static com.android.providers.media.util.Metrics.translateReason;
+
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
@@ -514,12 +516,20 @@
queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_INCLUDE);
- try (Cursor c = mResolver.query(mFilesUri, new String[] { FileColumns._ID },
+ final int[] countPerMediaType = new int[FileColumns.MEDIA_TYPE_COUNT];
+ try (Cursor c = mResolver.query(mFilesUri,
+ new String[] { FileColumns._ID, FileColumns.MEDIA_TYPE },
queryArgs, mSignal)) {
while (c.moveToNext()) {
final long id = c.getLong(0);
if (Arrays.binarySearch(scannedIds, id) < 0) {
mUnknownIds.add(id);
+ final int mediaType = c.getInt(1);
+ // Avoid ArrayIndexOutOfBounds if more mediaTypes are added,
+ // but mediaTypeSize is not updated
+ if (mediaType < countPerMediaType.length) {
+ countPerMediaType[mediaType]++;
+ }
}
}
} finally {
@@ -541,6 +551,10 @@
}
applyPending();
} finally {
+ if (mUnknownIds.size() > 0) {
+ String scanReason = "scan triggered by reason: " + translateReason(mReason);
+ Metrics.logDeletionPersistent(mVolumeName, scanReason, countPerMediaType);
+ }
Trace.endSection();
}
}
diff --git a/src/com/android/providers/media/util/Metrics.java b/src/com/android/providers/media/util/Metrics.java
index 86a0302..410da3a 100644
--- a/src/com/android/providers/media/util/Metrics.java
+++ b/src/com/android/providers/media/util/Metrics.java
@@ -60,12 +60,30 @@
normalizedInsertCount, normalizedUpdateCount, normalizedDeleteCount);
}
- public static void logDeletion(@NonNull String volumeName, int uid, String packageName,
- int itemCount) {
- Logging.logPersistent(String.format(
- "Deleted %3$d items on %1$s due to %2$s",
- volumeName, packageName, itemCount));
+ /**
+ * Logs persistent deletion logs on-device.
+ */
+ public static void logDeletionPersistent(@NonNull String volumeName, String reason,
+ int[] countPerMediaType) {
+ final StringBuilder builder = new StringBuilder("Deleted ");
+ for (int count: countPerMediaType) {
+ builder.append(count).append(' ');
+ }
+ builder.append("items on ")
+ .append(volumeName)
+ .append(" due to ")
+ .append(reason);
+ Logging.logPersistent(builder.toString());
+ }
+
+ /**
+ * Logs persistent deletion logs on-device and stats metrics. Count of items per-media-type
+ * are not uploaded to MediaProviderStats logs.
+ */
+ public static void logDeletion(@NonNull String volumeName, int uid, String packageName,
+ int itemCount, int[] countPerMediaType) {
+ logDeletionPersistent(volumeName, packageName, countPerMediaType);
MediaProviderStatsLog.write(MEDIA_CONTENT_DELETED,
translateVolumeName(volumeName), uid, itemCount);
}
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index d9765ac..5b3639c 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -16,10 +16,12 @@
package com.android.providers.media.util;
+import static android.Manifest.permission.ACCESS_MEDIA_LOCATION;
import static android.Manifest.permission.ACCESS_MTP;
import static android.Manifest.permission.BACKUP;
import static android.Manifest.permission.INSTALL_PACKAGES;
import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
+import static android.Manifest.permission.MANAGE_MEDIA;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
@@ -114,6 +116,26 @@
generateAppOpMessage(packageName,sOpDescription.get()));
}
+ /**
+ * Check if the given package has been granted the
+ * android.Manifest.permission#ACCESS_MEDIA_LOCATION permission.
+ */
+ public static boolean checkPermissionAccessMediaLocation(@NonNull Context context, int pid,
+ int uid, @NonNull String packageName, @Nullable String attributionTag) {
+ return checkPermissionForDataDelivery(context, ACCESS_MEDIA_LOCATION, pid, uid, packageName,
+ attributionTag, generateAppOpMessage(packageName, sOpDescription.get()));
+ }
+
+ /**
+ * Check if the given package has been granted the
+ * android.Manifest.permission#MANAGE_MEDIA permission.
+ */
+ public static boolean checkPermissionManageMedia(@NonNull Context context, int pid, int uid,
+ @NonNull String packageName, @Nullable String attributionTag) {
+ return checkPermissionForDataDelivery(context, MANAGE_MEDIA, pid, uid, packageName,
+ attributionTag, generateAppOpMessage(packageName, sOpDescription.get()));
+ }
+
public static boolean checkIsLegacyStorageGranted(@NonNull Context context, int uid,
String packageName, @Nullable String attributionTag) {
if (context.getSystemService(AppOpsManager.class)
@@ -407,6 +429,7 @@
private static boolean isAppOpPermission(String permission) {
switch (permission) {
case MANAGE_EXTERNAL_STORAGE:
+ case MANAGE_MEDIA:
return true;
}
return false;
@@ -414,6 +437,7 @@
private static boolean isRuntimePermission(String permission) {
switch (permission) {
+ case ACCESS_MEDIA_LOCATION:
case READ_EXTERNAL_STORAGE:
case WRITE_EXTERNAL_STORAGE:
return true;
diff --git a/tests/client/AndroidTest.xml b/tests/client/AndroidTest.xml
index b9c3dfa..22ca7d3 100644
--- a/tests/client/AndroidTest.xml
+++ b/tests/client/AndroidTest.xml
@@ -27,8 +27,4 @@
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
<option name="hidden-api-checks" value="false"/>
</test>
-
- <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
- <option name="mainline-module-package-name" value="com.google.android.mediaprovider" />
- </object>
</configuration>
diff --git a/tests/src/com/android/providers/media/PermissionActivityTest.java b/tests/src/com/android/providers/media/PermissionActivityTest.java
index 7bbce62..ce72d97 100644
--- a/tests/src/com/android/providers/media/PermissionActivityTest.java
+++ b/tests/src/com/android/providers/media/PermissionActivityTest.java
@@ -16,6 +16,26 @@
package com.android.providers.media;
+import static android.Manifest.permission.ACCESS_MEDIA_LOCATION;
+import static android.Manifest.permission.MANAGE_APP_OPS_MODES;
+import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
+import static android.Manifest.permission.MANAGE_MEDIA;
+import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.Manifest.permission.UPDATE_APP_OPS_STATS;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static com.android.providers.media.PermissionActivity.VERB_FAVORITE;
+import static com.android.providers.media.PermissionActivity.VERB_TRASH;
+import static com.android.providers.media.PermissionActivity.VERB_UNFAVORITE;
+import static com.android.providers.media.PermissionActivity.VERB_WRITE;
+import static com.android.providers.media.PermissionActivity.shouldShowActionDialog;
+import static com.android.providers.media.util.TestUtils.adoptShellPermission;
+import static com.android.providers.media.util.TestUtils.dropShellPermission;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AppOpsManager;
import android.app.Instrumentation;
import android.content.ClipData;
import android.content.ContentValues;
@@ -30,6 +50,7 @@
import com.android.providers.media.scan.MediaScannerTest;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -42,6 +63,17 @@
*/
@RunWith(AndroidJUnit4.class)
public class PermissionActivityTest {
+ private static final String TEST_APP_PACKAGE_NAME =
+ "com.android.providers.media.testapp.withstorageperms";
+
+ private static final int TEST_APP_PID = -1;
+ private int mTestAppUid = -1;
+
+ @Before
+ public void setUp() throws Exception {
+ mTestAppUid = getContext().getPackageManager().getPackageUid(TEST_APP_PACKAGE_NAME, 0);
+ }
+
@Test
public void testSimple() throws Exception {
final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
@@ -52,6 +84,165 @@
activity.startActivityForResult(createIntent(), 42);
}
+ @Test
+ public void testShouldShowActionDialog_favorite_false() throws Exception {
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+ TEST_APP_PACKAGE_NAME, null, VERB_FAVORITE)).isFalse();
+ }
+
+ @Test
+ public void testShouldShowActionDialog_unfavorite_false() throws Exception {
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+ TEST_APP_PACKAGE_NAME, null, VERB_UNFAVORITE)).isFalse();
+ }
+
+ @Test
+ public void testShouldShowActionDialog_noRESAndMES_true() throws Exception {
+ final String[] enableAppOpsList = {AppOpsManager.permissionToOp(MANAGE_MEDIA)};
+ final String[] disableAppOpsList = {
+ AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE),
+ AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE)};
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ for (String op : enableAppOpsList) {
+ modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ALLOWED);
+ }
+
+ for (String op : disableAppOpsList) {
+ modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ERRORED);
+ }
+
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+ TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isTrue();
+ } finally {
+ dropShellPermission();
+ }
+ }
+
+ @Test
+ public void testShouldShowActionDialog_noMANAGE_MEDIA_true() throws Exception {
+ final String[] enableAppOpsList = {
+ AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE),
+ AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE)};
+ final String[] disableAppOpsList = {AppOpsManager.permissionToOp(MANAGE_MEDIA)};
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ for (String op : enableAppOpsList) {
+ modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ALLOWED);
+ }
+
+ for (String op : disableAppOpsList) {
+ modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ERRORED);
+ }
+
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+ TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isTrue();
+ } finally {
+ dropShellPermission();
+ }
+ }
+
+ @Test
+ public void testShouldShowActionDialog_hasPermissionWithRES_false() throws Exception {
+ final String[] enableAppOpsList = {
+ AppOpsManager.permissionToOp(MANAGE_MEDIA),
+ AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE)};
+ final String[] disableAppOpsList = {AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE)};
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ for (String op : enableAppOpsList) {
+ modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ALLOWED);
+ }
+
+ for (String op : disableAppOpsList) {
+ modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ERRORED);
+ }
+
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+ TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isFalse();
+ } finally {
+ dropShellPermission();
+ }
+ }
+
+ @Test
+ public void testShouldShowActionDialog_hasPermissionWithMES_false() throws Exception {
+ final String[] enableAppOpsList = {
+ AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE),
+ AppOpsManager.permissionToOp(MANAGE_MEDIA)};
+ final String[] disableAppOpsList = {AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE)};
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ for (String op : enableAppOpsList) {
+ modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ALLOWED);
+ }
+
+ for (String op : disableAppOpsList) {
+ modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ERRORED);
+ }
+
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+ TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isFalse();
+ } finally {
+ dropShellPermission();
+ }
+ }
+
+ @Test
+ public void testShouldShowActionDialog_writeNoACCESS_MEDIA_LOCATION_true() throws Exception {
+ final String[] enableAppOpsList = {
+ AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE),
+ AppOpsManager.permissionToOp(MANAGE_MEDIA),
+ AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE)};
+ final String[] disableAppOpsList = {AppOpsManager.permissionToOp(ACCESS_MEDIA_LOCATION)};
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ for (String op : enableAppOpsList) {
+ modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ALLOWED);
+ }
+
+ for (String op : disableAppOpsList) {
+ modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ERRORED);
+ }
+
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+ TEST_APP_PACKAGE_NAME, null, VERB_WRITE)).isTrue();
+ } finally {
+ dropShellPermission();
+ }
+ }
+
+ @Test
+ public void testShouldShowActionDialog_writeHasACCESS_MEDIA_LOCATION_false() throws Exception {
+ final String[] enableAppOpsList = {
+ AppOpsManager.permissionToOp(ACCESS_MEDIA_LOCATION),
+ AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE),
+ AppOpsManager.permissionToOp(MANAGE_MEDIA),
+ AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE)};
+ final String[] disableAppOpsList = new String[]{};
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ for (String op : enableAppOpsList) {
+ modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ALLOWED);
+ }
+
+ for (String op : disableAppOpsList) {
+ modifyAppOp(mTestAppUid, op, AppOpsManager.MODE_ERRORED);
+ }
+
+ assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
+ TEST_APP_PACKAGE_NAME, null, VERB_WRITE)).isFalse();
+ } finally {
+ dropShellPermission();
+ }
+ }
+
private static Intent createIntent() throws Exception {
final Context context = InstrumentationRegistry.getContext();
@@ -67,4 +258,8 @@
intent.putExtra(MediaStore.EXTRA_CONTENT_VALUES, new ContentValues());
return intent;
}
+
+ private static void modifyAppOp(int uid, String op, int mode) {
+ getContext().getSystemService(AppOpsManager.class).setUidMode(op, uid, mode);
+ }
}
diff --git a/tests/src/com/android/providers/media/util/MetricsTest.java b/tests/src/com/android/providers/media/util/MetricsTest.java
index 41e25b5..688e2e9 100644
--- a/tests/src/com/android/providers/media/util/MetricsTest.java
+++ b/tests/src/com/android/providers/media/util/MetricsTest.java
@@ -41,7 +41,8 @@
final String packageName = "com.example";
Metrics.logScan(volumeName, MediaScanner.REASON_UNKNOWN, 42, 42, 42, 42, 42);
- Metrics.logDeletion(volumeName, 42, packageName, 42);
+ Metrics.logDeletionPersistent(volumeName, "scanReason", new int[] { 42 });
+ Metrics.logDeletion(volumeName, 42, packageName, 42, new int[] { 42 });
Metrics.logPermissionGranted(volumeName, 42, packageName, 42);
Metrics.logPermissionDenied(volumeName, 42, packageName, 42);
Metrics.logSchemaChange(volumeName, 42, 42, 42, 42);
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
index 1fc43cb..da89b5c 100644
--- a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -17,6 +17,8 @@
package com.android.providers.media.util;
import static android.Manifest.permission.MANAGE_APP_OPS_MODES;
+import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
+import static android.Manifest.permission.MANAGE_MEDIA;
import static android.Manifest.permission.UPDATE_APP_OPS_STATS;
import static android.app.AppOpsManager.OPSTR_NO_ISOLATED_STORAGE;
import static android.app.AppOpsManager.OPSTR_READ_MEDIA_AUDIO;
@@ -32,9 +34,11 @@
import static com.android.providers.media.util.PermissionUtils.checkAppOpRequestInstallPackagesForSharedUid;
import static com.android.providers.media.util.PermissionUtils.checkIsLegacyStorageGranted;
import static com.android.providers.media.util.PermissionUtils.checkNoIsolatedStorageGranted;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMediaLocation;
import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMtp;
import static com.android.providers.media.util.PermissionUtils.checkPermissionDelegator;
import static com.android.providers.media.util.PermissionUtils.checkPermissionInstallPackages;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionManageMedia;
import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
@@ -113,6 +117,9 @@
assertThat(checkPermissionShell(context, pid, uid)).isFalse();
assertThat(checkPermissionManager(context, pid, uid, packageName, null)).isFalse();
assertThat(checkPermissionDelegator(context, pid, uid)).isFalse();
+ assertThat(checkPermissionManageMedia(context, pid, uid, packageName, null)).isFalse();
+ assertThat(checkPermissionAccessMediaLocation(context, pid, uid,
+ packageName, null)).isFalse();
assertThat(checkPermissionReadStorage(context, pid, uid, packageName, null)).isTrue();
assertThat(checkPermissionWriteStorage(context, pid, uid, packageName, null)).isTrue();
@@ -123,8 +130,7 @@
assertThat(checkPermissionWriteVideo(context, pid, uid, packageName, null)).isFalse();
assertThat(checkPermissionReadImages(context, pid, uid, packageName, null)).isTrue();
assertThat(checkPermissionWriteImages(context, pid, uid, packageName, null)).isFalse();
- assertThat(checkPermissionInstallPackages(getContext(), pid, uid,
- getContext().getPackageName(), null)).isFalse();
+ assertThat(checkPermissionInstallPackages(context, pid, uid, packageName, null)).isFalse();
}
/**
@@ -149,9 +155,6 @@
assertThat(checkPermissionSelf(getContext(), testAppPid, testAppUid)).isFalse();
assertThat(checkPermissionShell(getContext(), testAppPid, testAppUid)).isFalse();
assertThat(
- checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
- null)).isFalse();
- assertThat(
checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
null)).isFalse();
assertThat(
@@ -182,6 +185,13 @@
assertThat(
checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
null)).isFalse();
+
+ assertThat(checkPermissionManageMedia(getContext(), testAppPid, testAppUid, packageName,
+ null)).isFalse();
+
+ assertThat(checkPermissionAccessMediaLocation(getContext(), testAppPid, testAppUid,
+ packageName, null)).isFalse();
+
assertThat(
checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
null)).isFalse();
@@ -214,6 +224,13 @@
assertThat(
checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
null)).isFalse();
+
+ assertThat(checkPermissionManageMedia(getContext(), testAppPid, testAppUid, packageName,
+ null)).isFalse();
+
+ assertThat(checkPermissionAccessMediaLocation(getContext(), testAppPid, testAppUid,
+ packageName, null)).isTrue();
+
assertThat(
checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
null)).isTrue();
@@ -232,6 +249,51 @@
}
}
+ @Test
+ public void testManageExternalStoragePermissionsOnTestApp() throws Exception {
+ final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
+ final int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
+ final int testAppPid = pidMap.get(packageName);
+ final String op = AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE);
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ modifyAppOp(testAppUid, op, AppOpsManager.MODE_ERRORED);
+
+ assertThat(checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
+ null)).isFalse();
+
+ modifyAppOp(testAppUid, op, AppOpsManager.MODE_ALLOWED);
+
+ assertThat(checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
+ null)).isTrue();
+ } finally {
+ dropShellPermission();
+ }
+ }
+
+ @Test
+ public void testManageMediaPermissionsOnTestApp() throws Exception {
+ final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
+ final int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
+ final int testAppPid = pidMap.get(packageName);
+ final String op = AppOpsManager.permissionToOp(MANAGE_MEDIA);
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ modifyAppOp(testAppUid, op, AppOpsManager.MODE_ERRORED);
+
+ assertThat(checkPermissionManageMedia(getContext(), testAppPid, testAppUid, packageName,
+ null)).isFalse();
+
+ modifyAppOp(testAppUid, op, AppOpsManager.MODE_ALLOWED);
+
+ assertThat(checkPermissionManageMedia(getContext(), testAppPid, testAppUid, packageName,
+ null)).isTrue();
+ } finally {
+ dropShellPermission();
+ }
+ }
@Test
public void testSystemGalleryPermissionsOnTestApp() throws Exception {
diff --git a/tests/src/com/android/providers/media/util/TestUtils.java b/tests/src/com/android/providers/media/util/TestUtils.java
index 1581b03..f2d3696 100644
--- a/tests/src/com/android/providers/media/util/TestUtils.java
+++ b/tests/src/com/android/providers/media/util/TestUtils.java
@@ -26,7 +26,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-class TestUtils {
+public class TestUtils {
public static final String QUERY_TYPE = "com.android.providers.media.util.QUERY_TYPE";
public static final String RUN_INFINITE_ACTIVITY =
"com.android.providers.media.util.RUN_INFINITE_ACTIVITY";
diff --git a/tests/test_app/TestAppWithStoragePerms.xml b/tests/test_app/TestAppWithStoragePerms.xml
index 694e17f..c37b025 100644
--- a/tests/test_app/TestAppWithStoragePerms.xml
+++ b/tests/test_app/TestAppWithStoragePerms.xml
@@ -20,8 +20,11 @@
android:versionCode="1"
android:versionName="1.0">
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
+ <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.MANAGE_MEDIA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application android:label="TestAppPerms">
<activity android:name="com.android.providers.media.util.TestAppActivity"
diff --git a/tools/dialogs/AndroidManifest.xml b/tools/dialogs/AndroidManifest.xml
index 8ee5f2c..960cb13 100644
--- a/tools/dialogs/AndroidManifest.xml
+++ b/tools/dialogs/AndroidManifest.xml
@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.MANAGE_MEDIA"/>
<application android:label="DialogsTool">
<activity android:name=".DialogsActivity"