Loading services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java +139 −69 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.server.pm; import static android.media.AudioAttributes.USAGE_ALARM; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.Notification; Loading @@ -42,6 +43,8 @@ import android.util.Log; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.util.List; public class BackgroundUserSoundNotifier { private static final boolean DEBUG = false; Loading @@ -49,11 +52,21 @@ public class BackgroundUserSoundNotifier { private static final String BUSN_CHANNEL_ID = "bg_user_sound_channel"; private static final String BUSN_CHANNEL_NAME = "BackgroundUserSound"; public static final String ACTION_MUTE_SOUND = "com.android.server.ACTION_MUTE_BG_USER"; private static final String EXTRA_NOTIFICATION_ID = "com.android.server.EXTRA_CLIENT_UID"; private static final String EXTRA_CURRENT_USER_ID = "com.android.server.EXTRA_CURRENT_USER_ID"; private static final String ACTION_SWITCH_USER = "com.android.server.ACTION_SWITCH_TO_USER"; /** ID of user with notification displayed, -1 if notification is not showing*/ private int mUserWithNotification = -1; private static final String ACTION_DISMISS_NOTIFICATION = "com.android.server.ACTION_DISMISS_NOTIFICATION"; /** * The clientUid from the AudioFocusInfo of the background user, * for which an active notification is currently displayed. * Set to -1 if no notification is being shown. * TODO: b/367615180 - add support for multiple simultaneous alarms */ @VisibleForTesting int mNotificationClientUid = -1; @VisibleForTesting AudioPolicy mFocusControlAudioPolicy; @VisibleForTesting BackgroundUserListener mBgUserListener; private final Context mSystemUserContext; @VisibleForTesting final NotificationManager mNotificationManager; Loading @@ -67,11 +80,18 @@ public class BackgroundUserSoundNotifier { mSystemUserContext = context; mNotificationManager = mSystemUserContext.getSystemService(NotificationManager.class); mUserManager = mSystemUserContext.getSystemService(UserManager.class); createNotificationChannel(); setupFocusControlAudioPolicy(); } /** * Creates a dedicated channel for background user related notifications. */ private void createNotificationChannel() { NotificationChannel channel = new NotificationChannel(BUSN_CHANNEL_ID, BUSN_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH); channel.setSound(null, null); mNotificationManager.createNotificationChannel(channel); setupFocusControlAudioPolicy(); } private void setupFocusControlAudioPolicy() { Loading @@ -81,15 +101,16 @@ public class BackgroundUserSoundNotifier { ActivityManager am = mSystemUserContext.getSystemService(ActivityManager.class); registerReceiver(am); BackgroundUserListener bgUserListener = new BackgroundUserListener(mSystemUserContext); mBgUserListener = new BackgroundUserListener(mSystemUserContext); AudioPolicy.Builder focusControlPolicyBuilder = new AudioPolicy.Builder(mSystemUserContext); focusControlPolicyBuilder.setLooper(Looper.getMainLooper()); focusControlPolicyBuilder.setAudioPolicyFocusListener(bgUserListener); focusControlPolicyBuilder.setAudioPolicyFocusListener(mBgUserListener); AudioPolicy mFocusControlAudioPolicy = focusControlPolicyBuilder.build(); mFocusControlAudioPolicy = focusControlPolicyBuilder.build(); int status = mSystemUserContext.getSystemService(AudioManager.class) .registerAudioPolicy(mFocusControlAudioPolicy); if (status != AudioManager.SUCCESS) { Log.w(LOG_TAG , "Could not register the service's focus" + " control audio policy, error: " + status); Loading Loading @@ -117,8 +138,13 @@ public class BackgroundUserSoundNotifier { @SuppressLint("MissingPermission") public void onAudioFocusLoss(AudioFocusInfo afi, boolean wasNotified) { BackgroundUserSoundNotifier.this.dismissNotificationIfNecessary(afi); BackgroundUserSoundNotifier.this.dismissNotificationIfNecessary(); } } @VisibleForTesting BackgroundUserListener getAudioPolicyFocusListener() { return mBgUserListener; } /** Loading @@ -126,114 +152,156 @@ public class BackgroundUserSoundNotifier { * When ACTION_MUTE_SOUND is received, it mutes a background user's alarm sound. * When ACTION_SWITCH_USER is received, a switch to the background user with alarm is started. */ private void registerReceiver(ActivityManager service) { private void registerReceiver(ActivityManager activityManager) { BroadcastReceiver backgroundUserNotificationBroadcastReceiver = new BroadcastReceiver() { @SuppressLint("MissingPermission") @Override public void onReceive(Context context, Intent intent) { if (!(intent.hasExtra(EXTRA_NOTIFICATION_ID) && intent.hasExtra(EXTRA_CURRENT_USER_ID) && intent.hasExtra(Intent.EXTRA_USER_ID))) { if (mNotificationClientUid == -1) { return; } final int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1); dismissNotification(); if (DEBUG) { Log.d(LOG_TAG, "User with alarm id " + intent.getIntExtra(Intent.EXTRA_USER_ID, -1) + " current user id " + intent.getIntExtra( EXTRA_CURRENT_USER_ID, -1)); } mUserWithNotification = -1; mNotificationManager.cancelAsUser(LOG_TAG, notificationId, UserHandle.of(intent.getIntExtra(EXTRA_CURRENT_USER_ID, -1))); if (ACTION_MUTE_SOUND.equals(intent.getAction())) { final AudioManager audioManager = mSystemUserContext.getSystemService(AudioManager.class); if (audioManager != null) { for (AudioPlaybackConfiguration apc : audioManager.getActivePlaybackConfigurations()) { if (apc.getAudioAttributes().getUsage() == USAGE_ALARM) { if (apc.getPlayerProxy() != null) { apc.getPlayerProxy().stop(); } } } final int actionIndex = intent.getAction().lastIndexOf(".") + 1; final String action = intent.getAction().substring(actionIndex); Log.d(LOG_TAG, "Action requested: " + action + ", by userId " + ActivityManager.getCurrentUser() + " for alarm on user " + UserHandle.getUserHandleForUid(mNotificationClientUid)); } if (ACTION_MUTE_SOUND.equals(intent.getAction())) { muteAlarmSounds(mSystemUserContext); } else if (ACTION_SWITCH_USER.equals(intent.getAction())) { service.switchUser(intent.getIntExtra(Intent.EXTRA_USER_ID, -1)); activityManager.switchUser(UserHandle.getUserId(mNotificationClientUid)); } mNotificationClientUid = -1; } }; IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_MUTE_SOUND); filter.addAction(ACTION_SWITCH_USER); filter.addAction(ACTION_DISMISS_NOTIFICATION); mSystemUserContext.registerReceiver(backgroundUserNotificationBroadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED); } /** * Stop player proxy for the ongoing alarm and drop focus for its AudioFocusInfo. */ @VisibleForTesting void muteAlarmSounds(Context context) { AudioManager audioManager = context.getSystemService(AudioManager.class); if (audioManager != null) { for (AudioPlaybackConfiguration apc : audioManager.getActivePlaybackConfigurations()) { if (apc.getClientUid() == mNotificationClientUid && apc.getPlayerProxy() != null) { apc.getPlayerProxy().stop(); } } } } /** * Check if sound is coming from background user and show notification is required. */ @VisibleForTesting void notifyForegroundUserAboutSoundIfNecessary(AudioFocusInfo afi, Context foregroundContext) throws RemoteException { void notifyForegroundUserAboutSoundIfNecessary(AudioFocusInfo afi, Context foregroundContext) throws RemoteException { final int userId = UserHandle.getUserId(afi.getClientUid()); final int usage = afi.getAttributes().getUsage(); UserInfo userInfo = mUserManager.getUserInfo(userId); if (userInfo != null && userId != foregroundContext.getUserId()) { // Only show notification if the sound is coming from background user and the notification // is not already shown. if (userInfo != null && userId != foregroundContext.getUserId() && mNotificationClientUid == -1) { //TODO: b/349138482 - Add handling of cases when usage == USAGE_NOTIFICATION_RINGTONE if (usage == USAGE_ALARM) { Intent muteIntent = createIntent(ACTION_MUTE_SOUND, afi, foregroundContext, userId); PendingIntent mutePI = PendingIntent.getBroadcast(mSystemUserContext, 0, muteIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); Intent switchIntent = createIntent(ACTION_SWITCH_USER, afi, foregroundContext, userId); PendingIntent switchPI = PendingIntent.getBroadcast(mSystemUserContext, 0, switchIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); mUserWithNotification = foregroundContext.getUserId(); mNotificationManager.notifyAsUser(LOG_TAG, afi.getClientUid(), createNotification(userInfo.name, mutePI, switchPI, foregroundContext), if (DEBUG) { Log.d(LOG_TAG, "Alarm ringing on background user " + userId + ", displaying notification for current user " + foregroundContext.getUserId()); } mNotificationClientUid = afi.getClientUid(); mNotificationManager.notifyAsUser(LOG_TAG, mNotificationClientUid, createNotification(userInfo.name, foregroundContext), foregroundContext.getUser()); } } } /** * If notification is present, dismisses it. To be called when the relevant sound loses focus. * Dismisses notification if the associated focus has been removed from the focus stack. * Notification remains if the focus is temporarily lost due to another client taking over the * focus ownership. */ private void dismissNotificationIfNecessary(AudioFocusInfo afi) { if (mUserWithNotification >= 0) { mNotificationManager.cancelAsUser(LOG_TAG, afi.getClientUid(), UserHandle.of(mUserWithNotification)); @VisibleForTesting void dismissNotificationIfNecessary() { if (getAudioFocusInfoForNotification() == null && mNotificationClientUid >= 0) { if (DEBUG) { Log.d(LOG_TAG, "Alarm ringing on background user " + UserHandle.getUserHandleForUid(mNotificationClientUid).getIdentifier() + " left focus stack, dismissing notification"); } dismissNotification(); mNotificationClientUid = -1; } mUserWithNotification = -1; } private Intent createIntent(String intentAction, AudioFocusInfo afi, Context fgUserContext, int userId) { /** * Dismisses notification for all users in case user switch occurred after notification was * shown. */ @SuppressLint("MissingPermission") private void dismissNotification() { mNotificationManager.cancelAsUser(LOG_TAG, mNotificationClientUid, UserHandle.ALL); } /** * Returns AudioFocusInfo associated with the current notification. */ @SuppressLint("MissingPermission") @VisibleForTesting @Nullable AudioFocusInfo getAudioFocusInfoForNotification() { if (mNotificationClientUid >= 0) { List<AudioFocusInfo> stack = mFocusControlAudioPolicy.getFocusStack(); for (int i = stack.size() - 1; i >= 0; i--) { if (stack.get(i).getClientUid() == mNotificationClientUid) { return stack.get(i); } } } return null; } private PendingIntent createPendingIntent(String intentAction) { final Intent intent = new Intent(intentAction); intent.putExtra(EXTRA_CURRENT_USER_ID, fgUserContext.getUserId()); intent.putExtra(EXTRA_NOTIFICATION_ID, afi.getClientUid()); intent.putExtra(Intent.EXTRA_USER_ID, userId); return intent; PendingIntent resultPI = PendingIntent.getBroadcast(mSystemUserContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); return resultPI; } private Notification createNotification(String userName, PendingIntent muteIntent, PendingIntent switchIntent, Context fgContext) { @VisibleForTesting Notification createNotification(String userName, Context fgContext) { final String title = fgContext.getString(R.string.bg_user_sound_notification_title_alarm, userName); final int icon = R.drawable.ic_audio_alarm; PendingIntent mutePI = createPendingIntent(ACTION_MUTE_SOUND); PendingIntent switchPI = createPendingIntent(ACTION_SWITCH_USER); PendingIntent dismissNotificationPI = createPendingIntent(ACTION_DISMISS_NOTIFICATION); final Notification.Action mute = new Notification.Action.Builder(null, fgContext.getString(R.string.bg_user_sound_notification_button_mute), muteIntent).build(); mutePI).build(); final Notification.Action switchUser = new Notification.Action.Builder(null, fgContext.getString(R.string.bg_user_sound_notification_button_switch_user), switchIntent).build(); switchPI).build(); Notification.Builder notificationBuilder = new Notification.Builder(mSystemUserContext, BUSN_CHANNEL_ID) .setSmallIcon(icon) Loading @@ -243,16 +311,18 @@ public class BackgroundUserSoundNotifier { .setOngoing(true) .setColor(fgContext.getColor(R.color.system_notification_accent_color)) .setContentTitle(title) .setContentIntent(muteIntent) .setContentIntent(mutePI) .setAutoCancel(true) .setDeleteIntent(dismissNotificationPI) .setVisibility(Notification.VISIBILITY_PUBLIC); if (mUserManager.isUserSwitcherEnabled() && (mUserManager.getUserSwitchability( UserHandle.of(fgContext.getUserId())) == UserManager.SWITCHABILITY_STATUS_OK)) { fgContext.getUser()) == UserManager.SWITCHABILITY_STATUS_OK)) { notificationBuilder.setActions(mute, switchUser); } else { notificationBuilder.setActions(mute); } return notificationBuilder.build(); } } services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java +126 −11 Original line number Diff line number Diff line Loading @@ -16,13 +16,19 @@ package com.android.server.pm; import static android.media.AudioAttributes.USAGE_ALARM; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.testng.AssertJUnit.assertEquals; import android.app.Notification; import android.app.NotificationManager; Loading @@ -31,6 +37,9 @@ import android.content.pm.UserInfo; import android.media.AudioAttributes; import android.media.AudioFocusInfo; import android.media.AudioManager; import android.media.AudioPlaybackConfiguration; import android.media.PlayerProxy; import android.media.audiopolicy.AudioPolicy; import android.os.Build; import android.os.RemoteException; import android.os.UserHandle; Loading @@ -45,6 +54,10 @@ import org.junit.runners.JUnit4; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.List; import java.util.Stack; @RunWith(JUnit4.class) public class BackgroundUserSoundNotifierTest { Loading @@ -63,7 +76,10 @@ public class BackgroundUserSoundNotifierTest { MockitoAnnotations.initMocks(this); mSpiedContext = spy(mRealContext); mUsersToRemove = new ArraySet<>(); mUserManager = UserManager.get(mRealContext); mUserManager = spy(mSpiedContext.getSystemService(UserManager.class)); doReturn(mUserManager) .when(mSpiedContext).getSystemService(UserManager.class); doReturn(mNotificationManager) .when(mSpiedContext).getSystemService(NotificationManager.class); mBackgroundUserSoundNotifier = new BackgroundUserSoundNotifier(mSpiedContext); Loading @@ -74,12 +90,9 @@ public class BackgroundUserSoundNotifierTest { mUsersToRemove.stream().toList().forEach(this::removeUser); } @Test public void testAlarmOnBackgroundUser_ForegroundUserNotified() throws RemoteException { AudioAttributes aa = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ALARM).build(); UserInfo user = createUser("User", UserManager.USER_TYPE_FULL_SECONDARY, 0); public void testAlarmOnBackgroundUser_foregroundUserNotified() throws RemoteException { AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build(); UserInfo user = createUser("User", UserManager.USER_TYPE_FULL_SECONDARY, 0); final int fgUserId = mSpiedContext.getUserId(); final int bgUserUid = user.id * 100000; doReturn(UserHandle.of(fgUserId)).when(mSpiedContext).getUser(); Loading @@ -95,10 +108,9 @@ public class BackgroundUserSoundNotifierTest { } @Test public void testMediaOnBackgroundUser_ForegroundUserNotNotified() throws RemoteException { public void testMediaOnBackgroundUser_foregroundUserNotNotified() throws RemoteException { AudioAttributes aa = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA).build(); UserInfo user = createUser("User", UserManager.USER_TYPE_FULL_SECONDARY, 0); final int bgUserUid = mSpiedContext.getUserId() * 100000; AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "", /* packageName= */ "com.android.car.audio", AudioManager.AUDIOFOCUS_GAIN, Loading @@ -109,9 +121,9 @@ public class BackgroundUserSoundNotifierTest { } @Test public void testAlarmOnForegroundUser_ForegroundUserNotNotified() throws RemoteException { public void testAlarmOnForegroundUser_foregroundUserNotNotified() throws RemoteException { AudioAttributes aa = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ALARM).build(); .setUsage(USAGE_ALARM).build(); final int fgUserId = mSpiedContext.getUserId(); final int fgUserUid = fgUserId * 100000; doReturn(UserHandle.of(fgUserId)).when(mSpiedContext).getUser(); Loading @@ -123,6 +135,109 @@ public class BackgroundUserSoundNotifierTest { verifyZeroInteractions(mNotificationManager); } @Test public void testMuteAlarmSounds() { final int fgUserId = mSpiedContext.getUserId(); int bgUserId = fgUserId + 1; int bgUserUid = bgUserId * 100000; mBackgroundUserSoundNotifier.mNotificationClientUid = bgUserUid; AudioManager mockAudioManager = mock(AudioManager.class); when(mSpiedContext.getSystemService(AudioManager.class)).thenReturn(mockAudioManager); AudioPlaybackConfiguration apc1 = mock(AudioPlaybackConfiguration.class); when(apc1.getClientUid()).thenReturn(bgUserUid); when(apc1.getPlayerProxy()).thenReturn(mock(PlayerProxy.class)); AudioPlaybackConfiguration apc2 = mock(AudioPlaybackConfiguration.class); when(apc2.getClientUid()).thenReturn(bgUserUid + 1); when(apc2.getPlayerProxy()).thenReturn(mock(PlayerProxy.class)); List<AudioPlaybackConfiguration> configs = new ArrayList<>(); configs.add(apc1); configs.add(apc2); when(mockAudioManager.getActivePlaybackConfigurations()).thenReturn(configs); AudioPolicy mockAudioPolicy = mock(AudioPolicy.class); AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build(); AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "", /* packageName= */ "", AudioManager.AUDIOFOCUS_GAIN, AudioManager.AUDIOFOCUS_NONE, /* flags= */ 0, Build.VERSION.SDK_INT); Stack<AudioFocusInfo> focusStack = new Stack<>(); focusStack.add(afi); doReturn(focusStack).when(mockAudioPolicy).getFocusStack(); mBackgroundUserSoundNotifier.mFocusControlAudioPolicy = mockAudioPolicy; mBackgroundUserSoundNotifier.muteAlarmSounds(mSpiedContext); verify(apc1.getPlayerProxy()).stop(); verify(apc2.getPlayerProxy(), never()).stop(); } @Test public void testOnAudioFocusGrant_alarmOnBackgroundUser_notifiesForegroundUser() { final int fgUserId = mSpiedContext.getUserId(); UserInfo bgUser = createUser("Background User", UserManager.USER_TYPE_FULL_SECONDARY, 0); int bgUserUid = bgUser.id * 100000; AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build(); AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "", "", AudioManager.AUDIOFOCUS_GAIN, 0, 0, Build.VERSION.SDK_INT); mBackgroundUserSoundNotifier.getAudioPolicyFocusListener() .onAudioFocusGrant(afi, AudioManager.AUDIOFOCUS_REQUEST_GRANTED); verify(mNotificationManager) .notifyAsUser(eq(BackgroundUserSoundNotifier.class.getSimpleName()), eq(afi.getClientUid()), any(Notification.class), eq(UserHandle.of(fgUserId))); } @Test public void testCreateNotification_UserSwitcherEnabled_bothActionsAvailable() { String userName = "BgUser"; doReturn(true).when(mUserManager).isUserSwitcherEnabled(); doReturn(UserManager.SWITCHABILITY_STATUS_OK) .when(mUserManager).getUserSwitchability(any()); Notification notification = mBackgroundUserSoundNotifier.createNotification(userName, mSpiedContext); assertEquals("Alarm for BgUser", notification.extras.getString( Notification.EXTRA_TITLE)); assertEquals(Notification.CATEGORY_REMINDER, notification.category); assertEquals(Notification.VISIBILITY_PUBLIC, notification.visibility); assertEquals(com.android.internal.R.drawable.ic_audio_alarm, notification.getSmallIcon().getResId()); assertEquals(2, notification.actions.length); assertEquals(mSpiedContext.getString( com.android.internal.R.string.bg_user_sound_notification_button_mute), notification.actions[0].title); assertEquals(mSpiedContext.getString( com.android.internal.R.string.bg_user_sound_notification_button_switch_user), notification.actions[1].title); } @Test public void testCreateNotification_UserSwitcherDisabled_onlyMuteActionAvailable() { String userName = "BgUser"; doReturn(false).when(mUserManager).isUserSwitcherEnabled(); doReturn(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED) .when(mUserManager).getUserSwitchability(any()); Notification notification = mBackgroundUserSoundNotifier.createNotification(userName, mSpiedContext); assertEquals(1, notification.actions.length); assertEquals(mSpiedContext.getString( com.android.internal.R.string.bg_user_sound_notification_button_mute), notification.actions[0].title); } private UserInfo createUser(String name, String userType, int flags) { UserInfo user = mUserManager.createUser(name, userType, flags); Loading Loading
services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java +139 −69 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.server.pm; import static android.media.AudioAttributes.USAGE_ALARM; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.Notification; Loading @@ -42,6 +43,8 @@ import android.util.Log; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.util.List; public class BackgroundUserSoundNotifier { private static final boolean DEBUG = false; Loading @@ -49,11 +52,21 @@ public class BackgroundUserSoundNotifier { private static final String BUSN_CHANNEL_ID = "bg_user_sound_channel"; private static final String BUSN_CHANNEL_NAME = "BackgroundUserSound"; public static final String ACTION_MUTE_SOUND = "com.android.server.ACTION_MUTE_BG_USER"; private static final String EXTRA_NOTIFICATION_ID = "com.android.server.EXTRA_CLIENT_UID"; private static final String EXTRA_CURRENT_USER_ID = "com.android.server.EXTRA_CURRENT_USER_ID"; private static final String ACTION_SWITCH_USER = "com.android.server.ACTION_SWITCH_TO_USER"; /** ID of user with notification displayed, -1 if notification is not showing*/ private int mUserWithNotification = -1; private static final String ACTION_DISMISS_NOTIFICATION = "com.android.server.ACTION_DISMISS_NOTIFICATION"; /** * The clientUid from the AudioFocusInfo of the background user, * for which an active notification is currently displayed. * Set to -1 if no notification is being shown. * TODO: b/367615180 - add support for multiple simultaneous alarms */ @VisibleForTesting int mNotificationClientUid = -1; @VisibleForTesting AudioPolicy mFocusControlAudioPolicy; @VisibleForTesting BackgroundUserListener mBgUserListener; private final Context mSystemUserContext; @VisibleForTesting final NotificationManager mNotificationManager; Loading @@ -67,11 +80,18 @@ public class BackgroundUserSoundNotifier { mSystemUserContext = context; mNotificationManager = mSystemUserContext.getSystemService(NotificationManager.class); mUserManager = mSystemUserContext.getSystemService(UserManager.class); createNotificationChannel(); setupFocusControlAudioPolicy(); } /** * Creates a dedicated channel for background user related notifications. */ private void createNotificationChannel() { NotificationChannel channel = new NotificationChannel(BUSN_CHANNEL_ID, BUSN_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH); channel.setSound(null, null); mNotificationManager.createNotificationChannel(channel); setupFocusControlAudioPolicy(); } private void setupFocusControlAudioPolicy() { Loading @@ -81,15 +101,16 @@ public class BackgroundUserSoundNotifier { ActivityManager am = mSystemUserContext.getSystemService(ActivityManager.class); registerReceiver(am); BackgroundUserListener bgUserListener = new BackgroundUserListener(mSystemUserContext); mBgUserListener = new BackgroundUserListener(mSystemUserContext); AudioPolicy.Builder focusControlPolicyBuilder = new AudioPolicy.Builder(mSystemUserContext); focusControlPolicyBuilder.setLooper(Looper.getMainLooper()); focusControlPolicyBuilder.setAudioPolicyFocusListener(bgUserListener); focusControlPolicyBuilder.setAudioPolicyFocusListener(mBgUserListener); AudioPolicy mFocusControlAudioPolicy = focusControlPolicyBuilder.build(); mFocusControlAudioPolicy = focusControlPolicyBuilder.build(); int status = mSystemUserContext.getSystemService(AudioManager.class) .registerAudioPolicy(mFocusControlAudioPolicy); if (status != AudioManager.SUCCESS) { Log.w(LOG_TAG , "Could not register the service's focus" + " control audio policy, error: " + status); Loading Loading @@ -117,8 +138,13 @@ public class BackgroundUserSoundNotifier { @SuppressLint("MissingPermission") public void onAudioFocusLoss(AudioFocusInfo afi, boolean wasNotified) { BackgroundUserSoundNotifier.this.dismissNotificationIfNecessary(afi); BackgroundUserSoundNotifier.this.dismissNotificationIfNecessary(); } } @VisibleForTesting BackgroundUserListener getAudioPolicyFocusListener() { return mBgUserListener; } /** Loading @@ -126,114 +152,156 @@ public class BackgroundUserSoundNotifier { * When ACTION_MUTE_SOUND is received, it mutes a background user's alarm sound. * When ACTION_SWITCH_USER is received, a switch to the background user with alarm is started. */ private void registerReceiver(ActivityManager service) { private void registerReceiver(ActivityManager activityManager) { BroadcastReceiver backgroundUserNotificationBroadcastReceiver = new BroadcastReceiver() { @SuppressLint("MissingPermission") @Override public void onReceive(Context context, Intent intent) { if (!(intent.hasExtra(EXTRA_NOTIFICATION_ID) && intent.hasExtra(EXTRA_CURRENT_USER_ID) && intent.hasExtra(Intent.EXTRA_USER_ID))) { if (mNotificationClientUid == -1) { return; } final int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1); dismissNotification(); if (DEBUG) { Log.d(LOG_TAG, "User with alarm id " + intent.getIntExtra(Intent.EXTRA_USER_ID, -1) + " current user id " + intent.getIntExtra( EXTRA_CURRENT_USER_ID, -1)); } mUserWithNotification = -1; mNotificationManager.cancelAsUser(LOG_TAG, notificationId, UserHandle.of(intent.getIntExtra(EXTRA_CURRENT_USER_ID, -1))); if (ACTION_MUTE_SOUND.equals(intent.getAction())) { final AudioManager audioManager = mSystemUserContext.getSystemService(AudioManager.class); if (audioManager != null) { for (AudioPlaybackConfiguration apc : audioManager.getActivePlaybackConfigurations()) { if (apc.getAudioAttributes().getUsage() == USAGE_ALARM) { if (apc.getPlayerProxy() != null) { apc.getPlayerProxy().stop(); } } } final int actionIndex = intent.getAction().lastIndexOf(".") + 1; final String action = intent.getAction().substring(actionIndex); Log.d(LOG_TAG, "Action requested: " + action + ", by userId " + ActivityManager.getCurrentUser() + " for alarm on user " + UserHandle.getUserHandleForUid(mNotificationClientUid)); } if (ACTION_MUTE_SOUND.equals(intent.getAction())) { muteAlarmSounds(mSystemUserContext); } else if (ACTION_SWITCH_USER.equals(intent.getAction())) { service.switchUser(intent.getIntExtra(Intent.EXTRA_USER_ID, -1)); activityManager.switchUser(UserHandle.getUserId(mNotificationClientUid)); } mNotificationClientUid = -1; } }; IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_MUTE_SOUND); filter.addAction(ACTION_SWITCH_USER); filter.addAction(ACTION_DISMISS_NOTIFICATION); mSystemUserContext.registerReceiver(backgroundUserNotificationBroadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED); } /** * Stop player proxy for the ongoing alarm and drop focus for its AudioFocusInfo. */ @VisibleForTesting void muteAlarmSounds(Context context) { AudioManager audioManager = context.getSystemService(AudioManager.class); if (audioManager != null) { for (AudioPlaybackConfiguration apc : audioManager.getActivePlaybackConfigurations()) { if (apc.getClientUid() == mNotificationClientUid && apc.getPlayerProxy() != null) { apc.getPlayerProxy().stop(); } } } } /** * Check if sound is coming from background user and show notification is required. */ @VisibleForTesting void notifyForegroundUserAboutSoundIfNecessary(AudioFocusInfo afi, Context foregroundContext) throws RemoteException { void notifyForegroundUserAboutSoundIfNecessary(AudioFocusInfo afi, Context foregroundContext) throws RemoteException { final int userId = UserHandle.getUserId(afi.getClientUid()); final int usage = afi.getAttributes().getUsage(); UserInfo userInfo = mUserManager.getUserInfo(userId); if (userInfo != null && userId != foregroundContext.getUserId()) { // Only show notification if the sound is coming from background user and the notification // is not already shown. if (userInfo != null && userId != foregroundContext.getUserId() && mNotificationClientUid == -1) { //TODO: b/349138482 - Add handling of cases when usage == USAGE_NOTIFICATION_RINGTONE if (usage == USAGE_ALARM) { Intent muteIntent = createIntent(ACTION_MUTE_SOUND, afi, foregroundContext, userId); PendingIntent mutePI = PendingIntent.getBroadcast(mSystemUserContext, 0, muteIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); Intent switchIntent = createIntent(ACTION_SWITCH_USER, afi, foregroundContext, userId); PendingIntent switchPI = PendingIntent.getBroadcast(mSystemUserContext, 0, switchIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); mUserWithNotification = foregroundContext.getUserId(); mNotificationManager.notifyAsUser(LOG_TAG, afi.getClientUid(), createNotification(userInfo.name, mutePI, switchPI, foregroundContext), if (DEBUG) { Log.d(LOG_TAG, "Alarm ringing on background user " + userId + ", displaying notification for current user " + foregroundContext.getUserId()); } mNotificationClientUid = afi.getClientUid(); mNotificationManager.notifyAsUser(LOG_TAG, mNotificationClientUid, createNotification(userInfo.name, foregroundContext), foregroundContext.getUser()); } } } /** * If notification is present, dismisses it. To be called when the relevant sound loses focus. * Dismisses notification if the associated focus has been removed from the focus stack. * Notification remains if the focus is temporarily lost due to another client taking over the * focus ownership. */ private void dismissNotificationIfNecessary(AudioFocusInfo afi) { if (mUserWithNotification >= 0) { mNotificationManager.cancelAsUser(LOG_TAG, afi.getClientUid(), UserHandle.of(mUserWithNotification)); @VisibleForTesting void dismissNotificationIfNecessary() { if (getAudioFocusInfoForNotification() == null && mNotificationClientUid >= 0) { if (DEBUG) { Log.d(LOG_TAG, "Alarm ringing on background user " + UserHandle.getUserHandleForUid(mNotificationClientUid).getIdentifier() + " left focus stack, dismissing notification"); } dismissNotification(); mNotificationClientUid = -1; } mUserWithNotification = -1; } private Intent createIntent(String intentAction, AudioFocusInfo afi, Context fgUserContext, int userId) { /** * Dismisses notification for all users in case user switch occurred after notification was * shown. */ @SuppressLint("MissingPermission") private void dismissNotification() { mNotificationManager.cancelAsUser(LOG_TAG, mNotificationClientUid, UserHandle.ALL); } /** * Returns AudioFocusInfo associated with the current notification. */ @SuppressLint("MissingPermission") @VisibleForTesting @Nullable AudioFocusInfo getAudioFocusInfoForNotification() { if (mNotificationClientUid >= 0) { List<AudioFocusInfo> stack = mFocusControlAudioPolicy.getFocusStack(); for (int i = stack.size() - 1; i >= 0; i--) { if (stack.get(i).getClientUid() == mNotificationClientUid) { return stack.get(i); } } } return null; } private PendingIntent createPendingIntent(String intentAction) { final Intent intent = new Intent(intentAction); intent.putExtra(EXTRA_CURRENT_USER_ID, fgUserContext.getUserId()); intent.putExtra(EXTRA_NOTIFICATION_ID, afi.getClientUid()); intent.putExtra(Intent.EXTRA_USER_ID, userId); return intent; PendingIntent resultPI = PendingIntent.getBroadcast(mSystemUserContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); return resultPI; } private Notification createNotification(String userName, PendingIntent muteIntent, PendingIntent switchIntent, Context fgContext) { @VisibleForTesting Notification createNotification(String userName, Context fgContext) { final String title = fgContext.getString(R.string.bg_user_sound_notification_title_alarm, userName); final int icon = R.drawable.ic_audio_alarm; PendingIntent mutePI = createPendingIntent(ACTION_MUTE_SOUND); PendingIntent switchPI = createPendingIntent(ACTION_SWITCH_USER); PendingIntent dismissNotificationPI = createPendingIntent(ACTION_DISMISS_NOTIFICATION); final Notification.Action mute = new Notification.Action.Builder(null, fgContext.getString(R.string.bg_user_sound_notification_button_mute), muteIntent).build(); mutePI).build(); final Notification.Action switchUser = new Notification.Action.Builder(null, fgContext.getString(R.string.bg_user_sound_notification_button_switch_user), switchIntent).build(); switchPI).build(); Notification.Builder notificationBuilder = new Notification.Builder(mSystemUserContext, BUSN_CHANNEL_ID) .setSmallIcon(icon) Loading @@ -243,16 +311,18 @@ public class BackgroundUserSoundNotifier { .setOngoing(true) .setColor(fgContext.getColor(R.color.system_notification_accent_color)) .setContentTitle(title) .setContentIntent(muteIntent) .setContentIntent(mutePI) .setAutoCancel(true) .setDeleteIntent(dismissNotificationPI) .setVisibility(Notification.VISIBILITY_PUBLIC); if (mUserManager.isUserSwitcherEnabled() && (mUserManager.getUserSwitchability( UserHandle.of(fgContext.getUserId())) == UserManager.SWITCHABILITY_STATUS_OK)) { fgContext.getUser()) == UserManager.SWITCHABILITY_STATUS_OK)) { notificationBuilder.setActions(mute, switchUser); } else { notificationBuilder.setActions(mute); } return notificationBuilder.build(); } }
services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java +126 −11 Original line number Diff line number Diff line Loading @@ -16,13 +16,19 @@ package com.android.server.pm; import static android.media.AudioAttributes.USAGE_ALARM; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.testng.AssertJUnit.assertEquals; import android.app.Notification; import android.app.NotificationManager; Loading @@ -31,6 +37,9 @@ import android.content.pm.UserInfo; import android.media.AudioAttributes; import android.media.AudioFocusInfo; import android.media.AudioManager; import android.media.AudioPlaybackConfiguration; import android.media.PlayerProxy; import android.media.audiopolicy.AudioPolicy; import android.os.Build; import android.os.RemoteException; import android.os.UserHandle; Loading @@ -45,6 +54,10 @@ import org.junit.runners.JUnit4; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.List; import java.util.Stack; @RunWith(JUnit4.class) public class BackgroundUserSoundNotifierTest { Loading @@ -63,7 +76,10 @@ public class BackgroundUserSoundNotifierTest { MockitoAnnotations.initMocks(this); mSpiedContext = spy(mRealContext); mUsersToRemove = new ArraySet<>(); mUserManager = UserManager.get(mRealContext); mUserManager = spy(mSpiedContext.getSystemService(UserManager.class)); doReturn(mUserManager) .when(mSpiedContext).getSystemService(UserManager.class); doReturn(mNotificationManager) .when(mSpiedContext).getSystemService(NotificationManager.class); mBackgroundUserSoundNotifier = new BackgroundUserSoundNotifier(mSpiedContext); Loading @@ -74,12 +90,9 @@ public class BackgroundUserSoundNotifierTest { mUsersToRemove.stream().toList().forEach(this::removeUser); } @Test public void testAlarmOnBackgroundUser_ForegroundUserNotified() throws RemoteException { AudioAttributes aa = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ALARM).build(); UserInfo user = createUser("User", UserManager.USER_TYPE_FULL_SECONDARY, 0); public void testAlarmOnBackgroundUser_foregroundUserNotified() throws RemoteException { AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build(); UserInfo user = createUser("User", UserManager.USER_TYPE_FULL_SECONDARY, 0); final int fgUserId = mSpiedContext.getUserId(); final int bgUserUid = user.id * 100000; doReturn(UserHandle.of(fgUserId)).when(mSpiedContext).getUser(); Loading @@ -95,10 +108,9 @@ public class BackgroundUserSoundNotifierTest { } @Test public void testMediaOnBackgroundUser_ForegroundUserNotNotified() throws RemoteException { public void testMediaOnBackgroundUser_foregroundUserNotNotified() throws RemoteException { AudioAttributes aa = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA).build(); UserInfo user = createUser("User", UserManager.USER_TYPE_FULL_SECONDARY, 0); final int bgUserUid = mSpiedContext.getUserId() * 100000; AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "", /* packageName= */ "com.android.car.audio", AudioManager.AUDIOFOCUS_GAIN, Loading @@ -109,9 +121,9 @@ public class BackgroundUserSoundNotifierTest { } @Test public void testAlarmOnForegroundUser_ForegroundUserNotNotified() throws RemoteException { public void testAlarmOnForegroundUser_foregroundUserNotNotified() throws RemoteException { AudioAttributes aa = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ALARM).build(); .setUsage(USAGE_ALARM).build(); final int fgUserId = mSpiedContext.getUserId(); final int fgUserUid = fgUserId * 100000; doReturn(UserHandle.of(fgUserId)).when(mSpiedContext).getUser(); Loading @@ -123,6 +135,109 @@ public class BackgroundUserSoundNotifierTest { verifyZeroInteractions(mNotificationManager); } @Test public void testMuteAlarmSounds() { final int fgUserId = mSpiedContext.getUserId(); int bgUserId = fgUserId + 1; int bgUserUid = bgUserId * 100000; mBackgroundUserSoundNotifier.mNotificationClientUid = bgUserUid; AudioManager mockAudioManager = mock(AudioManager.class); when(mSpiedContext.getSystemService(AudioManager.class)).thenReturn(mockAudioManager); AudioPlaybackConfiguration apc1 = mock(AudioPlaybackConfiguration.class); when(apc1.getClientUid()).thenReturn(bgUserUid); when(apc1.getPlayerProxy()).thenReturn(mock(PlayerProxy.class)); AudioPlaybackConfiguration apc2 = mock(AudioPlaybackConfiguration.class); when(apc2.getClientUid()).thenReturn(bgUserUid + 1); when(apc2.getPlayerProxy()).thenReturn(mock(PlayerProxy.class)); List<AudioPlaybackConfiguration> configs = new ArrayList<>(); configs.add(apc1); configs.add(apc2); when(mockAudioManager.getActivePlaybackConfigurations()).thenReturn(configs); AudioPolicy mockAudioPolicy = mock(AudioPolicy.class); AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build(); AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "", /* packageName= */ "", AudioManager.AUDIOFOCUS_GAIN, AudioManager.AUDIOFOCUS_NONE, /* flags= */ 0, Build.VERSION.SDK_INT); Stack<AudioFocusInfo> focusStack = new Stack<>(); focusStack.add(afi); doReturn(focusStack).when(mockAudioPolicy).getFocusStack(); mBackgroundUserSoundNotifier.mFocusControlAudioPolicy = mockAudioPolicy; mBackgroundUserSoundNotifier.muteAlarmSounds(mSpiedContext); verify(apc1.getPlayerProxy()).stop(); verify(apc2.getPlayerProxy(), never()).stop(); } @Test public void testOnAudioFocusGrant_alarmOnBackgroundUser_notifiesForegroundUser() { final int fgUserId = mSpiedContext.getUserId(); UserInfo bgUser = createUser("Background User", UserManager.USER_TYPE_FULL_SECONDARY, 0); int bgUserUid = bgUser.id * 100000; AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build(); AudioFocusInfo afi = new AudioFocusInfo(aa, bgUserUid, "", "", AudioManager.AUDIOFOCUS_GAIN, 0, 0, Build.VERSION.SDK_INT); mBackgroundUserSoundNotifier.getAudioPolicyFocusListener() .onAudioFocusGrant(afi, AudioManager.AUDIOFOCUS_REQUEST_GRANTED); verify(mNotificationManager) .notifyAsUser(eq(BackgroundUserSoundNotifier.class.getSimpleName()), eq(afi.getClientUid()), any(Notification.class), eq(UserHandle.of(fgUserId))); } @Test public void testCreateNotification_UserSwitcherEnabled_bothActionsAvailable() { String userName = "BgUser"; doReturn(true).when(mUserManager).isUserSwitcherEnabled(); doReturn(UserManager.SWITCHABILITY_STATUS_OK) .when(mUserManager).getUserSwitchability(any()); Notification notification = mBackgroundUserSoundNotifier.createNotification(userName, mSpiedContext); assertEquals("Alarm for BgUser", notification.extras.getString( Notification.EXTRA_TITLE)); assertEquals(Notification.CATEGORY_REMINDER, notification.category); assertEquals(Notification.VISIBILITY_PUBLIC, notification.visibility); assertEquals(com.android.internal.R.drawable.ic_audio_alarm, notification.getSmallIcon().getResId()); assertEquals(2, notification.actions.length); assertEquals(mSpiedContext.getString( com.android.internal.R.string.bg_user_sound_notification_button_mute), notification.actions[0].title); assertEquals(mSpiedContext.getString( com.android.internal.R.string.bg_user_sound_notification_button_switch_user), notification.actions[1].title); } @Test public void testCreateNotification_UserSwitcherDisabled_onlyMuteActionAvailable() { String userName = "BgUser"; doReturn(false).when(mUserManager).isUserSwitcherEnabled(); doReturn(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED) .when(mUserManager).getUserSwitchability(any()); Notification notification = mBackgroundUserSoundNotifier.createNotification(userName, mSpiedContext); assertEquals(1, notification.actions.length); assertEquals(mSpiedContext.getString( com.android.internal.R.string.bg_user_sound_notification_button_mute), notification.actions[0].title); } private UserInfo createUser(String name, String userType, int flags) { UserInfo user = mUserManager.createUser(name, userType, flags); Loading