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.AudioDeviceInfo; 22 import android.media.AudioFormat; 23 import android.media.AudioManager; 24 import android.media.AudioRecordingConfiguration; 25 import android.media.AudioSystem; 26 import android.media.IRecordingConfigDispatcher; 27 import android.media.MediaRecorder; 28 import android.media.audiofx.AudioEffect; 29 import android.os.IBinder; 30 import android.os.RemoteException; 31 import android.util.Log; 32 33 import com.android.server.utils.EventLogger; 34 35 import java.io.PrintWriter; 36 import java.text.DateFormat; 37 import java.util.ArrayList; 38 import java.util.Date; 39 import java.util.Iterator; 40 import java.util.List; 41 import java.util.concurrent.atomic.AtomicBoolean; 42 import java.util.concurrent.atomic.AtomicInteger; 43 44 /** 45 * Class to receive and dispatch updates from AudioSystem about recording configurations. 46 */ 47 public final class RecordingActivityMonitor implements AudioSystem.AudioRecordingCallback { 48 49 public final static String TAG = "AudioService.RecordingActivityMonitor"; 50 51 private ArrayList<RecMonitorClient> mClients = new ArrayList<RecMonitorClient>(); 52 // a public client is one that needs an anonymized version of the playback configurations, we 53 // keep track of whether there is at least one to know when we need to create the list of 54 // playback configurations that do not contain uid/package name information. 55 private boolean mHasPublicClients = false; 56 57 58 // When legacy remote submix device is active, remote submix device should not be fixed and 59 // full volume device. When legacy remote submix device is active, there will be a recording 60 // activity using device with type as {@link AudioSystem.DEVICE_OUT_REMOTE_SUBMIX} and address 61 // as {@link AudioSystem.LEGACY_REMOTE_SUBMIX_ADDRESS}. Cache riid of legacy remote submix 62 // since remote submix state is not cached in mRecordStates. 63 private AtomicInteger mLegacyRemoteSubmixRiid = 64 new AtomicInteger(AudioManager.RECORD_RIID_INVALID); 65 private AtomicBoolean mLegacyRemoteSubmixActive = new AtomicBoolean(false); 66 67 static final class RecordingState { 68 private final int mRiid; 69 private final RecorderDeathHandler mDeathHandler; 70 private boolean mIsActive; 71 private AudioRecordingConfiguration mConfig; 72 RecordingState(int riid, RecorderDeathHandler handler)73 RecordingState(int riid, RecorderDeathHandler handler) { 74 mRiid = riid; 75 mDeathHandler = handler; 76 } 77 RecordingState(AudioRecordingConfiguration config)78 RecordingState(AudioRecordingConfiguration config) { 79 mRiid = AudioManager.RECORD_RIID_INVALID; 80 mDeathHandler = null; 81 mConfig = config; 82 } 83 getRiid()84 int getRiid() { 85 return mRiid; 86 } 87 getPortId()88 int getPortId() { 89 return mConfig != null ? mConfig.getClientPortId() : -1; 90 } 91 getConfig()92 AudioRecordingConfiguration getConfig() { 93 return mConfig; 94 } 95 hasDeathHandler()96 boolean hasDeathHandler() { 97 return mDeathHandler != null; 98 } 99 isActiveConfiguration()100 boolean isActiveConfiguration() { 101 return mIsActive && mConfig != null; 102 } 103 release()104 void release() { 105 if (mDeathHandler != null) { 106 mDeathHandler.release(); 107 } 108 } 109 110 // returns true if status of an active recording has changed setActive(boolean active)111 boolean setActive(boolean active) { 112 if (mIsActive == active) return false; 113 mIsActive = active; 114 return mConfig != null; 115 } 116 117 // returns true if an active recording has been updated setConfig(AudioRecordingConfiguration config)118 boolean setConfig(AudioRecordingConfiguration config) { 119 if (config.equals(mConfig)) return false; 120 mConfig = config; 121 return mIsActive; 122 } 123 dump(PrintWriter pw)124 void dump(PrintWriter pw) { 125 pw.println("riid " + mRiid + "; active? " + mIsActive); 126 if (mConfig != null) { 127 mConfig.dump(pw); 128 } else { 129 pw.println(" no config"); 130 } 131 } 132 } 133 private List<RecordingState> mRecordStates = new ArrayList<RecordingState>(); 134 135 private final PackageManager mPackMan; 136 RecordingActivityMonitor(Context ctxt)137 RecordingActivityMonitor(Context ctxt) { 138 RecMonitorClient.sMonitor = this; 139 RecorderDeathHandler.sMonitor = this; 140 mPackMan = ctxt.getPackageManager(); 141 } 142 143 /** 144 * Implementation of android.media.AudioSystem.AudioRecordingCallback 145 */ 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)146 public void onRecordingConfigurationChanged(int event, int riid, int uid, int session, 147 int source, int portId, boolean silenced, 148 int[] recordingInfo, 149 AudioEffect.Descriptor[] clientEffects, 150 AudioEffect.Descriptor[] effects, 151 int activeSource, String packName) { 152 final AudioRecordingConfiguration config = createRecordingConfiguration( 153 uid, session, source, recordingInfo, 154 portId, silenced, activeSource, clientEffects, effects); 155 if (source == MediaRecorder.AudioSource.REMOTE_SUBMIX 156 && (event == AudioManager.RECORD_CONFIG_EVENT_START 157 || event == AudioManager.RECORD_CONFIG_EVENT_UPDATE)) { 158 final AudioDeviceInfo device = config.getAudioDevice(); 159 if (device != null 160 && AudioSystem.LEGACY_REMOTE_SUBMIX_ADDRESS.equals(device.getAddress())) { 161 mLegacyRemoteSubmixRiid.set(riid); 162 mLegacyRemoteSubmixActive.set(true); 163 } 164 } 165 if (MediaRecorder.isSystemOnlyAudioSource(source)) { 166 // still want to log event, it just won't appear in recording configurations; 167 sEventLogger.enqueue(new RecordingEvent(event, riid, config).printLog(TAG)); 168 return; 169 } 170 dispatchCallbacks(updateSnapshot(event, riid, config)); 171 } 172 173 /** 174 * Track a recorder provided by the client 175 */ trackRecorder(IBinder recorder)176 public int trackRecorder(IBinder recorder) { 177 if (recorder == null) { 178 Log.e(TAG, "trackRecorder called with null token"); 179 return AudioManager.RECORD_RIID_INVALID; 180 } 181 final int newRiid = AudioSystem.newAudioRecorderId(); 182 RecorderDeathHandler handler = new RecorderDeathHandler(newRiid, recorder); 183 if (!handler.init()) { 184 // probably means that the AudioRecord has already died 185 return AudioManager.RECORD_RIID_INVALID; 186 } 187 synchronized (mRecordStates) { 188 mRecordStates.add(new RecordingState(newRiid, handler)); 189 } 190 // a newly added record is inactive, no change in active configs is possible. 191 return newRiid; 192 } 193 194 /** 195 * Receive an event from the client about a tracked recorder 196 */ recorderEvent(int riid, int event)197 public void recorderEvent(int riid, int event) { 198 if (mLegacyRemoteSubmixRiid.get() == riid) { 199 mLegacyRemoteSubmixActive.set(event == AudioManager.RECORDER_STATE_STARTED); 200 } 201 int configEvent = event == AudioManager.RECORDER_STATE_STARTED 202 ? AudioManager.RECORD_CONFIG_EVENT_START : 203 event == AudioManager.RECORDER_STATE_STOPPED 204 ? AudioManager.RECORD_CONFIG_EVENT_STOP : AudioManager.RECORD_CONFIG_EVENT_NONE; 205 if (riid == AudioManager.RECORD_RIID_INVALID 206 || configEvent == AudioManager.RECORD_CONFIG_EVENT_NONE) { 207 sEventLogger.enqueue(new RecordingEvent(event, riid, null).printLog(TAG)); 208 return; 209 } 210 dispatchCallbacks(updateSnapshot(configEvent, riid, null)); 211 } 212 213 /** 214 * Stop tracking the recorder 215 */ releaseRecorder(int riid)216 public void releaseRecorder(int riid) { 217 dispatchCallbacks(updateSnapshot(AudioManager.RECORD_CONFIG_EVENT_RELEASE, riid, null)); 218 } 219 220 /** 221 * Returns true if a recorder belonging to the app with given uid is active. 222 * 223 * @param uid the app uid 224 * @return true if a recorder is active, false otherwise 225 */ isRecordingActiveForUid(int uid)226 public boolean isRecordingActiveForUid(int uid) { 227 synchronized (mRecordStates) { 228 for (RecordingState state : mRecordStates) { 229 // Note: isActiveConfiguration() == true => state.getConfig() != null 230 if (state.isActiveConfiguration() && state.getConfig().getClientUid() == uid 231 && !state.getConfig().isClientSilenced()) { 232 return true; 233 } 234 } 235 } 236 return false; 237 } 238 dispatchCallbacks(List<AudioRecordingConfiguration> configs)239 private void dispatchCallbacks(List<AudioRecordingConfiguration> configs) { 240 if (configs == null) { // null means "no changes" 241 return; 242 } 243 synchronized (mClients) { 244 // list of recording configurations for "public consumption". It is only computed if 245 // there are non-system recording activity listeners. 246 final List<AudioRecordingConfiguration> configsPublic = mHasPublicClients 247 ? anonymizeForPublicConsumption(configs) : 248 new ArrayList<AudioRecordingConfiguration>(); 249 for (RecMonitorClient rmc : mClients) { 250 try { 251 if (rmc.mIsPrivileged) { 252 rmc.mDispatcherCb.dispatchRecordingConfigChange(configs); 253 } else { 254 rmc.mDispatcherCb.dispatchRecordingConfigChange(configsPublic); 255 } 256 } catch (RemoteException e) { 257 Log.w(TAG, "Could not call dispatchRecordingConfigChange() on client", e); 258 } 259 } 260 } 261 } 262 dump(PrintWriter pw)263 protected void dump(PrintWriter pw) { 264 // recorders 265 pw.println("\nRecordActivityMonitor dump time: " 266 + DateFormat.getTimeInstance().format(new Date())); 267 synchronized (mRecordStates) { 268 for (RecordingState state : mRecordStates) { 269 state.dump(pw); 270 } 271 } 272 pw.println("\n"); 273 // log 274 sEventLogger.dump(pw); 275 } 276 anonymizeForPublicConsumption( List<AudioRecordingConfiguration> sysConfigs)277 private static ArrayList<AudioRecordingConfiguration> anonymizeForPublicConsumption( 278 List<AudioRecordingConfiguration> sysConfigs) { 279 ArrayList<AudioRecordingConfiguration> publicConfigs = 280 new ArrayList<AudioRecordingConfiguration>(); 281 // only add active anonymized configurations, 282 for (AudioRecordingConfiguration config : sysConfigs) { 283 publicConfigs.add(AudioRecordingConfiguration.anonymizedCopy(config)); 284 } 285 return publicConfigs; 286 } 287 initMonitor()288 void initMonitor() { 289 AudioSystem.setRecordingCallback(this); 290 } 291 onAudioServerDied()292 void onAudioServerDied() { 293 // Remove all RecordingState entries that do not have a death handler (that means 294 // they are tracked by the Audio Server). If there were active entries among removed, 295 // dispatch active configuration changes. 296 List<AudioRecordingConfiguration> configs = null; 297 synchronized (mRecordStates) { 298 boolean configChanged = false; 299 for (Iterator<RecordingState> it = mRecordStates.iterator(); it.hasNext(); ) { 300 RecordingState state = it.next(); 301 if (!state.hasDeathHandler()) { 302 if (state.isActiveConfiguration()) { 303 configChanged = true; 304 sEventLogger.enqueue(new RecordingEvent( 305 AudioManager.RECORD_CONFIG_EVENT_RELEASE, 306 state.getRiid(), state.getConfig())); 307 } 308 it.remove(); 309 } 310 } 311 if (configChanged) { 312 configs = getActiveRecordingConfigurations(true /*isPrivileged*/); 313 } 314 } 315 dispatchCallbacks(configs); 316 } 317 registerRecordingCallback(IRecordingConfigDispatcher rcdb, boolean isPrivileged)318 void registerRecordingCallback(IRecordingConfigDispatcher rcdb, boolean isPrivileged) { 319 if (rcdb == null) { 320 return; 321 } 322 synchronized (mClients) { 323 final RecMonitorClient rmc = new RecMonitorClient(rcdb, isPrivileged); 324 if (rmc.init()) { 325 if (!isPrivileged) { 326 mHasPublicClients = true; 327 } 328 mClients.add(rmc); 329 } 330 } 331 } 332 unregisterRecordingCallback(IRecordingConfigDispatcher rcdb)333 void unregisterRecordingCallback(IRecordingConfigDispatcher rcdb) { 334 if (rcdb == null) { 335 return; 336 } 337 synchronized (mClients) { 338 final Iterator<RecMonitorClient> clientIterator = mClients.iterator(); 339 boolean hasPublicClients = false; 340 while (clientIterator.hasNext()) { 341 RecMonitorClient rmc = clientIterator.next(); 342 if (rcdb.asBinder().equals(rmc.mDispatcherCb.asBinder())) { 343 rmc.release(); 344 clientIterator.remove(); 345 } else { 346 if (!rmc.mIsPrivileged) { 347 hasPublicClients = true; 348 } 349 } 350 } 351 mHasPublicClients = hasPublicClients; 352 } 353 } 354 getActiveRecordingConfigurations(boolean isPrivileged)355 List<AudioRecordingConfiguration> getActiveRecordingConfigurations(boolean isPrivileged) { 356 List<AudioRecordingConfiguration> configs = new ArrayList<AudioRecordingConfiguration>(); 357 synchronized (mRecordStates) { 358 for (RecordingState state : mRecordStates) { 359 if (state.isActiveConfiguration()) { 360 configs.add(state.getConfig()); 361 } 362 } 363 } 364 // AudioRecordingConfiguration objects never get updated. If config changes, 365 // the reference to the config is set in RecordingState. 366 if (!isPrivileged) { 367 configs = anonymizeForPublicConsumption(configs); 368 } 369 return configs; 370 } 371 372 /** 373 * Return true if legacy remote submix device is active. Otherwise, return false. 374 */ isLegacyRemoteSubmixActive()375 boolean isLegacyRemoteSubmixActive() { 376 return mLegacyRemoteSubmixActive.get(); 377 } 378 379 /** 380 * Create a recording configuration from the provided parameters 381 * @param uid 382 * @param session 383 * @param source 384 * @param recordingFormat see 385 * {@link AudioSystem.AudioRecordingCallback#onRecordingConfigurationChanged(int, int, int,\ 386 int, int, boolean, int[], AudioEffect.Descriptor[], AudioEffect.Descriptor[], int, String)} 387 * for the definition of the contents of the array 388 * @param portId 389 * @param silenced 390 * @param activeSource 391 * @param clientEffects 392 * @param effects 393 * @return null a configuration object. 394 */ createRecordingConfiguration(int uid, int session, int source, int[] recordingInfo, int portId, boolean silenced, int activeSource, AudioEffect.Descriptor[] clientEffects, AudioEffect.Descriptor[] effects)395 private AudioRecordingConfiguration createRecordingConfiguration(int uid, 396 int session, int source, int[] recordingInfo, int portId, boolean silenced, 397 int activeSource, AudioEffect.Descriptor[] clientEffects, 398 AudioEffect.Descriptor[] effects) { 399 final AudioFormat clientFormat = new AudioFormat.Builder() 400 .setEncoding(recordingInfo[0]) 401 // FIXME this doesn't support index-based masks 402 .setChannelMask(recordingInfo[1]) 403 .setSampleRate(recordingInfo[2]) 404 .build(); 405 final AudioFormat deviceFormat = new AudioFormat.Builder() 406 .setEncoding(recordingInfo[3]) 407 // FIXME this doesn't support index-based masks 408 .setChannelMask(recordingInfo[4]) 409 .setSampleRate(recordingInfo[5]) 410 .build(); 411 final int patchHandle = recordingInfo[6]; 412 final String[] packages = mPackMan.getPackagesForUid(uid); 413 final String packageName; 414 if (packages != null && packages.length > 0) { 415 packageName = packages[0]; 416 } else { 417 packageName = ""; 418 } 419 return new AudioRecordingConfiguration(uid, session, source, 420 clientFormat, deviceFormat, patchHandle, packageName, 421 portId, silenced, activeSource, clientEffects, effects); 422 } 423 424 /** 425 * Update the internal "view" of the active recording sessions 426 * @param event RECORD_CONFIG_EVENT_... 427 * @param riid 428 * @param config 429 * @return null if the list of active recording sessions has not been modified, a list 430 * with the current active configurations otherwise. 431 */ updateSnapshot( int event, int riid, AudioRecordingConfiguration config)432 private List<AudioRecordingConfiguration> updateSnapshot( 433 int event, int riid, AudioRecordingConfiguration config) { 434 List<AudioRecordingConfiguration> configs = null; 435 synchronized (mRecordStates) { 436 int stateIndex = -1; 437 if (riid != AudioManager.RECORD_RIID_INVALID) { 438 stateIndex = findStateByRiid(riid); 439 } else if (config != null) { 440 stateIndex = findStateByPortId(config.getClientPortId()); 441 } 442 if (stateIndex == -1) { 443 if (event == AudioManager.RECORD_CONFIG_EVENT_START && config != null) { 444 // First time registration for a recorder tracked by AudioServer. 445 mRecordStates.add(new RecordingState(config)); 446 stateIndex = mRecordStates.size() - 1; 447 } else { 448 if (config == null) { 449 // Records tracked by clients must be registered first via trackRecorder. 450 Log.e(TAG, String.format( 451 "Unexpected event %d for riid %d", event, riid)); 452 } 453 return configs; 454 } 455 } 456 final RecordingState state = mRecordStates.get(stateIndex); 457 458 boolean configChanged; 459 switch (event) { 460 case AudioManager.RECORD_CONFIG_EVENT_START: 461 configChanged = state.setActive(true); 462 if (config != null) { 463 configChanged = state.setConfig(config) || configChanged; 464 } 465 break; 466 case AudioManager.RECORD_CONFIG_EVENT_UPDATE: 467 // For this event config != null 468 configChanged = state.setConfig(config); 469 break; 470 case AudioManager.RECORD_CONFIG_EVENT_STOP: 471 configChanged = state.setActive(false); 472 if (!state.hasDeathHandler()) { 473 // A recorder tracked by AudioServer has to be removed now so it 474 // does not leak. It will be re-registered if recording starts again. 475 mRecordStates.remove(stateIndex); 476 } 477 break; 478 case AudioManager.RECORD_CONFIG_EVENT_RELEASE: 479 configChanged = state.isActiveConfiguration(); 480 state.release(); 481 mRecordStates.remove(stateIndex); 482 break; 483 default: 484 Log.e(TAG, String.format("Unknown event %d for riid %d / portid %d", 485 event, riid, state.getPortId())); 486 configChanged = false; 487 } 488 if (configChanged) { 489 sEventLogger.enqueue(new RecordingEvent(event, riid, state.getConfig())); 490 configs = getActiveRecordingConfigurations(true /*isPrivileged*/); 491 } 492 } 493 return configs; 494 } 495 496 // riid is assumed to be valid findStateByRiid(int riid)497 private int findStateByRiid(int riid) { 498 synchronized (mRecordStates) { 499 for (int i = 0; i < mRecordStates.size(); i++) { 500 if (mRecordStates.get(i).getRiid() == riid) { 501 return i; 502 } 503 } 504 } 505 return -1; 506 } 507 findStateByPortId(int portId)508 private int findStateByPortId(int portId) { 509 // Lookup by portId is unambiguous only for recordings managed by the Audio Server. 510 synchronized (mRecordStates) { 511 for (int i = 0; i < mRecordStates.size(); i++) { 512 if (!mRecordStates.get(i).hasDeathHandler() 513 && mRecordStates.get(i).getPortId() == portId) { 514 return i; 515 } 516 } 517 } 518 return -1; 519 } 520 521 /** 522 * Inner class to track clients that want to be notified of recording updates 523 */ 524 private final static class RecMonitorClient implements IBinder.DeathRecipient { 525 526 // can afford to be static because only one RecordingActivityMonitor ever instantiated 527 static RecordingActivityMonitor sMonitor; 528 529 final IRecordingConfigDispatcher mDispatcherCb; 530 final boolean mIsPrivileged; 531 RecMonitorClient(IRecordingConfigDispatcher rcdb, boolean isPrivileged)532 RecMonitorClient(IRecordingConfigDispatcher rcdb, boolean isPrivileged) { 533 mDispatcherCb = rcdb; 534 mIsPrivileged = isPrivileged; 535 } 536 binderDied()537 public void binderDied() { 538 Log.w(TAG, "client died"); 539 sMonitor.unregisterRecordingCallback(mDispatcherCb); 540 } 541 init()542 boolean init() { 543 try { 544 mDispatcherCb.asBinder().linkToDeath(this, 0); 545 return true; 546 } catch (RemoteException e) { 547 Log.w(TAG, "Could not link to client death", e); 548 return false; 549 } 550 } 551 release()552 void release() { 553 mDispatcherCb.asBinder().unlinkToDeath(this, 0); 554 } 555 } 556 557 private static final class RecorderDeathHandler implements IBinder.DeathRecipient { 558 559 // can afford to be static because only one RecordingActivityMonitor ever instantiated 560 static RecordingActivityMonitor sMonitor; 561 562 final int mRiid; 563 private final IBinder mRecorderToken; 564 RecorderDeathHandler(int riid, IBinder recorderToken)565 RecorderDeathHandler(int riid, IBinder recorderToken) { 566 mRiid = riid; 567 mRecorderToken = recorderToken; 568 } 569 binderDied()570 public void binderDied() { 571 sMonitor.releaseRecorder(mRiid); 572 } 573 init()574 boolean init() { 575 try { 576 mRecorderToken.linkToDeath(this, 0); 577 return true; 578 } catch (RemoteException e) { 579 Log.w(TAG, "Could not link to recorder death", e); 580 return false; 581 } 582 } 583 release()584 void release() { 585 mRecorderToken.unlinkToDeath(this, 0); 586 } 587 } 588 589 /** 590 * Inner class for recording event logging 591 */ 592 private static final class RecordingEvent extends EventLogger.Event { 593 private final int mRecEvent; 594 private final int mRIId; 595 private final int mClientUid; 596 private final int mSession; 597 private final int mSource; 598 private final String mPackName; 599 private final boolean mSilenced; 600 RecordingEvent(int event, int riid, AudioRecordingConfiguration config)601 RecordingEvent(int event, int riid, AudioRecordingConfiguration config) { 602 mRecEvent = event; 603 mRIId = riid; 604 if (config != null) { 605 mClientUid = config.getClientUid(); 606 mSession = config.getClientAudioSessionId(); 607 mSource = config.getClientAudioSource(); 608 mPackName = config.getClientPackageName(); 609 mSilenced = config.isClientSilenced(); 610 } else { 611 mClientUid = -1; 612 mSession = -1; 613 mSource = -1; 614 mPackName = null; 615 mSilenced = false; 616 } 617 } 618 recordEventToString(int recEvent)619 private static String recordEventToString(int recEvent) { 620 switch (recEvent) { 621 case AudioManager.RECORD_CONFIG_EVENT_START: 622 return "start"; 623 case AudioManager.RECORD_CONFIG_EVENT_UPDATE: 624 return "update"; 625 case AudioManager.RECORD_CONFIG_EVENT_STOP: 626 return "stop"; 627 case AudioManager.RECORD_CONFIG_EVENT_RELEASE: 628 return "release"; 629 default: 630 return "unknown (" + recEvent + ")"; 631 } 632 } 633 634 @Override eventToString()635 public String eventToString() { 636 return new StringBuilder("rec ").append(recordEventToString(mRecEvent)) 637 .append(" riid:").append(mRIId) 638 .append(" uid:").append(mClientUid) 639 .append(" session:").append(mSession) 640 .append(" src:").append(MediaRecorder.toLogFriendlyAudioSource(mSource)) 641 .append(mSilenced ? " silenced" : " not silenced") 642 .append(mPackName == null ? "" : " pack:" + mPackName).toString(); 643 } 644 } 645 646 private static final EventLogger 647 sEventLogger = new EventLogger(50, 648 "recording activity received by AudioService"); 649 } 650