diff options
Diffstat (limited to 'src/com/android/server/telecom/TransactionalServiceWrapper.java')
-rw-r--r-- | src/com/android/server/telecom/TransactionalServiceWrapper.java | 659 |
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)); + } + } +} |