/* * 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.internal.telecom; import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS; import android.os.Binder; import android.os.Bundle; import android.os.OutcomeReceiver; import android.os.ResultReceiver; import android.telecom.CallAttributes; import android.telecom.CallControl; import android.telecom.CallControlCallback; import android.telecom.CallEndpoint; import android.telecom.CallEventCallback; import android.telecom.CallException; import android.telecom.DisconnectCause; import android.telecom.PhoneAccountHandle; import android.text.TextUtils; import android.util.Log; import com.android.server.telecom.flags.Flags; import java.util.List; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.function.Consumer; /** * wraps {@link CallControlCallback}, {@link CallEventCallback}, and {@link CallControl} on a * per-{@link android.telecom.PhoneAccountHandle} basis to track ongoing calls. * * @hide */ public class ClientTransactionalServiceWrapper { private static final String TAG = ClientTransactionalServiceWrapper.class.getSimpleName(); private final PhoneAccountHandle mPhoneAccountHandle; private final ClientTransactionalServiceRepository mRepository; private final ConcurrentHashMap mCallIdToTransactionalCall = new ConcurrentHashMap<>(); private static final String EXECUTOR_FAIL_MSG = "Telecom hit an exception while handling a CallEventCallback on an executor: "; public ClientTransactionalServiceWrapper(PhoneAccountHandle handle, ClientTransactionalServiceRepository repo) { mPhoneAccountHandle = handle; mRepository = repo; } /** * remove the given call from the class HashMap * * @param callId that is tied to TransactionalCall object */ public void untrackCall(String callId) { Log.i(TAG, TextUtils.formatSimple("removeCall: with id=[%s]", callId)); if (mCallIdToTransactionalCall.containsKey(callId)) { // remove the call from the hashmap TransactionalCall call = mCallIdToTransactionalCall.remove(callId); // null out interface to avoid memory leaks CallControl control = call.getCallControl(); if (control != null) { call.setCallControl(null); } } // possibly cleanup service wrapper if there are no more calls if (mCallIdToTransactionalCall.size() == 0) { mRepository.removeServiceWrapper(mPhoneAccountHandle); } } /** * start tracking a newly created call for a particular package * * @param callAttributes of the new call * @param executor to run callbacks on * @param pendingControl that allows telecom to call into the client * @param handshakes that overrides the CallControlCallback * @param events that overrides the CallStateCallback * @return the callId of the newly created call */ public String trackCall(CallAttributes callAttributes, Executor executor, OutcomeReceiver pendingControl, CallControlCallback handshakes, CallEventCallback events) { // generate a new id for this new call String newCallId = UUID.randomUUID().toString(); // couple the objects passed from the client side mCallIdToTransactionalCall.put(newCallId, new TransactionalCall(newCallId, callAttributes, executor, pendingControl, handshakes, events)); return newCallId; } public ICallEventCallback getCallEventCallback() { return mCallEventCallback; } /** * Consumers that is to be completed by the client and the result relayed back to telecom server * side via a {@link ResultReceiver}. see com.android.server.telecom.TransactionalServiceWrapper * for how the response is handled. */ private class ReceiverWrapper implements Consumer { private final ResultReceiver mRepeaterReceiver; ReceiverWrapper(ResultReceiver resultReceiver) { mRepeaterReceiver = resultReceiver; } @Override public void accept(Boolean clientCompletedCallbackSuccessfully) { if (clientCompletedCallbackSuccessfully) { mRepeaterReceiver.send(TELECOM_TRANSACTION_SUCCESS, null); } else { mRepeaterReceiver.send(CallException.CODE_ERROR_UNKNOWN, null); } } @Override public Consumer andThen(Consumer after) { return Consumer.super.andThen(after); } } private final ICallEventCallback mCallEventCallback = new ICallEventCallback.Stub() { private static final String ON_SET_ACTIVE = "onSetActive"; private static final String ON_SET_INACTIVE = "onSetInactive"; private static final String ON_ANSWER = "onAnswer"; private static final String ON_DISCONNECT = "onDisconnect"; private static final String ON_STREAMING_STARTED = "onStreamingStarted"; private static final String ON_REQ_ENDPOINT_CHANGE = "onRequestEndpointChange"; private static final String ON_AVAILABLE_CALL_ENDPOINTS = "onAvailableCallEndpointsChanged"; private static final String ON_MUTE_STATE_CHANGED = "onMuteStateChanged"; private static final String ON_VIDEO_STATE_CHANGED = "onVideoStateChanged"; private static final String ON_CALL_STREAMING_FAILED = "onCallStreamingFailed"; private static final String ON_EVENT = "onEvent"; private void handleCallEventCallback(String action, String callId, ResultReceiver ackResultReceiver, Object... args) { Log.i(TAG, TextUtils.formatSimple("hCEC: id=[%s], action=[%s]", callId, action)); // lookup the callEventCallback associated with the particular call TransactionalCall call = mCallIdToTransactionalCall.get(callId); if (call != null) { // Get the CallEventCallback interface CallControlCallback callback = call.getCallControlCallback(); // Get Receiver to wait on client ack ReceiverWrapper outcomeReceiverWrapper = new ReceiverWrapper(ackResultReceiver); // wait for the client to complete the CallEventCallback final long identity = Binder.clearCallingIdentity(); try { call.getExecutor().execute(() -> { switch (action) { case ON_SET_ACTIVE: callback.onSetActive(outcomeReceiverWrapper); break; case ON_SET_INACTIVE: callback.onSetInactive(outcomeReceiverWrapper); break; case ON_DISCONNECT: callback.onDisconnect((DisconnectCause) args[0], outcomeReceiverWrapper); untrackCall(callId); break; case ON_ANSWER: callback.onAnswer((int) args[0], outcomeReceiverWrapper); break; case ON_STREAMING_STARTED: callback.onCallStreamingStarted(outcomeReceiverWrapper); break; } }); } catch (Exception e) { Log.e(TAG, EXECUTOR_FAIL_MSG + e); } finally { Binder.restoreCallingIdentity(identity); } } } @Override public void onAddCallControl(String callId, int resultCode, ICallControl callControl, CallException transactionalException) { Log.i(TAG, TextUtils.formatSimple("oACC: id=[%s], code=[%d]", callId, resultCode)); TransactionalCall call = mCallIdToTransactionalCall.get(callId); if (call != null) { OutcomeReceiver pendingControl = call.getPendingControl(); if (resultCode == TELECOM_TRANSACTION_SUCCESS) { // create the interface object that the client will interact with CallControl control = new CallControl(callId, callControl); // give the client the object via the OR that was passed into addCall pendingControl.onResult(control); // store for later reference call.setCallControl(control); } else { pendingControl.onError(transactionalException); mCallIdToTransactionalCall.remove(callId); } } else { untrackCall(callId); Log.e(TAG, "oACC: TransactionalCall object not found for call w/ id=" + callId); } } @Override public void onSetActive(String callId, ResultReceiver resultReceiver) { handleCallEventCallback(ON_SET_ACTIVE, callId, resultReceiver); } @Override public void onSetInactive(String callId, ResultReceiver resultReceiver) { handleCallEventCallback(ON_SET_INACTIVE, callId, resultReceiver); } @Override public void onAnswer(String callId, int videoState, ResultReceiver resultReceiver) { handleCallEventCallback(ON_ANSWER, callId, resultReceiver, videoState); } @Override public void onDisconnect(String callId, DisconnectCause cause, ResultReceiver resultReceiver) { handleCallEventCallback(ON_DISCONNECT, callId, resultReceiver, cause); } @Override public void onCallEndpointChanged(String callId, CallEndpoint endpoint) { handleEventCallback(callId, ON_REQ_ENDPOINT_CHANGE, endpoint); } @Override public void onAvailableCallEndpointsChanged(String callId, List endpoints) { handleEventCallback(callId, ON_AVAILABLE_CALL_ENDPOINTS, endpoints); } @Override public void onMuteStateChanged(String callId, boolean isMuted) { handleEventCallback(callId, ON_MUTE_STATE_CHANGED, isMuted); } @Override public void onVideoStateChanged(String callId, int videoState) { handleEventCallback(callId, ON_VIDEO_STATE_CHANGED, videoState); } public void handleEventCallback(String callId, String action, Object arg) { Log.d(TAG, TextUtils.formatSimple("hEC: [%s], callId=[%s]", action, callId)); // lookup the callEventCallback associated with the particular call TransactionalCall call = mCallIdToTransactionalCall.get(callId); if (call != null) { CallEventCallback callback = call.getCallStateCallback(); Executor executor = call.getExecutor(); final long identity = Binder.clearCallingIdentity(); try { executor.execute(() -> { switch (action) { case ON_REQ_ENDPOINT_CHANGE: callback.onCallEndpointChanged((CallEndpoint) arg); break; case ON_AVAILABLE_CALL_ENDPOINTS: callback.onAvailableCallEndpointsChanged((List) arg); break; case ON_MUTE_STATE_CHANGED: callback.onMuteStateChanged((boolean) arg); break; case ON_VIDEO_STATE_CHANGED: if (Flags.transactionalVideoState()) { callback.onVideoStateChanged((int) arg); } break; case ON_CALL_STREAMING_FAILED: callback.onCallStreamingFailed((int) arg /* reason */); break; } }); } finally { Binder.restoreCallingIdentity(identity); } } } @Override public void removeCallFromTransactionalServiceWrapper(String callId) { untrackCall(callId); } @Override public void onCallStreamingStarted(String callId, ResultReceiver resultReceiver) { handleCallEventCallback(ON_STREAMING_STARTED, callId, resultReceiver); } @Override public void onCallStreamingFailed(String callId, int reason) { Log.i(TAG, TextUtils.formatSimple("oCSF: id=[%s], reason=[%s]", callId, reason)); handleEventCallback(callId, ON_CALL_STREAMING_FAILED, reason); } @Override public void onEvent(String callId, String event, Bundle extras) { // lookup the callEventCallback associated with the particular call TransactionalCall call = mCallIdToTransactionalCall.get(callId); if (call != null) { CallEventCallback callback = call.getCallStateCallback(); Executor executor = call.getExecutor(); final long identity = Binder.clearCallingIdentity(); try { executor.execute(() -> { callback.onEvent(event, extras); }); } finally { Binder.restoreCallingIdentity(identity); } } } }; }