1 /*
2  * Copyright (C) 2017 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.media;
18 
19 import android.annotation.NonNull;
20 import android.content.Context;
21 import android.media.AudioManager;
22 import android.media.AudioPlaybackConfiguration;
23 import android.os.Handler;
24 import android.os.Looper;
25 import android.os.Message;
26 import android.os.UserHandle;
27 import android.util.ArrayMap;
28 import android.util.ArraySet;
29 import android.util.Log;
30 
31 import com.android.internal.annotations.GuardedBy;
32 
33 import java.io.PrintWriter;
34 import java.util.ArrayList;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Set;
38 
39 /**
40  * Monitors the state changes of audio players.
41  */
42 class AudioPlayerStateMonitor {
43     private static final boolean DEBUG = MediaSessionService.DEBUG;
44     private static String TAG = "AudioPlayerStateMonitor";
45 
46     private static AudioPlayerStateMonitor sInstance;
47 
48     /**
49      * Listener for handling the active state changes of audio players.
50      */
51     interface OnAudioPlayerActiveStateChangedListener {
52         /**
53          * Called when the active state of audio player is changed.
54          *
55          * @param config The audio playback configuration for the audio player for which active
56          *              state was changed. If {@param isRemoved} is {@code true}, this holds
57          *              outdated information.
58          * @param isRemoved {@code true} if the audio player is removed.
59          */
onAudioPlayerActiveStateChanged( @onNull AudioPlaybackConfiguration config, boolean isRemoved)60         void onAudioPlayerActiveStateChanged(
61                 @NonNull AudioPlaybackConfiguration config, boolean isRemoved);
62     }
63 
64     private final static class MessageHandler extends Handler {
65         private static final int MSG_AUDIO_PLAYER_ACTIVE_STATE_CHANGED = 1;
66 
67         private final OnAudioPlayerActiveStateChangedListener mListener;
68 
MessageHandler(Looper looper, OnAudioPlayerActiveStateChangedListener listener)69         MessageHandler(Looper looper, OnAudioPlayerActiveStateChangedListener listener) {
70             super(looper);
71             mListener = listener;
72         }
73 
74         @Override
handleMessage(Message msg)75         public void handleMessage(Message msg) {
76             switch (msg.what) {
77                 case MSG_AUDIO_PLAYER_ACTIVE_STATE_CHANGED:
78                     mListener.onAudioPlayerActiveStateChanged((AudioPlaybackConfiguration) msg.obj,
79                             msg.arg1 != 0);
80                     break;
81             }
82         }
83 
sendAudioPlayerActiveStateChangedMessage( final AudioPlaybackConfiguration config, final boolean isRemoved)84         void sendAudioPlayerActiveStateChangedMessage(
85                 final AudioPlaybackConfiguration config, final boolean isRemoved) {
86             obtainMessage(MSG_AUDIO_PLAYER_ACTIVE_STATE_CHANGED,
87                     isRemoved ? 1 : 0, 0 /* unused */, config).sendToTarget();
88         }
89     }
90 
91     private final Object mLock = new Object();
92     @GuardedBy("mLock")
93     private final Map<OnAudioPlayerActiveStateChangedListener, MessageHandler> mListenerMap =
94             new ArrayMap<>();
95     @GuardedBy("mLock")
96     @SuppressWarnings("WeakerAccess") /* synthetic access */
97     final Set<Integer> mActiveAudioUids = new ArraySet<>();
98     @GuardedBy("mLock")
99     @SuppressWarnings("WeakerAccess") /* synthetic access */
100     ArrayMap<Integer, AudioPlaybackConfiguration> mPrevActiveAudioPlaybackConfigs =
101             new ArrayMap<>();
102     // Sorted array of UIDs that had active audio playback. (i.e. playing an audio/video)
103     // The UID whose audio playback becomes active at the last comes first.
104     // TODO(b/35278867): Find and use unique identifier for apps because apps may share the UID.
105     @GuardedBy("mLock")
106     @SuppressWarnings("WeakerAccess") /* synthetic access */
107     final List<Integer> mSortedAudioPlaybackClientUids = new ArrayList<>();
108 
getInstance(Context context)109     static AudioPlayerStateMonitor getInstance(Context context) {
110         synchronized (AudioPlayerStateMonitor.class) {
111             if (sInstance == null) {
112                 sInstance = new AudioPlayerStateMonitor(context);
113             }
114             return sInstance;
115         }
116     }
117 
AudioPlayerStateMonitor(Context context)118     private AudioPlayerStateMonitor(Context context) {
119         AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
120         am.registerAudioPlaybackCallback(new AudioManagerPlaybackListener(), null);
121     }
122 
123     /**
124      * Registers OnAudioPlayerActiveStateChangedListener.
125      */
registerListener( OnAudioPlayerActiveStateChangedListener listener, Handler handler)126     public void registerListener(
127             OnAudioPlayerActiveStateChangedListener listener, Handler handler) {
128         synchronized (mLock) {
129             mListenerMap.put(listener, new MessageHandler((handler == null) ?
130                     Looper.myLooper() : handler.getLooper(), listener));
131         }
132     }
133 
134     /**
135      * Unregisters OnAudioPlayerActiveStateChangedListener.
136      */
unregisterListener(OnAudioPlayerActiveStateChangedListener listener)137     public void unregisterListener(OnAudioPlayerActiveStateChangedListener listener) {
138         synchronized (mLock) {
139             mListenerMap.remove(listener);
140         }
141     }
142 
143     /**
144      * Returns the sorted list of UIDs that have had active audio playback. (i.e. playing an
145      * audio/video) The UID whose audio is currently playing comes first, then the UID whose audio
146      * playback becomes active at the last comes next.
147      */
getSortedAudioPlaybackClientUids()148     public List<Integer> getSortedAudioPlaybackClientUids() {
149         List<Integer> sortedAudioPlaybackClientUids = new ArrayList();
150         synchronized (mLock) {
151             sortedAudioPlaybackClientUids.addAll(mSortedAudioPlaybackClientUids);
152         }
153         return sortedAudioPlaybackClientUids;
154     }
155 
156     /**
157      * Returns whether the given uid corresponds to the last process to audio or not.
158      *
159      * <p> This is useful for handling the cases where the foreground app is playing media without
160      * a media session. Then, volume events should affect the local music stream rather than other
161      * media sessions.
162      *
163      * @return {@code true} if the given uid corresponds to the last process to audio or
164      * {@code false} otherwise.
165      */
hasUidPlayedAudioLast(int uid)166     public boolean hasUidPlayedAudioLast(int uid) {
167         synchronized (mLock) {
168             return !mSortedAudioPlaybackClientUids.isEmpty()
169                     && uid == mSortedAudioPlaybackClientUids.get(0);
170         }
171     }
172 
173     /**
174      * Returns if the audio playback is active for the uid.
175      */
isPlaybackActive(int uid)176     public boolean isPlaybackActive(int uid) {
177         synchronized (mLock) {
178             return mActiveAudioUids.contains(uid);
179         }
180     }
181 
182     /**
183      * Cleans up the sorted list of audio playback client UIDs with given {@param
184      * mediaButtonSessionUid}.
185      * <p>UIDs whose audio playback are inactive and have started before the media button session's
186      * audio playback cannot be the lastly played media app. So they won't be needed anymore.
187      *
188      * @param mediaButtonSessionUid UID of the media button session.
189      */
cleanUpAudioPlaybackUids(int mediaButtonSessionUid)190     public void cleanUpAudioPlaybackUids(int mediaButtonSessionUid) {
191         synchronized (mLock) {
192             int userId = UserHandle.getUserHandleForUid(mediaButtonSessionUid).getIdentifier();
193             for (int i = mSortedAudioPlaybackClientUids.size() - 1; i >= 0; i--) {
194                 if (mSortedAudioPlaybackClientUids.get(i) == mediaButtonSessionUid) {
195                     break;
196                 }
197                 int uid = mSortedAudioPlaybackClientUids.get(i);
198                 if (userId == UserHandle.getUserHandleForUid(uid).getIdentifier()
199                         && !isPlaybackActive(uid)) {
200                     // Clean up unnecessary UIDs.
201                     // It doesn't need to be managed profile aware because it's just to prevent
202                     // the list from increasing indefinitely. The media button session updating
203                     // shouldn't be affected by cleaning up.
204                     mSortedAudioPlaybackClientUids.remove(i);
205                 }
206             }
207         }
208     }
209 
210     /**
211      * Dumps {@link AudioPlayerStateMonitor}.
212      */
dump(Context context, PrintWriter pw, String prefix)213     public void dump(Context context, PrintWriter pw, String prefix) {
214         synchronized (mLock) {
215             pw.println(prefix + "Audio playback (lastly played comes first)");
216             String indent = prefix + "  ";
217             for (int i = 0; i < mSortedAudioPlaybackClientUids.size(); i++) {
218                 int uid = mSortedAudioPlaybackClientUids.get(i);
219                 pw.print(indent + "uid=" + uid + " packages=");
220                 String[] packages = context.getPackageManager().getPackagesForUid(uid);
221                 if (packages != null && packages.length > 0) {
222                     for (int j = 0; j < packages.length; j++) {
223                         pw.print(packages[j] + " ");
224                     }
225                 }
226                 pw.println();
227             }
228         }
229     }
230 
231     @GuardedBy("mLock")
sendAudioPlayerActiveStateChangedMessageLocked( final AudioPlaybackConfiguration config, final boolean isRemoved)232     private void sendAudioPlayerActiveStateChangedMessageLocked(
233             final AudioPlaybackConfiguration config, final boolean isRemoved) {
234         for (MessageHandler messageHandler : mListenerMap.values()) {
235             messageHandler.sendAudioPlayerActiveStateChangedMessage(config, isRemoved);
236         }
237     }
238 
239     private class AudioManagerPlaybackListener extends AudioManager.AudioPlaybackCallback {
240         @Override
onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs)241         public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {
242             synchronized (mLock) {
243                 // Update mActiveAudioUids
244                 mActiveAudioUids.clear();
245                 ArrayMap<Integer, AudioPlaybackConfiguration> activeAudioPlaybackConfigs =
246                         new ArrayMap<>();
247                 for (AudioPlaybackConfiguration config : configs) {
248                     if (config.isActive()) {
249                         mActiveAudioUids.add(config.getClientUid());
250                         activeAudioPlaybackConfigs.put(config.getPlayerInterfaceId(), config);
251                     }
252                 }
253 
254                 // Update mSortedAudioPlaybackClientUids.
255                 for (int i = 0; i < activeAudioPlaybackConfigs.size(); ++i) {
256                     AudioPlaybackConfiguration config = activeAudioPlaybackConfigs.valueAt(i);
257                     final int uid = config.getClientUid();
258                     if (!mPrevActiveAudioPlaybackConfigs.containsKey(
259                             config.getPlayerInterfaceId())) {
260                         if (DEBUG) {
261                             Log.d(TAG, "Found a new active media playback. " + config);
262                         }
263                         // New active audio playback.
264                         int index = mSortedAudioPlaybackClientUids.indexOf(uid);
265                         if (index == 0) {
266                             // It's the lastly played music app already. Skip updating.
267                             continue;
268                         } else if (index > 0) {
269                             mSortedAudioPlaybackClientUids.remove(index);
270                         }
271                         mSortedAudioPlaybackClientUids.add(0, uid);
272                     }
273                 }
274 
275                 if (mActiveAudioUids.size() > 0
276                         && !mActiveAudioUids.contains(mSortedAudioPlaybackClientUids.get(0))) {
277                     int firstActiveUid = -1;
278                     int firstActiveUidIndex = -1;
279                     for (int i = 1; i < mSortedAudioPlaybackClientUids.size(); ++i) {
280                         int uid = mSortedAudioPlaybackClientUids.get(i);
281                         if (mActiveAudioUids.contains(uid)) {
282                             firstActiveUidIndex = i;
283                             firstActiveUid = uid;
284                             break;
285                         }
286                     }
287                     for (int i = firstActiveUidIndex; i > 0; --i) {
288                         mSortedAudioPlaybackClientUids.set(i,
289                                 mSortedAudioPlaybackClientUids.get(i - 1));
290                     }
291                     mSortedAudioPlaybackClientUids.set(0, firstActiveUid);
292                 }
293 
294                 // Notify the active state change of audio players.
295                 for (AudioPlaybackConfiguration config : configs) {
296                     final int pii = config.getPlayerInterfaceId();
297                     boolean wasActive = mPrevActiveAudioPlaybackConfigs.remove(pii) != null;
298                     if (wasActive != config.isActive()) {
299                         sendAudioPlayerActiveStateChangedMessageLocked(
300                                 config, /* isRemoved */ false);
301                     }
302                 }
303                 for (AudioPlaybackConfiguration config : mPrevActiveAudioPlaybackConfigs.values()) {
304                     sendAudioPlayerActiveStateChangedMessageLocked(config, /* isRemoved */ true);
305                 }
306 
307                 // Update mPrevActiveAudioPlaybackConfigs
308                 mPrevActiveAudioPlaybackConfigs = activeAudioPlaybackConfigs;
309             }
310         }
311     }
312 }
313