| /* |
| * Copyright (C) 2024 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.server.telecom; |
| |
| import static android.media.AudioPlaybackConfiguration.PLAYER_STATE_STARTED; |
| |
| import android.annotation.IntDef; |
| import android.media.AudioAttributes; |
| import android.media.AudioManager; |
| import android.media.AudioManager.AudioPlaybackCallback; |
| import android.media.AudioPlaybackConfiguration; |
| import android.media.AudioRecord; |
| import android.media.AudioRecordingConfiguration; |
| import android.media.AudioTrack; |
| import android.media.MediaRecorder; |
| import android.os.Handler; |
| import android.os.Process; |
| import android.telecom.Log; |
| import android.telecom.Logging.EventManager; |
| import android.telecom.PhoneAccountHandle; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.IndentingPrintWriter; |
| import android.util.LocalLog; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.server.telecom.metrics.TelecomMetricsController; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.text.SimpleDateFormat; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.UUID; |
| |
| /** |
| * Monitors {@link AudioRecord}, {@link AudioTrack}, and {@link AudioManager#getMode()} to determine |
| * the reliability of audio operations for a call. Augments the Telecom dumpsys with Telecom calls |
| * with information about calls. |
| */ |
| public class CallAudioWatchdog extends CallsManagerListenerBase |
| implements AudioModeTracker.AudioModeListener { |
| |
| public static final UUID WATCHDOG_SUPPRESSED_SELF_MANAGED_CALL_UUID = |
| UUID.fromString("51307b0c-45fb-4972-8d77-6cb5f6063e7c"); |
| public static final String WATCHDOG_SUPPRESSED_SELF_MANAGED_CALL_MSG = |
| "A self-managed call was suppressed due to a platform action (e.g. losing foreground " |
| + "service)"; |
| /** |
| * Bit flag set on a {@link CommunicationSession#sessionAttr} to indicate that the session has |
| * audio recording resources. |
| */ |
| public static final int SESSION_ATTR_HAS_AUDIO_RECORD = 1 << 0; |
| |
| /** |
| * Bit flag set on a {@link CommunicationSession#sessionAttr} to indicate that the session has |
| * audio playback resources. |
| */ |
| public static final int SESSION_ATTR_HAS_AUDIO_PLAYBACK = 1 << 1; |
| |
| /** |
| * Bit flag set on a {@link CommunicationSession#sessionAttr} to indicate that the uid for the |
| * session has a phone account allocated. This helps us track cases where an app is telecom |
| * capable but chooses not to use the telecom integration. |
| */ |
| public static final int SESSION_ATTR_HAS_PHONE_ACCOUNT = 1 << 2; |
| |
| /** |
| * Bit flag set on a {@link CommunicationSession#sessionAttr} to indicate that the audio mode |
| * was in communication during this session. |
| */ |
| public static final int SESSION_ATTR_IS_IN_COMMUNICATION = 1 << 3; |
| |
| @IntDef(prefix = { "SESSION_ATTR_" }, |
| value = {SESSION_ATTR_HAS_AUDIO_RECORD, SESSION_ATTR_HAS_AUDIO_PLAYBACK, |
| SESSION_ATTR_HAS_PHONE_ACCOUNT, SESSION_ATTR_IS_IN_COMMUNICATION}, |
| flag = true) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface SessionAttribute {} |
| |
| /** |
| * Proxy for operations related to phone accounts. |
| */ |
| public interface PhoneAccountRegistrarProxy { |
| /** |
| * Determines if a specified {@code uid} has an associated phone account registered. |
| * @param uid the uid. |
| * @return {@code true} if there is a phone account registered, {@code false} otherwise |
| */ |
| boolean hasPhoneAccountForUid(int uid); |
| |
| /** |
| * Given a {@link PhoneAccountHandle} determines the uid for the app owning the account. |
| * @param handle The phone account; the phone account handle's package and userhandle are |
| * ultimately used to find the associated uid. |
| * @return the uid for the phone account. |
| */ |
| int getUidForPhoneAccountHandle(PhoneAccountHandle handle); |
| } |
| |
| /** |
| * Keyed on uid, tracks a communication session and whether there are audio record and playback |
| * resources for that session. |
| */ |
| public class CommunicationSession { |
| private int uid; |
| @SessionAttribute |
| private int sessionAttr; |
| private ArrayMap<Integer, Set<Integer>> audioResourcesByType = new ArrayMap<>(); |
| private EventManager.Loggable telecomCall; |
| private long sessionStartMillis; |
| private long sessionStartClockMillis; |
| |
| /** |
| * @return {@code true} if audio record or playback is held for the session, {@code false} |
| * otherwise. |
| */ |
| public boolean hasMediaResources() { |
| return (getSessionAttr() |
| & (SESSION_ATTR_HAS_AUDIO_RECORD | SESSION_ATTR_HAS_AUDIO_PLAYBACK)) != 0; |
| } |
| |
| /** |
| * Sets a bit enabled for the session. |
| * @param bit the bit |
| */ |
| public void setBit(@SessionAttribute int bit) { |
| setSessionAttr(getSessionAttr() | bit); |
| } |
| |
| /** |
| * Clears the specified bit for the session. |
| * @param bit the bit |
| */ |
| public void clearBit(@SessionAttribute int bit) { |
| setSessionAttr(getSessionAttr() & ~bit); |
| } |
| |
| /** |
| * Determines if a bit is set in the given bitmask. |
| * @param mask the bitmask. |
| * @param bit The bit |
| * @return {@code true} if set, {@code false} otherwise. |
| */ |
| public static boolean isBitSet(@SessionAttribute int mask, @SessionAttribute int bit) { |
| return (mask & bit) == bit; |
| } |
| |
| /** |
| * Determines if a bit is set for the current session. |
| * @param bit The bit |
| * @return {@code true} if set, {@code false} otherwise. |
| */ |
| public boolean isBitSet(@SessionAttribute int bit) { |
| return isBitSet(getSessionAttr(), bit); |
| } |
| |
| /** |
| * Generate a string representing the session attributes bitmask, suitable for logging. |
| * @param attr The session attributes. |
| * @return String of bits! |
| */ |
| public static String sessionAttrToString(@SessionAttribute int attr) { |
| return (isBitSet(attr, SESSION_ATTR_HAS_PHONE_ACCOUNT) ? "phac, " : "") + |
| (isBitSet(attr, SESSION_ATTR_HAS_AUDIO_PLAYBACK) ? "ap, " : "") + |
| (isBitSet(attr, SESSION_ATTR_HAS_AUDIO_RECORD) ? "ar, " : "") + |
| (isBitSet(attr, SESSION_ATTR_IS_IN_COMMUNICATION) ? "comm, " : ""); |
| } |
| |
| @Override |
| public String toString() { |
| return "CommSess{" + |
| "uid=" + getUid() + |
| ", created=" + SimpleDateFormat.getDateTimeInstance().format( |
| new Date(getSessionStartClockMillis())) + |
| ", attr=" + sessionAttrToString(getSessionAttr()) + |
| ", callId=" + (getTelecomCall() != null ? getTelecomCall().getId() : "none") + |
| ", duration=" + (mClockProxy.elapsedRealtime() - getSessionStartMillis())/1000 + |
| '}'; |
| } |
| |
| /** |
| * The uid for the session. |
| */ |
| public int getUid() { |
| return uid; |
| } |
| |
| public void setUid(int uid) { |
| this.uid = uid; |
| } |
| |
| /** |
| * The attributes for the session. |
| */ |
| public int getSessionAttr() { |
| return sessionAttr; |
| } |
| |
| public void setSessionAttr(int sessionAttr) { |
| this.sessionAttr = sessionAttr; |
| } |
| |
| /** |
| * ArrayMap, keyed by {@link #SESSION_ATTR_HAS_AUDIO_PLAYBACK} and |
| * {@link #SESSION_ATTR_HAS_AUDIO_RECORD}. For each, contains a set of the |
| * {@link AudioManager} ids associated with active playback and recording sessions for a |
| * uid. |
| * |
| * {@link AudioPlaybackConfiguration#getPlayerInterfaceId()} is used for audio playback; |
| * per docs, this is an identifier unique for the lifetime of the player. |
| * |
| * {@link AudioRecordingConfiguration#getClientAudioSessionId()} is used for audio record |
| * tracking; this is unique similar to the audio playback config. |
| */ |
| public ArrayMap<Integer, Set<Integer>> getAudioResourcesByType() { |
| return audioResourcesByType; |
| } |
| |
| public void setAudioResourcesByType( |
| ArrayMap<Integer, Set<Integer>> audioResourcesByType) { |
| this.audioResourcesByType = audioResourcesByType; |
| } |
| |
| /** |
| * The Telecom call this session is associated with; set if the call takes place during a |
| * telecom call. |
| */ |
| public EventManager.Loggable getTelecomCall() { |
| return telecomCall; |
| } |
| |
| public void setTelecomCall(EventManager.Loggable telecomCall) { |
| this.telecomCall = telecomCall; |
| } |
| |
| /** |
| * The time in {@link android.os.SystemClock#elapsedRealtime()} timebase when the session |
| * started. Used only to determine duration. |
| */ |
| public long getSessionStartMillis() { |
| return sessionStartMillis; |
| } |
| |
| public void setSessionStartMillis(long sessionStartMillis) { |
| this.sessionStartMillis = sessionStartMillis; |
| } |
| |
| /** |
| * The time in {@link System#currentTimeMillis()} timebase when the session started; used |
| * to indicate the wall block time when the session started. |
| */ |
| public long getSessionStartClockMillis() { |
| return sessionStartClockMillis; |
| } |
| |
| public void setSessionStartClockMillis(long sessionStartClockMillis) { |
| this.sessionStartClockMillis = sessionStartClockMillis; |
| } |
| } |
| |
| /** |
| * Listener for AudioManager audio playback changes. Finds audio playback tagged for voice |
| * communication. Updates the {@link #mCommunicationSessions} based on this data to track if |
| * audio playback it taking place. |
| * |
| * Note: {@link AudioPlaybackCallback} reports information about audio playback for an app; if |
| * an app releases audio playback resources, the list of audio playback configurations no longer |
| * includes a {@link AudioPlaybackConfiguration} for that specific audio playback session. This |
| * API semantic is why the code below is a bit confusing; in the listener we need to track all |
| * the ids we've seen and then correlate that back to what we knew about it from the last |
| * callback. |
| * |
| * An app may have MULTIPLE {@link AudioPlaybackConfiguration} for voip use-cases and switch |
| * between them for a single call -- this was observed in live app testing. |
| */ |
| public class WatchdogAudioPlaybackCallback extends AudioPlaybackCallback { |
| @Override |
| public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) { |
| Map<Integer,Set<Integer>> sessionIdentifiersByUid = new ArrayMap<>(); |
| for (AudioPlaybackConfiguration config : configs) { |
| Log.d(this, "onPlaybackConfigChanged: config=%s", config); |
| // only track USAGE_VOICE_COMMUNICATION as this is for VOIP calls. |
| if (config.getAudioAttributes() != null |
| && config.getAudioAttributes().getUsage() |
| == AudioAttributes.USAGE_VOICE_COMMUNICATION) { |
| |
| // Skip if the client's pid is same as myself |
| if (config.getClientPid() == Process.myPid()) { |
| continue; |
| } |
| |
| // If an audio session is idle, we don't count it as playing. It must be in a |
| // started state. |
| boolean isPlaying = config.getPlayerState() == PLAYER_STATE_STARTED; |
| |
| maybeTrackAudioPlayback(config.getClientUid(), config.getPlayerInterfaceId(), |
| isPlaying); |
| if (isPlaying) { |
| // Track the list of player id active for each uid; we use it later for |
| // cleanup of stale sessions. |
| putOrDefault(sessionIdentifiersByUid,config.getClientUid(), |
| new ArraySet<>()).add(config.getPlayerInterfaceId()); |
| } |
| } |
| } |
| |
| // The listener will drop uid/playerInterfaceIds no longer active, so we need to go back |
| // and see if any sessions need to be removed now. |
| cleanupAttributeForSessions(SESSION_ATTR_HAS_AUDIO_PLAYBACK, |
| sessionIdentifiersByUid); |
| } |
| } |
| |
| /** |
| * Similar to {@link WatchdogAudioPlaybackCallback}, tracks audio recording an app performs. |
| * This code is handling the onRecordingConfigChanged event from the AudioManager. The event |
| * is fired when the list of active recording configurations changes. In this case, the code |
| * is only interested in recording configurations that are using the VOICE_COMMUNICATION |
| * audio source. For these configurations, the code tracks the session identifiers and |
| * potentially adds them to the SESSION_ATTR_HAS_AUDIO_RECORD attribute. The code also cleans |
| * up the attribute for any sessions that are no longer active. |
| * The same caveat/note applies here; a single app can have many audio recording sessions that |
| * the app swaps between during a call. |
| */ |
| public class WatchdogAudioRecordCallback extends AudioManager.AudioRecordingCallback { |
| @Override |
| public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) { |
| List<AudioRecordingConfiguration> theConfigs = |
| mAudioManager.getActiveRecordingConfigurations(); |
| Map<Integer,Set<Integer>> sessionIdentifiersByUid = new ArrayMap<>(); |
| for (AudioRecordingConfiguration config : theConfigs) { |
| if (config.getClientAudioSource() |
| == MediaRecorder.AudioSource.VOICE_COMMUNICATION) { |
| |
| putOrDefault(sessionIdentifiersByUid, config.getClientUid(), |
| new ArraySet<>()).add(config.getClientAudioSessionId()); |
| maybeTrackAudioRecord(config.getClientUid(), config.getClientAudioSessionId(), |
| true); |
| |
| // Note: This is a temporary measure. We are using the isClientSilenced |
| // API to detect when the platform has suppressed a transactional call's |
| // audio recording (for example, if the app loses its foreground service |
| // or permission). We report an anomaly so we can track the frequency |
| // of these occurrences. |
| if (config.isClientSilenced()) { |
| CommunicationSession session; |
| synchronized (mCommunicationSessionsLock) { |
| session = getSession(config.getClientUid()); |
| } |
| if (session != null && session.getTelecomCall() instanceof Call |
| && ((Call) session.getTelecomCall()).isTransactionalCall() |
| && ((Call) session.getTelecomCall()).isActive()) { |
| Log.i(CallAudioWatchdog.this, "onRecordingConfigChanged: transactional " |
| + "call for uid=%d was silenced by platform", config.getClientUid()); |
| mAnomalyReporter.reportAnomaly( |
| WATCHDOG_SUPPRESSED_SELF_MANAGED_CALL_UUID, |
| WATCHDOG_SUPPRESSED_SELF_MANAGED_CALL_MSG); |
| } |
| } |
| } |
| } |
| // The listener stops reporting audio sessions that go away, so we need to clean up the |
| // session potentially. |
| cleanupAttributeForSessions( |
| SESSION_ATTR_HAS_AUDIO_RECORD, |
| sessionIdentifiersByUid); |
| } |
| } |
| |
| // Proxies to make testing possible-ish. |
| private final ClockProxy mClockProxy; |
| private final PhoneAccountRegistrarProxy mPhoneAccountRegistrarProxy; |
| |
| private final WatchdogAudioPlaybackCallback mWatchdogAudioPlayback = |
| new WatchdogAudioPlaybackCallback(); |
| private final WatchdogAudioRecordCallback |
| mWatchdogAudioRecordCallack = new WatchdogAudioRecordCallback(); |
| private final AudioManager mAudioManager; |
| private final Handler mHandler; |
| |
| // Guards access to mCommunicationSessions. |
| private final Object mCommunicationSessionsLock = new Object(); |
| |
| /** |
| * Key - UID of communication app. |
| * Value - an instance of {@link CommunicationSession} tracking data for that uid. |
| */ |
| private final Map<Integer, CommunicationSession> mCommunicationSessions = new ArrayMap<>(); |
| |
| // Local logs for tracking non-telecom calls. |
| private final LocalLog mLocalLog = new LocalLog(30); |
| |
| private final TelecomMetricsController mMetricsController; |
| private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl(); |
| |
| // The current audio mode as reported by audio manager. |
| private int mCurrentAudioMode = AudioManager.MODE_NORMAL; |
| |
| public CallAudioWatchdog(AudioManager audioManager, |
| PhoneAccountRegistrarProxy phoneAccountRegistrarProxy, ClockProxy clockProxy, |
| Handler handler, TelecomMetricsController metricsController) { |
| mPhoneAccountRegistrarProxy = phoneAccountRegistrarProxy; |
| mClockProxy = clockProxy; |
| mAudioManager = audioManager; |
| mHandler = handler; |
| mAudioManager.registerAudioPlaybackCallback(mWatchdogAudioPlayback, mHandler); |
| mAudioManager.registerAudioRecordingCallback(mWatchdogAudioRecordCallack, mHandler); |
| mMetricsController = metricsController; |
| } |
| |
| /** |
| * Tracks Telecom adding a call; we use this to associate a uid's sessions with a call. |
| * Note: this is not 100% accurate if there are multiple calls -- we just associate with the |
| * first call and leave it at that. It's not possible to know which audio sessions belong to |
| * which Telecom calls. |
| * @param call the Telecom call being added. |
| */ |
| @Override |
| public void onCallAdded(Call call) { |
| // Only track for voip calls. |
| if (call.isSelfManaged() || call.isTransactionalCall()) { |
| maybeTrackTelecomCall(call); |
| } |
| } |
| |
| @Override |
| public void onCallRemoved(Call call) { |
| // Only track for voip calls. |
| if (call.isSelfManaged() || call.isTransactionalCall()) { |
| maybeRemoveCall(call); |
| } |
| } |
| |
| @VisibleForTesting |
| public WatchdogAudioPlaybackCallback getWatchdogAudioPlayback() { |
| return mWatchdogAudioPlayback; |
| } |
| |
| @VisibleForTesting |
| public WatchdogAudioRecordCallback getWatchdogAudioRecordCallack() { |
| return mWatchdogAudioRecordCallack; |
| } |
| |
| @VisibleForTesting |
| public Map<Integer, CommunicationSession> getCommunicationSessions() { |
| return mCommunicationSessions; |
| } |
| |
| /** |
| * Include info on audio stuff in the telecom dumpsys. |
| * @param pw |
| */ |
| void dump(IndentingPrintWriter pw) { |
| pw.println("CallAudioWatchdog:"); |
| pw.increaseIndent(); |
| pw.println("Active Sessions:"); |
| pw.increaseIndent(); |
| Collection<CommunicationSession> sessions; |
| synchronized (mCommunicationSessionsLock) { |
| sessions = mCommunicationSessions.values(); |
| } |
| sessions.forEach(pw::println); |
| pw.decreaseIndent(); |
| pw.println("Audio sessions Sessions:"); |
| pw.increaseIndent(); |
| mLocalLog.dump(pw); |
| pw.decreaseIndent(); |
| pw.decreaseIndent(); |
| } |
| |
| /** |
| * Tracks audio playback for a uid. |
| * @param uid the uid of the app having audio back change. |
| * @param playerInterfaceId From {@link AudioPlaybackConfiguration#getPlayerInterfaceId()} (see |
| * {@link CommunicationSession#audioResourcesByType} for keying info). |
| * @param isPlaying {@code true} if audio is starting for the client. |
| */ |
| private void maybeTrackAudioPlayback(int uid, int playerInterfaceId, boolean isPlaying) { |
| CommunicationSession session; |
| synchronized (mCommunicationSessionsLock) { |
| if (!isPlaying) { |
| // A session can start in an idle state and never go active; in this case we will |
| // not proactively add a new session; we'll just get one if it's already there. |
| // When the session goes active we can add it then. |
| session = getSession(uid); |
| } else { |
| // The playback is active, so we need to get or add a new communication session. |
| session = getOrAddSession(uid); |
| } |
| } |
| if (session == null) { |
| return; |
| } |
| |
| // First track individual player interface id playing status. |
| if (isPlaying) { |
| putOrDefault(session.getAudioResourcesByType(), SESSION_ATTR_HAS_AUDIO_PLAYBACK, |
| new ArraySet<>()).add(playerInterfaceId); |
| } else { |
| putOrDefault(session.getAudioResourcesByType(), SESSION_ATTR_HAS_AUDIO_PLAYBACK, |
| new ArraySet<>()).remove(playerInterfaceId); |
| } |
| |
| // Keep the bitmask up to date so that we have quicker access to the audio playback state. |
| int originalAttrs = session.getSessionAttr(); |
| // If there are active audio playback clients, then the session has playback. |
| if (!session.getAudioResourcesByType().get(SESSION_ATTR_HAS_AUDIO_PLAYBACK).isEmpty()) { |
| session.setBit(SESSION_ATTR_HAS_AUDIO_PLAYBACK); |
| } else { |
| session.clearBit(SESSION_ATTR_HAS_AUDIO_PLAYBACK); |
| } |
| |
| // If there was a change, log to a call if set. |
| if (originalAttrs != session.getSessionAttr() && session.getTelecomCall() != null) { |
| Log.addEvent(session.getTelecomCall(), LogUtils.Events.AUDIO_ATTR, |
| CommunicationSession.sessionAttrToString(originalAttrs) |
| + " -> " + CommunicationSession.sessionAttrToString( |
| session.getSessionAttr())); |
| } |
| Log.d(this, "maybeTrackAudioPlayback: %s", session); |
| } |
| |
| /** |
| * Similar to {@link #maybeTrackAudioPlayback(int, int, boolean)}, except tracks audio records |
| * for an app. |
| * @param uid the app uid. |
| * @param recordSessionID The recording session (per |
| * @param isRecording {@code true} if recording, {@code false} otherwise. |
| */ |
| private void maybeTrackAudioRecord(int uid, int recordSessionID, boolean isRecording) { |
| synchronized (mCommunicationSessionsLock) { |
| CommunicationSession session = getOrAddSession(uid); |
| |
| // First track individual recording status. |
| if (isRecording) { |
| putOrDefault(session.getAudioResourcesByType(), SESSION_ATTR_HAS_AUDIO_RECORD, |
| new ArraySet<>()).add(recordSessionID); |
| } else { |
| putOrDefault(session.getAudioResourcesByType(), SESSION_ATTR_HAS_AUDIO_RECORD, |
| new ArraySet<>()).remove(recordSessionID); |
| } |
| |
| int originalAttrs = session.getSessionAttr(); |
| if (!session.getAudioResourcesByType().get(SESSION_ATTR_HAS_AUDIO_RECORD).isEmpty()) { |
| session.setBit(SESSION_ATTR_HAS_AUDIO_RECORD); |
| } else { |
| session.clearBit(SESSION_ATTR_HAS_AUDIO_RECORD); |
| } |
| |
| if (originalAttrs != session.getSessionAttr() && session.getTelecomCall() != null) { |
| Log.addEvent(session.getTelecomCall(), LogUtils.Events.AUDIO_ATTR, |
| CommunicationSession.sessionAttrToString(originalAttrs) |
| + " -> " + CommunicationSession.sessionAttrToString( |
| session.getSessionAttr())); |
| } |
| |
| Log.d(this, "maybeTrackAudioRecord: %s", session); |
| } |
| } |
| |
| /** |
| * Given a new Telecom call, start a new session or annotate an existing one with this call. |
| * Helps to associated resources with a telecom call. |
| * @param call the call! |
| */ |
| private void maybeTrackTelecomCall(Call call) { |
| int uid = mPhoneAccountRegistrarProxy.getUidForPhoneAccountHandle( |
| call.getTargetPhoneAccount()); |
| CommunicationSession session; |
| synchronized (mCommunicationSessionsLock) { |
| session = getOrAddSession(uid); |
| } |
| session.setTelecomCall(call); |
| Log.d(this, "maybeTrackTelecomCall: %s", session); |
| Log.addEvent(session.getTelecomCall(), LogUtils.Events.AUDIO_ATTR, |
| CommunicationSession.sessionAttrToString(session.getSessionAttr())); |
| } |
| |
| /** |
| * Given a telecom call, cleanup the session if there are no audio resources remaining for that |
| * session. |
| * @param call The call. |
| */ |
| private void maybeRemoveCall(Call call) { |
| int uid = mPhoneAccountRegistrarProxy.getUidForPhoneAccountHandle( |
| call.getTargetPhoneAccount()); |
| CommunicationSession session; |
| synchronized (mCommunicationSessionsLock) { |
| session = getSession(uid); |
| if (session == null) { |
| return; |
| } |
| if (!session.hasMediaResources()) { |
| mLocalLog.log(session.toString()); |
| maybeLogMetrics(session); |
| mCommunicationSessions.remove(uid); |
| } |
| } |
| } |
| |
| @Override |
| public void onAudioModeChanged(int mode) { |
| mCurrentAudioMode = mode; |
| synchronized (mCommunicationSessionsLock) { |
| if (mode == AudioManager.MODE_IN_COMMUNICATION) { |
| for (CommunicationSession session : mCommunicationSessions.values()) { |
| int originalAttrs = session.getSessionAttr(); |
| session.setBit(SESSION_ATTR_IS_IN_COMMUNICATION); |
| if (originalAttrs != session.getSessionAttr() |
| && session.getTelecomCall() != null) { |
| Log.addEvent(session.getTelecomCall(), LogUtils.Events.AUDIO_ATTR, |
| CommunicationSession.sessionAttrToString(originalAttrs) |
| + " -> " + CommunicationSession.sessionAttrToString( |
| session.getSessionAttr())); |
| } |
| } |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| public void setAudioModeForTesting(int audioMode) { |
| mCurrentAudioMode = audioMode; |
| } |
| |
| /** |
| * Returns an existing session for a uid, or {@code null} if none exists. |
| * @param uid the uid, |
| * @return The session found, or {@code null}. |
| */ |
| private CommunicationSession getSession(int uid) { |
| return mCommunicationSessions.get(uid); |
| } |
| |
| /** |
| * Locates an existing session for the specified uid or creates a new one. |
| * @param uid the uid |
| * @return The session. |
| */ |
| private CommunicationSession getOrAddSession(int uid) { |
| CommunicationSession session = mCommunicationSessions.get(uid); |
| if (session != null) { |
| Log.i(this, "getOrAddSession: uid=%d, ex, %s", uid, session); |
| return session; |
| } else { |
| CommunicationSession newSession = new CommunicationSession(); |
| newSession.setSessionStartMillis(mClockProxy.elapsedRealtime()); |
| newSession.setSessionStartClockMillis(mClockProxy.currentTimeMillis()); |
| newSession.setUid(uid); |
| if (mPhoneAccountRegistrarProxy.hasPhoneAccountForUid(uid)) { |
| newSession.setBit(SESSION_ATTR_HAS_PHONE_ACCOUNT); |
| } |
| // IMPORTANT: DO NOT CALL AudioManager to get the mode; use the cached value here. |
| // Calling AudioManager from here to get the mode will result in a call back to Telecom |
| // from AudioManager, which will likely cause a deadlock. |
| if (mCurrentAudioMode == AudioManager.MODE_IN_COMMUNICATION) { |
| newSession.setBit(SESSION_ATTR_IS_IN_COMMUNICATION); |
| } |
| mCommunicationSessions.put(uid, newSession); |
| Log.i(this, "getOrAddSession: uid=%d, new, %s", uid, newSession); |
| return newSession; |
| } |
| } |
| |
| /** |
| * This method is used to cleanup any playback or recording sessions that may have went away |
| * after the {@link AudioPlaybackConfiguration} or {@link AudioRecordingConfiguration} updates. |
| * |
| * {@link CommunicationSession#audioResourcesByType} is keyed by |
| * {@link #SESSION_ATTR_HAS_AUDIO_RECORD} and {@link #SESSION_ATTR_HAS_AUDIO_PLAYBACK} and |
| * contains a list of each of the record or playback sessions we've been tracking. |
| * |
| * @param bit the type of resources to cleanup. |
| * @param sessionsByUid A map, keyed on uid of the set of play or record ids that were provided |
| * in the most recent {@link AudioPlaybackConfiguration} or |
| * {@link AudioRecordingConfiguration} update. |
| */ |
| private void cleanupAttributeForSessions(int bit, Map<Integer, Set<Integer>> sessionsByUid) { |
| synchronized (mCommunicationSessionsLock) { |
| // Use an iterator so we can do in-place removal. |
| Iterator<Map.Entry<Integer, CommunicationSession>> iterator = |
| mCommunicationSessions.entrySet().iterator(); |
| |
| // Lets loop through all the uids we're tracking and see that they still have an audio |
| // resource of type {@code bit} in {@code sessionsByUid}. |
| while (iterator.hasNext()) { |
| Map.Entry<Integer, CommunicationSession> next = iterator.next(); |
| int existingUid = next.getKey(); |
| CommunicationSession session = next.getValue(); |
| |
| // Get the set of sessions for this type, or emptyset if none present. |
| Set<Integer> sessionsForThisUid = sessionsByUid.getOrDefault(existingUid, |
| Collections.emptySet()); |
| |
| // Update the known sessions of this resource type in the CommunicationSession. |
| Set<Integer> trackedSessions = putOrDefault(session.getAudioResourcesByType(), bit, |
| new ArraySet<>()); |
| trackedSessions.clear(); |
| trackedSessions.addAll(sessionsForThisUid); |
| |
| // Set or unset the bit in the bitmask for quicker access. |
| if (!trackedSessions.isEmpty()) { |
| session.setBit(bit); |
| } else { |
| session.clearBit(bit); |
| } |
| |
| // If audio resources are no longer held for a uid, then we'll clean up its |
| // media session. |
| if (!session.hasMediaResources() && session.getTelecomCall() == null) { |
| Log.i(this, "cleanupAttributeForSessions: removing session %s", session); |
| mLocalLog.log(session.toString()); |
| maybeLogMetrics(session); |
| iterator.remove(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Generic method to put a key value to a map and set to a default it not found, in both cases |
| * returning the value. |
| * |
| * This is a concession due to the fact that {@link Map#putIfAbsent(Object, Object)} returns |
| * null if the default is set. 🙄 |
| * |
| * @param map The map. |
| * @param key The key to find. |
| * @param theDefault The default value for the key to use and return if nothing found. |
| * @return The existing key value or the default after adding. |
| * @param <K> The map key |
| * @param <V> The map value |
| */ |
| private <K,V> V putOrDefault(Map<K,V> map, K key, V theDefault) { |
| if (map.containsKey(key)) { |
| return map.get(key); |
| } |
| |
| map.put(key, theDefault); |
| return theDefault; |
| } |
| |
| /** |
| * If this call has no associated Telecom {@link Call} and metrics are enabled, log this as a |
| * non-telecom call. |
| * @param session the session to log. |
| */ |
| private void maybeLogMetrics(CommunicationSession session) { |
| if (mMetricsController != null && session.getTelecomCall() == null |
| && session.isBitSet(SESSION_ATTR_IS_IN_COMMUNICATION)) { |
| mMetricsController.getCallStats().onNonTelecomCallEnd( |
| session.isBitSet(SESSION_ATTR_HAS_PHONE_ACCOUNT), |
| session.getUid(), |
| mClockProxy.elapsedRealtime() - session.getSessionStartMillis()); |
| } |
| } |
| } |