/* * Copyright (C) 2015 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 android.annotation.NonNull; import android.content.Context; import android.media.IAudioService; import android.media.ToneGenerator; import android.os.Handler; import android.os.HandlerThread; import android.os.UserHandle; import android.telecom.CallAudioState; import android.telecom.Log; import android.telecom.VideoProfile; import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs.Builder; import com.android.server.telecom.bluetooth.BluetoothStateReceiver; import com.android.server.telecom.flags.FeatureFlags; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.LinkedHashSet; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; public class CallAudioManager extends CallsManagerListenerBase { public interface AudioServiceFactory { IAudioService getAudioService(); } private final String LOG_TAG = CallAudioManager.class.getSimpleName(); private final LinkedHashSet mActiveDialingOrConnectingCalls; private final LinkedHashSet mRingingCalls; private final LinkedHashSet mHoldingCalls; private final LinkedHashSet mAudioProcessingCalls; private final Set mCalls; private final SparseArray> mCallStateToCalls; private final CallAudioRouteAdapter mCallAudioRouteAdapter; private final CallAudioModeStateMachine mCallAudioModeStateMachine; private final BluetoothStateReceiver mBluetoothStateReceiver; private final CallsManager mCallsManager; private final InCallTonePlayer.Factory mPlayerFactory; private final Ringer mRinger; private final RingbackPlayer mRingbackPlayer; private final DtmfLocalTonePlayer mDtmfLocalTonePlayer; private final FeatureFlags mFeatureFlags; private Call mStreamingCall; private Call mForegroundCall; private CompletableFuture mCallRingingFuture; private Thread mBtIcsBindingThread; private boolean mIsTonePlaying = false; private boolean mIsDisconnectedTonePlaying = false; private InCallTonePlayer mHoldTonePlayer; private final HandlerThread mHandlerThread; private final Handler mHandler; public CallAudioManager(CallAudioRouteAdapter callAudioRouteAdapter, CallsManager callsManager, CallAudioModeStateMachine callAudioModeStateMachine, InCallTonePlayer.Factory playerFactory, Ringer ringer, RingbackPlayer ringbackPlayer, BluetoothStateReceiver bluetoothStateReceiver, DtmfLocalTonePlayer dtmfLocalTonePlayer, FeatureFlags featureFlags) { mActiveDialingOrConnectingCalls = new LinkedHashSet<>(1); mRingingCalls = new LinkedHashSet<>(1); mHoldingCalls = new LinkedHashSet<>(1); mAudioProcessingCalls = new LinkedHashSet<>(1); mStreamingCall = null; mCalls = new HashSet<>(); mCallStateToCalls = new SparseArray>() {{ put(CallState.CONNECTING, mActiveDialingOrConnectingCalls); put(CallState.ACTIVE, mActiveDialingOrConnectingCalls); put(CallState.DIALING, mActiveDialingOrConnectingCalls); put(CallState.PULLING, mActiveDialingOrConnectingCalls); put(CallState.RINGING, mRingingCalls); put(CallState.ON_HOLD, mHoldingCalls); put(CallState.SIMULATED_RINGING, mRingingCalls); put(CallState.AUDIO_PROCESSING, mAudioProcessingCalls); }}; mCallAudioRouteAdapter = callAudioRouteAdapter; mCallAudioModeStateMachine = callAudioModeStateMachine; mCallsManager = callsManager; mPlayerFactory = playerFactory; mRinger = ringer; mRingbackPlayer = ringbackPlayer; mBluetoothStateReceiver = bluetoothStateReceiver; mDtmfLocalTonePlayer = dtmfLocalTonePlayer; mFeatureFlags = featureFlags; mHandlerThread = new HandlerThread(this.getClass().getSimpleName()); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); mPlayerFactory.setCallAudioManager(this); mCallAudioModeStateMachine.setCallAudioManager(this); mCallAudioRouteAdapter.setCallAudioManager(this); } @Override public void onCallStateChanged(Call call, int oldState, int newState) { if (shouldIgnoreCallForAudio(call)) { // No audio management for calls in a conference, or external calls. return; } if (oldState == newState) { // State did not change, so no need to do anything. return; } Log.i(this, "onCallStateChanged: Call state changed for TC@%s: %s -> %s", call.getId(), CallState.toString(oldState), CallState.toString(newState)); removeCallFromAllBins(call); HashSet newBinForCall = getBinForCall(call); if (newBinForCall != null) { newBinForCall.add(call); } sendCallStatusToBluetoothStateReceiver(); updateForegroundCall(); if (shouldPlayDisconnectTone(oldState, newState)) { playToneForDisconnectedCall(call); } onCallLeavingState(call, oldState); onCallEnteringState(call, newState); } @Override public void onCallAdded(Call call) { if (shouldIgnoreCallForAudio(call)) { return; // Don't do audio handling for calls in a conference, or external calls. } addCall(call); } @Override public void onCallRemoved(Call call) { if (mStreamingCall == call) { mStreamingCall = null; } if (shouldIgnoreCallForAudio(call)) { return; // Don't do audio handling for calls in a conference, or external calls. } removeCall(call); } private void addCall(Call call) { if (mCalls.contains(call)) { Log.w(LOG_TAG, "Call TC@%s is being added twice.", call.getId()); return; // No guarantees that the same call won't get added twice. } Log.d(LOG_TAG, "Call added with id TC@%s in state %s", call.getId(), CallState.toString(call.getState())); HashSet newBinForCall = getBinForCall(call); if (newBinForCall != null) { newBinForCall.add(call); } updateForegroundCall(); mCalls.add(call); sendCallStatusToBluetoothStateReceiver(); onCallEnteringState(call, call.getState()); } private void removeCall(Call call) { if (!mCalls.contains(call)) { return; // No guarantees that the same call won't get removed twice. } Log.d(LOG_TAG, "Call removed with id TC@%s in state %s", call.getId(), CallState.toString(call.getState())); removeCallFromAllBins(call); updateForegroundCall(); mCalls.remove(call); sendCallStatusToBluetoothStateReceiver(); onCallLeavingState(call, call.getState()); } private void sendCallStatusToBluetoothStateReceiver() { // We're in a call if there are calls in mCalls that are not in mAudioProcessingCalls. boolean isInCall = !mAudioProcessingCalls.containsAll(mCalls); mBluetoothStateReceiver.setIsInCall(isInCall); } /** * Handles changes to the external state of a call. External calls which become regular calls * should be tracked, and regular calls which become external should no longer be tracked. * * @param call The call. * @param isExternalCall {@code True} if the call is now external, {@code false} if it is now * a regular call. */ @Override public void onExternalCallChanged(Call call, boolean isExternalCall) { if (isExternalCall) { Log.d(LOG_TAG, "Removing call which became external ID %s", call.getId()); removeCall(call); } else if (!isExternalCall) { Log.d(LOG_TAG, "Adding external call which was pulled with ID %s", call.getId()); addCall(call); if (mCallsManager.isSpeakerphoneAutoEnabledForVideoCalls(call.getVideoState())) { // When pulling a video call, automatically enable the speakerphone. Log.d(LOG_TAG, "Switching to speaker because external video call %s was pulled." + call.getId()); mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.SWITCH_SPEAKER); } } } /** * Handles the changes to the streaming state of a call. * @param call The call * @param isStreaming {@code true} if the call is streaming, {@code false} otherwise */ @Override public void onCallStreamingStateChanged(Call call, boolean isStreaming) { if (isStreaming) { if (mStreamingCall == null) { mStreamingCall = call; mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.START_CALL_STREAMING, makeArgsForModeStateMachine()); } else { Log.w(LOG_TAG, "Unexpected streaming call request for call %s while call " + "%s is streaming.", call.getId(), mStreamingCall.getId()); } } else { if (mStreamingCall == call) { mStreamingCall = null; mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.STOP_CALL_STREAMING, makeArgsForModeStateMachine()); } else { Log.w(LOG_TAG, "Unexpected call streaming stop request for call %s while this call " + "is not streaming.", call.getId()); } } } /** * Determines if {@link CallAudioManager} should do any audio routing operations for a call. * We ignore child calls of a conference and external calls for audio routing purposes. * * @param call The call to check. * @return {@code true} if the call should be ignored for audio routing, {@code false} * otherwise */ private boolean shouldIgnoreCallForAudio(Call call) { return call.getParentCall() != null || call.isExternalCall(); } @Override public void onIncomingCallAnswered(Call call) { if (!mCalls.contains(call)) { return; } // Turn off mute when a new incoming call is answered iff it's not a handover. if (!call.isHandoverInProgress()) { mute(false /* shouldMute */); } maybeStopRingingAndCallWaitingForAnsweredOrRejectedCall(call); } @Override public void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile) { if (videoProfile == null) { return; } if (call != mForegroundCall) { // We only play tones for foreground calls. return; } int previousVideoState = call.getVideoState(); int newVideoState = videoProfile.getVideoState(); Log.v(this, "onSessionModifyRequestReceived : videoProfile = " + VideoProfile .videoStateToString(newVideoState)); boolean isUpgradeRequest = !VideoProfile.isReceptionEnabled(previousVideoState) && VideoProfile.isReceptionEnabled(newVideoState); if (isUpgradeRequest) { mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_VIDEO_UPGRADE).startTone(); } } public void playRttUpgradeTone(Call call) { if (call != mForegroundCall) { // We only play tones for foreground calls. return; } mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_RTT_REQUEST).startTone(); } /** * Play or stop a call hold tone for a call. Triggered via * {@link Connection#sendConnectionEvent(String)} when the * {@link Connection#EVENT_ON_HOLD_TONE_START} event or * {@link Connection#EVENT_ON_HOLD_TONE_STOP} event is passed through to the * * @param call The call which requested the hold tone. */ @Override public void onHoldToneRequested(Call call) { maybePlayHoldTone(call); } @Override public void onIsVoipAudioModeChanged(Call call) { if (call != mForegroundCall) { return; } mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE, makeArgsForModeStateMachine()); } @Override public void onRingbackRequested(Call call, boolean shouldRingback) { if (call == mForegroundCall && shouldRingback) { mRingbackPlayer.startRingbackForCall(call); } else { mRingbackPlayer.stopRingbackForCall(call); } } @Override public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String message) { maybeStopRingingAndCallWaitingForAnsweredOrRejectedCall(call); } @Override public void onIsConferencedChanged(Call call) { // This indicates a conferencing change, which shouldn't impact any audio mode stuff. Call parentCall = call.getParentCall(); if (parentCall == null) { // Indicates that the call should be tracked for audio purposes. Treat it as if it were // just added. Log.i(LOG_TAG, "Call TC@" + call.getId() + " left conference and will" + " now be tracked by CallAudioManager."); onCallAdded(call); } else { // The call joined a conference, so stop tracking it. removeCallFromAllBins(call); updateForegroundCall(); mCalls.remove(call); } } @Override public void onConnectionServiceChanged(Call call, ConnectionServiceWrapper oldCs, ConnectionServiceWrapper newCs) { mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE); } @Override public void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) { if (call != getForegroundCall()) { Log.d(LOG_TAG, "Ignoring video state change from %s to %s for call %s -- not " + "foreground.", VideoProfile.videoStateToString(previousVideoState), VideoProfile.videoStateToString(newVideoState), call.getId()); return; } if (!VideoProfile.isVideo(previousVideoState) && mCallsManager.isSpeakerphoneAutoEnabledForVideoCalls(newVideoState)) { Log.d(LOG_TAG, "Switching to speaker because call %s transitioned video state from %s" + " to %s", call.getId(), VideoProfile.videoStateToString(previousVideoState), VideoProfile.videoStateToString(newVideoState)); mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.SWITCH_SPEAKER); } } public CallAudioState getCallAudioState() { return mCallAudioRouteAdapter.getCurrentCallAudioState(); } public Call getPossiblyHeldForegroundCall() { return mForegroundCall; } public Call getForegroundCall() { if (mForegroundCall != null && mForegroundCall.getState() != CallState.ON_HOLD) { return mForegroundCall; } return null; } @VisibleForTesting public void toggleMute() { // Don't mute if there are any emergency calls. if (mCallsManager.isInEmergencyCall()) { Log.v(this, "ignoring toggleMute for emergency call"); return; } mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.TOGGLE_MUTE); } @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public void onRingerModeChange() { mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.RINGER_MODE_CHANGE, makeArgsForModeStateMachine()); } @VisibleForTesting public void mute(boolean shouldMute) { Log.v(this, "mute, shouldMute: %b", shouldMute); // Don't mute if there are any emergency calls. if (mCallsManager.isInEmergencyCall()) { shouldMute = false; Log.v(this, "ignoring mute for emergency call"); } mCallAudioRouteAdapter.sendMessageWithSessionInfo(shouldMute ? CallAudioRouteStateMachine.MUTE_ON : CallAudioRouteStateMachine.MUTE_OFF); } /** * Changed the audio route, for example from earpiece to speaker phone. * * @param route The new audio route to use. See {@link CallAudioState}. * @param bluetoothAddress the address of the desired bluetooth device, if route is * {@link CallAudioState#ROUTE_BLUETOOTH}. */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED) public void setAudioRoute(int route, String bluetoothAddress) { Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route)); switch (route) { case CallAudioState.ROUTE_BLUETOOTH: mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.USER_SWITCH_BLUETOOTH, 0, bluetoothAddress); return; case CallAudioState.ROUTE_SPEAKER: mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.USER_SWITCH_SPEAKER); return; case CallAudioState.ROUTE_WIRED_HEADSET: mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.USER_SWITCH_HEADSET); return; case CallAudioState.ROUTE_EARPIECE: mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.USER_SWITCH_EARPIECE); return; case CallAudioState.ROUTE_WIRED_OR_EARPIECE: mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.USER_SWITCH_BASELINE_ROUTE, CallAudioRouteStateMachine.NO_INCLUDE_BLUETOOTH_IN_BASELINE); return; default: Log.w(this, "InCallService requested an invalid audio route: %d", route); } } /** * Switch call audio routing to the baseline route, including bluetooth headsets if there are * any connected. */ void switchBaseline() { Log.i(this, "switchBaseline"); mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.USER_SWITCH_BASELINE_ROUTE, CallAudioRouteStateMachine.INCLUDE_BLUETOOTH_IN_BASELINE); } Set silenceRingers(Context context, UserHandle callingUser, boolean hasCrossUserPermission) { // Store all users from calls that were silenced so that we can silence the // InCallServices which are associated with those users. Set userHandles = new HashSet<>(); boolean allCallSilenced = true; synchronized (mCallsManager.getLock()) { for (Call call : mRingingCalls) { UserHandle userFromCall = call.getAssociatedUser(); // Do not try to silence calls when calling user is different from the phone account // user, the account does not have CAPABILITY_MULTI_USER enabled, or if the user // does not have the INTERACT_ACROSS_USERS permission enabled. if (!hasCrossUserPermission && !mCallsManager .isCallVisibleForUser(call, callingUser)) { allCallSilenced = false; continue; } userHandles.add(userFromCall); call.silence(); } // If all the calls were silenced, we can stop the ringer. if (allCallSilenced) { mRinger.stopRinging(); mRinger.stopCallWaiting(); } } return userHandles; } public boolean isRingtonePlaying() { return mRinger.isRinging(); } @VisibleForTesting public boolean startRinging() { synchronized (mCallsManager.getLock()) { Call localForegroundCall = mForegroundCall; boolean result = mRinger.startRinging(localForegroundCall, mCallAudioRouteAdapter.isHfpDeviceAvailable()); if (result) { localForegroundCall.setStartRingTime(); } return result; } } @VisibleForTesting public void startCallWaiting(String reason) { synchronized (mCallsManager.getLock()) { if (mRingingCalls.size() == 1) { mRinger.startCallWaiting(mRingingCalls.iterator().next(), reason); } } } @VisibleForTesting public void stopRinging() { synchronized (mCallsManager.getLock()) { mRinger.stopRinging(); } } @VisibleForTesting public void stopCallWaiting() { synchronized (mCallsManager.getLock()) { mRinger.stopCallWaiting(); } } @VisibleForTesting public void setCallAudioRouteFocusState(int focusState) { mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.SWITCH_FOCUS, focusState); } public void notifyAudioOperationsComplete() { mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.AUDIO_OPERATIONS_COMPLETE, makeArgsForModeStateMachine()); } @VisibleForTesting public CallAudioRouteAdapter getCallAudioRouteAdapter() { return mCallAudioRouteAdapter; } @VisibleForTesting public CallAudioModeStateMachine getCallAudioModeStateMachine() { return mCallAudioModeStateMachine; } void dump(IndentingPrintWriter pw) { pw.println("All calls:"); pw.increaseIndent(); dumpCallsInCollection(pw, mCalls); pw.decreaseIndent(); pw.println("Active dialing, or connecting calls:"); pw.increaseIndent(); dumpCallsInCollection(pw, mActiveDialingOrConnectingCalls); pw.decreaseIndent(); pw.println("Ringing calls:"); pw.increaseIndent(); dumpCallsInCollection(pw, mRingingCalls); pw.decreaseIndent(); pw.println("Holding calls:"); pw.increaseIndent(); dumpCallsInCollection(pw, mHoldingCalls); pw.decreaseIndent(); pw.println("Foreground call:"); pw.println(mForegroundCall); pw.println("CallAudioModeStateMachine:"); pw.increaseIndent(); mCallAudioModeStateMachine.dump(pw); pw.decreaseIndent(); pw.println("mCallAudioRouteAdapter:"); pw.increaseIndent(); mCallAudioRouteAdapter.dump(pw); pw.decreaseIndent(); pw.println("BluetoothDeviceManager:"); pw.increaseIndent(); if (mBluetoothStateReceiver.getBluetoothDeviceManager() != null) { mBluetoothStateReceiver.getBluetoothDeviceManager().dump(pw); } pw.decreaseIndent(); } @VisibleForTesting public void setIsTonePlaying(Call call, boolean isTonePlaying) { Log.i(this, "setIsTonePlaying; isTonePlaying=%b", isTonePlaying); mIsTonePlaying = isTonePlaying; mCallAudioModeStateMachine.sendMessageWithArgs( isTonePlaying ? CallAudioModeStateMachine.TONE_STARTED_PLAYING : CallAudioModeStateMachine.TONE_STOPPED_PLAYING, makeArgsForModeStateMachine()); if (!isTonePlaying && mIsDisconnectedTonePlaying) { mCallsManager.onDisconnectedTonePlaying(call, false); mIsDisconnectedTonePlaying = false; } } private void onCallLeavingState(Call call, int state) { switch (state) { case CallState.ACTIVE: case CallState.CONNECTING: onCallLeavingActiveDialingOrConnecting(); break; case CallState.RINGING: case CallState.SIMULATED_RINGING: case CallState.ANSWERED: onCallLeavingRinging(); break; case CallState.ON_HOLD: onCallLeavingHold(); break; case CallState.PULLING: onCallLeavingActiveDialingOrConnecting(); break; case CallState.DIALING: stopRingbackForCall(call); onCallLeavingActiveDialingOrConnecting(); break; case CallState.AUDIO_PROCESSING: onCallLeavingAudioProcessing(); break; } } private void onCallEnteringState(Call call, int state) { switch (state) { case CallState.ACTIVE: case CallState.CONNECTING: onCallEnteringActiveDialingOrConnecting(); break; case CallState.RINGING: case CallState.SIMULATED_RINGING: onCallEnteringRinging(); break; case CallState.ON_HOLD: onCallEnteringHold(); break; case CallState.PULLING: onCallEnteringActiveDialingOrConnecting(); break; case CallState.DIALING: onCallEnteringActiveDialingOrConnecting(); playRingbackForCall(call); break; case CallState.ANSWERED: if (call.can(android.telecom.Call.Details.CAPABILITY_SPEED_UP_MT_AUDIO)) { onCallEnteringActiveDialingOrConnecting(); } break; case CallState.AUDIO_PROCESSING: onCallEnteringAudioProcessing(); break; } } private void onCallLeavingAudioProcessing() { if (mAudioProcessingCalls.size() == 0) { mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.NO_MORE_AUDIO_PROCESSING_CALLS, makeArgsForModeStateMachine()); } } private void onCallEnteringAudioProcessing() { if (mAudioProcessingCalls.size() == 1) { mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.NEW_AUDIO_PROCESSING_CALL, makeArgsForModeStateMachine()); } } private void onCallLeavingActiveDialingOrConnecting() { if (mActiveDialingOrConnectingCalls.size() == 0) { mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.NO_MORE_ACTIVE_OR_DIALING_CALLS, makeArgsForModeStateMachine()); } } private void onCallLeavingRinging() { if (mRingingCalls.size() == 0) { mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.NO_MORE_RINGING_CALLS, makeArgsForModeStateMachine()); } } private void onCallLeavingHold() { if (mHoldingCalls.size() == 0) { mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.NO_MORE_HOLDING_CALLS, makeArgsForModeStateMachine()); } } private void onCallEnteringActiveDialingOrConnecting() { if (mActiveDialingOrConnectingCalls.size() == 1) { mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL, makeArgsForModeStateMachine()); } } private void onCallEnteringRinging() { if (mRingingCalls.size() == 1) { Log.i(this, "onCallEnteringRinging: mFeatureFlags.separatelyBindToBtIncallService() ? %s", mFeatureFlags.separatelyBindToBtIncallService()); Log.i(this, "onCallEnteringRinging: mRingingCalls.getFirst().getBtIcsFuture() = %s", mRingingCalls.getFirst().getBtIcsFuture()); if (mFeatureFlags.separatelyBindToBtIncallService() && mRingingCalls.getFirst().getBtIcsFuture() != null) { mCallRingingFuture = mRingingCalls.getFirst().getBtIcsFuture() .thenComposeAsync((completed) -> { mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.NEW_RINGING_CALL, makeArgsForModeStateMachine()); return CompletableFuture.completedFuture(completed); }, new LoggedHandlerExecutor(mHandler, "CAM.oCER", mCallsManager.getLock())) .exceptionally((throwable) -> { Log.e(this, throwable, "Error while executing BT ICS future"); // Fallback on performing computation on a separate thread. handleBtBindingWaitFallback(); return null; }); } else { mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.NEW_RINGING_CALL, makeArgsForModeStateMachine()); } } } private void handleBtBindingWaitFallback() { // Wait until the BT ICS binding completed to request further audio route change mBtIcsBindingThread = new Thread(() -> { mRingingCalls.getFirst().waitForBtIcs(); mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.NEW_RINGING_CALL, makeArgsForModeStateMachine()); }); mBtIcsBindingThread.start(); } private void onCallEnteringHold() { if (mHoldingCalls.size() == 1) { mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.NEW_HOLDING_CALL, makeArgsForModeStateMachine()); } } private void updateForegroundCall() { Call oldForegroundCall = mForegroundCall; if (mActiveDialingOrConnectingCalls.size() > 0) { // Give preference for connecting calls over active/dialing for foreground-ness. Call possibleConnectingCall = null; for (Call call : mActiveDialingOrConnectingCalls) { if (call.getState() == CallState.CONNECTING) { possibleConnectingCall = call; } } if (mFeatureFlags.ensureAudioModeUpdatesOnForegroundCallChange()) { // Prefer a connecting call if (possibleConnectingCall != null) { mForegroundCall = possibleConnectingCall; } else { // Next, prefer an active or dialing call which is not in the process of being // disconnected. mForegroundCall = mActiveDialingOrConnectingCalls .stream() .filter(c -> (c.getState() == CallState.ACTIVE || c.getState() == CallState.DIALING) && !c.isLocallyDisconnecting()) .findFirst() // If we can't find one, then just fall back to the first one. .orElse(mActiveDialingOrConnectingCalls.iterator().next()); } } else { // Legacy (buggy) behavior. mForegroundCall = possibleConnectingCall == null ? mActiveDialingOrConnectingCalls.iterator().next() : possibleConnectingCall; } } else if (mRingingCalls.size() > 0) { mForegroundCall = mRingingCalls.iterator().next(); } else if (mHoldingCalls.size() > 0) { mForegroundCall = mHoldingCalls.iterator().next(); } else { mForegroundCall = null; } Log.i(this, "updateForegroundCall; oldFg=%s, newFg=%s, aDC=%s, ring=%s, hold=%s", (oldForegroundCall == null ? "none" : oldForegroundCall.getId()), (mForegroundCall == null ? "none" : mForegroundCall.getId()), mActiveDialingOrConnectingCalls.stream().map(c -> c.getId()).collect( Collectors.joining(",")), mRingingCalls.stream().map(c -> c.getId()).collect(Collectors.joining(",")), mHoldingCalls.stream().map(c -> c.getId()).collect(Collectors.joining(",")) ); if (mForegroundCall != oldForegroundCall) { mCallAudioRouteAdapter.sendMessageWithSessionInfo( CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE); if (mForegroundCall != null && mFeatureFlags.ensureAudioModeUpdatesOnForegroundCallChange()) { // Ensure the voip audio mode for the new foreground call is taken into account. mCallAudioModeStateMachine.sendMessageWithArgs( CallAudioModeStateMachine.FOREGROUND_VOIP_MODE_CHANGE, makeArgsForModeStateMachine()); } mDtmfLocalTonePlayer.onForegroundCallChanged(oldForegroundCall, mForegroundCall); maybePlayHoldTone(oldForegroundCall); } } @NonNull private CallAudioModeStateMachine.MessageArgs makeArgsForModeStateMachine() { return new Builder() .setHasActiveOrDialingCalls(mActiveDialingOrConnectingCalls.size() > 0) .setHasRingingCalls(mRingingCalls.size() > 0) .setHasHoldingCalls(mHoldingCalls.size() > 0) .setHasAudioProcessingCalls(mAudioProcessingCalls.size() > 0) .setIsTonePlaying(mIsTonePlaying) .setIsStreaming((mStreamingCall != null) && (!mStreamingCall.isDisconnected())) .setForegroundCallIsVoip( mForegroundCall != null && isCallVoip(mForegroundCall)) .setSession(Log.createSubsession()).build(); } /** * Determines if a {@link Call} is a VOIP call for audio purposes. * For top level calls, we get this from {@link Call#getIsVoipAudioMode()}. A {@link Call} * representing a {@link android.telecom.Conference}, however, has no means of specifying that * it is a VOIP conference, so we will get that attribute from one of the children. * @param call The call. * @return {@code true} if the call is a VOIP call, {@code false} if is a SIM call. */ @VisibleForTesting public boolean isCallVoip(Call call) { if (call.isConference() && call.getChildCalls() != null && call.getChildCalls().size() > 0 ) { // If this is a conference with children, we can get the VOIP audio mode attribute from // one of the children. The Conference doesn't have a VOIP audio mode property, so we // need to infer from the first child. Call firstChild = call.getChildCalls().get(0); return firstChild.getIsVoipAudioMode(); } return call.getIsVoipAudioMode(); } private HashSet getBinForCall(Call call) { if (call.getState() == CallState.ANSWERED) { // If the call has the speed-up-mt-audio capability, treat answered state as active // for audio purposes. if (call.can(android.telecom.Call.Details.CAPABILITY_SPEED_UP_MT_AUDIO)) { return mActiveDialingOrConnectingCalls; } return mRingingCalls; } return mCallStateToCalls.get(call.getState()); } private void removeCallFromAllBins(Call call) { for (int i = 0; i < mCallStateToCalls.size(); i++) { mCallStateToCalls.valueAt(i).remove(call); } } private void playToneForDisconnectedCall(Call call) { // If this call is being disconnected as a result of being handed over to another call, // we will not play a disconnect tone. if (call.isHandoverInProgress()) { Log.i(LOG_TAG, "Omitting tone because %s is being handed over.", call); completeDisconnectToneFuture(call); return; } if (mForegroundCall != null && call != mForegroundCall && mCalls.size() > 1) { Log.v(LOG_TAG, "Omitting tone because we are not foreground" + " and there is another call."); completeDisconnectToneFuture(call); return; } if (call.getDisconnectCause() != null) { int toneToPlay = InCallTonePlayer.TONE_INVALID; Log.v(this, "Disconnect cause: %s.", call.getDisconnectCause()); switch(call.getDisconnectCause().getTone()) { case ToneGenerator.TONE_SUP_BUSY: toneToPlay = InCallTonePlayer.TONE_BUSY; break; case ToneGenerator.TONE_SUP_CONGESTION: toneToPlay = InCallTonePlayer.TONE_CONGESTION; break; case ToneGenerator.TONE_CDMA_REORDER: toneToPlay = InCallTonePlayer.TONE_REORDER; break; case ToneGenerator.TONE_CDMA_ABBR_INTERCEPT: toneToPlay = InCallTonePlayer.TONE_INTERCEPT; break; case ToneGenerator.TONE_CDMA_CALLDROP_LITE: toneToPlay = InCallTonePlayer.TONE_CDMA_DROP; break; case ToneGenerator.TONE_SUP_ERROR: toneToPlay = InCallTonePlayer.TONE_UNOBTAINABLE_NUMBER; break; case ToneGenerator.TONE_PROP_PROMPT: toneToPlay = InCallTonePlayer.TONE_CALL_ENDED; break; } Log.d(this, "Found a disconnected call with tone to play %d.", toneToPlay); if (toneToPlay != InCallTonePlayer.TONE_INVALID) { boolean didToneStart = mPlayerFactory.createPlayer(call, toneToPlay).startTone(); if (didToneStart) { mCallsManager.onDisconnectedTonePlaying(call, true); mIsDisconnectedTonePlaying = true; } } else { completeDisconnectToneFuture(call); } } } private void playRingbackForCall(Call call) { if (call == mForegroundCall && call.isRingbackRequested()) { mRingbackPlayer.startRingbackForCall(call); } } private void stopRingbackForCall(Call call) { mRingbackPlayer.stopRingbackForCall(call); } /** * Determines if a hold tone should be played and then starts or stops it accordingly. */ private void maybePlayHoldTone(Call call) { if (shouldPlayHoldTone()) { if (mHoldTonePlayer == null) { mHoldTonePlayer = mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_CALL_WAITING); mHoldTonePlayer.startTone(); } } else { if (mHoldTonePlayer != null) { mHoldTonePlayer.stopTone(); mHoldTonePlayer = null; } } } /** * Determines if a hold tone should be played. * A hold tone should be played only if foreground call is equals with call which is * remotely held. * * @return {@code true} if the the hold tone should be played, {@code false} otherwise. */ private boolean shouldPlayHoldTone() { Call foregroundCall = getForegroundCall(); // If there is no foreground call, no hold tone should play. if (foregroundCall == null) { return false; } // If another call is ringing, no hold tone should play. if (mCallsManager.hasRingingCall()) { return false; } // If the foreground call isn't active, no hold tone should play. This might happen, for // example, if the user puts a remotely held call on hold itself. if (!foregroundCall.isActive()) { return false; } return foregroundCall.isRemotelyHeld(); } private void dumpCallsInCollection(IndentingPrintWriter pw, Collection calls) { for (Call call : calls) { if (call != null) pw.println(call.getId()); } } private void maybeStopRingingAndCallWaitingForAnsweredOrRejectedCall(Call call) { // Check to see if the call being answered/rejected is the only ringing call, since this // will be called before the connection service acknowledges the state change. synchronized (mCallsManager.getLock()) { if (mRingingCalls.size() == 0 || (mRingingCalls.size() == 1 && call == mRingingCalls.iterator().next())) { mRinger.stopRinging(); mRinger.stopCallWaiting(); } } } private boolean shouldPlayDisconnectTone(int oldState, int newState) { if (newState != CallState.DISCONNECTED) { return false; } return oldState == CallState.ACTIVE || oldState == CallState.DIALING || oldState == CallState.ON_HOLD; } private void completeDisconnectToneFuture(Call call) { CompletableFuture disconnectedToneFuture = mCallsManager.getInCallController() .getDisconnectedToneBtFutures().get(call.getId()); if (disconnectedToneFuture != null) { disconnectedToneFuture.complete(null); } } @VisibleForTesting public Set getTrackedCalls() { return mCalls; } @VisibleForTesting public SparseArray> getCallStateToCalls() { return mCallStateToCalls; } @VisibleForTesting public CompletableFuture getCallRingingFuture() { return mCallRingingFuture; } }