blob: 97538c1c833f18bf1f07638673d1140f9e6957e9 [file] [log] [blame]
/*
* 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.NonNull;
import android.annotation.Nullable;
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.ClientTransactionalServiceRepository;
import com.android.internal.telecom.ICallControl;
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.
*
* <p>
* 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 static final String INTERFACE_ERROR_MSG = "Call Control is not available";
private final String mCallId;
private final ICallControl mServerInterface;
private final PhoneAccountHandle mPhoneAccountHandle;
private final ClientTransactionalServiceRepository mRepository;
/** @hide */
public CallControl(@NonNull String callId, @Nullable ICallControl serverInterface,
@NonNull ClientTransactionalServiceRepository repository,
@NonNull PhoneAccountHandle pah) {
mCallId = callId;
mServerInterface = serverInterface;
mRepository = repository;
mPhoneAccountHandle = pah;
}
/**
* @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<Void, CallException> callback) {
if (mServerInterface != null) {
try {
mServerInterface.setActive(mCallId,
new CallControlResultReceiver("setActive", executor, callback));
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
} else {
throw new IllegalStateException(INTERFACE_ERROR_MSG);
}
}
/**
* 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<Void, CallException> callback) {
validateVideoState(videoState);
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
if (mServerInterface != null) {
try {
mServerInterface.answer(videoState, mCallId,
new CallControlResultReceiver("answer", executor, callback));
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
} else {
throw new IllegalStateException(INTERFACE_ERROR_MSG);
}
}
/**
* 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<Void, CallException> callback) {
if (mServerInterface != null) {
try {
mServerInterface.setInactive(mCallId,
new CallControlResultReceiver("setInactive", executor, callback));
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
} else {
throw new IllegalStateException(INTERFACE_ERROR_MSG);
}
}
/**
* 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:
* <ul>
* <li>{@link DisconnectCause#LOCAL}</li>
* <li>{@link DisconnectCause#REMOTE}</li>
* <li>{@link DisconnectCause#REJECTED}</li>
* <li>{@link DisconnectCause#MISSED}</li>
* </ul>
* @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.
*
* <p>
* 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<Void, CallException> callback) {
Objects.requireNonNull(disconnectCause);
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
validateDisconnectCause(disconnectCause);
if (mServerInterface != null) {
try {
mServerInterface.disconnect(mCallId, disconnectCause,
new CallControlResultReceiver("disconnect", executor, callback));
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
} else {
throw new IllegalStateException(INTERFACE_ERROR_MSG);
}
}
/**
* Request start a call streaming session. On receiving valid request, telecom will bind to
* the {@link 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<Void, CallException> callback) {
if (mServerInterface != null) {
try {
mServerInterface.startCallStreaming(mCallId,
new CallControlResultReceiver("startCallStreaming", executor, callback));
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
} else {
throw new IllegalStateException(INTERFACE_ERROR_MSG);
}
}
/**
* 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<Void, CallException> callback) {
Objects.requireNonNull(callEndpoint);
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
if (mServerInterface != null) {
try {
mServerInterface.requestCallEndpointChange(callEndpoint,
new CallControlResultReceiver("endpointChange", executor, callback));
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
} else {
throw new IllegalStateException(INTERFACE_ERROR_MSG);
}
}
/**
* 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 defined
* in Android X. 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 that is defined in AndroidX (ex. The number of participants changed)
* @param extras the updated value in relation to the event (ex. 4 participants)
*/
public void sendEvent(@NonNull String event, @NonNull Bundle extras) {
Objects.requireNonNull(event);
Objects.requireNonNull(extras);
if (mServerInterface != null) {
try {
mServerInterface.sendEvent(mCallId, event, extras);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
} else {
throw new IllegalStateException(INTERFACE_ERROR_MSG);
}
}
/**
* 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<Void, CallException> mClientCallback;
CallControlResultReceiver(String method, Executor executor,
OutcomeReceiver<Void, CallException> 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));
}
}
}