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