blob: 43438942a06058819f2f5951866a0a637c00c322 [file] [log] [blame]
/*
* Copyright 2019 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.audio;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioMixerAttributes;
import android.media.AudioSystem;
import android.media.IDevicesForAttributesCallback;
import android.media.ISoundDose;
import android.media.ISoundDoseCallback;
import android.media.audiopolicy.AudioMix;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import com.android.internal.annotations.GuardedBy;
import java.io.PrintWriter;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Provides an adapter to access functionality of the android.media.AudioSystem class for device
* related functionality.
* Use the "real" AudioSystem through the default adapter.
* Use the "always ok" adapter to avoid dealing with the APM behaviors during a test.
*/
public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback,
AudioSystem.VolumeRangeInitRequestCallback {
private static final String TAG = "AudioSystemAdapter";
// initialized in factory getDefaultAdapter()
private static AudioSystemAdapter sSingletonDefaultAdapter;
/**
* should be false by default unless enabling measurements of method call counts and time spent
* in measured methods
*/
private static final boolean ENABLE_GETDEVICES_STATS = false;
private static final int NB_MEASUREMENTS = 1;
private static final int METHOD_GETDEVICESFORATTRIBUTES = 0;
private long[] mMethodTimeNs;
private int[] mMethodCallCounter;
private String[] mMethodNames = {"getDevicesForAttributes"};
private static final boolean USE_CACHE_FOR_GETDEVICES = true;
private static final Object sDeviceCacheLock = new Object();
private ConcurrentHashMap<Pair<AudioAttributes, Boolean>, ArrayList<AudioDeviceAttributes>>
mLastDevicesForAttr = new ConcurrentHashMap<>();
@GuardedBy("sDeviceCacheLock")
private ConcurrentHashMap<Pair<AudioAttributes, Boolean>, ArrayList<AudioDeviceAttributes>>
mDevicesForAttrCache;
@GuardedBy("sDeviceCacheLock")
private long mDevicesForAttributesCacheClearTimeMs = System.currentTimeMillis();
private int[] mMethodCacheHit;
/**
* Map that stores all attributes + forVolume pairs that are registered for
* routing change callback. The key is the {@link IBinder} that corresponds
* to the remote callback.
*/
private final ArrayMap<IBinder, List<Pair<AudioAttributes, Boolean>>> mRegisteredAttributesMap =
new ArrayMap<>();
private final RemoteCallbackList<IDevicesForAttributesCallback>
mDevicesForAttributesCallbacks = new RemoteCallbackList<>();
private static final Object sRoutingListenerLock = new Object();
@GuardedBy("sRoutingListenerLock")
private static @Nullable OnRoutingUpdatedListener sRoutingListener;
private static final Object sVolRangeInitReqListenerLock = new Object();
@GuardedBy("sVolRangeInitReqListenerLock")
private static @Nullable OnVolRangeInitRequestListener sVolRangeInitReqListener;
/**
* should be false except when trying to debug caching errors. When true, the value retrieved
* from the cache will be compared against the real queried value, which defeats the purpose of
* the cache in terms of performance.
*/
private static final boolean DEBUG_CACHE = false;
/**
* Implementation of AudioSystem.RoutingUpdateCallback
*/
@Override
public void onRoutingUpdated() {
if (DEBUG_CACHE) {
Log.d(TAG, "---- onRoutingUpdated (from native) ----------");
}
invalidateRoutingCache();
final OnRoutingUpdatedListener listener;
synchronized (sRoutingListenerLock) {
listener = sRoutingListener;
}
if (listener != null) {
listener.onRoutingUpdatedFromNative();
}
synchronized (mRegisteredAttributesMap) {
final int nbCallbacks = mDevicesForAttributesCallbacks.beginBroadcast();
for (int i = 0; i < nbCallbacks; i++) {
IDevicesForAttributesCallback cb =
mDevicesForAttributesCallbacks.getBroadcastItem(i);
List<Pair<AudioAttributes, Boolean>> attrList =
mRegisteredAttributesMap.get(cb.asBinder());
if (attrList == null) {
throw new IllegalStateException("Attribute list must not be null");
}
for (Pair<AudioAttributes, Boolean> attr : attrList) {
ArrayList<AudioDeviceAttributes> devices =
getDevicesForAttributes(attr.first, attr.second);
if (!mLastDevicesForAttr.containsKey(attr)
|| !sameDeviceList(devices, mLastDevicesForAttr.get(attr))) {
try {
cb.onDevicesForAttributesChanged(
attr.first, attr.second, devices);
} catch (RemoteException e) { }
}
}
}
mDevicesForAttributesCallbacks.finishBroadcast();
}
}
interface OnRoutingUpdatedListener {
void onRoutingUpdatedFromNative();
}
static void setRoutingListener(@Nullable OnRoutingUpdatedListener listener) {
synchronized (sRoutingListenerLock) {
sRoutingListener = listener;
}
}
/**
* Empties the routing cache if enabled.
*/
public void clearRoutingCache() {
if (DEBUG_CACHE) {
Log.d(TAG, "---- routing cache clear (from java) ----------");
}
invalidateRoutingCache();
}
/**
* @see AudioManager#addOnDevicesForAttributesChangedListener(
* AudioAttributes, Executor, OnDevicesForAttributesChangedListener)
*/
public void addOnDevicesForAttributesChangedListener(AudioAttributes attributes,
boolean forVolume, @NonNull IDevicesForAttributesCallback listener) {
List<Pair<AudioAttributes, Boolean>> res;
final Pair<AudioAttributes, Boolean> attr = new Pair(attributes, forVolume);
synchronized (mRegisteredAttributesMap) {
res = mRegisteredAttributesMap.get(listener.asBinder());
if (res == null) {
res = new ArrayList<>();
mRegisteredAttributesMap.put(listener.asBinder(), res);
mDevicesForAttributesCallbacks.register(listener);
}
if (!res.contains(attr)) {
res.add(attr);
}
}
// Make query on registration to populate cache
getDevicesForAttributes(attributes, forVolume);
}
/**
* @see AudioManager#removeOnDevicesForAttributesChangedListener(
* OnDevicesForAttributesChangedListener)
*/
public void removeOnDevicesForAttributesChangedListener(
@NonNull IDevicesForAttributesCallback listener) {
synchronized (mRegisteredAttributesMap) {
if (!mRegisteredAttributesMap.containsKey(listener.asBinder())) {
Log.w(TAG, "listener to be removed is not found.");
return;
}
mRegisteredAttributesMap.remove(listener.asBinder());
mDevicesForAttributesCallbacks.unregister(listener);
}
}
/**
* Helper function to compare lists of {@link AudioDeviceAttributes}
* @return true if the two lists contains the same devices, false otherwise.
*/
private boolean sameDeviceList(@NonNull List<AudioDeviceAttributes> a,
@NonNull List<AudioDeviceAttributes> b) {
for (AudioDeviceAttributes device : a) {
if (!b.contains(device)) {
return false;
}
}
for (AudioDeviceAttributes device : b) {
if (!a.contains(device)) {
return false;
}
}
return true;
}
/**
* Implementation of AudioSystem.VolumeRangeInitRequestCallback
*/
@Override
public void onVolumeRangeInitializationRequested() {
final OnVolRangeInitRequestListener listener;
synchronized (sVolRangeInitReqListenerLock) {
listener = sVolRangeInitReqListener;
}
if (listener != null) {
listener.onVolumeRangeInitRequestFromNative();
}
}
interface OnVolRangeInitRequestListener {
void onVolumeRangeInitRequestFromNative();
}
static void setVolRangeInitReqListener(@Nullable OnVolRangeInitRequestListener listener) {
synchronized (sVolRangeInitReqListenerLock) {
sVolRangeInitReqListener = listener;
}
}
/**
* Create a wrapper around the {@link AudioSystem} static methods, all functions are directly
* forwarded to the AudioSystem class.
* @return an adapter around AudioSystem
*/
static final synchronized @NonNull AudioSystemAdapter getDefaultAdapter() {
if (sSingletonDefaultAdapter == null) {
sSingletonDefaultAdapter = new AudioSystemAdapter();
AudioSystem.setRoutingCallback(sSingletonDefaultAdapter);
AudioSystem.setVolumeRangeInitRequestCallback(sSingletonDefaultAdapter);
if (USE_CACHE_FOR_GETDEVICES) {
synchronized (sDeviceCacheLock) {
sSingletonDefaultAdapter.mDevicesForAttrCache =
new ConcurrentHashMap<>(AudioSystem.getNumStreamTypes());
sSingletonDefaultAdapter.mMethodCacheHit = new int[NB_MEASUREMENTS];
}
}
if (ENABLE_GETDEVICES_STATS) {
sSingletonDefaultAdapter.mMethodCallCounter = new int[NB_MEASUREMENTS];
sSingletonDefaultAdapter.mMethodTimeNs = new long[NB_MEASUREMENTS];
}
}
return sSingletonDefaultAdapter;
}
private void invalidateRoutingCache() {
if (DEBUG_CACHE) {
Log.d(TAG, "---- clearing cache ----------");
}
synchronized (sDeviceCacheLock) {
if (mDevicesForAttrCache != null) {
mDevicesForAttributesCacheClearTimeMs = System.currentTimeMillis();
// Save latest cache to determine routing updates
mLastDevicesForAttr.putAll(mDevicesForAttrCache);
mDevicesForAttrCache.clear();
}
}
}
/**
* Same as {@link AudioSystem#getDevicesForAttributes(AudioAttributes)}
* @param attributes the attributes for which the routing is queried
* @return the devices that the stream with the given attributes would be routed to
*/
public @NonNull ArrayList<AudioDeviceAttributes> getDevicesForAttributes(
@NonNull AudioAttributes attributes, boolean forVolume) {
if (!ENABLE_GETDEVICES_STATS) {
return getDevicesForAttributesImpl(attributes, forVolume);
}
mMethodCallCounter[METHOD_GETDEVICESFORATTRIBUTES]++;
final long startTime = SystemClock.uptimeNanos();
final ArrayList<AudioDeviceAttributes> res = getDevicesForAttributesImpl(
attributes, forVolume);
mMethodTimeNs[METHOD_GETDEVICESFORATTRIBUTES] += SystemClock.uptimeNanos() - startTime;
return res;
}
private @NonNull ArrayList<AudioDeviceAttributes> getDevicesForAttributesImpl(
@NonNull AudioAttributes attributes, boolean forVolume) {
if (USE_CACHE_FOR_GETDEVICES) {
ArrayList<AudioDeviceAttributes> res;
final Pair<AudioAttributes, Boolean> key = new Pair(attributes, forVolume);
synchronized (sDeviceCacheLock) {
res = mDevicesForAttrCache.get(key);
if (res == null) {
res = AudioSystem.getDevicesForAttributes(attributes, forVolume);
mDevicesForAttrCache.put(key, res);
if (DEBUG_CACHE) {
Log.d(TAG, mMethodNames[METHOD_GETDEVICESFORATTRIBUTES]
+ attrDeviceToDebugString(attributes, res));
}
return res;
}
// cache hit
mMethodCacheHit[METHOD_GETDEVICESFORATTRIBUTES]++;
if (DEBUG_CACHE) {
final ArrayList<AudioDeviceAttributes> real =
AudioSystem.getDevicesForAttributes(attributes, forVolume);
if (res.equals(real)) {
Log.d(TAG, mMethodNames[METHOD_GETDEVICESFORATTRIBUTES]
+ attrDeviceToDebugString(attributes, res) + " CACHE");
} else {
Log.e(TAG, mMethodNames[METHOD_GETDEVICESFORATTRIBUTES]
+ attrDeviceToDebugString(attributes, res)
+ " CACHE ERROR real:" + attrDeviceToDebugString(attributes, real));
}
}
}
return res;
}
// not using cache
return AudioSystem.getDevicesForAttributes(attributes, forVolume);
}
private static String attrDeviceToDebugString(@NonNull AudioAttributes attr,
@NonNull List<AudioDeviceAttributes> devices) {
return " attrUsage=" + attr.getSystemUsage() + " "
+ AudioSystem.deviceSetToString(AudioSystem.generateAudioDeviceTypesSet(devices));
}
/**
* Same as {@link AudioSystem#setDeviceConnectionState(AudioDeviceAttributes, int, int)}
* @param attributes
* @param state
* @param codecFormat
* @return
*/
public int setDeviceConnectionState(AudioDeviceAttributes attributes, int state,
int codecFormat) {
invalidateRoutingCache();
return AudioSystem.setDeviceConnectionState(attributes, state, codecFormat);
}
/**
* Same as {@link AudioSystem#getDeviceConnectionState(int, String)}
* @param device
* @param deviceAddress
* @return
*/
public int getDeviceConnectionState(int device, String deviceAddress) {
return AudioSystem.getDeviceConnectionState(device, deviceAddress);
}
/**
* Same as {@link AudioSystem#handleDeviceConfigChange(int, String, String, int)}
* @param device
* @param deviceAddress
* @param deviceName
* @param codecFormat
* @return
*/
public int handleDeviceConfigChange(int device, String deviceAddress,
String deviceName, int codecFormat) {
invalidateRoutingCache();
return AudioSystem.handleDeviceConfigChange(device, deviceAddress, deviceName,
codecFormat);
}
/**
* Same as {@link AudioSystem#setDevicesRoleForStrategy(int, int, List)}
* @param strategy
* @param role
* @param devices
* @return
*/
public int setDevicesRoleForStrategy(int strategy, int role,
@NonNull List<AudioDeviceAttributes> devices) {
invalidateRoutingCache();
return AudioSystem.setDevicesRoleForStrategy(strategy, role, devices);
}
/**
* Same as {@link AudioSystem#removeDevicesRoleForStrategy(int, int, List)}
* @param strategy
* @param role
* @param devices
* @return
*/
public int removeDevicesRoleForStrategy(int strategy, int role,
@NonNull List<AudioDeviceAttributes> devices) {
invalidateRoutingCache();
return AudioSystem.removeDevicesRoleForStrategy(strategy, role, devices);
}
/**
* Same as {@link AudioSystem#clearDevicesRoleForStrategy(int, int)}
* @param strategy
* @param role
* @return
*/
public int clearDevicesRoleForStrategy(int strategy, int role) {
invalidateRoutingCache();
return AudioSystem.clearDevicesRoleForStrategy(strategy, role);
}
/**
* Same as (@link AudioSystem#setDevicesRoleForCapturePreset(int, List))
* @param capturePreset
* @param role
* @param devices
* @return
*/
public int setDevicesRoleForCapturePreset(int capturePreset, int role,
@NonNull List<AudioDeviceAttributes> devices) {
invalidateRoutingCache();
return AudioSystem.setDevicesRoleForCapturePreset(capturePreset, role, devices);
}
/**
* Same as {@link AudioSystem#removeDevicesRoleForCapturePreset(int, int, List)}
* @param capturePreset
* @param role
* @param devicesToRemove
* @return
*/
public int removeDevicesRoleForCapturePreset(
int capturePreset, int role, @NonNull List<AudioDeviceAttributes> devicesToRemove) {
invalidateRoutingCache();
return AudioSystem.removeDevicesRoleForCapturePreset(capturePreset, role, devicesToRemove);
}
/**
* Same as {@link AudioSystem#addDevicesRoleForCapturePreset(int, int, List)}
* @param capturePreset the capture preset to configure
* @param role the role of the devices
* @param devices the list of devices to be added as role for the given capture preset
* @return {@link #SUCCESS} if successfully added
*/
public int addDevicesRoleForCapturePreset(
int capturePreset, int role, @NonNull List<AudioDeviceAttributes> devices) {
invalidateRoutingCache();
return AudioSystem.addDevicesRoleForCapturePreset(capturePreset, role, devices);
}
/**
* Same as {@link AudioSystem#}
* @param capturePreset
* @param role
* @return
*/
public int clearDevicesRoleForCapturePreset(int capturePreset, int role) {
invalidateRoutingCache();
return AudioSystem.clearDevicesRoleForCapturePreset(capturePreset, role);
}
/**
* Same as {@link AudioSystem#setParameters(String)}
* @param keyValuePairs
* @return
*/
public int setParameters(String keyValuePairs) {
invalidateRoutingCache();
return AudioSystem.setParameters(keyValuePairs);
}
/**
* Same as {@link AudioSystem#isMicrophoneMuted()}}
* Checks whether the microphone mute is on or off.
* @return true if microphone is muted, false if it's not
*/
public boolean isMicrophoneMuted() {
return AudioSystem.isMicrophoneMuted();
}
/**
* Same as {@link AudioSystem#muteMicrophone(boolean)}
* Sets the microphone mute on or off.
*
* @param on set <var>true</var> to mute the microphone;
* <var>false</var> to turn mute off
* @return command completion status see AUDIO_STATUS_OK, see AUDIO_STATUS_ERROR
*/
public int muteMicrophone(boolean on) {
return AudioSystem.muteMicrophone(on);
}
/**
* Same as {@link AudioSystem#setCurrentImeUid(int)}
* Communicate UID of current InputMethodService to audio policy service.
*/
public int setCurrentImeUid(int uid) {
return AudioSystem.setCurrentImeUid(uid);
}
/**
* Same as {@link AudioSystem#isStreamActive(int, int)}
*/
public boolean isStreamActive(int stream, int inPastMs) {
return AudioSystem.isStreamActive(stream, inPastMs);
}
/**
* Same as {@link AudioSystem#isStreamActiveRemotely(int, int)}
* @param stream
* @param inPastMs
* @return
*/
public boolean isStreamActiveRemotely(int stream, int inPastMs) {
return AudioSystem.isStreamActiveRemotely(stream, inPastMs);
}
/**
* Same as {@link AudioSystem#setStreamVolumeIndexAS(int, int, int)}
* @param stream
* @param index
* @param device
* @return
*/
public int setStreamVolumeIndexAS(int stream, int index, int device) {
return AudioSystem.setStreamVolumeIndexAS(stream, index, device);
}
/**
* Same as {@link AudioSystem#setPhoneState(int, int)}
* @param state
* @param uid
* @return
*/
public int setPhoneState(int state, int uid) {
invalidateRoutingCache();
return AudioSystem.setPhoneState(state, uid);
}
/**
* Same as {@link AudioSystem#setAllowedCapturePolicy(int, int)}
* @param uid
* @param flags
* @return
*/
public int setAllowedCapturePolicy(int uid, int flags) {
return AudioSystem.setAllowedCapturePolicy(uid, flags);
}
/**
* Same as {@link AudioSystem#setForceUse(int, int)}
* @param usage
* @param config
* @return
*/
public int setForceUse(int usage, int config) {
invalidateRoutingCache();
return AudioSystem.setForceUse(usage, config);
}
/**
* Same as {@link AudioSystem#getForceUse(int)}
* @param usage
* @return
*/
public int getForceUse(int usage) {
return AudioSystem.getForceUse(usage);
}
/**
* Same as {@link AudioSystem#registerPolicyMixes(ArrayList, boolean)}
* @param mixes
* @param register
* @return
*/
public int registerPolicyMixes(ArrayList<AudioMix> mixes, boolean register) {
invalidateRoutingCache();
return AudioSystem.registerPolicyMixes(mixes, register);
}
/**
* Same as {@link AudioSystem#setUidDeviceAffinities(int, int[], String[])}
* @param uid
* @param types
* @param addresses
* @return
*/
public int setUidDeviceAffinities(int uid, @NonNull int[] types, @NonNull String[] addresses) {
invalidateRoutingCache();
return AudioSystem.setUidDeviceAffinities(uid, types, addresses);
}
/**
* Same as {@link AudioSystem#removeUidDeviceAffinities(int)}
* @param uid
* @return
*/
public int removeUidDeviceAffinities(int uid) {
invalidateRoutingCache();
return AudioSystem.removeUidDeviceAffinities(uid);
}
/**
* Same as {@link AudioSystem#setUserIdDeviceAffinities(int, int[], String[])}
* @param userId
* @param types
* @param addresses
* @return
*/
public int setUserIdDeviceAffinities(int userId, @NonNull int[] types,
@NonNull String[] addresses) {
invalidateRoutingCache();
return AudioSystem.setUserIdDeviceAffinities(userId, types, addresses);
}
/**
* Same as {@link AudioSystem#removeUserIdDeviceAffinities(int)}
* @param userId
* @return
*/
public int removeUserIdDeviceAffinities(int userId) {
invalidateRoutingCache();
return AudioSystem.removeUserIdDeviceAffinities(userId);
}
/**
* Same as {@link AudioSystem#getSoundDoseInterface(ISoundDoseCallback)}
* @param callback
* @return
*/
public ISoundDose getSoundDoseInterface(ISoundDoseCallback callback) {
return AudioSystem.getSoundDoseInterface(callback);
}
/**
* Same as
* {@link AudioSystem#setPreferredMixerAttributes(
* AudioAttributes, int, int, AudioMixerAttributes)}
* @param attributes
* @param mixerAttributes
* @param uid
* @param portId
* @return
*/
public int setPreferredMixerAttributes(
@NonNull AudioAttributes attributes,
int portId,
int uid,
@NonNull AudioMixerAttributes mixerAttributes) {
return AudioSystem.setPreferredMixerAttributes(attributes, portId, uid, mixerAttributes);
}
/**
* Same as {@link AudioSystem#clearPreferredMixerAttributes(AudioAttributes, int, int)}
* @param attributes
* @param uid
* @param portId
* @return
*/
public int clearPreferredMixerAttributes(
@NonNull AudioAttributes attributes, int portId, int uid) {
return AudioSystem.clearPreferredMixerAttributes(attributes, portId, uid);
}
/**
* Part of AudioService dump
* @param pw
*/
public void dump(PrintWriter pw) {
pw.println("\nAudioSystemAdapter:");
final DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("MM-dd HH:mm:ss:SSS")
.withLocale(Locale.US)
.withZone(ZoneId.systemDefault());
synchronized (sDeviceCacheLock) {
pw.println(" last cache clear time: " + formatter.format(
Instant.ofEpochMilli(mDevicesForAttributesCacheClearTimeMs)));
pw.println(" mDevicesForAttrCache:");
if (mDevicesForAttrCache != null) {
for (Map.Entry<Pair<AudioAttributes, Boolean>, ArrayList<AudioDeviceAttributes>>
entry : mDevicesForAttrCache.entrySet()) {
final AudioAttributes attributes = entry.getKey().first;
try {
final int stream = attributes.getVolumeControlStream();
pw.println("\t" + attributes + " forVolume: " + entry.getKey().second
+ " stream: "
+ AudioSystem.STREAM_NAMES[stream] + "(" + stream + ")");
for (AudioDeviceAttributes devAttr : entry.getValue()) {
pw.println("\t\t" + devAttr);
}
} catch (IllegalArgumentException e) {
// dump could fail if attributes do not map to a stream.
pw.println("\t dump failed for attributes: " + attributes);
Log.e(TAG, "dump failed", e);
}
}
}
}
if (!ENABLE_GETDEVICES_STATS) {
// only stats in the rest of this dump
return;
}
for (int i = 0; i < NB_MEASUREMENTS; i++) {
pw.println(mMethodNames[i]
+ ": counter=" + mMethodCallCounter[i]
+ " time(ms)=" + (mMethodTimeNs[i] / 1E6)
+ (USE_CACHE_FOR_GETDEVICES
? (" FScacheHit=" + mMethodCacheHit[METHOD_GETDEVICESFORATTRIBUTES])
: ""));
}
pw.println("\n");
}
}