| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.media; |
| |
| import android.annotation.NonNull; |
| import android.content.Context; |
| import android.media.AudioManager; |
| import android.media.AudioPlaybackConfiguration; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.UserHandle; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.IntArray; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.GuardedBy; |
| |
| import java.io.PrintWriter; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Monitors the state changes of audio players. |
| */ |
| class AudioPlayerStateMonitor { |
| private static boolean DEBUG = MediaSessionService.DEBUG; |
| private static String TAG = "AudioPlayerStateMonitor"; |
| |
| private static AudioPlayerStateMonitor sInstance; |
| |
| /** |
| * Listener for handling the active state changes of audio players. |
| */ |
| interface OnAudioPlayerActiveStateChangedListener { |
| /** |
| * Called when the active state of audio player is changed. |
| * |
| * @param config The audio playback configuration for the audio player for which active |
| * state was changed. If {@param isRemoved} is {@code true}, this holds |
| * outdated information. |
| * @param isRemoved {@code true} if the audio player is removed. |
| */ |
| void onAudioPlayerActiveStateChanged( |
| @NonNull AudioPlaybackConfiguration config, boolean isRemoved); |
| } |
| |
| private final static class MessageHandler extends Handler { |
| private static final int MSG_AUDIO_PLAYER_ACTIVE_STATE_CHANGED = 1; |
| |
| private final OnAudioPlayerActiveStateChangedListener mListener; |
| |
| MessageHandler(Looper looper, OnAudioPlayerActiveStateChangedListener listener) { |
| super(looper); |
| mListener = listener; |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_AUDIO_PLAYER_ACTIVE_STATE_CHANGED: |
| mListener.onAudioPlayerActiveStateChanged((AudioPlaybackConfiguration) msg.obj, |
| msg.arg1 != 0); |
| break; |
| } |
| } |
| |
| void sendAudioPlayerActiveStateChangedMessage( |
| final AudioPlaybackConfiguration config, final boolean isRemoved) { |
| obtainMessage(MSG_AUDIO_PLAYER_ACTIVE_STATE_CHANGED, |
| isRemoved ? 1 : 0, 0 /* unused */, config).sendToTarget(); |
| } |
| } |
| |
| private final Object mLock = new Object(); |
| @GuardedBy("mLock") |
| private final Map<OnAudioPlayerActiveStateChangedListener, MessageHandler> mListenerMap = |
| new ArrayMap<>(); |
| @GuardedBy("mLock") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| final Set<Integer> mActiveAudioUids = new ArraySet<>(); |
| @GuardedBy("mLock") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| ArrayMap<Integer, AudioPlaybackConfiguration> mPrevActiveAudioPlaybackConfigs = |
| new ArrayMap<>(); |
| // Sorted array of UIDs that had active audio playback. (i.e. playing an audio/video) |
| // The UID whose audio playback becomes active at the last comes first. |
| // TODO(b/35278867): Find and use unique identifier for apps because apps may share the UID. |
| @GuardedBy("mLock") |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| final IntArray mSortedAudioPlaybackClientUids = new IntArray(); |
| |
| static AudioPlayerStateMonitor getInstance(Context context) { |
| synchronized (AudioPlayerStateMonitor.class) { |
| if (sInstance == null) { |
| sInstance = new AudioPlayerStateMonitor(context); |
| } |
| return sInstance; |
| } |
| } |
| |
| private AudioPlayerStateMonitor(Context context) { |
| AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); |
| am.registerAudioPlaybackCallback(new AudioManagerPlaybackListener(), null); |
| } |
| |
| /** |
| * Registers OnAudioPlayerActiveStateChangedListener. |
| */ |
| public void registerListener( |
| OnAudioPlayerActiveStateChangedListener listener, Handler handler) { |
| synchronized (mLock) { |
| mListenerMap.put(listener, new MessageHandler((handler == null) ? |
| Looper.myLooper() : handler.getLooper(), listener)); |
| } |
| } |
| |
| /** |
| * Unregisters OnAudioPlayerActiveStateChangedListener. |
| */ |
| public void unregisterListener(OnAudioPlayerActiveStateChangedListener listener) { |
| synchronized (mLock) { |
| mListenerMap.remove(listener); |
| } |
| } |
| |
| /** |
| * Returns the sorted list of UIDs that have had active audio playback. (i.e. playing an |
| * audio/video) The UID whose audio is currently playing comes first, then the UID whose audio |
| * playback becomes active at the last comes next. |
| */ |
| public IntArray getSortedAudioPlaybackClientUids() { |
| IntArray sortedAudioPlaybackClientUids = new IntArray(); |
| synchronized (mLock) { |
| sortedAudioPlaybackClientUids.addAll(mSortedAudioPlaybackClientUids); |
| } |
| return sortedAudioPlaybackClientUids; |
| } |
| |
| /** |
| * Returns if the audio playback is active for the uid. |
| */ |
| public boolean isPlaybackActive(int uid) { |
| synchronized (mLock) { |
| return mActiveAudioUids.contains(uid); |
| } |
| } |
| |
| /** |
| * Cleans up the sorted list of audio playback client UIDs with given {@param |
| * mediaButtonSessionUid}. |
| * <p>UIDs whose audio playback are inactive and have started before the media button session's |
| * audio playback cannot be the lastly played media app. So they won't needed anymore. |
| * |
| * @param mediaButtonSessionUid UID of the media button session. |
| */ |
| public void cleanUpAudioPlaybackUids(int mediaButtonSessionUid) { |
| synchronized (mLock) { |
| int userId = UserHandle.getUserId(mediaButtonSessionUid); |
| for (int i = mSortedAudioPlaybackClientUids.size() - 1; i >= 0; i--) { |
| if (mSortedAudioPlaybackClientUids.get(i) == mediaButtonSessionUid) { |
| break; |
| } |
| int uid = mSortedAudioPlaybackClientUids.get(i); |
| if (userId == UserHandle.getUserId(uid) && !isPlaybackActive(uid)) { |
| // Clean up unnecessary UIDs. |
| // It doesn't need to be managed profile aware because it's just to prevent |
| // the list from increasing indefinitely. The media button session updating |
| // shouldn't be affected by cleaning up. |
| mSortedAudioPlaybackClientUids.remove(i); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Dumps {@link AudioPlayerStateMonitor}. |
| */ |
| public void dump(Context context, PrintWriter pw, String prefix) { |
| synchronized (mLock) { |
| pw.println(prefix + "Audio playback (lastly played comes first)"); |
| String indent = prefix + " "; |
| for (int i = 0; i < mSortedAudioPlaybackClientUids.size(); i++) { |
| int uid = mSortedAudioPlaybackClientUids.get(i); |
| pw.print(indent + "uid=" + uid + " packages="); |
| String[] packages = context.getPackageManager().getPackagesForUid(uid); |
| if (packages != null && packages.length > 0) { |
| for (int j = 0; j < packages.length; j++) { |
| pw.print(packages[j] + " "); |
| } |
| } |
| pw.println(); |
| } |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void sendAudioPlayerActiveStateChangedMessageLocked( |
| final AudioPlaybackConfiguration config, final boolean isRemoved) { |
| for (MessageHandler messageHandler : mListenerMap.values()) { |
| messageHandler.sendAudioPlayerActiveStateChangedMessage(config, isRemoved); |
| } |
| } |
| |
| private class AudioManagerPlaybackListener extends AudioManager.AudioPlaybackCallback { |
| @Override |
| public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) { |
| synchronized (mLock) { |
| // Update mActiveAudioUids |
| mActiveAudioUids.clear(); |
| ArrayMap<Integer, AudioPlaybackConfiguration> activeAudioPlaybackConfigs = |
| new ArrayMap<>(); |
| for (AudioPlaybackConfiguration config : configs) { |
| if (config.isActive()) { |
| mActiveAudioUids.add(config.getClientUid()); |
| activeAudioPlaybackConfigs.put(config.getPlayerInterfaceId(), config); |
| } |
| } |
| |
| // Update mSortedAuioPlaybackClientUids. |
| for (int i = 0; i < activeAudioPlaybackConfigs.size(); ++i) { |
| AudioPlaybackConfiguration config = activeAudioPlaybackConfigs.valueAt(i); |
| final int uid = config.getClientUid(); |
| if (!mPrevActiveAudioPlaybackConfigs.containsKey( |
| config.getPlayerInterfaceId())) { |
| if (DEBUG) { |
| Log.d(TAG, "Found a new active media playback. " |
| + AudioPlaybackConfiguration.toLogFriendlyString(config)); |
| } |
| // New active audio playback. |
| int index = mSortedAudioPlaybackClientUids.indexOf(uid); |
| if (index == 0) { |
| // It's the lastly played music app already. Skip updating. |
| continue; |
| } else if (index > 0) { |
| mSortedAudioPlaybackClientUids.remove(index); |
| } |
| mSortedAudioPlaybackClientUids.add(0, uid); |
| } |
| } |
| |
| if (mActiveAudioUids.size() > 0 |
| && !mActiveAudioUids.contains(mSortedAudioPlaybackClientUids.get(0))) { |
| int firstActiveUid = -1; |
| int firatActiveUidIndex = -1; |
| for (int i = 1; i < mSortedAudioPlaybackClientUids.size(); ++i) { |
| int uid = mSortedAudioPlaybackClientUids.get(i); |
| if (mActiveAudioUids.contains(uid)) { |
| firatActiveUidIndex = i; |
| firstActiveUid = uid; |
| break; |
| } |
| } |
| for (int i = firatActiveUidIndex; i > 0; --i) { |
| mSortedAudioPlaybackClientUids.set(i, |
| mSortedAudioPlaybackClientUids.get(i - 1)); |
| } |
| mSortedAudioPlaybackClientUids.set(0, firstActiveUid); |
| } |
| |
| // Notify the active state change of audio players. |
| for (AudioPlaybackConfiguration config : configs) { |
| final int pii = config.getPlayerInterfaceId(); |
| boolean wasActive = mPrevActiveAudioPlaybackConfigs.remove(pii) != null; |
| if (wasActive != config.isActive()) { |
| sendAudioPlayerActiveStateChangedMessageLocked( |
| config, /* isRemoved */ false); |
| } |
| } |
| for (AudioPlaybackConfiguration config : mPrevActiveAudioPlaybackConfigs.values()) { |
| sendAudioPlayerActiveStateChangedMessageLocked(config, /* isRemoved */ true); |
| } |
| |
| // Update mPrevActiveAudioPlaybackConfigs |
| mPrevActiveAudioPlaybackConfigs = activeAudioPlaybackConfigs; |
| } |
| } |
| } |
| } |