From 215d2ae95cc56feb1eb367d1ef4b2b06b2eabaeb Mon Sep 17 00:00:00 2001 From: Winson Chung Date: Tue, 18 Oct 2022 05:21:30 +0000 Subject: Ensure that only SysUI can override pending intent launch flags - Originally added in ag/5139951, this method ensured that activities launched from widgets are always started in a new task (if the activity is launched in the home task, the task is not brough forward with the recents transition). We can restrict this to only recents callers since this only applies to 1p launchers in gesture nav (both the gesture with 3p launchers and button nav in general will always start the home intent directly, which makes adding the NEW_TASK flag unnecessary). Bug: 243794108 Test: Ensure that the original bug b/112508020 still works (with the test app in the bug, swipe up still works after launching an activity from the widget, and fails without applying the override flags) (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:c4d3106e347922610f8c554de3ae238175ed393e) Merged-In: Id53c6a2aa6da5933d488ca06a0bfc4ef89a4c343 Change-Id: Id53c6a2aa6da5933d488ca06a0bfc4ef89a4c343 --- .../core/java/com/android/server/am/PendingIntentRecord.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index bda60ff2172b..8624ee031a93 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -379,11 +379,16 @@ public final class PendingIntentRecord extends IIntentSender.Stub { resolvedType = key.requestResolvedType; } - // Apply any launch flags from the ActivityOptions. This is to ensure that the caller - // can specify a consistent launch mode even if the PendingIntent is immutable + // Apply any launch flags from the ActivityOptions. This is used only by SystemUI + // to ensure that we can launch the pending intent with a consistent launch mode even + // if the provided PendingIntent is immutable (ie. to force an activity to launch into + // a new task, or to launch multiple instances if supported by the app) final ActivityOptions opts = ActivityOptions.fromBundle(options); if (opts != null) { - finalIntent.addFlags(opts.getPendingIntentLaunchFlags()); + // TODO(b/254490217): Move this check into SafeActivityOptions + if (controller.mAtmInternal.isCallerRecents(Binder.getCallingUid())) { + finalIntent.addFlags(opts.getPendingIntentLaunchFlags()); + } } // Extract options before clearing calling identity -- cgit v1.2.3 From d8ee42c347ed398935eb50c9cff81f45ab1d57da Mon Sep 17 00:00:00 2001 From: Winson Chung Date: Wed, 11 Jan 2023 18:58:41 +0000 Subject: Revert "Ensure that only SysUI can override pending intent launch flags" This reverts commit c4d3106e347922610f8c554de3ae238175ed393e. Reason for revert: b/264884187, b/264885689 (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:48acfb0f1d71912e757cadd505901471c1df4d4c) Merged-In: I9fb0d66327f3f872a92e6b9d682d58489e81e6ba Change-Id: I9fb0d66327f3f872a92e6b9d682d58489e81e6ba --- .../core/java/com/android/server/am/PendingIntentRecord.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 8624ee031a93..bda60ff2172b 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -379,16 +379,11 @@ public final class PendingIntentRecord extends IIntentSender.Stub { resolvedType = key.requestResolvedType; } - // Apply any launch flags from the ActivityOptions. This is used only by SystemUI - // to ensure that we can launch the pending intent with a consistent launch mode even - // if the provided PendingIntent is immutable (ie. to force an activity to launch into - // a new task, or to launch multiple instances if supported by the app) + // Apply any launch flags from the ActivityOptions. This is to ensure that the caller + // can specify a consistent launch mode even if the PendingIntent is immutable final ActivityOptions opts = ActivityOptions.fromBundle(options); if (opts != null) { - // TODO(b/254490217): Move this check into SafeActivityOptions - if (controller.mAtmInternal.isCallerRecents(Binder.getCallingUid())) { - finalIntent.addFlags(opts.getPendingIntentLaunchFlags()); - } + finalIntent.addFlags(opts.getPendingIntentLaunchFlags()); } // Extract options before clearing calling identity -- cgit v1.2.3 From 8275dfa544d12450b9b1ce88f897ced207f661b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Thu, 13 Apr 2023 17:58:22 +0200 Subject: Grant URI permissions to the CallStyle-related ones This will also verify that the caller app can actually grant them. Fix: 274592467 Test: atest NotificationManagerServiceTest (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:4dee5aab12e95cd8b4d663ad050f07b0f2433596) Merged-In: I83429f9e63e51c615a6e3f03befb76bb5b8ea7fc Change-Id: I83429f9e63e51c615a6e3f03befb76bb5b8ea7fc --- core/java/android/app/Notification.java | 8 ++++++++ .../NotificationManagerServiceTest.java | 23 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 7f48e39a9a79..eb9567c26d8d 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -2883,6 +2883,14 @@ public class Notification implements Parcelable } } + if (isStyle(CallStyle.class) & extras != null) { + Person callPerson = extras.getParcelable(EXTRA_CALL_PERSON); + if (callPerson != null) { + visitor.accept(callPerson.getIconUri()); + } + visitIconUri(visitor, extras.getParcelable(EXTRA_VERIFICATION_ICON)); + } + if (mBubbleMetadata != null) { visitIconUri(visitor, mBubbleMetadata.getIcon()); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 874846d333cf..6827bbe1b43b 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -5390,6 +5390,29 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(visitor, times(1)).accept(eq(backgroundImage)); } + @Test + public void testVisitUris_callStyle() { + Icon personIcon = Icon.createWithContentUri("content://media/person"); + Icon verificationIcon = Icon.createWithContentUri("content://media/verification"); + Person callingPerson = new Person.Builder().setName("Someone") + .setIcon(personIcon) + .build(); + PendingIntent hangUpIntent = PendingIntent.getActivity(mContext, 0, new Intent(), + PendingIntent.FLAG_IMMUTABLE); + Notification n = new Notification.Builder(mContext, "a") + .setStyle(Notification.CallStyle.forOngoingCall(callingPerson, hangUpIntent) + .setVerificationIcon(verificationIcon)) + .setContentTitle("Calling...") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .build(); + + Consumer visitor = (Consumer) spy(Consumer.class); + n.visitUris(visitor); + + verify(visitor, times(1)).accept(eq(personIcon.getUri())); + verify(visitor, times(1)).accept(eq(verificationIcon.getUri())); + } + @Test public void testVisitUris_audioContentsString() throws Exception { final Uri audioContents = Uri.parse("content://com.example/audio"); -- cgit v1.2.3 From f0390ffcebeabe0399e7323e6c67453810fbe6c4 Mon Sep 17 00:00:00 2001 From: Winson Chung Date: Wed, 8 Feb 2023 01:04:46 +0000 Subject: Only allow NEW_TASK flag when adjusting pending intents Bug: 243794108 Test: atest CtsSecurityBulletinHostTestCases:android.security.cts.CVE_2023_20918 (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:c62d2e1021a030f4f0ae5fcfc8fe8e0875fa669f) Merged-In: I5d329beecef1902c36704e93d0bc5cb60d0e2f5b Change-Id: I5d329beecef1902c36704e93d0bc5cb60d0e2f5b --- core/java/android/app/ActivityOptions.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index d6f44e60eb0c..2540f1dea732 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -21,6 +21,8 @@ import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIO import static android.Manifest.permission.START_TASKS_FROM_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.content.Intent.FLAG_RECEIVER_FOREGROUND; import static android.view.Display.INVALID_DISPLAY; import android.annotation.IntDef; @@ -1727,7 +1729,9 @@ public class ActivityOptions extends ComponentOptions { * @hide */ public int getPendingIntentLaunchFlags() { - return mPendingIntentLaunchFlags; + // b/243794108: Ignore all flags except the new task flag, to be reconsidered in b/254490217 + return mPendingIntentLaunchFlags & + (FLAG_ACTIVITY_NEW_TASK | FLAG_RECEIVER_FOREGROUND); } /** -- cgit v1.2.3 From e54defeeab36308144dbbe3a577946904b68663c Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 28 Mar 2023 13:15:04 -0700 Subject: Dismiss keyguard when simpin auth'd and... security method is none. This is mostly to fix the case where we auth sim pin in the set up wizard and it goes straight to keyguard instead of the setup wizard activity. This works with the prevent bypass keyguard flag because the device should be noe secure in this case. Fixes: 222446076 Test: turn locked sim on, which opens the sim pin screen. Auth the screen and observe that keyguard is not shown. (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:48fa9bef3451e4a358c941af5b230f99881c5cb6) Cherry-picking this CL as a security fix Bug: 222446076 (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:65ea56f54c059584eb27ec53d486dba8161316ab) Merged-In: Id302c41f63028bc6dd58ba686e23d73565de9675 Change-Id: Id302c41f63028bc6dd58ba686e23d73565de9675 --- .../src/com/android/keyguard/KeyguardSecurityContainerController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 061bab8a7006..16299c7aff7b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -752,7 +752,7 @@ public class KeyguardSecurityContainerController extends ViewController Date: Fri, 21 Apr 2023 09:45:07 +0000 Subject: Truncate ShortcutInfo Id Creating Conversation with a ShortcutId longer than 65_535 (max unsigned short), we did not save the conversation settings into the notification_policy.xml due to a restriction in FastDataOutput. This put us to a state where the user changing the importance or turning off the notifications for the given conversation had no effect on notification behavior. Fixes: 273729476 Test: atest ShortcutManagerTest2 Test: Create a test app which creates a Conversation with a long shortcutId. Go to the Conversation Settings and turn off Notifications. Post a new Notification to this Conversation and see if it is displayed. (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:8a5223fcd59e805da510c31c5f8462c5d01ec6d5) Merged-In: I2617de6f9e8a7dbfd8fbeff589a7d592f00d87c5 Change-Id: I2617de6f9e8a7dbfd8fbeff589a7d592f00d87c5 --- core/java/android/content/pm/ShortcutInfo.java | 20 +++++++++++++++++--- .../com/android/server/pm/ShortcutManagerTest2.java | 9 +++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java index 52774e354c90..c33390398400 100644 --- a/core/java/android/content/pm/ShortcutInfo.java +++ b/core/java/android/content/pm/ShortcutInfo.java @@ -283,6 +283,12 @@ public final class ShortcutInfo implements Parcelable { */ public static final int DISABLED_REASON_OTHER_RESTORE_ISSUE = 103; + /** + * The maximum length of Shortcut ID. IDs will be truncated at this limit. + * @hide + */ + public static final int MAX_ID_LENGTH = 1000; + /** @hide */ @IntDef(prefix = { "DISABLED_REASON_" }, value = { DISABLED_REASON_NOT_DISABLED, @@ -475,8 +481,7 @@ public final class ShortcutInfo implements Parcelable { private ShortcutInfo(Builder b) { mUserId = b.mContext.getUserId(); - - mId = Preconditions.checkStringNotEmpty(b.mId, "Shortcut ID must be provided"); + mId = getSafeId(Preconditions.checkStringNotEmpty(b.mId, "Shortcut ID must be provided")); // Note we can't do other null checks here because SM.updateShortcuts() takes partial // information. @@ -582,6 +587,14 @@ public final class ShortcutInfo implements Parcelable { return ret; } + @NonNull + private static String getSafeId(@NonNull String id) { + if (id.length() > MAX_ID_LENGTH) { + return id.substring(0, MAX_ID_LENGTH); + } + return id; + } + /** * Throws if any of the mandatory fields is not set. * @@ -2336,7 +2349,8 @@ public final class ShortcutInfo implements Parcelable { final ClassLoader cl = getClass().getClassLoader(); mUserId = source.readInt(); - mId = source.readString8(); + mId = getSafeId(Preconditions.checkStringNotEmpty(source.readString8(), + "Shortcut ID must be provided")); mPackageName = source.readString8(); mActivity = source.readParcelable(cl, android.content.ComponentName.class); mFlags = source.readInt(); diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java index c78678431dac..86d4655e9d3a 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java @@ -228,6 +228,15 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest { }); } + public void testShortcutIdTruncated() { + ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), + "s".repeat(Short.MAX_VALUE)).build(); + + assertTrue( + "id must be truncated to MAX_ID_LENGTH", + si.getId().length() <= ShortcutInfo.MAX_ID_LENGTH); + } + public void testShortcutInfoParcel() { setCaller(CALLING_PACKAGE_1, USER_10); ShortcutInfo si = parceled(new ShortcutInfo.Builder(mClientContext) -- cgit v1.2.3 From 65364b7e1ca14a9d42e287ee2f8f49306d2f0ba3 Mon Sep 17 00:00:00 2001 From: Evan Severson Date: Tue, 12 Apr 2022 17:36:41 -0700 Subject: Watch uid proc state instead of importance for 1-time permissions The system process may bind to an app with the flag BIND_FOREGROUND_SERVICE, this will put the client in the foreground service importance level without the normal requirement that foreground services must show a notification. Looking at proc states instead allows us to differentiate between these two levels of foreground service and revoke the client when not in use. This change makes the parameters `importanceToResetTimer` and `importanceToKeepSessionAlive` in PermissionManager#startOneTimePermissionSession obsolete. Test: atest CtsPermissionTestCases + manual testing with mic/cam/loc Bug: 217981062 (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:0be78fbbf7d92bf29858aa0c48b171045ab5057f) Merged-In: I7a725647c001062d1a76a82b680a02e3e2edcb03 Change-Id: I7a725647c001062d1a76a82b680a02e3e2edcb03 --- .../android/permission/IPermissionManager.aidl | 3 +- .../java/android/permission/PermissionManager.java | 3 +- .../permission/OneTimePermissionUserManager.java | 231 ++++++++++----------- .../pm/permission/PermissionManagerService.java | 6 +- 4 files changed, 116 insertions(+), 127 deletions(-) diff --git a/core/java/android/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl index 6a93b354f4da..45dad9861406 100644 --- a/core/java/android/permission/IPermissionManager.aidl +++ b/core/java/android/permission/IPermissionManager.aidl @@ -77,8 +77,7 @@ interface IPermissionManager { List getSplitPermissions(); void startOneTimePermissionSession(String packageName, int userId, long timeout, - long revokeAfterKilledDelay, int importanceToResetTimer, - int importanceToKeepSessionAlive); + long revokeAfterKilledDelay); void stopOneTimePermissionSession(String packageName, int userId); diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java index 6b540d72bba0..67699543131a 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -1371,8 +1371,7 @@ public final class PermissionManager { @ActivityManager.RunningAppProcessInfo.Importance int importanceToKeepSessionAlive) { try { mPermissionManager.startOneTimePermissionSession(packageName, mContext.getUserId(), - timeoutMillis, revokeAfterKilledDelayMillis, importanceToResetTimer, - importanceToKeepSessionAlive); + timeoutMillis, revokeAfterKilledDelayMillis); } catch (RemoteException e) { e.rethrowFromSystemServer(); } diff --git a/services/core/java/com/android/server/pm/permission/OneTimePermissionUserManager.java b/services/core/java/com/android/server/pm/permission/OneTimePermissionUserManager.java index 881f8707fdd8..a1c98109052e 100644 --- a/services/core/java/com/android/server/pm/permission/OneTimePermissionUserManager.java +++ b/services/core/java/com/android/server/pm/permission/OneTimePermissionUserManager.java @@ -16,17 +16,18 @@ package com.android.server.pm.permission; -import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED; - import android.annotation.NonNull; import android.app.ActivityManager; import android.app.AlarmManager; +import android.app.IActivityManager; +import android.app.IUidObserver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.os.Handler; +import android.os.RemoteException; import android.permission.PermissionControllerManager; import android.provider.DeviceConfig; import android.util.Log; @@ -47,7 +48,7 @@ public class OneTimePermissionUserManager { "one_time_permissions_killed_delay_millis"; private final @NonNull Context mContext; - private final @NonNull ActivityManager mActivityManager; + private final @NonNull IActivityManager mIActivityManager; private final @NonNull AlarmManager mAlarmManager; private final @NonNull PermissionControllerManager mPermissionControllerManager; @@ -77,49 +78,14 @@ public class OneTimePermissionUserManager { OneTimePermissionUserManager(@NonNull Context context) { mContext = context; - mActivityManager = context.getSystemService(ActivityManager.class); + mIActivityManager = ActivityManager.getService(); mAlarmManager = context.getSystemService(AlarmManager.class); mPermissionControllerManager = context.getSystemService(PermissionControllerManager.class); mHandler = context.getMainThreadHandler(); } - /** - * Starts a one-time permission session for a given package. A one-time permission session is - * ended if app becomes inactive. Inactivity is defined as the package's uid importance level - * staying > importanceToResetTimer for timeoutMillis milliseconds. If the package's uid - * importance level goes <= importanceToResetTimer then the timer is reset and doesn't start - * until going > importanceToResetTimer. - *

