Skip to content

Commit 1a42ae5

Browse files
Beth ThibodeauAndroid Build Coastguard Worker
authored andcommitted
Check URI permissions for resumable media artwork
When resumable media is added that has artwork set via URI, check the permissions for the URI before attempting to load it Test: atest MediaDataManagerTest UriGrantsManagerServiceTest Test: manual with test app Bug: 284297452 (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:a3b7a10a15aa41ad75866922d528a4dc02fc8ca3) Merged-In: Ie79915d3d1712f08dc2e8dfbd5bc7fd32bb308a3 Change-Id: Ie79915d3d1712f08dc2e8dfbd5bc7fd32bb308a3
1 parent f4644b5 commit 1a42ae5

4 files changed

Lines changed: 182 additions & 3 deletions

File tree

core/java/android/app/IUriGrantsManager.aidl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,7 @@ interface IUriGrantsManager {
3939
void clearGrantedUriPermissions(in String packageName, int userId);
4040
ParceledListSlice getUriPermissions(in String packageName, boolean incoming,
4141
boolean persistedOnly);
42+
43+
int checkGrantUriPermission_ignoreNonSystem(
44+
int sourceUid, String targetPkg, in Uri uri, int modeFlags, int userId);
4245
}

packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ package com.android.systemui.media
1818

1919
import android.app.Notification
2020
import android.app.PendingIntent
21+
import android.app.UriGrantsManager
2122
import android.app.smartspace.SmartspaceConfig
2223
import android.app.smartspace.SmartspaceManager
2324
import android.app.smartspace.SmartspaceSession
2425
import android.app.smartspace.SmartspaceTarget
2526
import android.content.BroadcastReceiver
27+
import android.content.ContentProvider
2628
import android.content.ContentResolver
2729
import android.content.Context
2830
import android.content.Intent
@@ -592,20 +594,21 @@ class MediaDataManager(
592594
Log.d(TAG, "adding track for $userId from browser: $desc")
593595
}
594596

597+
val currentEntry = mediaEntries.get(packageName)
598+
val appUid = currentEntry?.appUid ?: Process.INVALID_UID
599+
595600
// Album art
596601
var artworkBitmap = desc.iconBitmap
597602
if (artworkBitmap == null && desc.iconUri != null) {
598-
artworkBitmap = loadBitmapFromUri(desc.iconUri!!)
603+
artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName)
599604
}
600605
val artworkIcon = if (artworkBitmap != null) {
601606
Icon.createWithBitmap(artworkBitmap)
602607
} else {
603608
null
604609
}
605610

606-
val currentEntry = mediaEntries.get(packageName)
607611
val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
608-
val appUid = currentEntry?.appUid ?: Process.INVALID_UID
609612

