Merge "Check URI permissions for resumable media artwork" into tm-dev
diff --git a/core/java/android/app/IUriGrantsManager.aidl b/core/java/android/app/IUriGrantsManager.aidl
index 9e7f2fe..b630d03 100644
--- a/core/java/android/app/IUriGrantsManager.aidl
+++ b/core/java/android/app/IUriGrantsManager.aidl
@@ -39,4 +39,7 @@
void clearGrantedUriPermissions(in String packageName, int userId);
ParceledListSlice getUriPermissions(in String packageName, boolean incoming,
boolean persistedOnly);
+
+ int checkGrantUriPermission_ignoreNonSystem(
+ int sourceUid, String targetPkg, in Uri uri, int modeFlags, int userId);
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index 4c2336e..db343e5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -18,11 +18,13 @@
import android.app.Notification
import android.app.PendingIntent
+import android.app.UriGrantsManager
import android.app.smartspace.SmartspaceConfig
import android.app.smartspace.SmartspaceManager
import android.app.smartspace.SmartspaceSession
import android.app.smartspace.SmartspaceTarget
import android.content.BroadcastReceiver
+import android.content.ContentProvider
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
@@ -592,10 +594,13 @@
Log.d(TAG, "adding track for $userId from browser: $desc")
}
+ val currentEntry = mediaEntries.get(packageName)
+ val appUid = currentEntry?.appUid ?: Process.INVALID_UID
+
// Album art
var artworkBitmap = desc.iconBitmap
if (artworkBitmap == null && desc.iconUri != null) {
- artworkBitmap = loadBitmapFromUri(desc.iconUri!!)
+ artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName)
}
val artworkIcon = if (artworkBitmap != null) {
Icon.createWithBitmap(artworkBitmap)
@@ -603,9 +608,7 @@
null
}
- val currentEntry = mediaEntries.get(packageName)
val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
- val appUid = currentEntry?.appUid ?: Process.INVALID_UID
val mediaAction = getResumeMediaAction(resumeAction)
val lastActive = systemClock.elapsedRealtime()
@@ -1000,6 +1003,30 @@
false
}
}
+
+ /** Returns a bitmap if the user can access the given URI, else null */
+ private fun loadBitmapFromUriForUser(
+ uri: Uri,
+ userId: Int,
+ appUid: Int,
+ packageName: String,
+ ): Bitmap? {
+ try {
+ val ugm = UriGrantsManager.getService()
+ ugm.checkGrantUriPermission_ignoreNonSystem(
+ appUid,
+ packageName,
+ ContentProvider.getUriWithoutUserId(uri),
+ Intent.FLAG_GRANT_READ_URI_PERMISSION,
+ ContentProvider.getUserIdFromUri(uri, userId)
+ )
+ return loadBitmapFromUri(uri)
+ } catch (e: SecurityException) {
+ Log.e(TAG, "Failed to get URI permission: $e")
+ }
+ return null
+ }
+
/**
* Load a bitmap from a URI
* @param uri the uri to load
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
index 52266f9..347ea3b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
@@ -1,18 +1,22 @@
package com.android.systemui.media
+import android.app.IUriGrantsManager
import android.app.Notification
import android.app.Notification.MediaStyle
import android.app.PendingIntent
+import android.app.UriGrantsManager
import android.app.smartspace.SmartspaceAction
import android.app.smartspace.SmartspaceTarget
import android.content.Intent
import android.graphics.Bitmap
+import android.graphics.ImageDecoder
import android.graphics.drawable.Icon
import android.media.MediaDescription
import android.media.MediaMetadata
import android.media.session.MediaController
import android.media.session.MediaSession
import android.media.session.PlaybackState
+import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.service.notification.StatusBarNotification
@@ -20,6 +24,7 @@
import android.testing.TestableLooper.RunWithLooper
import androidx.media.utils.MediaConstants
import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.internal.logging.InstanceId
import com.android.systemui.InstanceIdSequenceFake
import com.android.systemui.R
@@ -53,8 +58,10 @@
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.MockitoSession
import org.mockito.junit.MockitoJUnit
import org.mockito.Mockito.`when` as whenever
+import org.mockito.quality.Strictness
private const val KEY = "KEY"
private const val KEY_2 = "KEY_2"
@@ -111,14 +118,25 @@
private val clock = FakeSystemClock()
@Mock private lateinit var tunerService: TunerService
@Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable>
+ @Mock private lateinit var ugm: IUriGrantsManager
+ @Mock private lateinit var imageSource: ImageDecoder.Source
private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
private val originalSmartspaceSetting = Settings.Secure.getInt(context.contentResolver,
Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1)
+ private lateinit var staticMockSession: MockitoSession
+
@Before
fun setup() {
+ staticMockSession =
+ ExtendedMockito.mockitoSession()
+ .mockStatic<UriGrantsManager>(UriGrantsManager::class.java)
+ .mockStatic<ImageDecoder>(ImageDecoder::class.java)
+ .strictness(Strictness.LENIENT)
+ .startMocking()
+ whenever(UriGrantsManager.getService()).thenReturn(ugm)
foregroundExecutor = FakeExecutor(clock)
backgroundExecutor = FakeExecutor(clock)
smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
@@ -195,6 +213,7 @@
@After
fun tearDown() {
+ staticMockSession.finishMocking()
session.release()
mediaDataManager.destroy()
Settings.Secure.putInt(context.contentResolver,
@@ -1093,6 +1112,66 @@
anyBoolean())
}
+ @Test
+ fun testResumeMediaLoaded_hasArtPermission_artLoaded() {
+ // When resume media is loaded and user/app has permission to access the art URI,
+ whenever(
+ ugm.checkGrantUriPermission_ignoreNonSystem(
+ anyInt(),
+ any(),
+ any(),
+ anyInt(),
+ anyInt()
+ )
+ )
+ .thenReturn(1)
+ val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+ val uri = Uri.parse("content://example")
+ whenever(ImageDecoder.createSource(any(), eq(uri))).thenReturn(imageSource)
+ whenever(ImageDecoder.decodeBitmap(any(), any())).thenReturn(artwork)
+
+ val desc =
+ MediaDescription.Builder().run {
+ setTitle(SESSION_TITLE)
+ setIconUri(uri)
+ build()
+ }
+ addResumeControlAndLoad(desc)
+
+ // Then the artwork is loaded
+ assertThat(mediaDataCaptor.value.artwork).isNotNull()
+ }
+
+ @Test
+ fun testResumeMediaLoaded_noArtPermission_noArtLoaded() {
+ // When resume media is loaded and user/app does not have permission to access the art URI
+ whenever(
+ ugm.checkGrantUriPermission_ignoreNonSystem(
+ anyInt(),
+ any(),
+ any(),
+ anyInt(),
+ anyInt()
+ )
+ )
+ .thenThrow(SecurityException("Test no permission"))
+ val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+ val uri = Uri.parse("content://example")
+ whenever(ImageDecoder.createSource(any(), eq(uri))).thenReturn(imageSource)
+ whenever(ImageDecoder.decodeBitmap(any(), any())).thenReturn(artwork)
+
+ val desc =
+ MediaDescription.Builder().run {
+ setTitle(SESSION_TITLE)
+ setIconUri(uri)
+ build()
+ }
+ addResumeControlAndLoad(desc)
+
+ // Then the artwork is not loaded
+ assertThat(mediaDataCaptor.value.artwork).isNull()
+ }
+
/**
* Helper function to add a media notification and capture the resulting MediaData
*/
@@ -1103,4 +1182,32 @@
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
eq(0), eq(false))
}
+
+ /** Helper function to add a resumption control and capture the resulting MediaData */
+ private fun addResumeControlAndLoad(
+ desc: MediaDescription,
+ packageName: String = PACKAGE_NAME
+ ) {
+ mediaDataManager.addResumptionControls(
+ USER_ID,
+ desc,
+ Runnable {},
+ session.sessionToken,
+ APP_NAME,
+ pendingIntent,
+ packageName
+ )
+ assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+ assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(packageName),
+ eq(null),
+ capture(mediaDataCaptor),
+ eq(true),
+ eq(0),
+ eq(false)
+ )
+ }
}
diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerService.java b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
index 6aa06e8..92329cf 100644
--- a/services/core/java/com/android/server/uri/UriGrantsManagerService.java
+++ b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
@@ -41,6 +41,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AppGlobals;
@@ -62,6 +63,7 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
+import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -1304,6 +1306,46 @@
return false;
}
+ /**
+ * Check if the targetPkg can be granted permission to access uri by
+ * the callingUid using the given modeFlags. See {@link #checkGrantUriPermissionUnlocked}.
+ *
+ * @param callingUid The uid of the grantor app that has permissions to the uri.
+ * @param targetPkg The package name of the granted app that needs permissions to the uri.
+ * @param uri The uri for which permissions should be granted.
+ * @param modeFlags The modes to grant. See {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}, etc.
+ * @param userId The userId in which the uri is to be resolved.
+ * @return uid of the target or -1 if permission grant not required. Returns -1 if the caller
+ * does not hold INTERACT_ACROSS_USERS_FULL
+ * @throws SecurityException if the grant is not allowed.
+ */
+ @Override
+ @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+ public int checkGrantUriPermission_ignoreNonSystem(int callingUid, String targetPkg, Uri uri,
+ int modeFlags, int userId) {
+ if (!isCallerIsSystemOrPrivileged()) {
+ return Process.INVALID_UID;
+ }
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ return checkGrantUriPermissionUnlocked(callingUid, targetPkg, uri, modeFlags,
+ userId);
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+
+ private boolean isCallerIsSystemOrPrivileged() {
+ final int uid = Binder.getCallingUid();
+ if (uid == Process.SYSTEM_UID || uid == Process.ROOT_UID) {
+ return true;
+ }
+ return ActivityManager.checkComponentPermission(
+ android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+ uid, /* owningUid = */-1, /* exported = */ true)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
@Override
public ArrayList<UriPermission> providePersistentUriGrants() {
final ArrayList<UriPermission> result = new ArrayList<>();