/*
|
* Copyright (C) 2016 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.content.Context;
|
import android.content.pm.PackageManager;
|
import android.media.AudioFormat;
|
import android.media.AudioManager;
|
import android.media.AudioRecordingConfiguration;
|
import android.media.AudioSystem;
|
import android.media.IRecordingConfigDispatcher;
|
import android.media.MediaRecorder;
|
import android.media.audiofx.AudioEffect;
|
import android.os.IBinder;
|
import android.os.RemoteException;
|
import android.util.Log;
|
|
import java.io.PrintWriter;
|
import java.text.DateFormat;
|
import java.util.ArrayList;
|
import java.util.Date;
|
import java.util.Iterator;
|
import java.util.List;
|
|
/**
|
* Class to receive and dispatch updates from AudioSystem about recording configurations.
|
*/
|
public final class RecordingActivityMonitor implements AudioSystem.AudioRecordingCallback {
|
|
public final static String TAG = "AudioService.RecordingActivityMonitor";
|
|
private ArrayList<RecMonitorClient> mClients = new ArrayList<RecMonitorClient>();
|
// a public client is one that needs an anonymized version of the playback configurations, we
|
// keep track of whether there is at least one to know when we need to create the list of
|
// playback configurations that do not contain uid/package name information.
|
private boolean mHasPublicClients = false;
|
|
static final class RecordingState {
|
private final int mRiid;
|
private final RecorderDeathHandler mDeathHandler;
|
private boolean mIsActive;
|
private AudioRecordingConfiguration mConfig;
|
|
RecordingState(int riid, RecorderDeathHandler handler) {
|
mRiid = riid;
|
mDeathHandler = handler;
|
}
|
|
RecordingState(AudioRecordingConfiguration config) {
|
mRiid = AudioManager.RECORD_RIID_INVALID;
|
mDeathHandler = null;
|
mConfig = config;
|
}
|
|
int getRiid() {
|
return mRiid;
|
}
|
|
int getPortId() {
|
return mConfig != null ? mConfig.getClientPortId() : -1;
|
}
|
|
AudioRecordingConfiguration getConfig() {
|
return mConfig;
|
}
|
|
boolean hasDeathHandler() {
|
return mDeathHandler != null;
|
}
|
|
boolean isActiveConfiguration() {
|
return mIsActive && mConfig != null;
|
}
|
|
// returns true if status of an active recording has changed
|
boolean setActive(boolean active) {
|
if (mIsActive == active) return false;
|
mIsActive = active;
|
return mConfig != null;
|
}
|
|
// returns true if an active recording has been updated
|
boolean setConfig(AudioRecordingConfiguration config) {
|
if (config.equals(mConfig)) return false;
|
mConfig = config;
|
return mIsActive;
|
}
|
|
void dump(PrintWriter pw) {
|
pw.println("riid " + mRiid + "; active? " + mIsActive);
|
if (mConfig != null) {
|
mConfig.dump(pw);
|
} else {
|
pw.println(" no config");
|
}
|
}
|
}
|
private List<RecordingState> mRecordStates = new ArrayList<RecordingState>();
|
|
private final PackageManager mPackMan;
|
|
RecordingActivityMonitor(Context ctxt) {
|
RecMonitorClient.sMonitor = this;
|
RecorderDeathHandler.sMonitor = this;
|
mPackMan = ctxt.getPackageManager();
|
}
|
|
/**
|
* Implementation of android.media.AudioSystem.AudioRecordingCallback
|
*/
|
public void onRecordingConfigurationChanged(int event, int riid, int uid, int session,
|
int source, int portId, boolean silenced,
|
int[] recordingInfo,
|
AudioEffect.Descriptor[] clientEffects,
|
AudioEffect.Descriptor[] effects,
|
int activeSource, String packName) {
|
final AudioRecordingConfiguration config = createRecordingConfiguration(
|
uid, session, source, recordingInfo,
|
portId, silenced, activeSource, clientEffects, effects);
|
if (MediaRecorder.isSystemOnlyAudioSource(source)) {
|
// still want to log event, it just won't appear in recording configurations;
|
sEventLogger.log(new RecordingEvent(event, riid, config).printLog(TAG));
|
return;
|
}
|
dispatchCallbacks(updateSnapshot(event, riid, config));
|
}
|
|
/**
|
* Track a recorder provided by the client
|
*/
|
public int trackRecorder(IBinder recorder) {
|
if (recorder == null) {
|
Log.e(TAG, "trackRecorder called with null token");
|
return AudioManager.RECORD_RIID_INVALID;
|
}
|
final int newRiid = AudioSystem.newAudioRecorderId();
|
RecorderDeathHandler handler = new RecorderDeathHandler(newRiid, recorder);
|
if (!handler.init()) {
|
// probably means that the AudioRecord has already died
|
return AudioManager.RECORD_RIID_INVALID;
|
}
|
synchronized (mRecordStates) {
|
mRecordStates.add(new RecordingState(newRiid, handler));
|
}
|
// a newly added record is inactive, no change in active configs is possible.
|
return newRiid;
|
}
|
|
/**
|
* Receive an event from the client about a tracked recorder
|
*/
|
public void recorderEvent(int riid, int event) {
|
int configEvent = event == AudioManager.RECORDER_STATE_STARTED
|
? AudioManager.RECORD_CONFIG_EVENT_START :
|
event == AudioManager.RECORDER_STATE_STOPPED
|
? AudioManager.RECORD_CONFIG_EVENT_STOP : AudioManager.RECORD_CONFIG_EVENT_NONE;
|
if (riid == AudioManager.RECORD_RIID_INVALID
|
|| configEvent == AudioManager.RECORD_CONFIG_EVENT_NONE) {
|
sEventLogger.log(new RecordingEvent(event, riid, null).printLog(TAG));
|
return;
|
}
|
dispatchCallbacks(updateSnapshot(configEvent, riid, null));
|
}
|
|
/**
|
* Stop tracking the recorder
|
*/
|
public void releaseRecorder(int riid) {
|
dispatchCallbacks(updateSnapshot(AudioManager.RECORD_CONFIG_EVENT_RELEASE, riid, null));
|
}
|
|
private void dispatchCallbacks(List<AudioRecordingConfiguration> configs) {
|
if (configs == null) { // null means "no changes"
|
return;
|
}
|
synchronized (mClients) {
|
// list of recording configurations for "public consumption". It is only computed if
|
// there are non-system recording activity listeners.
|
final List<AudioRecordingConfiguration> configsPublic = mHasPublicClients
|
? anonymizeForPublicConsumption(configs) :
|
new ArrayList<AudioRecordingConfiguration>();
|
for (RecMonitorClient rmc : mClients) {
|
try {
|
if (rmc.mIsPrivileged) {
|
rmc.mDispatcherCb.dispatchRecordingConfigChange(configs);
|
} else {
|
rmc.mDispatcherCb.dispatchRecordingConfigChange(configsPublic);
|
}
|
} catch (RemoteException e) {
|
Log.w(TAG, "Could not call dispatchRecordingConfigChange() on client", e);
|
}
|
}
|
}
|
}
|
|
protected void dump(PrintWriter pw) {
|
// recorders
|
pw.println("\nRecordActivityMonitor dump time: "
|
+ DateFormat.getTimeInstance().format(new Date()));
|
synchronized (mRecordStates) {
|
for (RecordingState state : mRecordStates) {
|
state.dump(pw);
|
}
|
}
|
pw.println("\n");
|
// log
|
sEventLogger.dump(pw);
|
}
|
|
private static ArrayList<AudioRecordingConfiguration> anonymizeForPublicConsumption(
|
List<AudioRecordingConfiguration> sysConfigs) {
|
ArrayList<AudioRecordingConfiguration> publicConfigs =
|
new ArrayList<AudioRecordingConfiguration>();
|
// only add active anonymized configurations,
|
for (AudioRecordingConfiguration config : sysConfigs) {
|
publicConfigs.add(AudioRecordingConfiguration.anonymizedCopy(config));
|
}
|
return publicConfigs;
|
}
|
|
void initMonitor() {
|
AudioSystem.setRecordingCallback(this);
|
}
|
|
void onAudioServerDied() {
|
// Remove all RecordingState entries that do not have a death handler (that means
|
// they are tracked by the Audio Server). If there were active entries among removed,
|
// dispatch active configuration changes.
|
List<AudioRecordingConfiguration> configs = null;
|
synchronized (mRecordStates) {
|
boolean configChanged = false;
|
for (Iterator<RecordingState> it = mRecordStates.iterator(); it.hasNext(); ) {
|
RecordingState state = it.next();
|
if (!state.hasDeathHandler()) {
|
if (state.isActiveConfiguration()) {
|
configChanged = true;
|
sEventLogger.log(new RecordingEvent(
|
AudioManager.RECORD_CONFIG_EVENT_RELEASE,
|
state.getRiid(), state.getConfig()));
|
}
|
it.remove();
|
}
|
}
|
if (configChanged) {
|
configs = getActiveRecordingConfigurations(true /*isPrivileged*/);
|
}
|
}
|
dispatchCallbacks(configs);
|
}
|
|
void registerRecordingCallback(IRecordingConfigDispatcher rcdb, boolean isPrivileged) {
|
if (rcdb == null) {
|
return;
|
}
|
synchronized (mClients) {
|
final RecMonitorClient rmc = new RecMonitorClient(rcdb, isPrivileged);
|
if (rmc.init()) {
|
if (!isPrivileged) {
|
mHasPublicClients = true;
|
}
|
mClients.add(rmc);
|
}
|
}
|
}
|
|
void unregisterRecordingCallback(IRecordingConfigDispatcher rcdb) {
|
if (rcdb == null) {
|
return;
|
}
|
synchronized (mClients) {
|
final Iterator<RecMonitorClient> clientIterator = mClients.iterator();
|
boolean hasPublicClients = false;
|
while (clientIterator.hasNext()) {
|
RecMonitorClient rmc = clientIterator.next();
|
if (rcdb.equals(rmc.mDispatcherCb)) {
|
rmc.release();
|
clientIterator.remove();
|
} else {
|
if (!rmc.mIsPrivileged) {
|
hasPublicClients = true;
|
}
|
}
|
}
|
mHasPublicClients = hasPublicClients;
|
}
|
}
|
|
List<AudioRecordingConfiguration> getActiveRecordingConfigurations(boolean isPrivileged) {
|
List<AudioRecordingConfiguration> configs = new ArrayList<AudioRecordingConfiguration>();
|
synchronized (mRecordStates) {
|
for (RecordingState state : mRecordStates) {
|
if (state.isActiveConfiguration()) {
|
configs.add(state.getConfig());
|
}
|
}
|
}
|
// AudioRecordingConfiguration objects never get updated. If config changes,
|
// the reference to the config is set in RecordingState.
|
if (!isPrivileged) {
|
configs = anonymizeForPublicConsumption(configs);
|
}
|
return configs;
|
}
|
|
/**
|
* Create a recording configuration from the provided parameters
|
* @param uid
|
* @param session
|
* @param source
|
* @param recordingFormat see
|
* {@link AudioSystem.AudioRecordingCallback#onRecordingConfigurationChanged(int, int, int,\
|
int, int, boolean, int[], AudioEffect.Descriptor[], AudioEffect.Descriptor[], int, String)}
|
* for the definition of the contents of the array
|
* @param portId
|
* @param silenced
|
* @param activeSource
|
* @param clientEffects
|
* @param effects
|
* @return null a configuration object.
|
*/
|
private AudioRecordingConfiguration createRecordingConfiguration(int uid,
|
int session, int source, int[] recordingInfo, int portId, boolean silenced,
|
int activeSource, AudioEffect.Descriptor[] clientEffects,
|
AudioEffect.Descriptor[] effects) {
|
final AudioFormat clientFormat = new AudioFormat.Builder()
|
.setEncoding(recordingInfo[0])
|
// FIXME this doesn't support index-based masks
|
.setChannelMask(recordingInfo[1])
|
.setSampleRate(recordingInfo[2])
|
.build();
|
final AudioFormat deviceFormat = new AudioFormat.Builder()
|
.setEncoding(recordingInfo[3])
|
// FIXME this doesn't support index-based masks
|
.setChannelMask(recordingInfo[4])
|
.setSampleRate(recordingInfo[5])
|
.build();
|
final int patchHandle = recordingInfo[6];
|
final String[] packages = mPackMan.getPackagesForUid(uid);
|
final String packageName;
|
if (packages != null && packages.length > 0) {
|
packageName = packages[0];
|
} else {
|
packageName = "";
|
}
|
return new AudioRecordingConfiguration(uid, session, source,
|
clientFormat, deviceFormat, patchHandle, packageName,
|
portId, silenced, activeSource, clientEffects, effects);
|
}
|
|
/**
|
* Update the internal "view" of the active recording sessions
|
* @param event RECORD_CONFIG_EVENT_...
|
* @param riid
|
* @param config
|
* @return null if the list of active recording sessions has not been modified, a list
|
* with the current active configurations otherwise.
|
*/
|
private List<AudioRecordingConfiguration> updateSnapshot(
|
int event, int riid, AudioRecordingConfiguration config) {
|
List<AudioRecordingConfiguration> configs = null;
|
synchronized (mRecordStates) {
|
int stateIndex = -1;
|
if (riid != AudioManager.RECORD_RIID_INVALID) {
|
stateIndex = findStateByRiid(riid);
|
} else if (config != null) {
|
stateIndex = findStateByPortId(config.getClientPortId());
|
}
|
if (stateIndex == -1) {
|
if (event == AudioManager.RECORD_CONFIG_EVENT_START && config != null) {
|
// First time registration for a recorder tracked by AudioServer.
|
mRecordStates.add(new RecordingState(config));
|
stateIndex = mRecordStates.size() - 1;
|
} else {
|
if (config == null) {
|
// Records tracked by clients must be registered first via trackRecorder.
|
Log.e(TAG, String.format(
|
"Unexpected event %d for riid %d", event, riid));
|
}
|
return configs;
|
}
|
}
|
final RecordingState state = mRecordStates.get(stateIndex);
|
|
boolean configChanged;
|
switch (event) {
|
case AudioManager.RECORD_CONFIG_EVENT_START:
|
configChanged = state.setActive(true);
|
if (config != null) {
|
configChanged = state.setConfig(config) || configChanged;
|
}
|
break;
|
case AudioManager.RECORD_CONFIG_EVENT_UPDATE:
|
// For this event config != null
|
configChanged = state.setConfig(config);
|
break;
|
case AudioManager.RECORD_CONFIG_EVENT_STOP:
|
configChanged = state.setActive(false);
|
if (!state.hasDeathHandler()) {
|
// A recorder tracked by AudioServer has to be removed now so it
|
// does not leak. It will be re-registered if recording starts again.
|
mRecordStates.remove(stateIndex);
|
}
|
break;
|
case AudioManager.RECORD_CONFIG_EVENT_RELEASE:
|
configChanged = state.isActiveConfiguration();
|
mRecordStates.remove(stateIndex);
|
break;
|
default:
|
Log.e(TAG, String.format("Unknown event %d for riid %d / portid %d",
|
event, riid, state.getPortId()));
|
configChanged = false;
|
}
|
if (configChanged) {
|
sEventLogger.log(new RecordingEvent(event, riid, state.getConfig()));
|
configs = getActiveRecordingConfigurations(true /*isPrivileged*/);
|
}
|
}
|
return configs;
|
}
|
|
// riid is assumed to be valid
|
private int findStateByRiid(int riid) {
|
synchronized (mRecordStates) {
|
for (int i = 0; i < mRecordStates.size(); i++) {
|
if (mRecordStates.get(i).getRiid() == riid) {
|
return i;
|
}
|
}
|
}
|
return -1;
|
}
|
|
private int findStateByPortId(int portId) {
|
// Lookup by portId is unambiguous only for recordings managed by the Audio Server.
|
synchronized (mRecordStates) {
|
for (int i = 0; i < mRecordStates.size(); i++) {
|
if (!mRecordStates.get(i).hasDeathHandler()
|
&& mRecordStates.get(i).getPortId() == portId) {
|
return i;
|
}
|
}
|
}
|
return -1;
|
}
|
|
/**
|
* Inner class to track clients that want to be notified of recording updates
|
*/
|
private final static class RecMonitorClient implements IBinder.DeathRecipient {
|
|
// can afford to be static because only one RecordingActivityMonitor ever instantiated
|
static RecordingActivityMonitor sMonitor;
|
|
final IRecordingConfigDispatcher mDispatcherCb;
|
final boolean mIsPrivileged;
|
|
RecMonitorClient(IRecordingConfigDispatcher rcdb, boolean isPrivileged) {
|
mDispatcherCb = rcdb;
|
mIsPrivileged = isPrivileged;
|
}
|
|
public void binderDied() {
|
Log.w(TAG, "client died");
|
sMonitor.unregisterRecordingCallback(mDispatcherCb);
|
}
|
|
boolean init() {
|
try {
|
mDispatcherCb.asBinder().linkToDeath(this, 0);
|
return true;
|
} catch (RemoteException e) {
|
Log.w(TAG, "Could not link to client death", e);
|
return false;
|
}
|
}
|
|
void release() {
|
mDispatcherCb.asBinder().unlinkToDeath(this, 0);
|
}
|
}
|
|
private static final class RecorderDeathHandler implements IBinder.DeathRecipient {
|
|
// can afford to be static because only one RecordingActivityMonitor ever instantiated
|
static RecordingActivityMonitor sMonitor;
|
|
final int mRiid;
|
private final IBinder mRecorderToken;
|
|
RecorderDeathHandler(int riid, IBinder recorderToken) {
|
mRiid = riid;
|
mRecorderToken = recorderToken;
|
}
|
|
public void binderDied() {
|
sMonitor.releaseRecorder(mRiid);
|
}
|
|
boolean init() {
|
try {
|
mRecorderToken.linkToDeath(this, 0);
|
return true;
|
} catch (RemoteException e) {
|
Log.w(TAG, "Could not link to recorder death", e);
|
return false;
|
}
|
}
|
}
|
|
/**
|
* Inner class for recording event logging
|
*/
|
private static final class RecordingEvent extends AudioEventLogger.Event {
|
private final int mRecEvent;
|
private final int mRIId;
|
private final int mClientUid;
|
private final int mSession;
|
private final int mSource;
|
private final String mPackName;
|
|
RecordingEvent(int event, int riid, AudioRecordingConfiguration config) {
|
mRecEvent = event;
|
mRIId = riid;
|
if (config != null) {
|
mClientUid = config.getClientUid();
|
mSession = config.getClientAudioSessionId();
|
mSource = config.getClientAudioSource();
|
mPackName = config.getClientPackageName();
|
} else {
|
mClientUid = -1;
|
mSession = -1;
|
mSource = -1;
|
mPackName = null;
|
}
|
}
|
|
private static String recordEventToString(int recEvent) {
|
switch (recEvent) {
|
case AudioManager.RECORD_CONFIG_EVENT_START:
|
return "start";
|
case AudioManager.RECORD_CONFIG_EVENT_UPDATE:
|
return "update";
|
case AudioManager.RECORD_CONFIG_EVENT_STOP:
|
return "stop";
|
case AudioManager.RECORD_CONFIG_EVENT_RELEASE:
|
return "release";
|
default:
|
return "unknown (" + recEvent + ")";
|
}
|
}
|
|
@Override
|
public String eventToString() {
|
return new StringBuilder("rec ").append(recordEventToString(mRecEvent))
|
.append(" riid:").append(mRIId)
|
.append(" uid:").append(mClientUid)
|
.append(" session:").append(mSession)
|
.append(" src:").append(MediaRecorder.toLogFriendlyAudioSource(mSource))
|
.append(mPackName == null ? "" : " pack:" + mPackName).toString();
|
}
|
}
|
|
private static final AudioEventLogger sEventLogger = new AudioEventLogger(50,
|
"recording activity received by AudioService");
|
}
|