610613
val mediaAction = getResumeMediaAction(resumeAction)
611614
val lastActive = systemClock.elapsedRealtime()
@@ -1000,6 +1003,30 @@ class MediaDataManager(
10001003
false
10011004
}
10021005
}
1006+
1007+
/** Returns a bitmap if the user can access the given URI, else null */
1008+
private fun loadBitmapFromUriForUser(
1009+
uri: Uri,
1010+
userId: Int,
1011+
appUid: Int,
1012+
packageName: String,
1013+
): Bitmap? {
1014+
try {
1015+
val ugm = UriGrantsManager.getService()
1016+
ugm.checkGrantUriPermission_ignoreNonSystem(
1017+
appUid,
1018+
packageName,
1019+
ContentProvider.getUriWithoutUserId(uri),
1020+
Intent.FLAG_GRANT_READ_URI_PERMISSION,
1021+
ContentProvider.getUserIdFromUri(uri, userId)
1022+
)
1023+
return loadBitmapFromUri(uri)
1024+
} catch (e: SecurityException) {
1025+
Log.e(TAG, "Failed to get URI permission: $e")
1026+
}
1027+
return null
1028+
}
1029+
10031030
/**
10041031
* Load a bitmap from a URI
10051032
* @param uri the uri to load

packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
package com.android.systemui.media
22

3+
import android.app.IUriGrantsManager
34
import android.app.Notification
45
import android.app.Notification.MediaStyle
56
import android.app.PendingIntent
7+
import android.app.UriGrantsManager
68
import android.app.smartspace.SmartspaceAction
79
import android.app.smartspace.SmartspaceTarget
810
import android.content.Intent
911
import android.graphics.Bitmap
12+
import android.graphics.ImageDecoder
1013
import android.graphics.drawable.Icon
1114
import android.media.MediaDescription
1215
import android.media.MediaMetadata
1316
import android.media.session.MediaController
1417
import android.media.session.MediaSession
1518
import android.media.session.PlaybackState
19+
import android.net.Uri
1620
import android.os.Bundle
1721
import android.provider.Settings
1822
import android.service.notification.StatusBarNotification
1923
import android.testing.AndroidTestingRunner
2024
import android.testing.TestableLooper.RunWithLooper
2125
import androidx.media.utils.MediaConstants
2226
import androidx.test.filters.SmallTest
27+
import com.android.dx.mockito.inline.extended.ExtendedMockito
2328
import com.android.internal.logging.InstanceId
2429
import com.android.systemui.InstanceIdSequenceFake
2530
import com.android.systemui.R
@@ -53,8 +58,10 @@ import org.mockito.Mockito.reset
5358
import org.mockito.Mockito.times
5459
import org.mockito.Mockito.verify
5560
import org.mockito.Mockito.verifyNoMoreInteractions
61+
import org.mockito.MockitoSession
5662
import org.mockito.junit.MockitoJUnit
5763
import org.mockito.Mockito.`when` as whenever
64+
import org.mockito.quality.Strictness
5865

5966
private const val KEY = "KEY"
6067
private const val KEY_2 = "KEY_2"
@@ -111,14 +118,25 @@ class MediaDataManagerTest : SysuiTestCase() {
111118
private val clock = FakeSystemClock()
112119
@Mock private lateinit var tunerService: TunerService
113120
@Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable>
121+
@Mock private lateinit var ugm: IUriGrantsManager
122+
@Mock private lateinit var imageSource: ImageDecoder.Source
114123

115124
private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
116125

117126
private val originalSmartspaceSetting = Settings.Secure.getInt(context.contentResolver,
118127
Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1)
119128

129+
private lateinit var staticMockSession: MockitoSession
130+
120131
@Before
121132
fun setup() {
133+
staticMockSession =
134+
ExtendedMockito.mockitoSession()
135+
.mockStatic<UriGrantsManager>(UriGrantsManager::class.java)
136+
.mockStatic<ImageDecoder>(ImageDecoder::class.java)
137+
.strictness(Strictness.LENIENT)
138+
.startMocking()
139+
whenever(UriGrantsManager.getService()).thenReturn(ugm)
122140
foregroundExecutor = FakeExecutor(clock)
123141
backgroundExecutor = FakeExecutor(clock)
124142
smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
@@ -195,6 +213,7 @@ class MediaDataManagerTest : SysuiTestCase() {
195213

196214
@After
197215
fun tearDown() {
216+
staticMockSession.finishMocking()
198217
session.release()
199218
mediaDataManager.destroy()
200219
Settings.Secure.putInt(context.contentResolver,
@@ -1093,6 +1112,66 @@ class MediaDataManagerTest : SysuiTestCase() {
10931112
anyBoolean())
10941113
}
10951114

1115+
@Test
1116+
fun testResumeMediaLoaded_hasArtPermission_artLoaded() {
1117+
// When resume media is loaded and user/app has permission to access the art URI,
1118+
whenever(
1119+
ugm.checkGrantUriPermission_ignoreNonSystem(
1120+
anyInt(),
1121+
any(),
1122+
any(),
1123+
anyInt(),
1124+
anyInt()
1125+
)
1126+
)
1127+
.thenReturn(1)
1128+
val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
1129+
val uri = Uri.parse("content://example")
1130+
whenever(ImageDecoder.createSource(any(), eq(uri))).thenReturn(imageSource)
1131+
whenever(ImageDecoder.decodeBitmap(any(), any())).thenReturn(artwork)
1132+
1133+
val desc =
1134+
MediaDescription.Builder().run {
1135+
setTitle(SESSION_TITLE)
1136+
setIconUri(uri)
1137+
build()
1138+
}
1139+
addResumeControlAndLoad(desc)
1140+
1141+
// Then the artwork is loaded
1142+
assertThat(mediaDataCaptor.value.artwork).isNotNull()
1143+
}
1144+
1145+
@Test
1146+
fun testResumeMediaLoaded_noArtPermission_noArtLoaded() {
1147+
// When resume media is loaded and user/app does not have permission to access the art URI
1148+
whenever(
1149+
ugm.checkGrantUriPermission_ignoreNonSystem(
1150+
anyInt(),
1151+
any(),
1152+
any(),
1153+
anyInt(),
1154+
anyInt()
1155+
)
1156+
)
1157+
.thenThrow(SecurityException("Test no permission"))
1158+
val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
1159+
val uri = Uri.parse("content://example")
1160+
whenever(ImageDecoder.createSource(any(), eq(uri))).thenReturn(imageSource)
1161+
whenever(ImageDecoder.decodeBitmap(any(), any())).thenReturn(artwork)
1162+
1163+
val desc =
1164+
MediaDescription.Builder().run {
1165+
setTitle(SESSION_TITLE)
1166+
setIconUri(uri)
1167+
build()
1168+
}
1169+
addResumeControlAndLoad(desc)
1170+
1171+
// Then the artwork is not loaded
1172+
assertThat(mediaDataCaptor.value.artwork).isNull()
1173+
}
1174+
10961175
/**
10971176
* Helper function to add a media notification and capture the resulting MediaData
10981177
*/
@@ -1103,4 +1182,32 @@ class MediaDataManagerTest : SysuiTestCase() {
11031182
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
11041183
eq(0), eq(false))
11051184
}
1185+
1186+
/** Helper function to add a resumption control and capture the resulting MediaData */
1187+
private fun addResumeControlAndLoad(
1188+
desc: MediaDescription,
1189+
packageName: String = PACKAGE_NAME
1190+
) {
1191+
mediaDataManager.addResumptionControls(
1192+
USER_ID,
1193+
desc,
1194+
Runnable {},
1195+
session.sessionToken,
1196+
APP_NAME,
1197+
pendingIntent,
1198+
packageName
1199+
)
1200+
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
1201+
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
1202+
1203+
verify(listener)
1204+
.onMediaDataLoaded(
1205+
eq(packageName),
1206+
eq(null),
1207+
capture(mediaDataCaptor),
1208+
eq(true),
1209+
eq(0),
1210+
eq(false)
1211+
)
1212+
}
11061213
}

