1 /*
2  * Copyright (C) 2023 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.audio;
18 
19 import static android.content.pm.PackageManager.MATCH_ANY_USER;
20 
21 import static com.android.server.audio.AudioService.MUSICFX_HELPER_MSG_START;
22 
23 import android.annotation.NonNull;
24 import android.annotation.RequiresPermission;
25 import android.app.ActivityManager;
26 import android.app.IUidObserver;
27 import android.app.UidObserver;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.ServiceConnection;
32 import android.content.pm.PackageManager;
33 import android.content.pm.ResolveInfo;
34 import android.content.pm.UserInfo;
35 import android.media.AudioManager;
36 import android.media.audiofx.AudioEffect;
37 import android.os.Binder;
38 import android.os.IBinder;
39 import android.os.Message;
40 import android.os.RemoteException;
41 import android.os.UserHandle;
42 import android.util.Log;
43 import android.util.SparseArray;
44 
45 import com.android.internal.annotations.GuardedBy;
46 import com.android.server.audio.AudioService.AudioHandler;
47 
48 import java.util.ArrayList;
49 import java.util.List;
50 
51 /**
52  * MusicFx management.
53  */
54 public class MusicFxHelper {
55     private static final String TAG = "AS.MusicFxHelper";
56 
57     @NonNull private final Context mContext;
58 
59     @NonNull private final AudioHandler mAudioHandler;
60 
61     // Synchronization UidSessionMap access between UidObserver and AudioServiceBroadcastReceiver.
62     private final Object mClientUidMapLock = new Object();
63 
64     private final String mPackageName = this.getClass().getPackage().getName();
65 
66     private final String mMusicFxPackageName = "com.android.musicfx";
67 
68     /*package*/ static final int MSG_EFFECT_CLIENT_GONE = MUSICFX_HELPER_MSG_START + 1;
69 
70     // The binder token identifying the UidObserver registration.
71     private IBinder mUidObserverToken = null;
72 
73     private boolean mIsBound;
74 
75     // Package name and list of open audio sessions for this package
76     private static class PackageSessions {
77         String mPackageName;
78         List<Integer> mSessions;
79     }
80 
81     /*
82      * Override of SparseArray class to add bind/unbind and UID observer in the put/remove methods.
83      *
84      * put:
85      *  - the first key/value set put into MySparseArray will trigger a procState bump (bindService)
86      *  - if no valid observer token exist, will call registerUidObserver for put
87      *  - for each new uid put into array, it will be added to uid observer list
88      *
89      * remove:
90      *  - for each uid removed from array, it will be removed from uid observer list as well
91      *  - if it's the last uid in array, no more MusicFx procState bump (unbindService), uid
92      *    observer will also be removed, and observer token reset to null
93      */
94     private class MySparseArray extends SparseArray<PackageSessions> {
95 
96         @RequiresPermission(anyOf = {
97                 android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
98                 android.Manifest.permission.INTERACT_ACROSS_USERS,
99                 android.Manifest.permission.INTERACT_ACROSS_PROFILES
100         })
101         @Override
put(int uid, PackageSessions pkgSessions)102         public void put(int uid, PackageSessions pkgSessions) {
103             if (size() == 0) {
104                 int procState = ActivityManager.PROCESS_STATE_NONEXISTENT;
105                 try {
106                     procState = ActivityManager.getService().getPackageProcessState(
107                             mMusicFxPackageName, mPackageName);
108                 } catch (RemoteException e) {
109                     Log.e(TAG, "RemoteException with getPackageProcessState: " + e);
110                 }
111                 if (procState > ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
112                     Intent bindIntent = new Intent().setClassName(mMusicFxPackageName,
113                             "com.android.musicfx.KeepAliveService");
114                     mIsBound = true;
115                     mContext.bindServiceAsUser(
116                             bindIntent, mMusicFxBindConnection, Context.BIND_AUTO_CREATE,
117                             UserHandle.of(getCurrentUserId()));
118                     Log.i(TAG, "bindService to " + mMusicFxPackageName);
119                 }
120 
121                 Log.i(TAG, mMusicFxPackageName + " procState " + procState);
122             }
123             try {
124                 if (mUidObserverToken == null) {
125                     mUidObserverToken = ActivityManager.getService().registerUidObserverForUids(
126                             mEffectUidObserver, ActivityManager.UID_OBSERVER_GONE,
127                             ActivityManager.PROCESS_STATE_UNKNOWN, mPackageName,
128                             new int[]{uid});
129                     Log.i(TAG, "registered to observer with UID " + uid);
130                 } else if (get(uid) == null) { // addUidToObserver if this is a new UID
131                     ActivityManager.getService().addUidToObserver(mUidObserverToken, mPackageName,
132                             uid);
133                     Log.i(TAG, " UID " + uid + " add to observer");
134                 }
135             } catch (RemoteException e) {
136                 Log.e(TAG, "RemoteException with UID observer add/register: " + e);
137             }
138 
139             super.put(uid, pkgSessions);
140         }
141 
142         @Override
remove(int uid)143         public void remove(int uid) {
144             if (get(uid) != null) {
145                 try {
146                     ActivityManager.getService().removeUidFromObserver(mUidObserverToken,
147                             mPackageName, uid);
148                 } catch (RemoteException e) {
149                     Log.e(TAG, "RemoteException with removeUidFromObserver: " + e);
150                 }
151             }
152 
153             super.remove(uid);
154 
155             // stop foreground service delegate and unregister UID observers with the last UID
156             if (size() == 0) {
157                 try {
158                     ActivityManager.getService().unregisterUidObserver(mEffectUidObserver);
159                 } catch (RemoteException e) {
160                     Log.e(TAG, "RemoteException with unregisterUidObserver: " + e);
161                 }
162                 mUidObserverToken = null;
163                 if (mIsBound) {
164                     mContext.unbindService(mMusicFxBindConnection);
165                     mIsBound = false;
166                     Log.i(TAG, "last session closed, unregister UID observer, and unbind "
167                             + mMusicFxPackageName);
168                 }
169             }
170         }
171     }
172 
173     // Hashmap of UID and list of open sessions for this UID.
174     @GuardedBy("mClientUidMapLock")
175     private MySparseArray mClientUidSessionMap = new MySparseArray();
176 
177     // UID observer for effect MusicFx clients
178     private final IUidObserver mEffectUidObserver = new UidObserver() {
179         @Override public void onUidGone(int uid, boolean disabled) {
180             Log.w(TAG, " send MSG_EFFECT_CLIENT_GONE");
181             mAudioHandler.sendMessageAtTime(
182                     mAudioHandler.obtainMessage(MSG_EFFECT_CLIENT_GONE,
183                             uid /* arg1 */, 0 /* arg2 */,
184                             null /* obj */), 0 /* delay */);
185         }
186     };
187 
188     // BindService connection implementation, we don't need any implementation now
189     private ServiceConnection mMusicFxBindConnection = new ServiceConnection() {
190         @Override
191         public void onServiceConnected(ComponentName name, IBinder service) {
192             Log.d(TAG, " service connected to " + name);
193         }
194 
195         @Override
196         public void onServiceDisconnected(ComponentName name) {
197             Log.d(TAG, " service disconnected from " + name);
198         }
199     };
200 
MusicFxHelper(@onNull Context context, @NonNull AudioHandler audioHandler)201     MusicFxHelper(@NonNull Context context, @NonNull AudioHandler audioHandler) {
202         mContext = context;
203         mAudioHandler = audioHandler;
204     }
205 
206     /**
207      * Handle the broadcast {@link #ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION} and
208      * {@link #ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION} intents.
209      *
210      * Only intents without target application package {@link android.content.Intent#getPackage}
211      * will be handled by the MusicFxHelper, all intents handled and forwarded by MusicFxHelper
212      * will have the target application package.
213      *
214      * If the intent is {@link #ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION}:
215      *  - If the MusicFx process is not running, call bindServiceAsUser with AUTO_CREATE to create.
216      *  - If this is the first audio session of MusicFx, call set foreground service delegate.
217      *  - If this is the first audio session for a given UID, add the UID into observer.
218      *
219      * If the intent is {@link #ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION}
220      *  - The KeepAliveService of MusicFx will be unbound, and MusicFx will not be foreground
221      *    delegated anymore if the last session of the last package was closed.
222      *  - The Uid Observer will be removed when the last session of a package was closed.
223      */
224     @RequiresPermission(allOf = {android.Manifest.permission.INTERACT_ACROSS_USERS})
handleAudioEffectBroadcast(Context context, Intent intent)225     public void handleAudioEffectBroadcast(Context context, Intent intent) {
226         String target = intent.getPackage();
227         if (target != null) {
228             Log.w(TAG, "effect broadcast already targeted to " + target);
229             return;
230         }
231         final PackageManager pm = context.getPackageManager();
232         // TODO this should target a user-selected panel
233         List<ResolveInfo> ril = pm.queryBroadcastReceivers(intent, 0 /* flags */);
234         if (ril != null && ril.size() != 0) {
235             ResolveInfo ri = ril.get(0);
236             final String senderPackageName = intent.getStringExtra(AudioEffect.EXTRA_PACKAGE_NAME);
237             if (senderPackageName == null) {
238                 Log.w(TAG, "Intent package name must not be null");
239                 return;
240             }
241             try {
242                 if (ri != null && ri.activityInfo != null && ri.activityInfo.packageName != null) {
243                     final int senderUid = pm.getPackageUidAsUser(senderPackageName,
244                             PackageManager.PackageInfoFlags.of(MATCH_ANY_USER), getCurrentUserId());
245                     intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
246                     intent.setPackage(ri.activityInfo.packageName);
247                     if (setMusicFxServiceWithObserver(intent, senderUid, senderPackageName)) {
248                         context.sendBroadcastAsUser(intent, UserHandle.ALL);
249                     }
250                     return;
251                 }
252             } catch (PackageManager.NameNotFoundException e) {
253                 Log.e(TAG, "Not able to find UID from package: " + senderPackageName + " error: "
254                         + e);
255             }
256         }
257         Log.w(TAG, "couldn't find receiver package for effect intent");
258     }
259 
260     @RequiresPermission(anyOf = {
261             android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
262             android.Manifest.permission.INTERACT_ACROSS_USERS,
263             android.Manifest.permission.INTERACT_ACROSS_PROFILES
264     })
265     @GuardedBy("mClientUidMapLock")
handleAudioEffectSessionOpen( int senderUid, String senderPackageName, int sessionId)266     private boolean handleAudioEffectSessionOpen(
267             int senderUid, String senderPackageName, int sessionId) {
268         Log.d(TAG, senderPackageName + " UID " + senderUid + " open MusicFx session " + sessionId);
269 
270         PackageSessions pkgSessions = mClientUidSessionMap.get(Integer.valueOf(senderUid));
271         if (pkgSessions != null && pkgSessions.mSessions != null) {
272             if (pkgSessions.mSessions.contains(sessionId)) {
273                 Log.e(TAG, "Audio session " + sessionId + " already open for UID: "
274                         + senderUid + ", package: " + senderPackageName + ", abort");
275                 return false;
276             }
277             if (!pkgSessions.mPackageName.equals(senderPackageName)) {
278                 Log.w(TAG, "Inconsistency package names for UID open: " + senderUid + " prev: "
279                         + pkgSessions.mPackageName + ", now: " + senderPackageName);
280                 return false;
281             }
282         } else {
283             // first session for this UID, create a new Package/Sessions pair
284             pkgSessions = new PackageSessions();
285             pkgSessions.mSessions = new ArrayList();
286             pkgSessions.mPackageName = senderPackageName;
287         }
288 
289         pkgSessions.mSessions.add(Integer.valueOf(sessionId));
290         mClientUidSessionMap.put(Integer.valueOf(senderUid), pkgSessions);
291         return true;
292     }
293 
294     @RequiresPermission(anyOf = {
295             android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
296             android.Manifest.permission.INTERACT_ACROSS_USERS,
297             android.Manifest.permission.INTERACT_ACROSS_PROFILES
298     })
299     @GuardedBy("mClientUidMapLock")
handleAudioEffectSessionClose( int senderUid, String senderPackageName, int sessionId)300     private boolean handleAudioEffectSessionClose(
301             int senderUid, String senderPackageName, int sessionId) {
302         Log.d(TAG, senderPackageName + " UID " + senderUid + " close MusicFx session " + sessionId);
303 
304         PackageSessions pkgSessions = mClientUidSessionMap.get(Integer.valueOf(senderUid));
305         if (pkgSessions == null) {
306             Log.e(TAG, senderPackageName + " UID " + senderUid + " does not exist in map, abort");
307             return false;
308         }
309         if (!pkgSessions.mPackageName.equals(senderPackageName)) {
310             Log.w(TAG, "Inconsistency package names for UID " + senderUid + " close, prev: "
311                     + pkgSessions.mPackageName + ", now: " + senderPackageName);
312             return false;
313         }
314 
315         if (pkgSessions.mSessions != null && pkgSessions.mSessions.size() != 0) {
316             if (!pkgSessions.mSessions.contains(sessionId)) {
317                 Log.e(TAG, senderPackageName + " UID " + senderUid + " session " + sessionId
318                         + " does not exist in map, abort");
319                 return false;
320             }
321 
322             pkgSessions.mSessions.remove(Integer.valueOf(sessionId));
323         }
324 
325         if (pkgSessions.mSessions == null || pkgSessions.mSessions.size() == 0) {
326             // remove UID from map as well as the UID observer with the last session close
327             mClientUidSessionMap.remove(Integer.valueOf(senderUid));
328         } else {
329             mClientUidSessionMap.put(Integer.valueOf(senderUid), pkgSessions);
330         }
331 
332         return true;
333     }
334 
335     /**
336      * @return true if the intent is validated and handled successfully, false with any error
337      * (invalid sender/intent for example).
338      */
339     @RequiresPermission(anyOf = {
340             android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
341             android.Manifest.permission.INTERACT_ACROSS_USERS,
342             android.Manifest.permission.INTERACT_ACROSS_PROFILES
343     })
setMusicFxServiceWithObserver( Intent intent, int senderUid, String packageName)344     private boolean setMusicFxServiceWithObserver(
345             Intent intent, int senderUid, String packageName) {
346         final int session = intent.getIntExtra(AudioEffect.EXTRA_AUDIO_SESSION,
347                 AudioManager.AUDIO_SESSION_ID_GENERATE);
348         if (AudioManager.AUDIO_SESSION_ID_GENERATE == session) {
349             Log.e(TAG, packageName + " intent have no invalid audio session");
350             return false;
351         }
352 
353         synchronized (mClientUidMapLock) {
354             if (intent.getAction().equals(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)) {
355                 return handleAudioEffectSessionOpen(senderUid, packageName, session);
356             } else {
357                 return handleAudioEffectSessionClose(senderUid, packageName, session);
358             }
359         }
360     }
361 
getCurrentUserId()362     private int getCurrentUserId() {
363         final long ident = Binder.clearCallingIdentity();
364         try {
365             UserInfo currentUser = ActivityManager.getService().getCurrentUser();
366             return currentUser.id;
367         } catch (RemoteException e) {
368             // Activity manager not running, nothing we can do assume user 0.
369         } finally {
370             Binder.restoreCallingIdentity(ident);
371         }
372         return UserHandle.USER_SYSTEM;
373     }
374 
375 
376     /**
377      * Handle the UidObserver onUidGone callback of MusicFx clients.
378      * Send close intent for all open audio sessions of this UID. The mClientUidSessionMap will be
379      * updated with the handling of close intent in setMusicFxServiceWithObserver.
380      */
381     @RequiresPermission(allOf = {android.Manifest.permission.INTERACT_ACROSS_USERS})
handleEffectClientUidGone(int uid)382     private void handleEffectClientUidGone(int uid) {
383         synchronized (mClientUidMapLock) {
384             Log.d(TAG, "handle MSG_EFFECT_CLIENT_GONE uid: " + uid + " mapSize: "
385                     + mClientUidSessionMap.size());
386             // Once the uid is no longer running, close all remain audio session(s) for this UID
387             final PackageSessions pkgSessions = mClientUidSessionMap.get(Integer.valueOf(uid));
388             if (pkgSessions != null) {
389                 Log.i(TAG, "UID " + uid + " gone, closing all sessions");
390 
391                 // send close intent for each open session of the gone UID
392                 for (Integer sessionId : pkgSessions.mSessions) {
393                     Intent closeIntent =
394                             new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
395                     closeIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, pkgSessions.mPackageName);
396                     closeIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, sessionId);
397                     closeIntent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
398                     // set broadcast target
399                     closeIntent.setPackage(mMusicFxPackageName);
400                     mContext.sendBroadcastAsUser(closeIntent, UserHandle.ALL);
401                 }
402                 mClientUidSessionMap.remove(Integer.valueOf(uid));
403             }
404         }
405     }
406 
407     @RequiresPermission(allOf = {android.Manifest.permission.INTERACT_ACROSS_USERS})
handleMessage(Message msg)408     /*package*/ void handleMessage(Message msg) {
409         switch (msg.what) {
410             case MSG_EFFECT_CLIENT_GONE:
411                 Log.w(TAG, " handle MSG_EFFECT_CLIENT_GONE");
412                 handleEffectClientUidGone(msg.arg1 /* uid */);
413                 break;
414             default:
415                 Log.e(TAG, "Unexpected msg to handle in MusicFxHelper: " + msg.what);
416                 break;
417         }
418     }
419 }
420