• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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