1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.pm; 18 19 import static android.media.AudioAttributes.USAGE_ALARM; 20 21 import android.annotation.Nullable; 22 import android.annotation.SuppressLint; 23 import android.app.ActivityManager; 24 import android.app.Notification; 25 import android.app.NotificationChannel; 26 import android.app.NotificationManager; 27 import android.app.PendingIntent; 28 import android.content.BroadcastReceiver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.content.pm.UserInfo; 33 import android.media.AudioFocusInfo; 34 import android.media.AudioManager; 35 import android.media.AudioPlaybackConfiguration; 36 import android.media.audiopolicy.AudioPolicy; 37 import android.multiuser.Flags; 38 import android.os.Looper; 39 import android.os.RemoteException; 40 import android.os.UserHandle; 41 import android.os.UserManager; 42 import android.util.ArraySet; 43 import android.util.Log; 44 45 import com.android.internal.R; 46 import com.android.internal.annotations.VisibleForTesting; 47 48 import java.util.List; 49 import java.util.Set; 50 51 public class BackgroundUserSoundNotifier { 52 53 private static final boolean DEBUG = false; 54 private static final String LOG_TAG = BackgroundUserSoundNotifier.class.getSimpleName(); 55 private static final String BUSN_CHANNEL_ID = "bg_user_sound_channel"; 56 private static final String BUSN_CHANNEL_NAME = "BackgroundUserSound"; 57 public static final String ACTION_MUTE_SOUND = "com.android.server.ACTION_MUTE_BG_USER"; 58 private static final String ACTION_SWITCH_USER = "com.android.server.ACTION_SWITCH_TO_USER"; 59 private static final String ACTION_DISMISS_NOTIFICATION = 60 "com.android.server.ACTION_DISMISS_NOTIFICATION"; 61 private static final String EXTRA_NOTIFICATION_CLIENT_UID = 62 "com.android.server.EXTRA_CLIENT_UID"; 63 /** 64 * The clientUid from the AudioFocusInfo of the background user, 65 * for which an active notification is currently displayed. 66 * Set to -1 if no notification is being shown. 67 * TODO: b/367615180 - add support for multiple simultaneous alarms 68 */ 69 @VisibleForTesting 70 int mNotificationClientUid = -1; 71 /** 72 * UIDs of audio focus infos with active notifications. 73 */ 74 Set<Integer> mNotificationClientUids = new ArraySet<>(); 75 @VisibleForTesting 76 AudioPolicy mFocusControlAudioPolicy; 77 @VisibleForTesting 78 BackgroundUserListener mBgUserListener; 79 private final Context mSystemUserContext; 80 @VisibleForTesting 81 final NotificationManager mNotificationManager; 82 private final UserManager mUserManager; 83 84 /** 85 * Facilitates the display of notifications to current user when there is an alarm or timer 86 * going off on background user and allows to manage the sound through actions. 87 */ BackgroundUserSoundNotifier(Context context)88 public BackgroundUserSoundNotifier(Context context) { 89 mSystemUserContext = context; 90 mNotificationManager = mSystemUserContext.getSystemService(NotificationManager.class); 91 mUserManager = mSystemUserContext.getSystemService(UserManager.class); 92 createNotificationChannel(); 93 setupFocusControlAudioPolicy(); 94 } 95 96 /** 97 * Creates a dedicated channel for background user related notifications. 98 */ createNotificationChannel()99 private void createNotificationChannel() { 100 NotificationChannel channel = new NotificationChannel(BUSN_CHANNEL_ID, BUSN_CHANNEL_NAME, 101 NotificationManager.IMPORTANCE_HIGH); 102 channel.setSound(null, null); 103 mNotificationManager.createNotificationChannel(channel); 104 } 105 setupFocusControlAudioPolicy()106 private void setupFocusControlAudioPolicy() { 107 // Used to configure our audio policy to handle focus events. 108 // This gives us the ability to decide which audio focus requests to accept and bypasses 109 // the framework ducking logic. 110 ActivityManager am = mSystemUserContext.getSystemService(ActivityManager.class); 111 112 registerReceiver(am); 113 mBgUserListener = new BackgroundUserListener(); 114 AudioPolicy.Builder focusControlPolicyBuilder = new AudioPolicy.Builder(mSystemUserContext); 115 focusControlPolicyBuilder.setLooper(Looper.getMainLooper()); 116 117 focusControlPolicyBuilder.setAudioPolicyFocusListener(mBgUserListener); 118 119 mFocusControlAudioPolicy = focusControlPolicyBuilder.build(); 120 int status = mSystemUserContext.getSystemService(AudioManager.class) 121 .registerAudioPolicy(mFocusControlAudioPolicy); 122 123 if (status != AudioManager.SUCCESS) { 124 Log.w(LOG_TAG , "Could not register the service's focus" 125 + " control audio policy, error: " + status); 126 } 127 } 128 129 final class BackgroundUserListener extends AudioPolicy.AudioPolicyFocusListener { 130 onAudioFocusGrant(AudioFocusInfo afi, int requestResult)131 public void onAudioFocusGrant(AudioFocusInfo afi, int requestResult) { 132 try { 133 BackgroundUserSoundNotifier.this.notifyForegroundUserAboutSoundIfNecessary(afi); 134 } catch (RemoteException e) { 135 throw new RuntimeException(e); 136 } 137 } 138 onAudioFocusLoss(AudioFocusInfo afi, boolean wasNotified)139 public void onAudioFocusLoss(AudioFocusInfo afi, boolean wasNotified) { 140 BackgroundUserSoundNotifier.this.dismissNotificationIfNecessary(afi.getClientUid()); 141 } 142 } 143 144 @VisibleForTesting getAudioPolicyFocusListener()145 BackgroundUserListener getAudioPolicyFocusListener() { 146 return mBgUserListener; 147 } 148 149 /** 150 * Registers a BroadcastReceiver for actions related to background user sound notifications. 151 * When ACTION_MUTE_SOUND is received, it mutes a background user's alarm sound. 152 * When ACTION_SWITCH_USER is received, a switch to the background user with alarm is started. 153 */ registerReceiver(ActivityManager activityManager)154 private void registerReceiver(ActivityManager activityManager) { 155 BroadcastReceiver backgroundUserNotificationBroadcastReceiver = new BroadcastReceiver() { 156 @SuppressLint("MissingPermission") 157 @Override 158 public void onReceive(Context context, Intent intent) { 159 if (Flags.multipleAlarmNotificationsSupport()) { 160 if (!intent.hasExtra(EXTRA_NOTIFICATION_CLIENT_UID)) { 161 return; 162 } 163 } else { 164 if (mNotificationClientUid == -1) { 165 return; 166 } 167 } 168 169 int clientUid; 170 if (Flags.multipleAlarmNotificationsSupport()) { 171 clientUid = intent.getIntExtra(EXTRA_NOTIFICATION_CLIENT_UID, -1); 172 } else { 173 clientUid = mNotificationClientUid; 174 } 175 dismissNotification(clientUid); 176 177 if (DEBUG) { 178 final int actionIndex = intent.getAction().lastIndexOf(".") + 1; 179 final String action = intent.getAction().substring(actionIndex); 180 Log.d(LOG_TAG, "Action requested: " + action + ", by userId " 181 + ActivityManager.getCurrentUser() + " for alarm on user " 182 + UserHandle.getUserHandleForUid(clientUid).getIdentifier()); 183 } 184 185 if (ACTION_MUTE_SOUND.equals(intent.getAction())) { 186 muteAlarmSounds(clientUid); 187 } else if (ACTION_SWITCH_USER.equals(intent.getAction())) { 188 int userId = UserHandle.getUserId(clientUid); 189 if (mUserManager.isProfile(userId)) { 190 userId = mUserManager.getProfileParent(userId).id; 191 } 192 activityManager.switchUser(userId); 193 } 194 if (Flags.multipleAlarmNotificationsSupport()) { 195 mNotificationClientUids.remove(clientUid); 196 } else { 197 mNotificationClientUid = -1; 198 } 199 } 200 }; 201 202 IntentFilter filter = new IntentFilter(); 203 filter.addAction(ACTION_MUTE_SOUND); 204 filter.addAction(ACTION_SWITCH_USER); 205 filter.addAction(ACTION_DISMISS_NOTIFICATION); 206 mSystemUserContext.registerReceiver(backgroundUserNotificationBroadcastReceiver, filter, 207 Context.RECEIVER_NOT_EXPORTED); 208 } 209 210 /** 211 * Stop player proxy for the ongoing alarm and drop focus for its AudioFocusInfo. 212 */ 213 @SuppressLint("MissingPermission") 214 @VisibleForTesting muteAlarmSounds(int notificationClientUid)215 void muteAlarmSounds(int notificationClientUid) { 216 AudioManager audioManager = mSystemUserContext.getSystemService(AudioManager.class); 217 if (audioManager != null) { 218 for (AudioPlaybackConfiguration apc : audioManager.getActivePlaybackConfigurations()) { 219 if (apc.getClientUid() == notificationClientUid && apc.getPlayerProxy() != null) { 220 apc.getPlayerProxy().stop(); 221 } 222 } 223 } 224 225 AudioFocusInfo currentAfi = getAudioFocusInfoForNotification(notificationClientUid); 226 if (currentAfi != null) { 227 mFocusControlAudioPolicy.sendFocusLossAndUpdate(currentAfi); 228 } 229 } 230 231 /** 232 * Check if sound is coming from background user and show notification is required. 233 */ 234 @SuppressLint("MissingPermission") 235 @VisibleForTesting notifyForegroundUserAboutSoundIfNecessary(AudioFocusInfo afi)236 void notifyForegroundUserAboutSoundIfNecessary(AudioFocusInfo afi) throws RemoteException { 237 if (afi == null) { 238 return; 239 } 240 Context foregroundContext = mSystemUserContext.createContextAsUser( 241 UserHandle.of(ActivityManager.getCurrentUser()), 0); 242 final int userId = UserHandle.getUserId(afi.getClientUid()); 243 final int usage = afi.getAttributes().getUsage(); 244 UserInfo userInfo = mUserManager.isProfile(userId) ? mUserManager.getProfileParent(userId) : 245 mUserManager.getUserInfo(userId); 246 ActivityManager activityManager = foregroundContext.getSystemService(ActivityManager.class); 247 // Only show notification if the sound is coming from background user and the notification 248 // for this UID is not already shown. 249 if (userInfo != null && !activityManager.isProfileForeground(userInfo.getUserHandle()) 250 && !isNotificationShown(afi.getClientUid())) { 251 //TODO: b/349138482 - Add handling of cases when usage == USAGE_NOTIFICATION_RINGTONE 252 if (usage == USAGE_ALARM) { 253 if (DEBUG) { 254 Log.d(LOG_TAG, "Alarm ringing on background user " + userId 255 + ", displaying notification for current user " 256 + foregroundContext.getUserId()); 257 } 258 if (Flags.multipleAlarmNotificationsSupport()) { 259 mNotificationClientUids.add(afi.getClientUid()); 260 } else { 261 mNotificationClientUid = afi.getClientUid(); 262 } 263 264 mNotificationManager.notifyAsUser(LOG_TAG, afi.getClientUid(), 265 createNotification(userInfo.name, foregroundContext, afi.getClientUid()), 266 foregroundContext.getUser()); 267 } 268 } 269 } 270 271 /** 272 * Dismisses notification if the associated focus has been removed from the focus stack. 273 * Notification remains if the focus is temporarily lost due to another client taking over the 274 * focus ownership. 275 */ 276 @VisibleForTesting dismissNotificationIfNecessary(int notificationClientUid)277 void dismissNotificationIfNecessary(int notificationClientUid) { 278 279 if (getAudioFocusInfoForNotification(notificationClientUid) == null 280 && isNotificationShown(notificationClientUid)) { 281 if (DEBUG) { 282 Log.d(LOG_TAG, "Alarm ringing on background user " 283 + UserHandle.getUserHandleForUid(notificationClientUid).getIdentifier() 284 + " left focus stack, dismissing notification"); 285 } 286 dismissNotification(notificationClientUid); 287 288 if (Flags.multipleAlarmNotificationsSupport()) { 289 mNotificationClientUids.remove(notificationClientUid); 290 } else { 291 mNotificationClientUid = -1; 292 } 293 } 294 } 295 296 /** 297 * Dismisses notification for all users in case user switch occurred after notification was 298 * shown. 299 */ 300 @SuppressLint("MissingPermission") dismissNotification(int notificationClientUid)301 private void dismissNotification(int notificationClientUid) { 302 mNotificationManager.cancelAsUser(LOG_TAG, notificationClientUid, UserHandle.ALL); 303 } 304 305 /** 306 * Returns AudioFocusInfo associated with the current notification. 307 */ 308 @SuppressLint("MissingPermission") 309 @VisibleForTesting 310 @Nullable getAudioFocusInfoForNotification(int notificationClientUid)311 AudioFocusInfo getAudioFocusInfoForNotification(int notificationClientUid) { 312 if (notificationClientUid >= 0) { 313 List<AudioFocusInfo> stack = mFocusControlAudioPolicy.getFocusStack(); 314 for (int i = stack.size() - 1; i >= 0; i--) { 315 if (stack.get(i).getClientUid() == notificationClientUid) { 316 return stack.get(i); 317 } 318 } 319 } 320 return null; 321 } 322 createPendingIntent(String intentAction, int notificationClientUid)323 private PendingIntent createPendingIntent(String intentAction, int notificationClientUid) { 324 final Intent intent = new Intent(intentAction); 325 intent.putExtra(EXTRA_NOTIFICATION_CLIENT_UID, notificationClientUid); 326 return PendingIntent.getBroadcast(mSystemUserContext, notificationClientUid, intent, 327 PendingIntent.FLAG_IMMUTABLE); 328 } 329 330 @SuppressLint("MissingPermission") 331 @VisibleForTesting createNotification(String userName, Context fgContext, int notificationClientUid)332 Notification createNotification(String userName, Context fgContext, int notificationClientUid) { 333 final String title = fgContext.getString(R.string.bg_user_sound_notification_title_alarm, 334 userName); 335 final int icon = R.drawable.ic_audio_alarm; 336 337 PendingIntent mutePI = createPendingIntent(ACTION_MUTE_SOUND, notificationClientUid); 338 PendingIntent switchPI = createPendingIntent(ACTION_SWITCH_USER, notificationClientUid); 339 PendingIntent dismissNotificationPI = createPendingIntent(ACTION_DISMISS_NOTIFICATION, 340 notificationClientUid); 341 342 final Notification.Action mute = new Notification.Action.Builder(null, 343 fgContext.getString(R.string.bg_user_sound_notification_button_mute), 344 mutePI).build(); 345 final Notification.Action switchUser = new Notification.Action.Builder(null, 346 fgContext.getString(R.string.bg_user_sound_notification_button_switch_user), 347 switchPI).build(); 348 349 Notification.Builder notificationBuilder = new Notification.Builder(mSystemUserContext, 350 BUSN_CHANNEL_ID) 351 .setSmallIcon(icon) 352 .setTicker(title) 353 .setCategory(Notification.CATEGORY_REMINDER) 354 .setWhen(0) 355 .setOngoing(true) 356 .setColor(fgContext.getColor(R.color.system_notification_accent_color)) 357 .setContentTitle(title) 358 .setContentIntent(mutePI) 359 .setAutoCancel(true) 360 .setDeleteIntent(dismissNotificationPI) 361 .setVisibility(Notification.VISIBILITY_PUBLIC); 362 363 if (mUserManager.isUserSwitcherEnabled() && (mUserManager.getUserSwitchability( 364 fgContext.getUser()) == UserManager.SWITCHABILITY_STATUS_OK)) { 365 notificationBuilder.setActions(mute, switchUser); 366 } else { 367 notificationBuilder.setActions(mute); 368 } 369 370 return notificationBuilder.build(); 371 } 372 isNotificationShown(int notificationClientUid)373 private boolean isNotificationShown(int notificationClientUid) { 374 if (Flags.multipleAlarmNotificationsSupport()) { 375 return mNotificationClientUids.contains(notificationClientUid); 376 } else { 377 return mNotificationClientUid != -1; 378 } 379 } 380 } 381