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