1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.audio; 18 19 import android.content.Context; 20 import android.content.pm.PackageManager; 21 import android.media.AudioFormat; 22 import android.media.AudioManager; 23 import android.media.AudioPlaybackConfiguration; 24 import android.media.AudioRecordingConfiguration; 25 import android.media.AudioSystem; 26 import android.media.IRecordingConfigDispatcher; 27 import android.media.MediaRecorder; 28 import android.os.IBinder; 29 import android.os.RemoteException; 30 import android.util.Log; 31 32 import java.io.PrintWriter; 33 import java.text.DateFormat; 34 import java.util.ArrayList; 35 import java.util.Date; 36 import java.util.HashMap; 37 import java.util.Iterator; 38 import java.util.List; 39 40 /** 41 * Class to receive and dispatch updates from AudioSystem about recording configurations. 42 */ 43 public final class RecordingActivityMonitor implements AudioSystem.AudioRecordingCallback { 44 45 public final static String TAG = "AudioService.RecordingActivityMonitor"; 46 47 private ArrayList<RecMonitorClient> mClients = new ArrayList<RecMonitorClient>(); 48 // a public client is one that needs an anonymized version of the playback configurations, we 49 // keep track of whether there is at least one to know when we need to create the list of 50 // playback configurations that do not contain uid/package name information. 51 private boolean mHasPublicClients = false; 52 53 private HashMap<Integer, AudioRecordingConfiguration> mRecordConfigs = 54 new HashMap<Integer, AudioRecordingConfiguration>(); 55 56 private final PackageManager mPackMan; 57 RecordingActivityMonitor(Context ctxt)58 RecordingActivityMonitor(Context ctxt) { 59 RecMonitorClient.sMonitor = this; 60 mPackMan = ctxt.getPackageManager(); 61 } 62 63 /** 64 * Implementation of android.media.AudioSystem.AudioRecordingCallback 65 */ onRecordingConfigurationChanged(int event, int uid, int session, int source, int[] recordingInfo, String packName)66 public void onRecordingConfigurationChanged(int event, int uid, int session, int source, 67 int[] recordingInfo, String packName) { 68 if (MediaRecorder.isSystemOnlyAudioSource(source)) { 69 return; 70 } 71 final List<AudioRecordingConfiguration> configsSystem = 72 updateSnapshot(event, uid, session, source, recordingInfo); 73 if (configsSystem != null){ 74 synchronized (mClients) { 75 // list of recording configurations for "public consumption". It is only computed if 76 // there are non-system recording activity listeners. 77 final List<AudioRecordingConfiguration> configsPublic = mHasPublicClients ? 78 anonymizeForPublicConsumption(configsSystem) : 79 new ArrayList<AudioRecordingConfiguration>(); 80 final Iterator<RecMonitorClient> clientIterator = mClients.iterator(); 81 while (clientIterator.hasNext()) { 82 final RecMonitorClient rmc = clientIterator.next(); 83 try { 84 if (rmc.mIsPrivileged) { 85 rmc.mDispatcherCb.dispatchRecordingConfigChange(configsSystem); 86 } else { 87 rmc.mDispatcherCb.dispatchRecordingConfigChange(configsPublic); 88 } 89 } catch (RemoteException e) { 90 Log.w(TAG, "Could not call dispatchRecordingConfigChange() on client", e); 91 } 92 } 93 } 94 } 95 } 96 dump(PrintWriter pw)97 protected void dump(PrintWriter pw) { 98 // players 99 pw.println("\nRecordActivityMonitor dump time: " 100 + DateFormat.getTimeInstance().format(new Date())); 101 synchronized(mRecordConfigs) { 102 for (AudioRecordingConfiguration conf : mRecordConfigs.values()) { 103 conf.dump(pw); 104 } 105 } 106 pw.println("\n"); 107 // log 108 sEventLogger.dump(pw); 109 } 110 anonymizeForPublicConsumption( List<AudioRecordingConfiguration> sysConfigs)111 private ArrayList<AudioRecordingConfiguration> anonymizeForPublicConsumption( 112 List<AudioRecordingConfiguration> sysConfigs) { 113 ArrayList<AudioRecordingConfiguration> publicConfigs = 114 new ArrayList<AudioRecordingConfiguration>(); 115 // only add active anonymized configurations, 116 for (AudioRecordingConfiguration config : sysConfigs) { 117 publicConfigs.add(AudioRecordingConfiguration.anonymizedCopy(config)); 118 } 119 return publicConfigs; 120 } 121 initMonitor()122 void initMonitor() { 123 AudioSystem.setRecordingCallback(this); 124 } 125 registerRecordingCallback(IRecordingConfigDispatcher rcdb, boolean isPrivileged)126 void registerRecordingCallback(IRecordingConfigDispatcher rcdb, boolean isPrivileged) { 127 if (rcdb == null) { 128 return; 129 } 130 synchronized (mClients) { 131 final RecMonitorClient rmc = new RecMonitorClient(rcdb, isPrivileged); 132 if (rmc.init()) { 133 if (!isPrivileged) { 134 mHasPublicClients = true; 135 } 136 mClients.add(rmc); 137 } 138 } 139 } 140 unregisterRecordingCallback(IRecordingConfigDispatcher rcdb)141 void unregisterRecordingCallback(IRecordingConfigDispatcher rcdb) { 142 if (rcdb == null) { 143 return; 144 } 145 synchronized (mClients) { 146 final Iterator<RecMonitorClient> clientIterator = mClients.iterator(); 147 boolean hasPublicClients = false; 148 while (clientIterator.hasNext()) { 149 RecMonitorClient rmc = clientIterator.next(); 150 if (rcdb.equals(rmc.mDispatcherCb)) { 151 rmc.release(); 152 clientIterator.remove(); 153 } else { 154 if (!rmc.mIsPrivileged) { 155 hasPublicClients = true; 156 } 157 } 158 } 159 mHasPublicClients = hasPublicClients; 160 } 161 } 162 getActiveRecordingConfigurations(boolean isPrivileged)163 List<AudioRecordingConfiguration> getActiveRecordingConfigurations(boolean isPrivileged) { 164 synchronized(mRecordConfigs) { 165 if (isPrivileged) { 166 return new ArrayList<AudioRecordingConfiguration>(mRecordConfigs.values()); 167 } else { 168 final List<AudioRecordingConfiguration> configsPublic = 169 anonymizeForPublicConsumption( 170 new ArrayList<AudioRecordingConfiguration>(mRecordConfigs.values())); 171 return configsPublic; 172 } 173 } 174 } 175 176 /** 177 * Update the internal "view" of the active recording sessions 178 * @param event 179 * @param session 180 * @param source 181 * @param recordingFormat see 182 * {@link AudioSystem.AudioRecordingCallback#onRecordingConfigurationChanged(int, int, int, int[])} 183 * for the definition of the contents of the array 184 * @return null if the list of active recording sessions has not been modified, a list 185 * with the current active configurations otherwise. 186 */ updateSnapshot(int event, int uid, int session, int source, int[] recordingInfo)187 private List<AudioRecordingConfiguration> updateSnapshot(int event, int uid, int session, 188 int source, int[] recordingInfo) { 189 final boolean configChanged; 190 final ArrayList<AudioRecordingConfiguration> configs; 191 synchronized(mRecordConfigs) { 192 switch (event) { 193 case AudioManager.RECORD_CONFIG_EVENT_STOP: 194 // return failure if an unknown recording session stopped 195 configChanged = (mRecordConfigs.remove(new Integer(session)) != null); 196 if (configChanged) { 197 sEventLogger.log(new RecordingEvent(event, uid, session, source, null)); 198 } 199 break; 200 case AudioManager.RECORD_CONFIG_EVENT_START: 201 final AudioFormat clientFormat = new AudioFormat.Builder() 202 .setEncoding(recordingInfo[0]) 203 // FIXME this doesn't support index-based masks 204 .setChannelMask(recordingInfo[1]) 205 .setSampleRate(recordingInfo[2]) 206 .build(); 207 final AudioFormat deviceFormat = new AudioFormat.Builder() 208 .setEncoding(recordingInfo[3]) 209 // FIXME this doesn't support index-based masks 210 .setChannelMask(recordingInfo[4]) 211 .setSampleRate(recordingInfo[5]) 212 .build(); 213 final int patchHandle = recordingInfo[6]; 214 final Integer sessionKey = new Integer(session); 215 216 final String[] packages = mPackMan.getPackagesForUid(uid); 217 final String packageName; 218 if (packages != null && packages.length > 0) { 219 packageName = packages[0]; 220 } else { 221 packageName = ""; 222 } 223 final AudioRecordingConfiguration updatedConfig = 224 new AudioRecordingConfiguration(uid, session, source, 225 clientFormat, deviceFormat, patchHandle, packageName); 226 227 if (mRecordConfigs.containsKey(sessionKey)) { 228 if (updatedConfig.equals(mRecordConfigs.get(sessionKey))) { 229 configChanged = false; 230 } else { 231 // config exists but has been modified 232 mRecordConfigs.remove(sessionKey); 233 mRecordConfigs.put(sessionKey, updatedConfig); 234 configChanged = true; 235 } 236 } else { 237 mRecordConfigs.put(sessionKey, updatedConfig); 238 configChanged = true; 239 } 240 if (configChanged) { 241 sEventLogger.log(new RecordingEvent(event, uid, session, source, packageName)); 242 } 243 break; 244 default: 245 Log.e(TAG, String.format("Unknown event %d for session %d, source %d", 246 event, session, source)); 247 configChanged = false; 248 } 249 if (configChanged) { 250 configs = new ArrayList<AudioRecordingConfiguration>(mRecordConfigs.values()); 251 } else { 252 configs = null; 253 } 254 } 255 return configs; 256 } 257 258 /** 259 * Inner class to track clients that want to be notified of recording updates 260 */ 261 private final static class RecMonitorClient implements IBinder.DeathRecipient { 262 263 // can afford to be static because only one RecordingActivityMonitor ever instantiated 264 static RecordingActivityMonitor sMonitor; 265 266 final IRecordingConfigDispatcher mDispatcherCb; 267 final boolean mIsPrivileged; 268 RecMonitorClient(IRecordingConfigDispatcher rcdb, boolean isPrivileged)269 RecMonitorClient(IRecordingConfigDispatcher rcdb, boolean isPrivileged) { 270 mDispatcherCb = rcdb; 271 mIsPrivileged = isPrivileged; 272 } 273 binderDied()274 public void binderDied() { 275 Log.w(TAG, "client died"); 276 sMonitor.unregisterRecordingCallback(mDispatcherCb); 277 } 278 init()279 boolean init() { 280 try { 281 mDispatcherCb.asBinder().linkToDeath(this, 0); 282 return true; 283 } catch (RemoteException e) { 284 Log.w(TAG, "Could not link to client death", e); 285 return false; 286 } 287 } 288 release()289 void release() { 290 mDispatcherCb.asBinder().unlinkToDeath(this, 0); 291 } 292 } 293 294 /** 295 * Inner class for recording event logging 296 */ 297 private static final class RecordingEvent extends AudioEventLogger.Event { 298 private final int mRecEvent; 299 private final int mClientUid; 300 private final int mSession; 301 private final int mSource; 302 private final String mPackName; 303 RecordingEvent(int event, int uid, int session, int source, String packName)304 RecordingEvent(int event, int uid, int session, int source, String packName) { 305 mRecEvent = event; 306 mClientUid = uid; 307 mSession = session; 308 mSource = source; 309 mPackName = packName; 310 } 311 312 @Override eventToString()313 public String eventToString() { 314 return new StringBuilder("rec ").append( 315 mRecEvent == AudioManager.RECORD_CONFIG_EVENT_START ? "start" : "stop ") 316 .append(" uid:").append(mClientUid) 317 .append(" session:").append(mSession) 318 .append(" src:").append(MediaRecorder.toLogFriendlyAudioSource(mSource)) 319 .append(mPackName == null ? "" : " pack:" + mPackName).toString(); 320 } 321 } 322 323 private static final AudioEventLogger sEventLogger = new AudioEventLogger(50, 324 "recording activity as reported through AudioSystem.AudioRecordingCallback"); 325 } 326