blob: e538fb1a1b0a6eef727ec03d1ba109a7469f3965 [file]
/*
* 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.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.IndentingPrintWriter;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
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 {
private final String LOG_TAG = CallAudioManager.class.getSimpleName();
private final LinkedHashSet<Call> mActiveDialingOrConnectingCalls;
private final LinkedHashSet<Call> mRingingCalls;
private final LinkedHashSet<Call> mHoldingCalls;
private final LinkedHashSet<Call> mAudioProcessingCalls;
/**
* Realistically there can only be one, but for consistency we'll track using a hash set like
* the other states do.
*/
private final LinkedHashSet<Call> mLocalVoicemailCalls;
private final Set<Call> mCalls;
private final SparseArray<LinkedHashSet<Call>> mCallStateToCalls;
private final CallAudioRouteAdapter mCallAudioRouteAdapter;
private final CallAudioModeStateMachine mCallAudioModeStateMachine;
private final CallConnectedIndicatorSettings mCallConnectedIndicatorSettings;
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<Boolean> mCallRingingFuture;
private CompletableFuture<Boolean> mCallDialingActiveOrConnectingFuture;
private Thread mBtIcsBindingThread;
private boolean mIsTonePlaying = false;
private boolean mIsDisconnectedTonePlaying = false;
private InCallTonePlayer mHoldTonePlayer;
private final HandlerThread mHandlerThread;
private final Handler mHandler;
private final Set<Call> mSilencedCalls;
private boolean mIsCrsInCallMode = false;
private int mFocusState;
public CallAudioManager(CallAudioRouteAdapter callAudioRouteAdapter,
CallsManager callsManager,
CallAudioModeStateMachine callAudioModeStateMachine,
InCallTonePlayer.Factory playerFactory,
Ringer ringer,
RingbackPlayer ringbackPlayer,
BluetoothStateReceiver bluetoothStateReceiver,
DtmfLocalTonePlayer dtmfLocalTonePlayer,
FeatureFlags featureFlags,
CallConnectedIndicatorSettings callConnectedIndicator) {
mActiveDialingOrConnectingCalls = new LinkedHashSet<>(1);
mRingingCalls = new LinkedHashSet<>(1);
mHoldingCalls = new LinkedHashSet<>(1);
mAudioProcessingCalls = new LinkedHashSet<>(1);
mLocalVoicemailCalls = new LinkedHashSet<>(1);
mStreamingCall = null;
mCalls = new HashSet<>();
mCallStateToCalls = new SparseArray<LinkedHashSet<Call>>() {{
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);
put(CallState.LOCAL_VOICEMAIL, mLocalVoicemailCalls);
}};
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());
mSilencedCalls = new HashSet<>();
mFocusState = CallAudioRouteController.NO_FOCUS;
mPlayerFactory.setCallAudioManager(this);
mCallAudioModeStateMachine.setCallAudioManager(this);
mCallAudioRouteAdapter.setCallAudioManager(this);
mCallConnectedIndicatorSettings = callConnectedIndicator;
}
@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));
if (oldState == CallState.NEW && newState != CallState.NEW) {
mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteController.ON_CALL_ADDED);
}
removeCallFromAllBins(call);
HashSet<Call> newBinForCall = getBinForCall(call);
if (newBinForCall != null) {
newBinForCall.add(call);
}
sendCallStatusToBluetoothStateReceiver();
updateForegroundCall();
if (shouldPlayDisconnectTone(oldState, newState)) {
playToneForDisconnectedCall(call);
} else {
if (newState == CallState.DISCONNECTED) {
// This call is not disconnected, but it won't generate a disconnect tone, so
// complete the future to ensure we unbind from BT promptly.
completeDisconnectToneFuture(call);
}
}
if (mSilencedCalls.contains(call) && newState != CallState.RINGING) {
mSilencedCalls.remove(call);
}
if (newState == CallState.ACTIVE && oldState == CallState.DIALING) {
playToneAfterCallConnected(call);
}
if (mIsCrsInCallMode && newState != CallState.RINGING
&& (call == mForegroundCall || mForegroundCall == null)
&& getCrsAudioController() != null) {
getCrsAudioController().resetAudioDevices(this, mCallsManager, call, newState);
mIsCrsInCallMode = false;
mSilencedCalls.remove(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<Call> newBinForCall = getBinForCall(call);
if (newBinForCall != null) {
newBinForCall.add(call);
}
updateForegroundCall();
mCalls.add(call);
if (call.getState() != CallState.NEW) {
mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteController.ON_CALL_ADDED);
}
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);
mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteController.ON_CALL_REMOVED);
sendCallStatusToBluetoothStateReceiver();
onCallLeavingState(call, call.getState());
mSilencedCalls.remove(call);
}
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);
}
public void clearSilencedCalls() {
Log.i(this, "clearSilencedCalls");
for (Call call : mRingingCalls) {
mSilencedCalls.remove(call);
}
}
@Override
public void onCrsFallbackLocalRinging(Call call) {
if (mSilencedCalls.contains(call) || call != mForegroundCall) {
return;
}
mCallAudioModeStateMachine.sendMessageWithArgs(
CallAudioModeStateMachine.CRS_FALLBACK_TO_LOCAL_RINGING,
makeArgsForModeStateMachine());
}
/**
* 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(
CallAudioRouteController.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(
CallAudioRouteController.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;
}
mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteController.VIDEO_STATE_CHANGED, newVideoState);
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(
CallAudioRouteController.SWITCH_SPEAKER);
}
}
@VisibleForTesting
public void setAudioMode(int mode) {
mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteController.SET_AUDIO_MODE, mode);
}
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(
CallAudioRouteController.TOGGLE_MUTE);
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void onRingerModeChange() {
// Stop the current ringtone before attempting to start the new ringtone:
stopRinging();
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
? CallAudioRouteController.MUTE_ON : CallAudioRouteController.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(
CallAudioRouteController.USER_SWITCH_BLUETOOTH, 0, bluetoothAddress);
return;
case CallAudioState.ROUTE_SPEAKER:
mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteController.USER_SWITCH_SPEAKER);
return;
case CallAudioState.ROUTE_WIRED_HEADSET:
mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteController.USER_SWITCH_HEADSET);
return;
case CallAudioState.ROUTE_EARPIECE:
mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteController.USER_SWITCH_EARPIECE);
return;
case CallAudioState.ROUTE_WIRED_OR_EARPIECE:
mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteController.USER_SWITCH_BASELINE_ROUTE,
CallAudioRouteController.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(
CallAudioRouteController.USER_SWITCH_BASELINE_ROUTE,
CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE);
}
public Set<UserHandle> 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<UserHandle> 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();
mSilencedCalls.add(call);
if (getCrsAudioController() != null
&& getCrsAudioController().isCrsInCallMode(call)
&& getCrsAudioController().shouldControlCrsWithParameters()) {
// Send speech mute in case user explicitly mute the ring
getCrsAudioController().setCrsSpeechMuted(true);
}
}
// 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();
}
public Context getContext() {
return mCallsManager.getContext();
}
@VisibleForTesting
public boolean startRinging() {
synchronized (mCallsManager.getLock()) {
Call localForegroundCall = mForegroundCall;
if (localForegroundCall != null && localForegroundCall.isCrsCall()
&& mSilencedCalls.contains(localForegroundCall)) {
// This case is when user put the CRS call in silent and then CRS call fallbacks
// to normal call, it should not ring.
Log.v(this, "Skip startRinging for silenced ringing call");
return false;
}
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) {
mFocusState = focusState;
if (focusState == CallAudioRouteController.NO_FOCUS) {
mCallAudioRouteAdapter.sendMessageWithSessionInfoAtFront(
CallAudioRouteController.SWITCH_FOCUS, focusState, 0);
} else {
mCallAudioRouteAdapter.sendMessageWithSessionInfo(
CallAudioRouteController.SWITCH_FOCUS, focusState, 0);
}
}
public void setCallAudioRouteFocusStateForEndTone() {
mFocusState = CallAudioRouteController.ACTIVE_FOCUS;
mCallAudioRouteAdapter.sendMessageWithSessionInfoAtFront(
CallAudioRouteController.SWITCH_FOCUS,
CallAudioRouteController.ACTIVE_FOCUS, 1);
}
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;
}
}
public BluetoothStateReceiver getBluetoothStateReceiver() {
return mBluetoothStateReceiver;
}
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;
case CallState.LOCAL_VOICEMAIL:
onCallLeavingLocalVoicemail();
break;
}
}
private void onCallEnteringState(Call call, int state) {
switch (state) {
case CallState.ACTIVE:
case CallState.CONNECTING:
onCallEnteringActiveDialingOrConnecting();
break;
case CallState.RINGING:
if (getCrsAudioController() != null &&
getCrsAudioController().isCrsInCallMode(call)) {
getCrsAudioController().setCrsAudioRoute(this);
mIsCrsInCallMode = true;
}
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;
case CallState.LOCAL_VOICEMAIL:
onCallEnteringLocalVoicemail();
break;
}
}
private void onCallLeavingLocalVoicemail() {
if (mLocalVoicemailCalls.size() == 0) {
mCallAudioModeStateMachine.sendMessageWithArgs(
CallAudioModeStateMachine.NO_MORE_LOCAL_VOICEMAIL_CALLS,
makeArgsForModeStateMachine());
}
}
private void onCallEnteringLocalVoicemail() {
if (mLocalVoicemailCalls.size() == 1) {
mCallAudioModeStateMachine.sendMessageWithArgs(
CallAudioModeStateMachine.NEW_LOCAL_VOICEMAIL_CALL,
makeArgsForModeStateMachine());
}
}
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) {
Call focusCall = mActiveDialingOrConnectingCalls.getFirst();
if (focusCall.getBtIcsFuture() != null && !focusCall.getBtIcsFuture().isDone()) {
mCallDialingActiveOrConnectingFuture = focusCall.getBtIcsFuture()
.thenCompose((completed) -> {
// We should check that the call hasn't been disconnected or is in the
// middle of disconnecting. Otherwise, we shouldn't be signaling to the
// audio mode state machine to request audio focus.
if (mActiveDialingOrConnectingCalls.size() == 1
&& focusCall.getState() != CallState.DISCONNECTED
&& !focusCall.isLocallyDisconnecting()) {
mCallAudioModeStateMachine.sendMessageWithArgs(
CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL,
makeArgsForModeStateMachine());
}
return CompletableFuture.completedFuture(completed);
});
mCallDialingActiveOrConnectingFuture = completeBtIcsFutureExceptionally(
mCallDialingActiveOrConnectingFuture, false /* isHandlingRinging */);
} else {
mCallAudioModeStateMachine.sendMessageWithArgs(
CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL,
makeArgsForModeStateMachine());
}
}
}
private void onCallEnteringRinging() {
if (mRingingCalls.size() == 1) {
Call ringingCall = mRingingCalls.getFirst();
Log.i(this, "onCallEnteringRinging: mRingingCalls.getFirst().getBtIcsFuture() = %s",
ringingCall.getBtIcsFuture());
boolean shouldWaitForBtIcs = ringingCall.getBtIcsFuture() != null
&& !ringingCall.getBtIcsFuture().isDone();
if (shouldWaitForBtIcs) {
mCallRingingFuture = ringingCall.getBtIcsFuture()
.thenCompose((completed) -> {
// Do a performative check to see if the call is still ringing before
// sending the msg forward to the CallAudioModeStateMachine.
if (mRingingCalls.size() == 1
&& (ringingCall.getState() == CallState.RINGING
|| ringingCall.getState() == CallState.SIMULATED_RINGING)) {
mCallAudioModeStateMachine.sendMessageWithArgs(
CallAudioModeStateMachine.NEW_RINGING_CALL,
makeArgsForModeStateMachine());
}
return CompletableFuture.completedFuture(completed);}
);
mCallRingingFuture = completeBtIcsFutureExceptionally(mCallRingingFuture,
true /* isHandlingRinging */);
} else {
mCallAudioModeStateMachine.sendMessageWithArgs(
CallAudioModeStateMachine.NEW_RINGING_CALL,
makeArgsForModeStateMachine());
}
}
}
private CompletableFuture<Boolean> completeBtIcsFutureExceptionally(
CompletableFuture<Boolean> future, boolean isHandlingRinging) {
return future.exceptionally((throwable) -> {
Log.e(this, throwable, "Error while executing BT ICS future");
// Fallback on performing computation on a separate thread.
mBtIcsBindingThread = new Thread(() -> {
if (isHandlingRinging) {
if (!mRingingCalls.isEmpty()) {
Call ringingCall = mRingingCalls.getFirst();
// Wait for the BT ICS future to complete
ringingCall.waitForBtIcs();
// Only send the message if the call is still ringing
if (ringingCall.getState() == CallState.RINGING
|| ringingCall.getState() == CallState.SIMULATED_RINGING) {
mCallAudioModeStateMachine.sendMessageWithArgs(
CallAudioModeStateMachine.NEW_RINGING_CALL,
makeArgsForModeStateMachine());
}
}
} else {
if (!mActiveDialingOrConnectingCalls.isEmpty()) {
Call dialingActiveOrConnectingCall = mActiveDialingOrConnectingCalls
.getFirst();
// Wait for the BT ICS future to complete
dialingActiveOrConnectingCall.waitForBtIcs();
if (dialingActiveOrConnectingCall.getState() != CallState.DISCONNECTED
&& !dialingActiveOrConnectingCall.isLocallyDisconnecting()) {
mCallAudioModeStateMachine.sendMessageWithArgs(
CallAudioModeStateMachine.NEW_ACTIVE_OR_DIALING_CALL,
makeArgsForModeStateMachine());
}
}
}
});
mBtIcsBindingThread.start();
return null;
});
}
private void handleBtBindingWaitFallbackForRinging() {
// 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(
CallAudioRouteController.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
|| mLocalVoicemailCalls.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<Call> 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 playToneAfterCallConnected(Call call) {
if (!android.telecom.flags.Flags.callConnectedIndicatorPreference()) {
Log.i(LOG_TAG, "Call connected indicator of playing tone is disabled.");
return;
}
if (mCallConnectedIndicatorSettings != null &&
mCallConnectedIndicatorSettings.isCallConnectedToneEnabled()) {
mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_OUTGOING_CALL_ACCEPTED).startTone();
}
}
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<Call> 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<Void> disconnectedToneFuture = mCallsManager.getInCallController()
.getDisconnectedToneBtFutures().get(call.getId());
if (disconnectedToneFuture != null) {
Log.i(this,
"completeDisconnectToneFuture: completing deferred disconnect tone future for"
+ " call %s",
call.getId());
disconnectedToneFuture.complete(null);
}
// Make sure we schedule the unbinding of the BT ICS once the disconnected tone future has
// been completed.
mCallsManager.getInCallController().maybeScheduleBtUnbind(call);
}
@VisibleForTesting
public Set<Call> getTrackedCalls() {
return mCalls;
}
@VisibleForTesting
public SparseArray<LinkedHashSet<Call>> getCallStateToCalls() {
return mCallStateToCalls;
}
@VisibleForTesting
public CompletableFuture<Boolean> getCallRingingFuture() {
return mCallRingingFuture;
}
@VisibleForTesting
public CompletableFuture<Boolean> getCallDialingActiveOrConnectingFuture() {
return mCallDialingActiveOrConnectingFuture;
}
public boolean isCrsInCallMode() {
return mIsCrsInCallMode;
}
public boolean isFocusStateUnfocused() {
return mFocusState == CallAudioRouteController.NO_FOCUS;
}
public CrsAudioController getCrsAudioController() {
return mCallsManager.getCrsAudioController();
}
}