summaryrefslogtreecommitdiff
path: root/src/com/android/server/telecom/TransactionalServiceWrapper.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/server/telecom/TransactionalServiceWrapper.java')
-rw-r--r--src/com/android/server/telecom/TransactionalServiceWrapper.java659
1 files changed, 659 insertions, 0 deletions
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
new file mode 100644
index 000000000..25aaad789
--- /dev/null
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -0,0 +1,659 @@
+/*
+ * 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));
+ }
+ }
+}