441 lines
21 KiB
Java
441 lines
21 KiB
Java
/*
|
|
* 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.
|
|
*
|
|
* <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 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<Void, CallException> 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<Void, CallException> 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<Void, CallException> 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:
|
|
* <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);
|
|
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<Void, CallException> 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<Void, CallException> 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<Void, CallException> 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<Void, CallException> 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<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));
|
|
}
|
|
}
|
|
}
|