- * When this timeoutMillis is reached if the importance level is <= importanceToKeepSessionAlive - * then the session is extended until either the importance goes above - * importanceToKeepSessionAlive which will end the session or <= importanceToResetTimer which - * will continue the session and reset the timer. - *

- *

- * Importance levels are defined in {@link android.app.ActivityManager.RunningAppProcessInfo}. - *

- *

- * Once the session ends PermissionControllerService#onNotifyOneTimePermissionSessionTimeout - * is invoked. - *

- *

- * Note that if there is currently an active session for a package a new one isn't created and - * the existing one isn't changed. - *

- * @param packageName The package to start a one-time permission session for - * @param timeoutMillis Number of milliseconds for an app to be in an inactive state - * @param revokeAfterKilledDelayMillis Number of milliseconds to wait after the process dies - * before ending the session. Set to -1 to use default value - * for the device. - * @param importanceToResetTimer The least important level to uid must be to reset the timer - * @param importanceToKeepSessionAlive The least important level the uid must be to keep the - * session alive - * - * @hide - */ void startPackageOneTimeSession(@NonNull String packageName, long timeoutMillis, - long revokeAfterKilledDelayMillis, int importanceToResetTimer, - int importanceToKeepSessionAlive) { + long revokeAfterKilledDelayMillis) { int uid; try { uid = mContext.getPackageManager().getPackageUid(packageName, 0); @@ -131,13 +97,11 @@ public class OneTimePermissionUserManager { synchronized (mLock) { PackageInactivityListener listener = mListeners.get(uid); if (listener != null) { - listener.updateSessionParameters(timeoutMillis, revokeAfterKilledDelayMillis, - importanceToResetTimer, importanceToKeepSessionAlive); + listener.updateSessionParameters(timeoutMillis, revokeAfterKilledDelayMillis); return; } listener = new PackageInactivityListener(uid, packageName, timeoutMillis, - revokeAfterKilledDelayMillis, importanceToResetTimer, - importanceToKeepSessionAlive); + revokeAfterKilledDelayMillis); mListeners.put(uid, listener); } } @@ -182,34 +146,58 @@ public class OneTimePermissionUserManager { private static final long TIMER_INACTIVE = -1; + private static final int STATE_GONE = 0; + private static final int STATE_TIMER = 1; + private static final int STATE_ACTIVE = 2; + private final int mUid; private final @NonNull String mPackageName; private long mTimeout; private long mRevokeAfterKilledDelay; - private int mImportanceToResetTimer; - private int mImportanceToKeepSessionAlive; private boolean mIsAlarmSet; private boolean mIsFinished; private long mTimerStart = TIMER_INACTIVE; - private final ActivityManager.OnUidImportanceListener mStartTimerListener; - private final ActivityManager.OnUidImportanceListener mSessionKillableListener; - private final ActivityManager.OnUidImportanceListener mGoneListener; - private final Object mInnerLock = new Object(); private final Object mToken = new Object(); + private final IUidObserver.Stub mObserver = new IUidObserver.Stub() { + @Override + public void onUidGone(int uid, boolean disabled) { + if (uid == mUid) { + PackageInactivityListener.this.updateUidState(STATE_GONE); + } + } - private PackageInactivityListener(int uid, @NonNull String packageName, long timeout, - long revokeAfterkilledDelay, int importanceToResetTimer, - int importanceToKeepSessionAlive) { + @Override + public void onUidStateChanged(int uid, int procState, long procStateSeq, + int capability) { + if (uid == mUid) { + if (procState > ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE + && procState != ActivityManager.PROCESS_STATE_NONEXISTENT) { + PackageInactivityListener.this.updateUidState(STATE_TIMER); + } else { + PackageInactivityListener.this.updateUidState(STATE_ACTIVE); + } + } + } + + public void onUidActive(int uid) { + } + public void onUidIdle(int uid, boolean disabled) { + } + public void onUidProcAdjChanged(int uid) { + } + public void onUidCachedChanged(int uid, boolean cached) { + } + }; + private PackageInactivityListener(int uid, @NonNull String packageName, long timeout, + long revokeAfterkilledDelay) { Log.i(LOG_TAG, "Start tracking " + packageName + ". uid=" + uid + " timeout=" + timeout - + " killedDelay=" + revokeAfterkilledDelay - + " importanceToResetTimer=" + importanceToResetTimer - + " importanceToKeepSessionAlive=" + importanceToKeepSessionAlive); + + " killedDelay=" + revokeAfterkilledDelay); mUid = uid; mPackageName = packageName; @@ -219,27 +207,24 @@ public class OneTimePermissionUserManager { DeviceConfig.NAMESPACE_PERMISSIONS, PROPERTY_KILLED_DELAY_CONFIG_KEY, DEFAULT_KILLED_DELAY_MILLIS) : revokeAfterkilledDelay; - mImportanceToResetTimer = importanceToResetTimer; - mImportanceToKeepSessionAlive = importanceToKeepSessionAlive; - - mStartTimerListener = - (changingUid, importance) -> onImportanceChanged(changingUid, importance); - mSessionKillableListener = - (changingUid, importance) -> onImportanceChanged(changingUid, importance); - mGoneListener = - (changingUid, importance) -> onImportanceChanged(changingUid, importance); - - mActivityManager.addOnUidImportanceListener(mStartTimerListener, - importanceToResetTimer); - mActivityManager.addOnUidImportanceListener(mSessionKillableListener, - importanceToKeepSessionAlive); - mActivityManager.addOnUidImportanceListener(mGoneListener, IMPORTANCE_CACHED); - - onImportanceChanged(mUid, mActivityManager.getPackageImportance(packageName)); + + try { + mIActivityManager.registerUidObserver(mObserver, + ActivityManager.UID_OBSERVER_GONE | ActivityManager.UID_OBSERVER_PROCSTATE, + ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, + null); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Couldn't check uid proc state", e); + // Can't register uid observer, just revoke immediately + synchronized (mInnerLock) { + onPackageInactiveLocked(); + } + } + + updateUidState(); } - public void updateSessionParameters(long timeoutMillis, long revokeAfterKilledDelayMillis, - int importanceToResetTimer, int importanceToKeepSessionAlive) { + public void updateSessionParameters(long timeoutMillis, long revokeAfterKilledDelayMillis) { synchronized (mInnerLock) { mTimeout = Math.min(mTimeout, timeoutMillis); mRevokeAfterKilledDelay = Math.min(mRevokeAfterKilledDelay, @@ -248,63 +233,79 @@ public class OneTimePermissionUserManager { DeviceConfig.NAMESPACE_PERMISSIONS, PROPERTY_KILLED_DELAY_CONFIG_KEY, DEFAULT_KILLED_DELAY_MILLIS) : revokeAfterKilledDelayMillis); - mImportanceToResetTimer = Math.min(importanceToResetTimer, mImportanceToResetTimer); - mImportanceToKeepSessionAlive = Math.min(importanceToKeepSessionAlive, - mImportanceToKeepSessionAlive); Log.v(LOG_TAG, "Updated params for " + mPackageName + ". timeout=" + mTimeout - + " killedDelay=" + mRevokeAfterKilledDelay - + " importanceToResetTimer=" + mImportanceToResetTimer - + " importanceToKeepSessionAlive=" + mImportanceToKeepSessionAlive); - onImportanceChanged(mUid, mActivityManager.getPackageImportance(mPackageName)); + + " killedDelay=" + mRevokeAfterKilledDelay); + updateUidState(); } } - private void onImportanceChanged(int uid, int importance) { - if (uid != mUid) { - return; + private int getCurrentState() { + try { + return getStateFromProcState(mIActivityManager.getUidProcessState(mUid, null)); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Couldn't check uid proc state", e); } + return STATE_GONE; + } - Log.v(LOG_TAG, "Importance changed for " + mPackageName + " (" + mUid + ")." - + " importance=" + importance); + private int getStateFromProcState(int procState) { + if (procState == ActivityManager.PROCESS_STATE_NONEXISTENT) { + return STATE_GONE; + } else { + if (procState > ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) { + return STATE_TIMER; + } else { + return STATE_ACTIVE; + } + } + } + + private void updateUidState() { + updateUidState(getCurrentState()); + } + + private void updateUidState(int state) { + Log.v(LOG_TAG, "Updating state for " + mPackageName + " (" + mUid + ")." + + " state=" + state); synchronized (mInnerLock) { // Remove any pending inactivity callback mHandler.removeCallbacksAndMessages(mToken); - if (importance > IMPORTANCE_CACHED) { + if (state == STATE_GONE) { if (mRevokeAfterKilledDelay == 0) { onPackageInactiveLocked(); return; } // Delay revocation in case app is restarting mHandler.postDelayed(() -> { - int imp = mActivityManager.getUidImportance(mUid); - if (imp > IMPORTANCE_CACHED) { - onPackageInactiveLocked(); - } else { - if (DEBUG) { - Log.d(LOG_TAG, "No longer gone after delayed revocation. " - + "Rechecking for " + mPackageName + " (" + mUid + ")."); + int currentState; + synchronized (mInnerLock) { + currentState = getCurrentState(); + if (currentState == STATE_GONE) { + onPackageInactiveLocked(); + return; } - onImportanceChanged(mUid, imp); } + if (DEBUG) { + Log.d(LOG_TAG, "No longer gone after delayed revocation. " + + "Rechecking for " + mPackageName + " (" + mUid + + ")."); + } + updateUidState(currentState); }, mToken, mRevokeAfterKilledDelay); return; - } - if (importance > mImportanceToResetTimer) { + } else if (state == STATE_TIMER) { if (mTimerStart == TIMER_INACTIVE) { if (DEBUG) { Log.d(LOG_TAG, "Start the timer for " + mPackageName + " (" + mUid + ")."); } mTimerStart = System.currentTimeMillis(); + setAlarmLocked(); } - } else { + } else if (state == STATE_ACTIVE) { mTimerStart = TIMER_INACTIVE; - } - if (importance > mImportanceToKeepSessionAlive) { - setAlarmLocked(); - } else { cancelAlarmLocked(); } } @@ -318,19 +319,9 @@ public class OneTimePermissionUserManager { mIsFinished = true; cancelAlarmLocked(); try { - mActivityManager.removeOnUidImportanceListener(mStartTimerListener); - } catch (IllegalArgumentException e) { - Log.e(LOG_TAG, "Could not remove start timer listener", e); - } - try { - mActivityManager.removeOnUidImportanceListener(mSessionKillableListener); - } catch (IllegalArgumentException e) { - Log.e(LOG_TAG, "Could not remove session killable listener", e); - } - try { - mActivityManager.removeOnUidImportanceListener(mGoneListener); - } catch (IllegalArgumentException e) { - Log.e(LOG_TAG, "Could not remove gone listener", e); + mIActivityManager.unregisterUidObserver(mObserver); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Unable to unregister uid observer.", e); } } } @@ -394,9 +385,11 @@ public class OneTimePermissionUserManager { mPermissionControllerManager.notifyOneTimePermissionSessionTimeout( mPackageName); }); - mActivityManager.removeOnUidImportanceListener(mStartTimerListener); - mActivityManager.removeOnUidImportanceListener(mSessionKillableListener); - mActivityManager.removeOnUidImportanceListener(mGoneListener); + try { + mIActivityManager.unregisterUidObserver(mObserver); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Unable to unregister uid observer.", e); + } synchronized (mLock) { mListeners.remove(mUid); } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index d648d6fc663d..fd48cdaeebf2 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -386,8 +386,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { @Override public void startOneTimePermissionSession(String packageName, @UserIdInt int userId, - long timeoutMillis, long revokeAfterKilledDelayMillis, int importanceToResetTimer, - int importanceToKeepSessionAlive) { + long timeoutMillis, long revokeAfterKilledDelayMillis) { mContext.enforceCallingOrSelfPermission( Manifest.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS, "Must hold " + Manifest.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS @@ -397,8 +396,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { final long token = Binder.clearCallingIdentity(); try { getOneTimePermissionUserManager(userId).startPackageOneTimeSession(packageName, - timeoutMillis, revokeAfterKilledDelayMillis, importanceToResetTimer, - importanceToKeepSessionAlive); + timeoutMillis, revokeAfterKilledDelayMillis); } finally { Binder.restoreCallingIdentity(token); } -- cgit v1.2.3 From c6b497985f2a36fc36f158045c769b45c2a22ea5 Mon Sep 17 00:00:00 2001 From: Evan Severson Date: Tue, 31 Jan 2023 17:14:34 -0800 Subject: [1-time permissions] Use internal api to check proc states We need to check the proc state and the binder method has a filter that is affected by a bug that keeps a killed a proces in the "pending top" list. Using the internal api isn't affected by this filter and also is more correct for inprocess calls. Test: Install test app that requests permission and will exit immediately on granting, observe permission is no longer indefinitely held. Bug: 254736794 (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:e836611f3057cf9eae589a34a39fe80d0a9145f3) Merged-In: I30579090c803b231fd750abbc4ad645805f7ece2 Change-Id: I30579090c803b231fd750abbc4ad645805f7ece2 --- .../server/pm/permission/OneTimePermissionUserManager.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/services/core/java/com/android/server/pm/permission/OneTimePermissionUserManager.java b/services/core/java/com/android/server/pm/permission/OneTimePermissionUserManager.java index a1c98109052e..d28048ce74c7 100644 --- a/services/core/java/com/android/server/pm/permission/OneTimePermissionUserManager.java +++ b/services/core/java/com/android/server/pm/permission/OneTimePermissionUserManager.java @@ -18,6 +18,7 @@ package com.android.server.pm.permission; import android.annotation.NonNull; import android.app.ActivityManager; +import android.app.ActivityManagerInternal; import android.app.AlarmManager; import android.app.IActivityManager; import android.app.IUidObserver; @@ -34,6 +35,7 @@ import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.server.LocalServices; /** * Class that handles one-time permissions for a user @@ -49,6 +51,7 @@ public class OneTimePermissionUserManager { private final @NonNull Context mContext; private final @NonNull IActivityManager mIActivityManager; + private final @NonNull ActivityManagerInternal mActivityManagerInternal; private final @NonNull AlarmManager mAlarmManager; private final @NonNull PermissionControllerManager mPermissionControllerManager; @@ -79,6 +82,7 @@ public class OneTimePermissionUserManager { OneTimePermissionUserManager(@NonNull Context context) { mContext = context; mIActivityManager = ActivityManager.getService(); + mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); mAlarmManager = context.getSystemService(AlarmManager.class); mPermissionControllerManager = context.getSystemService(PermissionControllerManager.class); mHandler = context.getMainThreadHandler(); @@ -241,12 +245,7 @@ public class OneTimePermissionUserManager { } private int getCurrentState() { - try { - return getStateFromProcState(mIActivityManager.getUidProcessState(mUid, null)); - } catch (RemoteException e) { - Log.e(LOG_TAG, "Couldn't check uid proc state", e); - } - return STATE_GONE; + return getStateFromProcState(mActivityManagerInternal.getUidProcessState(mUid)); } private int getStateFromProcState(int procState) { -- cgit v1.2.3 From f24b1b321cf4f3302a15fbbb269a9b1237ccf7de Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Fri, 21 Apr 2023 15:39:22 +0000 Subject: Verify URI permissions for EXTRA_REMOTE_INPUT_HISTORY_ITEMS. Also added the person URIs in the test, since they weren't being checked. Test: atest NotificationManagerServiceTest & tested with POC from bug Bug: 276729064 (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:43b1711332763788c7abf05c3baa931296c45bbb) Merged-In: I848545f7aee202495c515f47a32871a2cb6ae707 Change-Id: I848545f7aee202495c515f47a32871a2cb6ae707 --- core/java/android/app/Notification.java | 11 ++++++++ .../NotificationManagerServiceTest.java | 32 ++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index eb9567c26d8d..de8fb509bde9 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -2853,6 +2853,17 @@ public class Notification implements Parcelable if (person != null) { visitor.accept(person.getIconUri()); } + + final RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[]) + extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS); + if (history != null) { + for (int i = 0; i < history.length; i++) { + RemoteInputHistoryItem item = history[i]; + if (item.getUri() != null) { + visitor.accept(item.getUri()); + } + } + } } if (isStyle(MessagingStyle.class) && extras != null) { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 6827bbe1b43b..8f8b1c50891a 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -128,6 +128,7 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Person; import android.app.RemoteInput; +import android.app.RemoteInputHistoryItem; import android.app.StatsManager; import android.app.admin.DevicePolicyManagerInternal; import android.app.usage.UsageStatsManagerInternal; @@ -5373,10 +5374,36 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testVisitUris() throws Exception { final Uri audioContents = Uri.parse("content://com.example/audio"); final Uri backgroundImage = Uri.parse("content://com.example/background"); + final Icon personIcon1 = Icon.createWithContentUri("content://media/person1"); + final Icon personIcon2 = Icon.createWithContentUri("content://media/person2"); + final Icon personIcon3 = Icon.createWithContentUri("content://media/person3"); + final Person person1 = new Person.Builder() + .setName("Messaging Person") + .setIcon(personIcon1) + .build(); + final Person person2 = new Person.Builder() + .setName("People List Person 1") + .setIcon(personIcon2) + .build(); + final Person person3 = new Person.Builder() + .setName("People List Person 2") + .setIcon(personIcon3) + .build(); + final Uri historyUri1 = Uri.parse("content://com.example/history1"); + final Uri historyUri2 = Uri.parse("content://com.example/history2"); + final RemoteInputHistoryItem historyItem1 = new RemoteInputHistoryItem(null, historyUri1, + "a"); + final RemoteInputHistoryItem historyItem2 = new RemoteInputHistoryItem(null, historyUri2, + "b"); Bundle extras = new Bundle(); extras.putParcelable(Notification.EXTRA_AUDIO_CONTENTS_URI, audioContents); extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI, backgroundImage.toString()); + extras.putParcelable(Notification.EXTRA_MESSAGING_PERSON, person1); + extras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, + new ArrayList<>(Arrays.asList(person2, person3))); + extras.putParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, + new RemoteInputHistoryItem[]{historyItem1, historyItem2}); Notification n = new Notification.Builder(mContext, "a") .setContentTitle("notification with uris") @@ -5388,6 +5415,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { n.visitUris(visitor); verify(visitor, times(1)).accept(eq(audioContents)); verify(visitor, times(1)).accept(eq(backgroundImage)); + verify(visitor, times(1)).accept(eq(personIcon1.getUri())); + verify(visitor, times(1)).accept(eq(personIcon2.getUri())); + verify(visitor, times(1)).accept(eq(personIcon3.getUri())); + verify(visitor, times(1)).accept(eq(historyUri1)); + verify(visitor, times(1)).accept(eq(historyUri2)); } @Test -- cgit v1.2.3 From be03cef81349041cc83020c4a0ba358d4489a7c6 Mon Sep 17 00:00:00 2001 From: Sumedh Sen Date: Thu, 23 Mar 2023 16:29:47 -0700 Subject: [RESTRICT AUTOMERGE] Prevent installing apps in policy restricted work profile using ADB If DISALLOW_DEBUGGING_FEATURES or DISALLOW_INSTALL_APPS restrictions are set on a work profile, prevent side loading of APKs using ADB in the work profile. Bug: 257443065 Test: atest CtsPackageInstallTestCases:UserRestrictionInstallTest (cherry picked from commit febe3918020a94b2af48ade98eb6a49cdd4a3bdf) (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:b988a09db551d9a8b2aeb0e8eb88e610605709e8) Merged-In: I169a1f72c84528ca606b6a4da165d4fbcd02b08d Change-Id: I169a1f72c84528ca606b6a4da165d4fbcd02b08d --- .../android/server/pm/InstallPackageHelper.java | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index c32a57c68ede..259701166147 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -2093,9 +2093,25 @@ final class InstallPackageHelper { // The caller explicitly specified INSTALL_ALL_USERS flag. // Thus, updating the settings to install the app for all users. for (int currentUserId : allUsers) { - ps.setInstalled(true, currentUserId); - ps.setEnabled(COMPONENT_ENABLED_STATE_DEFAULT, userId, - installerPackageName); + // If the app is already installed for the currentUser, + // keep it as installed as we might be updating the app at this place. + // If not currently installed, check if the currentUser is restricted by + // DISALLOW_INSTALL_APPS or DISALLOW_DEBUGGING_FEATURES device policy. + // Install / update the app if the user isn't restricted. Skip otherwise. + final boolean installedForCurrentUser = ArrayUtils.contains( + installedForUsers, currentUserId); + final boolean restrictedByPolicy = + mPm.isUserRestricted(currentUserId, + UserManager.DISALLOW_INSTALL_APPS) + || mPm.isUserRestricted(currentUserId, + UserManager.DISALLOW_DEBUGGING_FEATURES); + if (installedForCurrentUser || !restrictedByPolicy) { + ps.setInstalled(true, currentUserId); + ps.setEnabled(COMPONENT_ENABLED_STATE_DEFAULT, currentUserId, + installerPackageName); + } else { + ps.setInstalled(false, currentUserId); + } } } -- cgit v1.2.3 From 120d5b7d2e870c3a6ef37db8bd167e9c763fcd2a Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Thu, 27 Apr 2023 12:36:05 +0000 Subject: Visit URIs in landscape/portrait custom remote views. Bug: 277740848 Test: atest RemoteViewsTest NotificationManagerServiceTest & tested with POC from bug (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:b4692946c10d11c1e935869e11dc709a9cdcba69) Merged-In: I7d3d35df0ec38945019f71755bed8797b7af4517 Change-Id: I7d3d35df0ec38945019f71755bed8797b7af4517 --- core/java/android/widget/RemoteViews.java | 6 ++ .../src/android/widget/RemoteViewsTest.java | 64 ++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index a33906267736..c985ae82307c 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -726,6 +726,12 @@ public class RemoteViews implements Parcelable, Filter { mActions.get(i).visitUris(visitor); } } + if (mLandscape != null) { + mLandscape.visitUris(visitor); + } + if (mPortrait != null) { + mPortrait.visitUris(visitor); + } } private static void visitIconUri(Icon icon, @NonNull Consumer visitor) { diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java index 00b3693c902b..a9d3ce51b2c3 100644 --- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java +++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java @@ -24,6 +24,10 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import android.app.ActivityOptions; import android.app.PendingIntent; @@ -33,6 +37,8 @@ import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.net.Uri; import android.os.AsyncTask; import android.os.Binder; import android.os.Looper; @@ -58,6 +64,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.function.Consumer; /** * Tests for RemoteViews. @@ -690,4 +697,61 @@ public class RemoteViewsTest { return null; } } + + @Test + public void visitUris() { + RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test); + + final Uri imageUri = Uri.parse("content://media/image"); + final Icon icon1 = Icon.createWithContentUri("content://media/icon1"); + final Icon icon2 = Icon.createWithContentUri("content://media/icon2"); + final Icon icon3 = Icon.createWithContentUri("content://media/icon3"); + final Icon icon4 = Icon.createWithContentUri("content://media/icon4"); + views.setImageViewUri(R.id.image, imageUri); + views.setTextViewCompoundDrawables(R.id.text, icon1, icon2, icon3, icon4); + + Consumer visitor = (Consumer) spy(Consumer.class); + views.visitUris(visitor); + verify(visitor, times(1)).accept(eq(imageUri)); + verify(visitor, times(1)).accept(eq(icon1.getUri())); + verify(visitor, times(1)).accept(eq(icon2.getUri())); + verify(visitor, times(1)).accept(eq(icon3.getUri())); + verify(visitor, times(1)).accept(eq(icon4.getUri())); + } + + @Test + public void visitUris_separateOrientation() { + final RemoteViews landscape = new RemoteViews(mPackage, R.layout.remote_views_test); + final Uri imageUriL = Uri.parse("content://landscape/image"); + final Icon icon1L = Icon.createWithContentUri("content://landscape/icon1"); + final Icon icon2L = Icon.createWithContentUri("content://landscape/icon2"); + final Icon icon3L = Icon.createWithContentUri("content://landscape/icon3"); + final Icon icon4L = Icon.createWithContentUri("content://landscape/icon4"); + landscape.setImageViewUri(R.id.image, imageUriL); + landscape.setTextViewCompoundDrawables(R.id.text, icon1L, icon2L, icon3L, icon4L); + + final RemoteViews portrait = new RemoteViews(mPackage, 33); + final Uri imageUriP = Uri.parse("content://portrait/image"); + final Icon icon1P = Icon.createWithContentUri("content://portrait/icon1"); + final Icon icon2P = Icon.createWithContentUri("content://portrait/icon2"); + final Icon icon3P = Icon.createWithContentUri("content://portrait/icon3"); + final Icon icon4P = Icon.createWithContentUri("content://portrait/icon4"); + portrait.setImageViewUri(R.id.image, imageUriP); + portrait.setTextViewCompoundDrawables(R.id.text, icon1P, icon2P, icon3P, icon4P); + + RemoteViews views = new RemoteViews(landscape, portrait); + + Consumer visitor = (Consumer) spy(Consumer.class); + views.visitUris(visitor); + verify(visitor, times(1)).accept(eq(imageUriL)); + verify(visitor, times(1)).accept(eq(icon1L.getUri())); + verify(visitor, times(1)).accept(eq(icon2L.getUri())); + verify(visitor, times(1)).accept(eq(icon3L.getUri())); + verify(visitor, times(1)).accept(eq(icon4L.getUri())); + verify(visitor, times(1)).accept(eq(imageUriP)); + verify(visitor, times(1)).accept(eq(icon1P.getUri())); + verify(visitor, times(1)).accept(eq(icon2P.getUri())); + verify(visitor, times(1)).accept(eq(icon3P.getUri())); + verify(visitor, times(1)).accept(eq(icon4P.getUri())); + } } -- cgit v1.2.3