blob: 34309b62d2f7fbcc2467b8866fd4a2abf50076a7 [file] [log] [blame]
/*
* 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.AudioPlaybackConfiguration;
import android.media.AudioRecordingConfiguration;
import android.media.AudioSystem;
import android.media.IRecordingConfigDispatcher;
import android.media.MediaRecorder;
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.HashMap;
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;
private HashMap<Integer, AudioRecordingConfiguration> mRecordConfigs =
new HashMap<Integer, AudioRecordingConfiguration>();
private final PackageManager mPackMan;
RecordingActivityMonitor(Context ctxt) {
RecMonitorClient.sMonitor = this;
mPackMan = ctxt.getPackageManager();
}
/**
* Implementation of android.media.AudioSystem.AudioRecordingCallback
*/
public void onRecordingConfigurationChanged(int event, int uid, int session, int source,
int[] recordingInfo, String packName) {
if (MediaRecorder.isSystemOnlyAudioSource(source)) {
return;
}
final List<AudioRecordingConfiguration> configsSystem =
updateSnapshot(event, uid, session, source, recordingInfo);
if (configsSystem != null){
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(configsSystem) :
new ArrayList<AudioRecordingConfiguration>();
final Iterator<RecMonitorClient> clientIterator = mClients.iterator();
while (clientIterator.hasNext()) {
final RecMonitorClient rmc = clientIterator.next();
try {
if (rmc.mIsPrivileged) {
rmc.mDispatcherCb.dispatchRecordingConfigChange(configsSystem);
} else {
rmc.mDispatcherCb.dispatchRecordingConfigChange(configsPublic);
}
} catch (RemoteException e) {
Log.w(TAG, "Could not call dispatchRecordingConfigChange() on client", e);
}
}
}
}
}
protected void dump(PrintWriter pw) {
// players
pw.println("\nRecordActivityMonitor dump time: "
+ DateFormat.getTimeInstance().format(new Date()));
synchronized(mRecordConfigs) {
for (AudioRecordingConfiguration conf : mRecordConfigs.values()) {
conf.dump(pw);
}
}
}
private 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 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) {
synchronized(mRecordConfigs) {
if (isPrivileged) {
return new ArrayList<AudioRecordingConfiguration>(mRecordConfigs.values());
} else {
final List<AudioRecordingConfiguration> configsPublic =
anonymizeForPublicConsumption(
new ArrayList<AudioRecordingConfiguration>(mRecordConfigs.values()));
return configsPublic;
}
}
}
/**
* Update the internal "view" of the active recording sessions
* @param event
* @param session
* @param source
* @param recordingFormat see
* {@link AudioSystem.AudioRecordingCallback#onRecordingConfigurationChanged(int, int, int, int[])}
* for the definition of the contents of the array
* @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 uid, int session,
int source, int[] recordingInfo) {
final boolean configChanged;
final ArrayList<AudioRecordingConfiguration> configs;
synchronized(mRecordConfigs) {
switch (event) {
case AudioManager.RECORD_CONFIG_EVENT_STOP:
// return failure if an unknown recording session stopped
configChanged = (mRecordConfigs.remove(new Integer(session)) != null);
break;
case AudioManager.RECORD_CONFIG_EVENT_START:
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 Integer sessionKey = new Integer(session);
final String[] packages = mPackMan.getPackagesForUid(uid);
final String packageName;
if (packages != null && packages.length > 0) {
packageName = packages[0];
} else {
packageName = "";
}
final AudioRecordingConfiguration updatedConfig =
new AudioRecordingConfiguration(uid, session, source,
clientFormat, deviceFormat, patchHandle, packageName);
if (mRecordConfigs.containsKey(sessionKey)) {
if (updatedConfig.equals(mRecordConfigs.get(sessionKey))) {
configChanged = false;
} else {
// config exists but has been modified
mRecordConfigs.remove(sessionKey);
mRecordConfigs.put(sessionKey, updatedConfig);
configChanged = true;
}
} else {
mRecordConfigs.put(sessionKey, updatedConfig);
configChanged = true;
}
break;
default:
Log.e(TAG, String.format("Unknown event %d for session %d, source %d",
event, session, source));
configChanged = false;
}
if (configChanged) {
configs = new ArrayList<AudioRecordingConfiguration>(mRecordConfigs.values());
} else {
configs = null;
}
}
return configs;
}
/**
* 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);
}
}
}