1 /* 2 * Copyright (C) 2024 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.telecom; 18 19 import static android.media.AudioPlaybackConfiguration.PLAYER_STATE_STARTED; 20 21 import android.annotation.IntDef; 22 import android.media.AudioAttributes; 23 import android.media.AudioManager; 24 import android.media.AudioManager.AudioPlaybackCallback; 25 import android.media.AudioPlaybackConfiguration; 26 import android.media.AudioRecord; 27 import android.media.AudioRecordingConfiguration; 28 import android.media.AudioTrack; 29 import android.media.MediaRecorder; 30 import android.os.Handler; 31 import android.os.Process; 32 import android.telecom.Log; 33 import android.telecom.Logging.EventManager; 34 import android.telecom.PhoneAccountHandle; 35 import android.util.ArrayMap; 36 import android.util.ArraySet; 37 import android.util.LocalLog; 38 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.internal.util.IndentingPrintWriter; 41 import com.android.server.telecom.metrics.TelecomMetricsController; 42 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 import java.text.SimpleDateFormat; 46 import java.util.Collection; 47 import java.util.Collections; 48 import java.util.Date; 49 import java.util.Iterator; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Set; 53 54 /** 55 * Monitors {@link AudioRecord}, {@link AudioTrack}, and {@link AudioManager#getMode()} to determine 56 * the reliability of audio operations for a call. Augments the Telecom dumpsys with Telecom calls 57 * with information about calls. 58 */ 59 public class CallAudioWatchdog extends CallsManagerListenerBase { 60 /** 61 * Bit flag set on a {@link CommunicationSession#sessionAttr} to indicate that the session has 62 * audio recording resources. 63 */ 64 public static final int SESSION_ATTR_HAS_AUDIO_RECORD = 1 << 0; 65 66 /** 67 * Bit flag set on a {@link CommunicationSession#sessionAttr} to indicate that the session has 68 * audio playback resources. 69 */ 70 public static final int SESSION_ATTR_HAS_AUDIO_PLAYBACK = 1 << 1; 71 72 /** 73 * Bit flag set on a {@link CommunicationSession#sessionAttr} to indicate that the uid for the 74 * session has a phone account allocated. This helps us track cases where an app is telecom 75 * capable but chooses not to use the telecom integration. 76 */ 77 public static final int SESSION_ATTR_HAS_PHONE_ACCOUNT = 1 << 2; 78 79 @IntDef(prefix = { "SESSION_ATTR_" }, 80 value = {SESSION_ATTR_HAS_AUDIO_RECORD, SESSION_ATTR_HAS_AUDIO_PLAYBACK, 81 SESSION_ATTR_HAS_PHONE_ACCOUNT}, 82 flag = true) 83 @Retention(RetentionPolicy.SOURCE) 84 public @interface SessionAttribute {} 85 86 /** 87 * Proxy for operations related to phone accounts. 88 */ 89 public interface PhoneAccountRegistrarProxy { 90 /** 91 * Determines if a specified {@code uid} has an associated phone account registered. 92 * @param uid the uid. 93 * @return {@code true} if there is a phone account registered, {@code false} otherwise 94 */ hasPhoneAccountForUid(int uid)95 boolean hasPhoneAccountForUid(int uid); 96 97 /** 98 * Given a {@link PhoneAccountHandle} determines the uid for the app owning the account. 99 * @param handle The phone account; the phone account handle's package and userhandle are 100 * ultimately used to find the associated uid. 101 * @return the uid for the phone account. 102 */ getUidForPhoneAccountHandle(PhoneAccountHandle handle)103 int getUidForPhoneAccountHandle(PhoneAccountHandle handle); 104 } 105 106 /** 107 * Keyed on uid, tracks a communication session and whether there are audio record and playback 108 * resources for that session. 109 */ 110 public class CommunicationSession { 111 private int uid; 112 @SessionAttribute 113 private int sessionAttr; 114 private ArrayMap<Integer, Set<Integer>> audioResourcesByType = new ArrayMap<>(); 115 private EventManager.Loggable telecomCall; 116 private long sessionStartMillis; 117 private long sessionStartClockMillis; 118 119 /** 120 * @return {@code true} if audio record or playback is held for the session, {@code false} 121 * otherwise. 122 */ hasMediaResources()123 public boolean hasMediaResources() { 124 return (getSessionAttr() 125 & (SESSION_ATTR_HAS_AUDIO_RECORD | SESSION_ATTR_HAS_AUDIO_PLAYBACK)) != 0; 126 } 127 128 /** 129 * Sets a bit enabled for the session. 130 * @param bit the bit 131 */ setBit(@essionAttribute int bit)132 public void setBit(@SessionAttribute int bit) { 133 setSessionAttr(getSessionAttr() | bit); 134 } 135 136 /** 137 * Clears the specified bit for the session. 138 * @param bit the bit 139 */ clearBit(@essionAttribute int bit)140 public void clearBit(@SessionAttribute int bit) { 141 setSessionAttr(getSessionAttr() & ~bit); 142 } 143 144 /** 145 * Determines if a bit is set in the given bitmask. 146 * @param mask the bitmask. 147 * @param bit The bit 148 * @return {@code true} if set, {@code false} otherwise. 149 */ isBitSet(@essionAttribute int mask, @SessionAttribute int bit)150 public static boolean isBitSet(@SessionAttribute int mask, @SessionAttribute int bit) { 151 return (mask & bit) == bit; 152 } 153 154 /** 155 * Determines if a bit is set for the current session. 156 * @param bit The bit 157 * @return {@code true} if set, {@code false} otherwise. 158 */ isBitSet(@essionAttribute int bit)159 public boolean isBitSet(@SessionAttribute int bit) { 160 return isBitSet(getSessionAttr(), bit); 161 } 162 163 /** 164 * Generate a string representing the session attributes bitmask, suitable for logging. 165 * @param attr The session attributes. 166 * @return String of bits! 167 */ sessionAttrToString(@essionAttribute int attr)168 public static String sessionAttrToString(@SessionAttribute int attr) { 169 return (isBitSet(attr, SESSION_ATTR_HAS_PHONE_ACCOUNT) ? "phac, " : "") + 170 (isBitSet(attr, SESSION_ATTR_HAS_AUDIO_PLAYBACK) ? "ap, " : "") + 171 (isBitSet(attr, SESSION_ATTR_HAS_AUDIO_RECORD) ? "ar, " : ""); 172 } 173 174 @Override toString()175 public String toString() { 176 return "CommSess{" + 177 "uid=" + getUid() + 178 ", created=" + SimpleDateFormat.getDateTimeInstance().format( 179 new Date(getSessionStartClockMillis())) + 180 ", attr=" + sessionAttrToString(getSessionAttr()) + 181 ", callId=" + (getTelecomCall() != null ? getTelecomCall().getId() : "none") + 182 ", duration=" + (mClockProxy.elapsedRealtime() - getSessionStartMillis())/1000 + 183 '}'; 184 } 185 186 /** 187 * The uid for the session. 188 */ getUid()189 public int getUid() { 190 return uid; 191 } 192 setUid(int uid)193 public void setUid(int uid) { 194 this.uid = uid; 195 } 196 197 /** 198 * The attributes for the session. 199 */ getSessionAttr()200 public int getSessionAttr() { 201 return sessionAttr; 202 } 203 setSessionAttr(int sessionAttr)204 public void setSessionAttr(int sessionAttr) { 205 this.sessionAttr = sessionAttr; 206 } 207 208 /** 209 * ArrayMap, keyed by {@link #SESSION_ATTR_HAS_AUDIO_PLAYBACK} and 210 * {@link #SESSION_ATTR_HAS_AUDIO_RECORD}. For each, contains a set of the 211 * {@link AudioManager} ids associated with active playback and recording sessions for a 212 * uid. 213 * 214 * {@link AudioPlaybackConfiguration#getPlayerInterfaceId()} is used for audio playback; 215 * per docs, this is an identifier unique for the lifetime of the player. 216 * 217 * {@link AudioRecordingConfiguration#getClientAudioSessionId()} is used for audio record 218 * tracking; this is unique similar to the audio playback config. 219 */ getAudioResourcesByType()220 public ArrayMap<Integer, Set<Integer>> getAudioResourcesByType() { 221 return audioResourcesByType; 222 } 223 setAudioResourcesByType( ArrayMap<Integer, Set<Integer>> audioResourcesByType)224 public void setAudioResourcesByType( 225 ArrayMap<Integer, Set<Integer>> audioResourcesByType) { 226 this.audioResourcesByType = audioResourcesByType; 227 } 228 229 /** 230 * The Telecom call this session is associated with; set if the call takes place during a 231 * telecom call. 232 */ getTelecomCall()233 public EventManager.Loggable getTelecomCall() { 234 return telecomCall; 235 } 236 setTelecomCall(EventManager.Loggable telecomCall)237 public void setTelecomCall(EventManager.Loggable telecomCall) { 238 this.telecomCall = telecomCall; 239 } 240 241 /** 242 * The time in {@link android.os.SystemClock#elapsedRealtime()} timebase when the session 243 * started. Used only to determine duration. 244 */ getSessionStartMillis()245 public long getSessionStartMillis() { 246 return sessionStartMillis; 247 } 248 setSessionStartMillis(long sessionStartMillis)249 public void setSessionStartMillis(long sessionStartMillis) { 250 this.sessionStartMillis = sessionStartMillis; 251 } 252 253 /** 254 * The time in {@link System#currentTimeMillis()} timebase when the session started; used 255 * to indicate the wall block time when the session started. 256 */ getSessionStartClockMillis()257 public long getSessionStartClockMillis() { 258 return sessionStartClockMillis; 259 } 260 setSessionStartClockMillis(long sessionStartClockMillis)261 public void setSessionStartClockMillis(long sessionStartClockMillis) { 262 this.sessionStartClockMillis = sessionStartClockMillis; 263 } 264 } 265 266 /** 267 * Listener for AudioManager audio playback changes. Finds audio playback tagged for voice 268 * communication. Updates the {@link #mCommunicationSessions} based on this data to track if 269 * audio playback it taking place. 270 * 271 * Note: {@link AudioPlaybackCallback} reports information about audio playback for an app; if 272 * an app releases audio playback resources, the list of audio playback configurations no longer 273 * includes a {@link AudioPlaybackConfiguration} for that specific audio playback session. This 274 * API semantic is why the code below is a bit confusing; in the listener we need to track all 275 * the ids we've seen and then correlate that back to what we knew about it from the last 276 * callback. 277 * 278 * An app may have MULTIPLE {@link AudioPlaybackConfiguration} for voip use-cases and switch 279 * between them for a single call -- this was observed in live app testing. 280 */ 281 public class WatchdogAudioPlaybackCallback extends AudioPlaybackCallback { 282 @Override onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs)283 public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) { 284 Map<Integer,Set<Integer>> sessionIdentifiersByUid = new ArrayMap<>(); 285 for (AudioPlaybackConfiguration config : configs) { 286 Log.d(this, "onPlaybackConfigChanged: config=%s", config); 287 // only track USAGE_VOICE_COMMUNICATION as this is for VOIP calls. 288 if (config.getAudioAttributes() != null 289 && config.getAudioAttributes().getUsage() 290 == AudioAttributes.USAGE_VOICE_COMMUNICATION) { 291 292 // Skip if the client's pid is same as myself 293 if (config.getClientPid() == Process.myPid()) { 294 continue; 295 } 296 297 // If an audio session is idle, we don't count it as playing. It must be in a 298 // started state. 299 boolean isPlaying = config.getPlayerState() == PLAYER_STATE_STARTED; 300 301 maybeTrackAudioPlayback(config.getClientUid(), config.getPlayerInterfaceId(), 302 isPlaying); 303 if (isPlaying) { 304 // Track the list of player id active for each uid; we use it later for 305 // cleanup of stale sessions. 306 putOrDefault(sessionIdentifiersByUid,config.getClientUid(), 307 new ArraySet<>()).add(config.getPlayerInterfaceId()); 308 } 309 } 310 } 311 312 // The listener will drop uid/playerInterfaceIds no longer active, so we need to go back 313 // and see if any sessions need to be removed now. 314 cleanupAttributeForSessions(SESSION_ATTR_HAS_AUDIO_PLAYBACK, 315 sessionIdentifiersByUid); 316 } 317 } 318 319 /** 320 * Similar to {@link WatchdogAudioPlaybackCallback}, tracks audio recording an app performs. 321 * This code is handling the onRecordingConfigChanged event from the AudioManager. The event 322 * is fired when the list of active recording configurations changes. In this case, the code 323 * is only interested in recording configurations that are using the VOICE_COMMUNICATION 324 * audio source. For these configurations, the code tracks the session identifiers and 325 * potentially adds them to the SESSION_ATTR_HAS_AUDIO_RECORD attribute. The code also cleans 326 * up the attribute for any sessions that are no longer active. 327 * The same caveat/note applies here; a single app can have many audio recording sessions that 328 * the app swaps between during a call. 329 */ 330 public class WatchdogAudioRecordCallback extends AudioManager.AudioRecordingCallback { 331 @Override onRecordingConfigChanged(List<AudioRecordingConfiguration> configs)332 public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) { 333 List<AudioRecordingConfiguration> theConfigs = 334 mAudioManager.getActiveRecordingConfigurations(); 335 Map<Integer,Set<Integer>> sessionIdentifiersByUid = new ArrayMap<>(); 336 for (AudioRecordingConfiguration config : theConfigs) { 337 if (config.getClientAudioSource() 338 == MediaRecorder.AudioSource.VOICE_COMMUNICATION) { 339 340 putOrDefault(sessionIdentifiersByUid, config.getClientUid(), 341 new ArraySet<>()).add(config.getClientAudioSessionId()); 342 maybeTrackAudioRecord(config.getClientUid(), config.getClientAudioSessionId(), 343 true); 344 } 345 } 346 // The listener stops reporting audio sessions that go away, so we need to clean up the 347 // session potentially. 348 cleanupAttributeForSessions( 349 SESSION_ATTR_HAS_AUDIO_RECORD, 350 sessionIdentifiersByUid); 351 } 352 } 353 354 // Proxies to make testing possible-ish. 355 private final ClockProxy mClockProxy; 356 private final PhoneAccountRegistrarProxy mPhoneAccountRegistrarProxy; 357 358 private final WatchdogAudioPlaybackCallback mWatchdogAudioPlayback = 359 new WatchdogAudioPlaybackCallback(); 360 private final WatchdogAudioRecordCallback 361 mWatchdogAudioRecordCallack = new WatchdogAudioRecordCallback(); 362 private final AudioManager mAudioManager; 363 private final Handler mHandler; 364 365 // Guards access to mCommunicationSessions. 366 private final Object mCommunicationSessionsLock = new Object(); 367 368 /** 369 * Key - UID of communication app. 370 * Value - an instance of {@link CommunicationSession} tracking data for that uid. 371 */ 372 private final Map<Integer, CommunicationSession> mCommunicationSessions = new ArrayMap<>(); 373 374 // Local logs for tracking non-telecom calls. 375 private final LocalLog mLocalLog = new LocalLog(30); 376 377 private final TelecomMetricsController mMetricsController; 378 CallAudioWatchdog(AudioManager audioManager, PhoneAccountRegistrarProxy phoneAccountRegistrarProxy, ClockProxy clockProxy, Handler handler, TelecomMetricsController metricsController)379 public CallAudioWatchdog(AudioManager audioManager, 380 PhoneAccountRegistrarProxy phoneAccountRegistrarProxy, ClockProxy clockProxy, 381 Handler handler, TelecomMetricsController metricsController) { 382 mPhoneAccountRegistrarProxy = phoneAccountRegistrarProxy; 383 mClockProxy = clockProxy; 384 mAudioManager = audioManager; 385 mHandler = handler; 386 mAudioManager.registerAudioPlaybackCallback(mWatchdogAudioPlayback, mHandler); 387 mAudioManager.registerAudioRecordingCallback(mWatchdogAudioRecordCallack, mHandler); 388 mMetricsController = metricsController; 389 } 390 391 /** 392 * Tracks Telecom adding a call; we use this to associate a uid's sessions with a call. 393 * Note: this is not 100% accurate if there are multiple calls -- we just associate with the 394 * first call and leave it at that. It's not possible to know which audio sessions belong to 395 * which Telecom calls. 396 * @param call the Telecom call being added. 397 */ 398 @Override onCallAdded(Call call)399 public void onCallAdded(Call call) { 400 // Only track for voip calls. 401 if (call.isSelfManaged() || call.isTransactionalCall()) { 402 maybeTrackTelecomCall(call); 403 } 404 } 405 406 @Override onCallRemoved(Call call)407 public void onCallRemoved(Call call) { 408 // Only track for voip calls. 409 if (call.isSelfManaged() || call.isTransactionalCall()) { 410 maybeRemoveCall(call); 411 } 412 } 413 414 @VisibleForTesting getWatchdogAudioPlayback()415 public WatchdogAudioPlaybackCallback getWatchdogAudioPlayback() { 416 return mWatchdogAudioPlayback; 417 } 418 419 @VisibleForTesting getWatchdogAudioRecordCallack()420 public WatchdogAudioRecordCallback getWatchdogAudioRecordCallack() { 421 return mWatchdogAudioRecordCallack; 422 } 423 424 @VisibleForTesting getCommunicationSessions()425 public Map<Integer, CommunicationSession> getCommunicationSessions() { 426 return mCommunicationSessions; 427 } 428 429 /** 430 * Include info on audio stuff in the telecom dumpsys. 431 * @param pw 432 */ dump(IndentingPrintWriter pw)433 void dump(IndentingPrintWriter pw) { 434 pw.println("CallAudioWatchdog:"); 435 pw.increaseIndent(); 436 pw.println("Active Sessions:"); 437 pw.increaseIndent(); 438 Collection<CommunicationSession> sessions; 439 synchronized (mCommunicationSessionsLock) { 440 sessions = mCommunicationSessions.values(); 441 } 442 sessions.forEach(pw::println); 443 pw.decreaseIndent(); 444 pw.println("Audio sessions Sessions:"); 445 pw.increaseIndent(); 446 mLocalLog.dump(pw); 447 pw.decreaseIndent(); 448 pw.decreaseIndent(); 449 } 450 451 /** 452 * Tracks audio playback for a uid. 453 * @param uid the uid of the app having audio back change. 454 * @param playerInterfaceId From {@link AudioPlaybackConfiguration#getPlayerInterfaceId()} (see 455 * {@link CommunicationSession#audioResourcesByType} for keying info). 456 * @param isPlaying {@code true} if audio is starting for the client. 457 */ maybeTrackAudioPlayback(int uid, int playerInterfaceId, boolean isPlaying)458 private void maybeTrackAudioPlayback(int uid, int playerInterfaceId, boolean isPlaying) { 459 CommunicationSession session; 460 synchronized (mCommunicationSessionsLock) { 461 if (!isPlaying) { 462 // A session can start in an idle state and never go active; in this case we will 463 // not proactively add a new session; we'll just get one if it's already there. 464 // When the session goes active we can add it then. 465 session = getSession(uid); 466 } else { 467 // The playback is active, so we need to get or add a new communication session. 468 session = getOrAddSession(uid); 469 } 470 } 471 if (session == null) { 472 return; 473 } 474 475 // First track individual player interface id playing status. 476 if (isPlaying) { 477 putOrDefault(session.getAudioResourcesByType(), SESSION_ATTR_HAS_AUDIO_PLAYBACK, 478 new ArraySet<>()).add(playerInterfaceId); 479 } else { 480 putOrDefault(session.getAudioResourcesByType(), SESSION_ATTR_HAS_AUDIO_PLAYBACK, 481 new ArraySet<>()).remove(playerInterfaceId); 482 } 483 484 // Keep the bitmask up to date so that we have quicker access to the audio playback state. 485 int originalAttrs = session.getSessionAttr(); 486 // If there are active audio playback clients, then the session has playback. 487 if (!session.getAudioResourcesByType().get(SESSION_ATTR_HAS_AUDIO_PLAYBACK).isEmpty()) { 488 session.setBit(SESSION_ATTR_HAS_AUDIO_PLAYBACK); 489 } else { 490 session.clearBit(SESSION_ATTR_HAS_AUDIO_PLAYBACK); 491 } 492 493 // If there was a change, log to a call if set. 494 if (originalAttrs != session.getSessionAttr() && session.getTelecomCall() != null) { 495 Log.addEvent(session.getTelecomCall(), LogUtils.Events.AUDIO_ATTR, 496 CommunicationSession.sessionAttrToString(originalAttrs) 497 + " -> " + CommunicationSession.sessionAttrToString( 498 session.getSessionAttr())); 499 } 500 Log.d(this, "maybeTrackAudioPlayback: %s", session); 501 } 502 503 /** 504 * Similar to {@link #maybeTrackAudioPlayback(int, int, boolean)}, except tracks audio records 505 * for an app. 506 * @param uid the app uid. 507 * @param recordSessionID The recording session (per 508 * @param isRecording {@code true} if recording, {@code false} otherwise. 509 */ maybeTrackAudioRecord(int uid, int recordSessionID, boolean isRecording)510 private void maybeTrackAudioRecord(int uid, int recordSessionID, boolean isRecording) { 511 synchronized (mCommunicationSessionsLock) { 512 CommunicationSession session = getOrAddSession(uid); 513 514 // First track individual recording status. 515 if (isRecording) { 516 putOrDefault(session.getAudioResourcesByType(), SESSION_ATTR_HAS_AUDIO_RECORD, 517 new ArraySet<>()).add(recordSessionID); 518 } else { 519 putOrDefault(session.getAudioResourcesByType(), SESSION_ATTR_HAS_AUDIO_RECORD, 520 new ArraySet<>()).remove(recordSessionID); 521 } 522 523 int originalAttrs = session.getSessionAttr(); 524 if (!session.getAudioResourcesByType().get(SESSION_ATTR_HAS_AUDIO_RECORD).isEmpty()) { 525 session.setBit(SESSION_ATTR_HAS_AUDIO_RECORD); 526 } else { 527 session.clearBit(SESSION_ATTR_HAS_AUDIO_RECORD); 528 } 529 530 if (originalAttrs != session.getSessionAttr() && session.getTelecomCall() != null) { 531 Log.addEvent(session.getTelecomCall(), LogUtils.Events.AUDIO_ATTR, 532 CommunicationSession.sessionAttrToString(originalAttrs) 533 + " -> " + CommunicationSession.sessionAttrToString( 534 session.getSessionAttr())); 535 } 536 537 Log.d(this, "maybeTrackAudioRecord: %s", session); 538 } 539 } 540 541 /** 542 * Given a new Telecom call, start a new session or annotate an existing one with this call. 543 * Helps to associated resources with a telecom call. 544 * @param call the call! 545 */ maybeTrackTelecomCall(Call call)546 private void maybeTrackTelecomCall(Call call) { 547 int uid = mPhoneAccountRegistrarProxy.getUidForPhoneAccountHandle( 548 call.getTargetPhoneAccount()); 549 CommunicationSession session; 550 synchronized (mCommunicationSessionsLock) { 551 session = getOrAddSession(uid); 552 } 553 session.setTelecomCall(call); 554 Log.d(this, "maybeTrackTelecomCall: %s", session); 555 Log.addEvent(session.getTelecomCall(), LogUtils.Events.AUDIO_ATTR, 556 CommunicationSession.sessionAttrToString(session.getSessionAttr())); 557 } 558 559 /** 560 * Given a telecom call, cleanup the session if there are no audio resources remaining for that 561 * session. 562 * @param call The call. 563 */ maybeRemoveCall(Call call)564 private void maybeRemoveCall(Call call) { 565 int uid = mPhoneAccountRegistrarProxy.getUidForPhoneAccountHandle( 566 call.getTargetPhoneAccount()); 567 CommunicationSession session; 568 synchronized (mCommunicationSessionsLock) { 569 session = getSession(uid); 570 if (session == null) { 571 return; 572 } 573 if (!session.hasMediaResources()) { 574 mLocalLog.log(session.toString()); 575 maybeLogMetrics(session); 576 mCommunicationSessions.remove(uid); 577 } 578 } 579 } 580 581 /** 582 * Returns an existing session for a uid, or {@code null} if none exists. 583 * @param uid the uid, 584 * @return The session found, or {@code null}. 585 */ getSession(int uid)586 private CommunicationSession getSession(int uid) { 587 return mCommunicationSessions.get(uid); 588 } 589 590 /** 591 * Locates an existing session for the specified uid or creates a new one. 592 * @param uid the uid 593 * @return The session. 594 */ getOrAddSession(int uid)595 private CommunicationSession getOrAddSession(int uid) { 596 CommunicationSession session = mCommunicationSessions.get(uid); 597 if (session != null) { 598 Log.i(this, "getOrAddSession: uid=%d, ex, %s", uid, session); 599 return session; 600 } else { 601 CommunicationSession newSession = new CommunicationSession(); 602 newSession.setSessionStartMillis(mClockProxy.elapsedRealtime()); 603 newSession.setSessionStartClockMillis(mClockProxy.currentTimeMillis()); 604 newSession.setUid(uid); 605 if (mPhoneAccountRegistrarProxy.hasPhoneAccountForUid(uid)) { 606 newSession.setBit(SESSION_ATTR_HAS_PHONE_ACCOUNT); 607 } 608 mCommunicationSessions.put(uid, newSession); 609 Log.i(this, "getOrAddSession: uid=%d, new, %s", uid, newSession); 610 return newSession; 611 } 612 } 613 614 /** 615 * This method is used to cleanup any playback or recording sessions that may have went away 616 * after the {@link AudioPlaybackConfiguration} or {@link AudioRecordingConfiguration} updates. 617 * 618 * {@link CommunicationSession#audioResourcesByType} is keyed by 619 * {@link #SESSION_ATTR_HAS_AUDIO_RECORD} and {@link #SESSION_ATTR_HAS_AUDIO_PLAYBACK} and 620 * contains a list of each of the record or playback sessions we've been tracking. 621 * 622 * @param bit the type of resources to cleanup. 623 * @param sessionsByUid A map, keyed on uid of the set of play or record ids that were provided 624 * in the most recent {@link AudioPlaybackConfiguration} or 625 * {@link AudioRecordingConfiguration} update. 626 */ cleanupAttributeForSessions(int bit, Map<Integer, Set<Integer>> sessionsByUid)627 private void cleanupAttributeForSessions(int bit, Map<Integer, Set<Integer>> sessionsByUid) { 628 synchronized (mCommunicationSessionsLock) { 629 // Use an iterator so we can do in-place removal. 630 Iterator<Map.Entry<Integer, CommunicationSession>> iterator = 631 mCommunicationSessions.entrySet().iterator(); 632 633 // Lets loop through all the uids we're tracking and see that they still have an audio 634 // resource of type {@code bit} in {@code sessionsByUid}. 635 while (iterator.hasNext()) { 636 Map.Entry<Integer, CommunicationSession> next = iterator.next(); 637 int existingUid = next.getKey(); 638 CommunicationSession session = next.getValue(); 639 640 // Get the set of sessions for this type, or emptyset if none present. 641 Set<Integer> sessionsForThisUid = sessionsByUid.getOrDefault(existingUid, 642 Collections.emptySet()); 643 644 // Update the known sessions of this resource type in the CommunicationSession. 645 Set<Integer> trackedSessions = putOrDefault(session.getAudioResourcesByType(), bit, 646 new ArraySet<>()); 647 trackedSessions.clear(); 648 trackedSessions.addAll(sessionsForThisUid); 649 650 // Set or unset the bit in the bitmask for quicker access. 651 if (!trackedSessions.isEmpty()) { 652 session.setBit(bit); 653 } else { 654 session.clearBit(bit); 655 } 656 657 // If audio resources are no longer held for a uid, then we'll clean up its 658 // media session. 659 if (!session.hasMediaResources() && session.getTelecomCall() == null) { 660 Log.i(this, "cleanupAttributeForSessions: removing session %s", session); 661 mLocalLog.log(session.toString()); 662 maybeLogMetrics(session); 663 iterator.remove(); 664 } 665 } 666 } 667 } 668 669 /** 670 * Generic method to put a key value to a map and set to a default it not found, in both cases 671 * returning the value. 672 * 673 * This is a concession due to the fact that {@link Map#putIfAbsent(Object, Object)} returns 674 * null if the default is set. 675 * 676 * @param map The map. 677 * @param key The key to find. 678 * @param theDefault The default value for the key to use and return if nothing found. 679 * @return The existing key value or the default after adding. 680 * @param <K> The map key 681 * @param <V> The map value 682 */ putOrDefault(Map<K,V> map, K key, V theDefault)683 private <K,V> V putOrDefault(Map<K,V> map, K key, V theDefault) { 684 if (map.containsKey(key)) { 685 return map.get(key); 686 } 687 688 map.put(key, theDefault); 689 return theDefault; 690 } 691 692 /** 693 * If this call has no associated Telecom {@link Call} and metrics are enabled, log this as a 694 * non-telecom call. 695 * @param session the session to log. 696 */ maybeLogMetrics(CommunicationSession session)697 private void maybeLogMetrics(CommunicationSession session) { 698 if (mMetricsController != null && session.getTelecomCall() == null) { 699 mMetricsController.getCallStats().onNonTelecomCallEnd( 700 session.isBitSet(SESSION_ATTR_HAS_PHONE_ACCOUNT), 701 session.getUid(), 702 mClockProxy.elapsedRealtime() - session.getSessionStartMillis()); 703 } 704 } 705 } 706