blob: 25aaad789aa91a8640ff9da59d52b1fc98f4868c [file] [log] [blame]
/*
* Copyright (C) 2022 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.telecom.CallException.CODE_CALL_IS_NOT_BEING_TRACKED;
import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY;
import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
import android.content.ComponentName;
import android.os.Bundle;
import android.os.IBinder;
import android.os.OutcomeReceiver;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.telecom.CallEndpoint;
import android.telecom.CallException;
import android.telecom.CallStreamingService;
import android.telecom.DisconnectCause;
import android.telecom.Log;
import android.telecom.PhoneAccountHandle;
import android.text.TextUtils;
import androidx.annotation.VisibleForTesting;
import com.android.internal.telecom.ICallControl;
import com.android.internal.telecom.ICallEventCallback;
import com.android.server.telecom.voip.CallEventCallbackAckTransaction;
import com.android.server.telecom.voip.EndpointChangeTransaction;
import com.android.server.telecom.voip.HoldCallTransaction;
import com.android.server.telecom.voip.EndCallTransaction;
import com.android.server.telecom.voip.MaybeHoldCallForNewCallTransaction;
import com.android.server.telecom.voip.ParallelTransaction;
import com.android.server.telecom.voip.RequestNewActiveCallTransaction;
import com.android.server.telecom.voip.SerialTransaction;
import com.android.server.telecom.voip.TransactionManager;
import com.android.server.telecom.voip.VoipCallTransaction;
import com.android.server.telecom.voip.VoipCallTransactionResult;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Implements {@link android.telecom.CallEventCallback} and {@link android.telecom.CallControl}
* on a per-client basis which is tied to a {@link PhoneAccountHandle}
*/
public class TransactionalServiceWrapper implements
ConnectionServiceFocusManager.ConnectionServiceFocus {
private static final String TAG = TransactionalServiceWrapper.class.getSimpleName();
// CallControl : Client (ex. voip app) --> Telecom
public static final String SET_ACTIVE = "SetActive";
public static final String SET_INACTIVE = "SetInactive";
public static final String ANSWER = "Answer";
public static final String DISCONNECT = "Disconnect";
public static final String START_STREAMING = "StartStreaming";
// CallEventCallback : Telecom --> Client (ex. voip app)
public static final String ON_SET_ACTIVE = "onSetActive";
public static final String ON_SET_INACTIVE = "onSetInactive";
public static final String ON_ANSWER = "onAnswer";
public static final String ON_DISCONNECT = "onDisconnect";
public static final String ON_STREAMING_STARTED = "onStreamingStarted";
private final CallsManager mCallsManager;
private final ICallEventCallback mICallEventCallback;
private final PhoneAccountHandle mPhoneAccountHandle;
private final TransactionalServiceRepository mRepository;
private ConnectionServiceFocusManager.ConnectionServiceFocusListener mConnSvrFocusListener;
// init when constructor is called
private final ConcurrentHashMap<String, Call> mTrackedCalls = new ConcurrentHashMap<>();
private final TelecomSystem.SyncRoot mLock;
private final String mPackageName;
// needs to be non-final for testing
private TransactionManager mTransactionManager;
private CallStreamingController mStreamingController;
// Each TransactionalServiceWrapper should have their own Binder.DeathRecipient to clean up
// any calls in the event the application crashes or is force stopped.
private final IBinder.DeathRecipient mAppDeathListener = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
Log.i(TAG, "binderDied: for package=[%s]; cleaning calls", mPackageName);
cleanupTransactionalServiceWrapper();
mICallEventCallback.asBinder().unlinkToDeath(this, 0);
}
};
public TransactionalServiceWrapper(ICallEventCallback callEventCallback,
CallsManager callsManager, PhoneAccountHandle phoneAccountHandle, Call call,
TransactionalServiceRepository repo) {
// passed args
mICallEventCallback = callEventCallback;
mCallsManager = callsManager;
mPhoneAccountHandle = phoneAccountHandle;
mTrackedCalls.put(call.getId(), call); // service is now tracking its first call
mRepository = repo;
// init instance vars
mPackageName = phoneAccountHandle.getComponentName().getPackageName();
mTransactionManager = TransactionManager.getInstance();
mStreamingController = mCallsManager.getCallStreamingController();
mLock = mCallsManager.getLock();
setDeathRecipient(callEventCallback);
}
@VisibleForTesting
public void setTransactionManager(TransactionManager transactionManager) {
mTransactionManager = transactionManager;
}
public TransactionManager getTransactionManager() {
return mTransactionManager;
}
public PhoneAccountHandle getPhoneAccountHandle() {
return mPhoneAccountHandle;
}
public void trackCall(Call call) {
synchronized (mLock) {
if (call != null) {
mTrackedCalls.put(call.getId(), call);
}
}
}
@VisibleForTesting
public boolean untrackCall(Call call) {
Call removedCall = null;
synchronized (mLock) {
if (call != null) {
removedCall = mTrackedCalls.remove(call.getId());
if (mTrackedCalls.size() == 0) {
mRepository.removeServiceWrapper(mPhoneAccountHandle);
}
}
}
Log.i(TAG, "removedCall call=" + removedCall);
return removedCall != null;
}
@VisibleForTesting
public int getNumberOfTrackedCalls() {
int callCount = 0;
synchronized (mLock) {
callCount = mTrackedCalls.size();
}
return callCount;
}
public void cleanupTransactionalServiceWrapper() {
for (Call call : mTrackedCalls.values()) {
mCallsManager.markCallAsDisconnected(call,
new DisconnectCause(DisconnectCause.ERROR, "process died"));
mCallsManager.removeCall(call); // This will clear mTrackedCalls && ClientTWS
}
}
/***
*********************************************************************************************
** ICallControl: Client --> Server **
**********************************************************************************************
*/
public final ICallControl mICallControl = new ICallControl.Stub() {
@Override
public void setActive(String callId, android.os.ResultReceiver callback)
throws RemoteException {
try {
Log.startSession("TSW.sA");
createTransactions(callId, callback, SET_ACTIVE);
} finally {
Log.endSession();
}
}
@Override
public void answer(int videoState, String callId, android.os.ResultReceiver callback)
throws RemoteException {
try {
Log.startSession("TSW.a");
createTransactions(callId, callback, ANSWER, videoState);
} finally {
Log.endSession();
}
}
@Override
public void setInactive(String callId, android.os.ResultReceiver callback)
throws RemoteException {
try {
Log.startSession("TSW.sI");
createTransactions(callId, callback, SET_INACTIVE);
} finally {
Log.endSession();
}
}
@Override
public void disconnect(String callId, DisconnectCause disconnectCause,
android.os.ResultReceiver callback)
throws RemoteException {
try {
Log.startSession("TSW.d");
createTransactions(callId, callback, DISCONNECT, disconnectCause);
} finally {
Log.endSession();
}
}
@Override
public void startCallStreaming(String callId, android.os.ResultReceiver callback)
throws RemoteException {
try {
Log.startSession("TSW.sCS");
createTransactions(callId, callback, START_STREAMING);
} finally {
Log.endSession();
}
}
private void createTransactions(String callId, ResultReceiver callback, String action,
Object... objects) {
Log.d(TAG, "createTransactions: callId=" + callId);
Call call = mTrackedCalls.get(callId);
if (call != null) {
switch (action) {
case SET_ACTIVE:
handleCallControlNewCallFocusTransactions(call, SET_ACTIVE,
false /* isAnswer */, 0/*VideoState (ignored)*/, callback);
break;
case ANSWER:
handleCallControlNewCallFocusTransactions(call, ANSWER,
true /* isAnswer */, (int) objects[0] /*VideoState*/, callback);
break;
case DISCONNECT:
addTransactionsToManager(new EndCallTransaction(mCallsManager,
(DisconnectCause) objects[0], call), callback);
break;
case SET_INACTIVE:
addTransactionsToManager(
new HoldCallTransaction(mCallsManager, call), callback);
break;
case START_STREAMING:
addTransactionsToManager(mStreamingController.getStartStreamingTransaction(mCallsManager,
TransactionalServiceWrapper.this, call, mLock), callback);
break;
}
} else {
Bundle exceptionBundle = new Bundle();
exceptionBundle.putParcelable(TRANSACTION_EXCEPTION_KEY,
new CallException(TextUtils.formatSimple(
"Telecom cannot process [%s] because the call with id=[%s] is no longer "
+ "being tracked. This is most likely a result of the call "
+ "already being disconnected and removed. Try re-adding the call"
+ " via TelecomManager#addCall", action, callId),
CODE_CALL_IS_NOT_BEING_TRACKED));
callback.send(CODE_CALL_IS_NOT_BEING_TRACKED, exceptionBundle);
}
}
// The client is request their VoIP call state go ACTIVE/ANSWERED.
// This request is originating from the VoIP application.
private void handleCallControlNewCallFocusTransactions(Call call, String action,
boolean isAnswer, int potentiallyNewVideoState, ResultReceiver callback) {
mTransactionManager.addTransaction(createSetActiveTransactions(call),
new OutcomeReceiver<>() {
@Override
public void onResult(VoipCallTransactionResult result) {
Log.i(TAG, String.format(Locale.US,
"%s: onResult: callId=[%s]", action, call.getId()));
if (isAnswer) {
call.setVideoState(potentiallyNewVideoState);
}
callback.send(TELECOM_TRANSACTION_SUCCESS, new Bundle());
}
@Override
public void onError(CallException exception) {
Bundle extras = new Bundle();
extras.putParcelable(TRANSACTION_EXCEPTION_KEY, exception);
callback.send(exception == null ? CallException.CODE_ERROR_UNKNOWN :
exception.getCode(), extras);
}
});
}
@Override
public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
try {
Log.startSession("TSW.rCEC");
addTransactionsToManager(new EndpointChangeTransaction(endpoint, mCallsManager),
callback);
} finally {
Log.endSession();
}
}
/**
* Application would like to inform InCallServices of an event
*/
@Override
public void sendEvent(String callId, String event, Bundle extras) {
try {
Log.startSession("TSW.sE");
Call call = mTrackedCalls.get(callId);
if (call != null) {
call.onConnectionEvent(event, extras);
} else {
Log.i(TAG,
"sendEvent: was called but there is no call with id=[%s] cannot be "
+ "found. Most likely the call has been disconnected");
}
} finally {
Log.endSession();
}
}
};
public void addTransactionsToManager(VoipCallTransaction transaction,
ResultReceiver callback) {
Log.d(TAG, "addTransactionsToManager");
mTransactionManager.addTransaction(transaction, new OutcomeReceiver<>() {
@Override
public void onResult(VoipCallTransactionResult result) {
Log.d(TAG, "addTransactionsToManager: onResult:");
callback.send(TELECOM_TRANSACTION_SUCCESS, new Bundle());
}
@Override
public void onError(CallException exception) {
Log.d(TAG, "addTransactionsToManager: onError");
Bundle extras = new Bundle();
extras.putParcelable(TRANSACTION_EXCEPTION_KEY, exception);
callback.send(exception == null ? CallException.CODE_ERROR_UNKNOWN :
exception.getCode(), extras);
}
});
}
public ICallControl getICallControl() {
return mICallControl;
}
/***
*********************************************************************************************
** ICallEventCallback: Server --> Client **
**********************************************************************************************
*/
public void onSetActive(Call call) {
try {
Log.startSession("TSW.oSA");
Log.d(TAG, String.format(Locale.US, "onSetActive: callId=[%s]", call.getId()));
handleCallEventCallbackNewFocus(call, ON_SET_ACTIVE, false /*isAnswerRequest*/,
0 /*VideoState*/);
} finally {
Log.endSession();
}
}
public void onAnswer(Call call, int videoState) {
try {
Log.startSession("TSW.oA");
Log.d(TAG, String.format(Locale.US, "onAnswer: callId=[%s]", call.getId()));
handleCallEventCallbackNewFocus(call, ON_ANSWER, true /*isAnswerRequest*/,
videoState /*VideoState*/);
} finally {
Log.endSession();
}
}
// handle a CallEventCallback to set a call ACTIVE/ANSWERED. Must get ack from client since the
// request has come from another source (ex. Android Auto is requesting a call to go active)
private void handleCallEventCallbackNewFocus(Call call, String action, boolean isAnswerRequest,
int potentiallyNewVideoState) {
// save CallsManager state before sending client state changes
Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
SerialTransaction serialTransactions = createSetActiveTransactions(call);
// 3. get ack from client (that the requested call can go active)
if (isAnswerRequest) {
serialTransactions.appendTransaction(
new CallEventCallbackAckTransaction(mICallEventCallback,
action, call.getId(), potentiallyNewVideoState, mLock));
} else {
serialTransactions.appendTransaction(
new CallEventCallbackAckTransaction(mICallEventCallback,
action, call.getId(), mLock));
}
// do CallsManager workload before asking client and
// reset CallsManager state if client does NOT ack
mTransactionManager.addTransaction(serialTransactions,
new OutcomeReceiver<>() {
@Override
public void onResult(VoipCallTransactionResult result) {
Log.i(TAG, String.format(Locale.US,
"%s: onResult: callId=[%s]", action, call.getId()));
if (isAnswerRequest) {
call.setVideoState(potentiallyNewVideoState);
}
}
@Override
public void onError(CallException exception) {
if (isAnswerRequest) {
// This also sends the signal to untrack from TSW and the client_TSW
removeCallFromCallsManager(call,
new DisconnectCause(DisconnectCause.REJECTED,
"client rejected to answer the call;"
+ " force disconnecting"));
} else {
mCallsManager.markCallAsOnHold(call);
}
maybeResetForegroundCall(foregroundCallBeforeSwap, wasActive);
}
});
}
public void onSetInactive(Call call) {
try {
Log.startSession("TSW.oSI");
Log.i(TAG, String.format(Locale.US, "onSetInactive: callId=[%s]", call.getId()));
mTransactionManager.addTransaction(
new CallEventCallbackAckTransaction(mICallEventCallback,
ON_SET_INACTIVE, call.getId(), mLock), new OutcomeReceiver<>() {
@Override
public void onResult(VoipCallTransactionResult result) {
mCallsManager.markCallAsOnHold(call);
}
@Override
public void onError(CallException exception) {
Log.i(TAG, "onSetInactive: onError: with e=[%e]", exception);
}
});
} finally {
Log.endSession();
}
}
public void onDisconnect(Call call, DisconnectCause cause) {
try {
Log.startSession("TSW.oD");
Log.d(TAG, String.format(Locale.US, "onDisconnect: callId=[%s]", call.getId()));
mTransactionManager.addTransaction(
new CallEventCallbackAckTransaction(mICallEventCallback, ON_DISCONNECT,
call.getId(), cause, mLock), new OutcomeReceiver<>() {
@Override
public void onResult(VoipCallTransactionResult result) {
removeCallFromCallsManager(call, cause);
}
@Override
public void onError(CallException exception) {
removeCallFromCallsManager(call, cause);
}
}
);
} finally {
Log.endSession();
}
}
public void onCallStreamingStarted(Call call) {
try {
Log.startSession("TSW.oCSS");
Log.d(TAG, String.format(Locale.US, "onCallStreamingStarted: callId=[%s]",
call.getId()));
mTransactionManager.addTransaction(
new CallEventCallbackAckTransaction(mICallEventCallback, ON_STREAMING_STARTED,
call.getId(), mLock), new OutcomeReceiver<>() {
@Override
public void onResult(VoipCallTransactionResult result) {
}
@Override
public void onError(CallException exception) {
Log.i(TAG, "onCallStreamingStarted: onError: with e=[%e]",
exception);
stopCallStreaming(call);
}
}
);
} finally {
Log.endSession();
}
}
public void onCallStreamingFailed(Call call,
@CallStreamingService.StreamingFailedReason int streamingFailedReason) {
if (call != null) {
try {
mICallEventCallback.onCallStreamingFailed(call.getId(), streamingFailedReason);
} catch (RemoteException e) {
}
}
}
public void onCallEndpointChanged(Call call, CallEndpoint endpoint) {
if (call != null) {
try {
mICallEventCallback.onCallEndpointChanged(call.getId(), endpoint);
} catch (RemoteException e) {
}
}
}
public void onAvailableCallEndpointsChanged(Call call, Set<CallEndpoint> endpoints) {
if (call != null) {
try {
mICallEventCallback.onAvailableCallEndpointsChanged(call.getId(),
endpoints.stream().toList());
} catch (RemoteException e) {
}
}
}
public void onMuteStateChanged(Call call, boolean isMuted) {
if (call != null) {
try {
mICallEventCallback.onMuteStateChanged(call.getId(), isMuted);
} catch (RemoteException e) {
}
}
}
public void removeCallFromWrappers(Call call) {
if (call != null) {
try {
// remove the call from frameworks wrapper (client side)
mICallEventCallback.removeCallFromTransactionalServiceWrapper(call.getId());
} catch (RemoteException e) {
}
// remove the call from this class/wrapper (server side)
untrackCall(call);
}
}
public void onEvent(Call call, String event, Bundle extras) {
if (call != null) {
try {
mICallEventCallback.onEvent(call.getId(), event, extras);
} catch (RemoteException e) {
}
}
}
/***
*********************************************************************************************
** Helpers **
**********************************************************************************************
*/
private void maybeResetForegroundCall(Call foregroundCallBeforeSwap, boolean wasActive) {
if (foregroundCallBeforeSwap == null) {
return;
}
if (wasActive && !foregroundCallBeforeSwap.isActive()) {
mCallsManager.markCallAsActive(foregroundCallBeforeSwap);
}
}
private void removeCallFromCallsManager(Call call, DisconnectCause cause) {
if (cause.getCode() != DisconnectCause.REJECTED) {
mCallsManager.markCallAsDisconnected(call, cause);
}
mCallsManager.removeCall(call);
}
private SerialTransaction createSetActiveTransactions(Call call) {
// create list for multiple transactions
List<VoipCallTransaction> transactions = new ArrayList<>();
// potentially hold the current active call in order to set a new call (active/answered)
transactions.add(new MaybeHoldCallForNewCallTransaction(mCallsManager, call));
// And request a new focus call update
transactions.add(new RequestNewActiveCallTransaction(mCallsManager, call));
return new SerialTransaction(transactions, mLock);
}
private void setDeathRecipient(ICallEventCallback callEventCallback) {
try {
callEventCallback.asBinder().linkToDeath(mAppDeathListener, 0);
} catch (Exception e) {
Log.w(TAG, "setDeathRecipient: hit exception=[%s] trying to link binder to death",
e.toString());
}
}
/***
*********************************************************************************************
** FocusManager **
**********************************************************************************************
*/
@Override
public void connectionServiceFocusLost() {
if (mConnSvrFocusListener != null) {
mConnSvrFocusListener.onConnectionServiceReleased(this);
}
Log.i(TAG, String.format(Locale.US, "connectionServiceFocusLost for package=[%s]",
mPackageName));
}
@Override
public void connectionServiceFocusGained() {
Log.i(TAG, String.format(Locale.US, "connectionServiceFocusGained for package=[%s]",
mPackageName));
}
@Override
public void setConnectionServiceFocusListener(
ConnectionServiceFocusManager.ConnectionServiceFocusListener listener) {
mConnSvrFocusListener = listener;
}
@Override
public ComponentName getComponentName() {
return mPhoneAccountHandle.getComponentName();
}
/***
*********************************************************************************************
** CallStreaming **
*********************************************************************************************
*/
public void stopCallStreaming(Call call) {
Log.i(this, "stopCallStreaming; callid=%s", call.getId());
if (call != null && call.isStreaming()) {
VoipCallTransaction stopStreamingTransaction = mStreamingController
.getStopStreamingTransaction(call, mLock);
addTransactionsToManager(stopStreamingTransaction, new ResultReceiver(null));
}
}
}