• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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