/* * 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 android.telecom; import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.SuppressLint; import android.os.Binder; import android.os.Bundle; import android.os.OutcomeReceiver; import android.os.ParcelUuid; import android.os.RemoteException; import android.os.ResultReceiver; import android.text.TextUtils; import com.android.internal.telecom.ICallControl; import com.android.server.telecom.flags.Flags; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; /** * CallControl provides client side control of a call. Each Call will get an individual CallControl * instance in which the client can alter the state of the associated call. * *

* Each method is Transactional meaning that it can succeed or fail. If a transaction succeeds, * the {@link OutcomeReceiver#onResult} will be called by Telecom. Otherwise, the * {@link OutcomeReceiver#onError} is called and provides a {@link CallException} that details why * the operation failed. */ @SuppressLint("NotCloseable") public final class CallControl { private static final String TAG = CallControl.class.getSimpleName(); private final String mCallId; private final ICallControl mServerInterface; /** @hide */ public CallControl(@NonNull String callId, @NonNull ICallControl serverInterface) { mCallId = callId; mServerInterface = serverInterface; } /** * @return the callId Telecom assigned to this CallControl object which should be attached to * an individual call. */ @NonNull public ParcelUuid getCallId() { return ParcelUuid.fromString(mCallId); } /** * Request Telecom set the call state to active. This method should be called when either an * outgoing call is ready to go active or a held call is ready to go active again. For incoming * calls that are ready to be answered, use * {@link CallControl#answer(int, Executor, OutcomeReceiver)}. * * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback * will be called on. * @param callback that will be completed on the Telecom side that details success or failure * of the requested operation. * * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully * switched the call state to active * * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set * the call state to active. A {@link CallException} will be passed * that details why the operation failed. */ public void setActive(@CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mServerInterface.setActive(mCallId, new CallControlResultReceiver("setActive", executor, callback)); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } /** * Request Telecom answer an incoming call. For outgoing calls and calls that have been placed * on hold, use {@link CallControl#setActive(Executor, OutcomeReceiver)}. * * @param videoState to report to Telecom. Telecom will store VideoState in the event another * service/device requests it in order to continue the call on another screen. * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback * will be called on. * @param callback that will be completed on the Telecom side that details success or failure * of the requested operation. * * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully * switched the call state to active * * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set * the call state to active. A {@link CallException} will be passed * that details why the operation failed. */ public void answer(@android.telecom.CallAttributes.CallType int videoState, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver callback) { validateVideoState(videoState); Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mServerInterface.answer(videoState, mCallId, new CallControlResultReceiver("answer", executor, callback)); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } /** * Request Telecom set the call state to inactive. This the same as hold for two call endpoints * but can be extended to setting a meeting to inactive. * * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback * will be called on. * @param callback that will be completed on the Telecom side that details success or failure * of the requested operation. * * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully * switched the call state to inactive * * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set * the call state to inactive. A {@link CallException} will be passed * that details why the operation failed. */ public void setInactive(@CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mServerInterface.setInactive(mCallId, new CallControlResultReceiver("setInactive", executor, callback)); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } /** * Request Telecom disconnect the call and remove the call from telecom tracking. * * @param disconnectCause represents the cause for disconnecting the call. The only valid * codes for the {@link android.telecom.DisconnectCause} passed in are: *

* @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback * will be called on. * @param callback That will be completed on the Telecom side that details success or * failure of the requested operation. * * {@link OutcomeReceiver#onResult} will be called if Telecom has * successfully disconnected the call. * * {@link OutcomeReceiver#onError} will be called if Telecom has failed * to disconnect the call. A {@link CallException} will be passed * that details why the operation failed. * *

* Note: After the call has been successfully disconnected, calling any CallControl API will * result in the {@link OutcomeReceiver#onError} with * {@link CallException#CODE_CALL_IS_NOT_BEING_TRACKED}. */ public void disconnect(@NonNull DisconnectCause disconnectCause, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(disconnectCause); Objects.requireNonNull(executor); Objects.requireNonNull(callback); validateDisconnectCause(disconnectCause); try { mServerInterface.disconnect(mCallId, disconnectCause, new CallControlResultReceiver("disconnect", executor, callback)); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } /** * Request start a call streaming session. On receiving valid request, telecom will bind to * the {@code CallStreamingService} implemented by a general call streaming sender. So that the * call streaming sender can perform streaming local device audio to another remote device and * control the call during streaming. * * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback * will be called on. * @param callback that will be completed on the Telecom side that details success or failure * of the requested operation. * * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully * started the call streaming. * * {@link OutcomeReceiver#onError} will be called if Telecom has failed to * start the call streaming. A {@link CallException} will be passed that * details why the operation failed. */ public void startCallStreaming(@CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mServerInterface.startCallStreaming(mCallId, new CallControlResultReceiver("startCallStreaming", executor, callback)); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } /** * Request a CallEndpoint change. Clients should not define their own CallEndpoint when * requesting a change. Instead, the new endpoint should be one of the valid endpoints provided * by {@link CallEventCallback#onAvailableCallEndpointsChanged(List)}. * * @param callEndpoint The {@link CallEndpoint} to change to. * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback * will be called on. * @param callback The {@link OutcomeReceiver} that will be completed on the Telecom side * that details success or failure of the requested operation. * * {@link OutcomeReceiver#onResult} will be called if Telecom has * successfully changed the CallEndpoint that was requested. * * {@link OutcomeReceiver#onError} will be called if Telecom has failed to * switch to the requested CallEndpoint. A {@link CallException} will be * passed that details why the operation failed. */ public void requestCallEndpointChange(@NonNull CallEndpoint callEndpoint, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(callEndpoint); Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mServerInterface.requestCallEndpointChange(callEndpoint, new CallControlResultReceiver("requestCallEndpointChange", executor, callback)); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } /** * Request a new mute state. Note: {@link CallEventCallback#onMuteStateChanged(boolean)} * will be called every time the mute state is changed and can be used to track the current * mute state. * * @param isMuted The new mute state. Passing in a {@link Boolean#TRUE} for the isMuted * parameter will mute the call. {@link Boolean#FALSE} will unmute the call. * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback * will be called on. * @param callback The {@link OutcomeReceiver} that will be completed on the Telecom side * that details success or failure of the requested operation. * * {@link OutcomeReceiver#onResult} will be called if Telecom has * successfully changed the mute state. * * {@link OutcomeReceiver#onError} will be called if Telecom has failed to * switch to the mute state. A {@link CallException} will be * passed that details why the operation failed. */ @FlaggedApi(Flags.FLAG_SET_MUTE_STATE) public void requestMuteState(boolean isMuted, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mServerInterface.setMuteState(isMuted, new CallControlResultReceiver("requestMuteState", executor, callback)); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } /** * Request a new video state for the ongoing call. This can only be changed if the application * has registered a {@link PhoneAccount} with the * {@link PhoneAccount#CAPABILITY_SUPPORTS_VIDEO_CALLING} and set the * {@link CallAttributes#SUPPORTS_VIDEO_CALLING} when adding the call via * {@link TelecomManager#addCall(CallAttributes, Executor, OutcomeReceiver, * CallControlCallback, CallEventCallback)} * * @param videoState to report to Telecom. To see the valid argument to pass, * see {@link CallAttributes.CallType}. * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback * will be called on. * @param callback that will be completed on the Telecom side that details success or failure * of the requested operation. * * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully * switched the video state. * * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set * the new video state. A {@link CallException} will be passed * that details why the operation failed. * @throws IllegalArgumentException if the argument passed for videoState is invalid. To see a * list of valid states, see {@link CallAttributes.CallType}. */ @FlaggedApi(Flags.FLAG_TRANSACTIONAL_VIDEO_STATE) public void requestVideoState(@CallAttributes.CallType int videoState, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver callback) { validateVideoState(videoState); Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mServerInterface.requestVideoState(videoState, mCallId, new CallControlResultReceiver("requestVideoState", executor, callback)); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } /** * Raises an event to the {@link android.telecom.InCallService} implementations tracking this * call via {@link android.telecom.Call.Callback#onConnectionEvent(Call, String, Bundle)}. * These events and the associated extra keys for the {@code Bundle} parameter are mutually * defined by a VoIP application and {@link android.telecom.InCallService}. This API is used to * relay additional information about a call other than what is specified in the * {@link android.telecom.CallAttributes} to {@link android.telecom.InCallService}s. This might * include, for example, a change to the list of participants in a meeting, or the name of the * speakers who have their hand raised. Where appropriate, the {@link InCallService}s tracking * this call may choose to render this additional information about the call. An automotive * calling UX, for example may have enough screen real estate to indicate the number of * participants in a meeting, but to prevent distractions could suppress the list of * participants. * * @param event a string event identifier agreed upon between a VoIP application and an * {@link android.telecom.InCallService} * @param extras a {@link android.os.Bundle} containing information about the event, as agreed * upon between a VoIP application and {@link android.telecom.InCallService}. */ public void sendEvent(@NonNull String event, @NonNull Bundle extras) { Objects.requireNonNull(event); Objects.requireNonNull(extras); try { mServerInterface.sendEvent(mCallId, event, extras); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } /** * Since {@link OutcomeReceiver}s cannot be passed via AIDL, a ResultReceiver (which can) must * wrap the Clients {@link OutcomeReceiver} passed in and await for the Telecom Server side * response in {@link ResultReceiver#onReceiveResult(int, Bundle)}. * * @hide */ private class CallControlResultReceiver extends ResultReceiver { private final String mCallingMethod; private final Executor mExecutor; private final OutcomeReceiver mClientCallback; CallControlResultReceiver(String method, Executor executor, OutcomeReceiver clientCallback) { super(null); mCallingMethod = method; mExecutor = executor; mClientCallback = clientCallback; } @Override protected void onReceiveResult(int resultCode, Bundle resultData) { Log.d(CallControl.TAG, "%s: oRR: resultCode=[%s]", mCallingMethod, resultCode); super.onReceiveResult(resultCode, resultData); final long identity = Binder.clearCallingIdentity(); try { if (resultCode == TelecomManager.TELECOM_TRANSACTION_SUCCESS) { mExecutor.execute(() -> mClientCallback.onResult(null)); } else { mExecutor.execute(() -> mClientCallback.onError(getTransactionException(resultData))); } } finally { Binder.restoreCallingIdentity(identity); } } } /** @hide */ private CallException getTransactionException(Bundle resultData) { String message = "unknown error"; if (resultData != null && resultData.containsKey(TRANSACTION_EXCEPTION_KEY)) { return resultData.getParcelable(TRANSACTION_EXCEPTION_KEY, CallException.class); } return new CallException(message, CallException.CODE_ERROR_UNKNOWN); } /** @hide */ private void validateDisconnectCause(DisconnectCause disconnectCause) { final int code = disconnectCause.getCode(); if (code != DisconnectCause.LOCAL && code != DisconnectCause.REMOTE && code != DisconnectCause.MISSED && code != DisconnectCause.REJECTED) { throw new IllegalArgumentException(TextUtils.formatSimple( "The DisconnectCause code provided, %d , is not a valid Disconnect code. Valid " + "DisconnectCause codes are limited to [DisconnectCause.LOCAL, " + "DisconnectCause.REMOTE, DisconnectCause.MISSED, or " + "DisconnectCause.REJECTED]", disconnectCause.getCode())); } } /** @hide */ private void validateVideoState(@android.telecom.CallAttributes.CallType int videoState) { if (videoState != CallAttributes.AUDIO_CALL && videoState != CallAttributes.VIDEO_CALL) { throw new IllegalArgumentException(TextUtils.formatSimple( "The VideoState argument passed in, %d , is not a valid VideoState. The " + "VideoState choices are limited to CallAttributes.AUDIO_CALL or" + "CallAttributes.VIDEO_CALL", videoState)); } } }