services/core/java/com/android/server/uri/UriGrantsManagerService.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
import android.annotation.NonNull;
4343
import android.annotation.Nullable;
44+
import android.annotation.RequiresPermission;
4445
import android.app.ActivityManager;
4546
import android.app.ActivityManagerInternal;
4647
import android.app.AppGlobals;
@@ -62,6 +63,7 @@
6263
import android.os.IBinder;
6364
import android.os.Looper;
6465
import android.os.Message;
66+
import android.os.Process;
6567
import android.os.RemoteException;
6668
import android.os.SystemClock;
6769
import android.os.UserHandle;
@@ -1304,6 +1306,46 @@ private boolean checkUriPermissionLocked(GrantUri grantUri, int uid, final int m
13041306
return false;
13051307
}
13061308

1309+
/**
1310+
* Check if the targetPkg can be granted permission to access uri by
1311+
* the callingUid using the given modeFlags. See {@link #checkGrantUriPermissionUnlocked}.
1312+
*
1313+
* @param callingUid The uid of the grantor app that has permissions to the uri.
1314+
* @param targetPkg The package name of the granted app that needs permissions to the uri.
1315+
* @param uri The uri for which permissions should be granted.
1316+
* @param modeFlags The modes to grant. See {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}, etc.
1317+
* @param userId The userId in which the uri is to be resolved.
1318+
* @return uid of the target or -1 if permission grant not required. Returns -1 if the caller
1319+
* does not hold INTERACT_ACROSS_USERS_FULL
1320+
* @throws SecurityException if the grant is not allowed.
1321+
*/
1322+
@Override
1323+
@RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
1324+
public int checkGrantUriPermission_ignoreNonSystem(int callingUid, String targetPkg, Uri uri,
1325+
int modeFlags, int userId) {
1326+
if (!isCallerIsSystemOrPrivileged()) {
1327+
return Process.INVALID_UID;
1328+
}
1329+
final long origId = Binder.clearCallingIdentity();
1330+
try {
1331+
return checkGrantUriPermissionUnlocked(callingUid, targetPkg, uri, modeFlags,
1332+
userId);
1333+
} finally {
1334+
Binder.restoreCallingIdentity(origId);
1335+
}
1336+
}
1337+
1338+
private boolean isCallerIsSystemOrPrivileged() {
1339+
final int uid = Binder.getCallingUid();
1340+
if (uid == Process.SYSTEM_UID || uid == Process.ROOT_UID) {
1341+
return true;
1342+
}
1343+
return ActivityManager.checkComponentPermission(
1344+
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
1345+
uid, /* owningUid = */-1, /* exported = */ true)
1346+
== PackageManager.PERMISSION_GRANTED;
1347+
}
1348+
13071349
@Override
13081350
public ArrayList<UriPermission> providePersistentUriGrants() {
13091351
final ArrayList<UriPermission> result = new ArrayList<>();

0 commit comments

Comments
 (0)