/* * 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 mClients = new ArrayList(); // 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 mRecordConfigs = new HashMap(); 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 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 configsPublic = mHasPublicClients ? anonymizeForPublicConsumption(configsSystem) : new ArrayList(); final Iterator 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); } } pw.println("\n"); // log sEventLogger.dump(pw); } private ArrayList anonymizeForPublicConsumption( List sysConfigs) { ArrayList publicConfigs = new ArrayList(); // 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 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 getActiveRecordingConfigurations(boolean isPrivileged) { synchronized(mRecordConfigs) { if (isPrivileged) { return new ArrayList(mRecordConfigs.values()); } else { final List configsPublic = anonymizeForPublicConsumption( new ArrayList(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 updateSnapshot(int event, int uid, int session, int source, int[] recordingInfo) { final boolean configChanged; final ArrayList 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); if (configChanged) { sEventLogger.log(new RecordingEvent(event, uid, session, source, 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; } if (configChanged) { sEventLogger.log(new RecordingEvent(event, uid, session, source, packageName)); } 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(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); } } /** * Inner class for recording event logging */ private static final class RecordingEvent extends AudioEventLogger.Event { private final int mRecEvent; private final int mClientUid; private final int mSession; private final int mSource; private final String mPackName; RecordingEvent(int event, int uid, int session, int source, String packName) { mRecEvent = event; mClientUid = uid; mSession = session; mSource = source; mPackName = packName; } @Override public String eventToString() { return new StringBuilder("rec ").append( mRecEvent == AudioManager.RECORD_CONFIG_EVENT_START ? "start" : "stop ") .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 as reported through AudioSystem.AudioRecordingCallback"); }