blob: ce9597b26e948673ace3aa814e6dce4320dcddc1 [file]
/*
* 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.net.Uri;
import android.os.Binder;
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 androidx.annotation.VisibleForTesting;
import com.android.internal.telecom.ICallControl;
import com.android.internal.telecom.ICallEventCallback;
import com.android.server.telecom.callsequencing.CallTransaction;
import com.android.server.telecom.callsequencing.CallTransactionResult;
import com.android.server.telecom.callsequencing.TransactionManager;
import com.android.server.telecom.callsequencing.TransactionalCallSequencingAdapter;
import com.android.server.telecom.callsequencing.voip.CallEventCallbackAckTransaction;
import com.android.server.telecom.callsequencing.voip.EndpointChangeTransaction;
import com.android.server.telecom.callsequencing.voip.RequestVideoStateTransaction;
import com.android.server.telecom.callsequencing.voip.SetGroupCallStateTransaction;
import com.android.server.telecom.callsequencing.voip.SetMuteStateTransaction;
import com.android.server.telecom.flags.FeatureFlags;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
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, CallSourceService {
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";
public static final String REQUEST_VIDEO_STATE = "RequestVideoState";
public static final String SET_GROUP_CALL_STATE = "SetGroupCallState";
public static final String SET_CONTACT_URI = "SetContactUri";
public static final String SET_MUTE_STATE = "SetMuteState";
public static final String CALL_ENDPOINT_CHANGE = "CallEndpointChange";
// 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";
public static final String STOP_STREAMING = "stopStreaming";
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;
private final TransactionalCallSequencingAdapter mCallSequencingAdapter;
private final FeatureFlags mFeatureFlags;
private final AnomalyReporterAdapter mAnomalyReporter;
public static final UUID CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID =
UUID.fromString("8187cd59-97a7-4e9f-a772-638dda4b69bb");
public static final String CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG =
"A call update was attempted for a call no longer being tracked";
// 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, TransactionManager transactionManager,
FeatureFlags featureFlags, AnomalyReporterAdapter anomalyReporterAdapter) {
// passed args
mICallEventCallback = callEventCallback;
mCallsManager = callsManager;
mPhoneAccountHandle = phoneAccountHandle;
mTrackedCalls.put(call.getId(), call); // service is now tracking its first call
mRepository = repo;
mTransactionManager = transactionManager;
// init instance vars
mPackageName = phoneAccountHandle.getComponentName().getPackageName();
mStreamingController = mCallsManager.getCallStreamingController();
mLock = mCallsManager.getLock();
mCallSequencingAdapter = new TransactionalCallSequencingAdapter(mTransactionManager,
mCallsManager);
setDeathRecipient(callEventCallback);
mFeatureFlags = featureFlags;
mAnomalyReporter = anomalyReporterAdapter;
}
public TransactionManager getTransactionManager() {
return mTransactionManager;
}
@VisibleForTesting
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;
}
private void cleanupTransactionalServiceWrapper() {
mCallSequencingAdapter.cleanup(mTrackedCalls.values());
}
/***
*********************************************************************************************
** ICallControl: Client --> Server **
**********************************************************************************************
*/
private final ICallControl mICallControl = new ICallControl.Stub() {
@Override
public void setActive(String callId, android.os.ResultReceiver callback) {
long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sA");
createTransactions(callId, callback, SET_ACTIVE);
} finally {
Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@Override
public void answer(int callType, String callId, android.os.ResultReceiver callback) {
long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.a");
createTransactions(callId, callback, ANSWER, callType);
} finally {
Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@Override
public void setInactive(String callId, android.os.ResultReceiver callback) {
long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sI");
createTransactions(callId, callback, SET_INACTIVE);
} finally {
Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@Override
public void disconnect(String callId, DisconnectCause disconnectCause,
android.os.ResultReceiver callback) {
long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.d");
createTransactions(callId, callback, DISCONNECT, disconnectCause);
} finally {
Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@Override
public void setMuteState(boolean isMuted, android.os.ResultReceiver callback) {
long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sMS");
addTransactionsToManager(SET_MUTE_STATE,
new SetMuteStateTransaction(mCallsManager, isMuted), callback);
} finally {
Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@Override
public void startCallStreaming(String callId, android.os.ResultReceiver callback) {
long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sCS");
createTransactions(callId, callback, START_STREAMING);
} finally {
Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@Override
public void requestVideoState(int callType, String callId, ResultReceiver callback) {
long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.rVS");
createTransactions(callId, callback, REQUEST_VIDEO_STATE, callType);
} finally {
Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@Override
public void setGroupCallState(String callId, boolean isGroupCall,
ResultReceiver callback) {
long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sGCS");
createTransactions(callId, callback, SET_GROUP_CALL_STATE, isGroupCall);
} finally {
Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
@Override
public void setContactUri(String callId, Uri uri,
ResultReceiver callback) {
long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.sCU");
createTransactions(callId, callback, SET_CONTACT_URI, uri);
} finally {
Binder.restoreCallingIdentity(token);
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:
mCallSequencingAdapter.setActive(call,
getCompleteReceiver(action, callback));
break;
case ANSWER:
mCallSequencingAdapter.setAnswered(call, (int) objects[0] /*callType*/,
getCompleteReceiver(action, callback));
break;
case DISCONNECT:
DisconnectCause dc = (DisconnectCause) objects[0];
mCallSequencingAdapter.setDisconnected(call, dc,
getCompleteReceiver(action, callback));
break;
case SET_INACTIVE:
mCallSequencingAdapter.setInactive(call,
getCompleteReceiver(action,callback));
break;
case START_STREAMING:
addTransactionsToManager(action,
mStreamingController.getStartStreamingTransaction(mCallsManager,
TransactionalServiceWrapper.this, call, mLock), callback);
break;
case REQUEST_VIDEO_STATE:
addTransactionsToManager(action,
new RequestVideoStateTransaction(mCallsManager, call,
(int) objects[0]), callback);
break;
case SET_GROUP_CALL_STATE:
addTransactionsToManager(action,
new SetGroupCallStateTransaction(mCallsManager, call,
(boolean) objects[0]), callback);
break;
case SET_CONTACT_URI:
addTransactionsToManager(action,
new SetGroupCallStateTransaction(mCallsManager, call,
(Uri) objects[0]), callback);
break;
}
} else {
Bundle exceptionBundle = new Bundle();
exceptionBundle.putParcelable(TRANSACTION_EXCEPTION_KEY,
new CallException(String.format("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);
mAnomalyReporter.reportAnomaly(
CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID,
CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG);
}
}
@Override
public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
int uid = Binder.getCallingUid();
long token = Binder.clearCallingIdentity();
try {
Log.startSession("TSW.rCEC");
addTransactionsToManager(CALL_ENDPOINT_CHANGE,
new EndpointChangeTransaction(endpoint, mCallsManager, uid), callback);
} finally {
Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
/**
* Application would like to inform InCallServices of an event
*/
@Override
public void sendEvent(String callId, String event, Bundle extras) {
long token = Binder.clearCallingIdentity();
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 {
Binder.restoreCallingIdentity(token);
Log.endSession();
}
}
};
private void addTransactionsToManager(String action, CallTransaction transaction,
ResultReceiver callback) {
Log.d(TAG, "addTransactionsToManager");
CompletableFuture<Boolean> transactionResult = mTransactionManager
.addTransaction(transaction, getCompleteReceiver(action, callback));
}
private OutcomeReceiver<CallTransactionResult, CallException> getCompleteReceiver(
String action, ResultReceiver callback) {
return new OutcomeReceiver<>() {
@Override
public void onResult(CallTransactionResult result) {
Log.d(TAG, "completeReceiver: onResult[" + action + "]:" + result);
callback.send(TELECOM_TRANSACTION_SUCCESS, new Bundle());
}
@Override
public void onError(CallException exception) {
Log.d(TAG, "completeReceiver: onError[" + action + "]" + exception);
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 CompletableFuture<Boolean> onSetActive(Call call) {
CallTransaction callTransaction = new CallEventCallbackAckTransaction(
mICallEventCallback, ON_SET_ACTIVE, call.getId(), mLock);
CompletableFuture<Boolean> onSetActiveFuture;
try {
Log.startSession("TSW.oSA");
Log.d(TAG, String.format(Locale.US, "onSetActive: callId=[%s]", call.getId()));
onSetActiveFuture = mCallSequencingAdapter.onSetActive(call,
callTransaction, result ->
Log.i(TAG, String.format(Locale.US,
"%s: onResult: callId=[%s], result=[%s]", ON_SET_ACTIVE,
call.getId(), result)));
} finally {
Log.endSession();
}
return onSetActiveFuture;
}
public CompletableFuture<Boolean> onAnswer(Call call, int videoState) {
CompletableFuture<Boolean> onAnswerFuture;
try {
Log.startSession("TSW.oA");
Log.d(TAG, String.format(Locale.US, "onAnswer: callId=[%s]", call.getId()));
onAnswerFuture = mCallSequencingAdapter.onSetAnswered(call, videoState,
new CallEventCallbackAckTransaction(mICallEventCallback,
ON_ANSWER, call.getId(), videoState, mLock),
result -> Log.i(TAG, String.format(Locale.US,
"%s: onResult: callId=[%s], result=[%s]",
ON_ANSWER, call.getId(), result)));
} finally {
Log.endSession();
}
return onAnswerFuture;
}
public CompletableFuture<Boolean> onSetInactive(Call call) {
CallTransaction callTransaction = new CallEventCallbackAckTransaction(
mICallEventCallback, ON_SET_INACTIVE, call.getId(), mLock);
CompletableFuture<Boolean> onSetInactiveFuture;
try {
Log.startSession("TSW.oSI");
Log.i(TAG, String.format(Locale.US, "onSetInactive: callId=[%s]", call.getId()));
onSetInactiveFuture = mCallSequencingAdapter.onSetInactive(call,
callTransaction, new OutcomeReceiver<>() {
@Override
public void onResult(CallTransactionResult result) {
Log.i(TAG, String.format(Locale.US, "onSetInactive: callId=[%s]"
+ ", result=[%s]",
call.getId(), result));
}
@Override
public void onError(CallException exception) {
Log.w(TAG, "onSetInactive: onError: e.code=[%d], e.msg=[%s]",
exception.getCode(), exception.getMessage());
}
});
} finally {
Log.endSession();
}
return onSetInactiveFuture;
}
public CompletableFuture<Boolean> onDisconnect(Call call,
DisconnectCause cause) {
CallTransaction callTransaction = new CallEventCallbackAckTransaction(
mICallEventCallback, ON_DISCONNECT, call.getId(), cause, mLock);
CompletableFuture<Boolean> onDisconnectFuture;
try {
Log.startSession("TSW.oD");
Log.d(TAG, String.format(Locale.US, "onDisconnect: callId=[%s]", call.getId()));
onDisconnectFuture = mCallSequencingAdapter.onSetDisconnected(call, cause,
callTransaction,
result -> Log.i(TAG, String.format(Locale.US,
"%s: onResult: callId=[%s], result=[%s]",
ON_DISCONNECT, call.getId(), result)));
} finally {
Log.endSession();
}
return onDisconnectFuture;
}
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(CallTransactionResult result) {
}
@Override
public void onError(CallException exception) {
Log.w(TAG, "onCallStreamingStarted: onError: "
+ "e.code=[%d], e.msg=[%s]",
exception.getCode(), exception.getMessage());
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) {
}
}
}
@Override
public void onCallEndpointChanged(Call call, CallEndpoint endpoint) {
if (call != null) {
try {
mICallEventCallback.onCallEndpointChanged(call.getId(), endpoint);
} catch (RemoteException e) {
}
}
}
@Override
public void onAvailableCallEndpointsChanged(Call call, Set<CallEndpoint> endpoints) {
if (call != null) {
try {
mICallEventCallback.onAvailableCallEndpointsChanged(call.getId(),
endpoints.stream().toList());
} catch (RemoteException e) {
}
}
}
@Override
public void onMuteStateChanged(Call call, boolean isMuted) {
if (call != null) {
try {
mICallEventCallback.onMuteStateChanged(call.getId(), isMuted);
} catch (RemoteException e) {
}
}
}
@Override
public void onVideoStateChanged(Call call, int videoState) {
if (call != null) {
try {
mICallEventCallback.onVideoStateChanged(call.getId(), videoState);
} 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);
}
}
@Override
public void sendCallEvent(Call call, String event, Bundle extras) {
if (call != null) {
try {
mICallEventCallback.onEvent(call.getId(), event, extras);
} catch (RemoteException e) {
}
}
}
/***
*********************************************************************************************
** Helpers **
**********************************************************************************************
*/
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()) {
CallTransaction stopStreamingTransaction = mStreamingController
.getStopStreamingTransaction(call, mLock);
addTransactionsToManager(STOP_STREAMING, stopStreamingTransaction,
new ResultReceiver(null));
}
}
}