blob: 6b2bea032a001abb1d38d40b8da1363034bb2e97 [file] [log] [blame]
Thomas Stuart9bfb2432022-09-27 15:02:07 -07001/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.telecom;
18
19import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY;
20
21import android.annotation.CallbackExecutor;
22import android.annotation.NonNull;
23import android.annotation.Nullable;
Thomas Stuart73a8b872023-01-10 16:21:51 -080024import android.annotation.SuppressLint;
Thomas Stuart9bfb2432022-09-27 15:02:07 -070025import android.os.Binder;
26import android.os.Bundle;
27import android.os.OutcomeReceiver;
28import android.os.ParcelUuid;
29import android.os.RemoteException;
30import android.os.ResultReceiver;
Thomas Stuartc2427a72023-01-27 17:11:02 -080031import android.text.TextUtils;
Thomas Stuart9bfb2432022-09-27 15:02:07 -070032
33import com.android.internal.telecom.ClientTransactionalServiceRepository;
34import com.android.internal.telecom.ICallControl;
35
Thomas Stuart7aeccde2022-12-21 14:47:21 -080036import java.util.List;
37import java.util.Objects;
Thomas Stuart9bfb2432022-09-27 15:02:07 -070038import java.util.concurrent.Executor;
39
40/**
41 * CallControl provides client side control of a call. Each Call will get an individual CallControl
42 * instance in which the client can alter the state of the associated call.
43 *
44 * <p>
45 * Each method is Transactional meaning that it can succeed or fail. If a transaction succeeds,
46 * the {@link OutcomeReceiver#onResult} will be called by Telecom. Otherwise, the
47 * {@link OutcomeReceiver#onError} is called and provides a {@link CallException} that details why
48 * the operation failed.
49 */
Thomas Stuart73a8b872023-01-10 16:21:51 -080050@SuppressLint("NotCloseable")
51public final class CallControl {
Thomas Stuart9bfb2432022-09-27 15:02:07 -070052 private static final String TAG = CallControl.class.getSimpleName();
53 private static final String INTERFACE_ERROR_MSG = "Call Control is not available";
54 private final String mCallId;
55 private final ICallControl mServerInterface;
56 private final PhoneAccountHandle mPhoneAccountHandle;
57 private final ClientTransactionalServiceRepository mRepository;
58
59 /** @hide */
60 public CallControl(@NonNull String callId, @Nullable ICallControl serverInterface,
61 @NonNull ClientTransactionalServiceRepository repository,
62 @NonNull PhoneAccountHandle pah) {
63 mCallId = callId;
64 mServerInterface = serverInterface;
65 mRepository = repository;
66 mPhoneAccountHandle = pah;
67 }
68
69 /**
70 * @return the callId Telecom assigned to this CallControl object which should be attached to
Thomas Stuartc2427a72023-01-27 17:11:02 -080071 * an individual call.
Thomas Stuart9bfb2432022-09-27 15:02:07 -070072 */
73 @NonNull
74 public ParcelUuid getCallId() {
75 return ParcelUuid.fromString(mCallId);
76 }
77
78 /**
79 * Request Telecom set the call state to active.
80 *
81 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
Thomas Stuartc2427a72023-01-27 17:11:02 -080082 * will be called on.
Thomas Stuart9bfb2432022-09-27 15:02:07 -070083 * @param callback that will be completed on the Telecom side that details success or failure
Thomas Stuartc2427a72023-01-27 17:11:02 -080084 * of the requested operation.
Thomas Stuart9bfb2432022-09-27 15:02:07 -070085 *
86 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
87 * switched the call state to active
88 *
89 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
90 * the call state to active. A {@link CallException} will be passed
91 * that details why the operation failed.
92 */
93 public void setActive(@CallbackExecutor @NonNull Executor executor,
94 @NonNull OutcomeReceiver<Void, CallException> callback) {
95 if (mServerInterface != null) {
96 try {
97 mServerInterface.setActive(mCallId,
98 new CallControlResultReceiver("setActive", executor, callback));
99
100 } catch (RemoteException e) {
101 throw e.rethrowAsRuntimeException();
102 }
103 } else {
104 throw new IllegalStateException(INTERFACE_ERROR_MSG);
105 }
106 }
107
108 /**
109 * Request Telecom set the call state to inactive. This the same as hold for two call endpoints
110 * but can be extended to setting a meeting to inactive.
111 *
112 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
Thomas Stuartc2427a72023-01-27 17:11:02 -0800113 * will be called on.
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700114 * @param callback that will be completed on the Telecom side that details success or failure
Thomas Stuartc2427a72023-01-27 17:11:02 -0800115 * of the requested operation.
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700116 *
117 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
118 * switched the call state to inactive
119 *
120 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
121 * the call state to inactive. A {@link CallException} will be passed
122 * that details why the operation failed.
123 */
124 public void setInactive(@CallbackExecutor @NonNull Executor executor,
125 @NonNull OutcomeReceiver<Void, CallException> callback) {
126 if (mServerInterface != null) {
127 try {
128 mServerInterface.setInactive(mCallId,
129 new CallControlResultReceiver("setInactive", executor, callback));
130
131 } catch (RemoteException e) {
132 throw e.rethrowAsRuntimeException();
133 }
134 } else {
135 throw new IllegalStateException(INTERFACE_ERROR_MSG);
136 }
137 }
138
139 /**
Thomas Stuartc2427a72023-01-27 17:11:02 -0800140 * Request Telecom disconnect the call and remove the call from telecom tracking.
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700141 *
Thomas Stuartc2427a72023-01-27 17:11:02 -0800142 * @param disconnectCause represents the cause for disconnecting the call. The only valid
143 * codes for the {@link android.telecom.DisconnectCause} passed in are:
144 * <ul>
145 * <li>{@link DisconnectCause#LOCAL}</li>
146 * <li>{@link DisconnectCause#REMOTE}</li>
147 * <li>{@link DisconnectCause#REJECTED}</li>
148 * <li>{@link DisconnectCause#MISSED}</li>
149 * </ul>
Thomas Stuartc2427a72023-01-27 17:11:02 -0800150 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
151 * will be called on.
Thomas Stuartc2427a72023-01-27 17:11:02 -0800152 * @param callback That will be completed on the Telecom side that details success or
153 * failure of the requested operation.
154 *
155 * {@link OutcomeReceiver#onResult} will be called if Telecom has
156 * successfully disconnected the call.
157 *
158 * {@link OutcomeReceiver#onError} will be called if Telecom has failed
159 * to disconnect the call. A {@link CallException} will be passed
160 * that details why the operation failed.
161 *
162 * <p>
163 * Note: After the call has been successfully disconnected, calling any CallControl API will
164 * result in the {@link OutcomeReceiver#onError} with
165 * {@link CallException#CODE_CALL_IS_NOT_BEING_TRACKED}.
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700166 */
167 public void disconnect(@NonNull DisconnectCause disconnectCause,
168 @CallbackExecutor @NonNull Executor executor,
169 @NonNull OutcomeReceiver<Void, CallException> callback) {
Thomas Stuartc2427a72023-01-27 17:11:02 -0800170 Objects.requireNonNull(disconnectCause);
171 Objects.requireNonNull(executor);
172 Objects.requireNonNull(callback);
173 validateDisconnectCause(disconnectCause);
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700174 if (mServerInterface != null) {
175 try {
176 mServerInterface.disconnect(mCallId, disconnectCause,
177 new CallControlResultReceiver("disconnect", executor, callback));
178 } catch (RemoteException e) {
179 throw e.rethrowAsRuntimeException();
180 }
181 } else {
182 throw new IllegalStateException(INTERFACE_ERROR_MSG);
183 }
184 }
185
186 /**
Grace Jiaef5a4cc2022-12-13 11:08:55 -0800187 * Request start a call streaming session. On receiving valid request, telecom will bind to
188 * the {@link CallStreamingService} implemented by a general call streaming sender. So that the
189 * call streaming sender can perform streaming local device audio to another remote device and
190 * control the call during streaming.
191 *
192 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
193 * will be called on.
194 * @param callback that will be completed on the Telecom side that details success or failure
195 * of the requested operation.
196 *
197 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
Grace Jiaacd4dad2023-01-17 18:14:36 -0800198 * started the call streaming.
Grace Jiaef5a4cc2022-12-13 11:08:55 -0800199 *
200 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
Grace Jiaacd4dad2023-01-17 18:14:36 -0800201 * start the call streaming. A {@link CallException} will be passed that
Grace Jiaef5a4cc2022-12-13 11:08:55 -0800202 * details why the operation failed.
203 */
204 public void startCallStreaming(@CallbackExecutor @NonNull Executor executor,
205 @NonNull OutcomeReceiver<Void, CallException> callback) {
206 if (mServerInterface != null) {
207 try {
208 mServerInterface.startCallStreaming(mCallId,
209 new CallControlResultReceiver("startCallStreaming", executor, callback));
210 } catch (RemoteException e) {
211 throw e.rethrowAsRuntimeException();
212 }
213 } else {
214 throw new IllegalStateException(INTERFACE_ERROR_MSG);
215 }
216 }
217
218 /**
Thomas Stuart7aeccde2022-12-21 14:47:21 -0800219 * Request a CallEndpoint change. Clients should not define their own CallEndpoint when
220 * requesting a change. Instead, the new endpoint should be one of the valid endpoints provided
221 * by {@link CallEventCallback#onAvailableCallEndpointsChanged(List)}.
222 *
Thomas Stuartc2427a72023-01-27 17:11:02 -0800223 * @param callEndpoint The {@link CallEndpoint} to change to.
224 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
Thomas Stuart7aeccde2022-12-21 14:47:21 -0800225 * will be called on.
Thomas Stuartc2427a72023-01-27 17:11:02 -0800226 * @param callback The {@link OutcomeReceiver} that will be completed on the Telecom side
Thomas Stuart7aeccde2022-12-21 14:47:21 -0800227 * that details success or failure of the requested operation.
228 *
229 * {@link OutcomeReceiver#onResult} will be called if Telecom has
230 * successfully changed the CallEndpoint that was requested.
231 *
232 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
233 * switch to the requested CallEndpoint. A {@link CallException} will be
234 * passed that details why the operation failed.
235 */
236 public void requestCallEndpointChange(@NonNull CallEndpoint callEndpoint,
237 @CallbackExecutor @NonNull Executor executor,
238 @NonNull OutcomeReceiver<Void, CallException> callback) {
239 Objects.requireNonNull(callEndpoint);
240 Objects.requireNonNull(executor);
241 Objects.requireNonNull(callback);
242 if (mServerInterface != null) {
243 try {
244 mServerInterface.requestCallEndpointChange(callEndpoint,
245 new CallControlResultReceiver("endpointChange", executor, callback));
246 } catch (RemoteException e) {
247 throw e.rethrowAsRuntimeException();
248 }
249 } else {
250 throw new IllegalStateException(INTERFACE_ERROR_MSG);
251 }
252 }
253
254 /**
Thomas Stuart9ace3392023-02-15 15:01:09 -0800255 * Raises an event to the {@link android.telecom.InCallService} implementations tracking this
256 * call via {@link android.telecom.Call.Callback#onConnectionEvent(Call, String, Bundle)}.
257 * These events and the associated extra keys for the {@code Bundle} parameter are defined
258 * in Android X. This API is used to relay additional information about a call other than
259 * what is specified in the {@link android.telecom.CallAttributes} to
260 * {@link android.telecom.InCallService}s. This might include, for example, a change to the list
261 * of participants in a meeting, or the name of the speakers who have their hand raised. Where
262 * appropriate, the {@link InCallService}s tracking this call may choose to render this
263 * additional information about the call. An automotive calling UX, for example may have enough
264 * screen real estate to indicate the number of participants in a meeting, but to prevent
265 * distractions could suppress the list of participants.
266 *
267 * @param event that is defined in AndroidX (ex. The number of participants changed)
268 * @param extras the updated value in relation to the event (ex. 4 participants)
269 */
270 public void sendEvent(@NonNull String event, @NonNull Bundle extras) {
271 Objects.requireNonNull(event);
272 Objects.requireNonNull(extras);
273 if (mServerInterface != null) {
274 try {
275 mServerInterface.sendEvent(mCallId, event, extras);
276 } catch (RemoteException e) {
277 throw e.rethrowAsRuntimeException();
278 }
279 } else {
280 throw new IllegalStateException(INTERFACE_ERROR_MSG);
281 }
282 }
283
284 /**
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700285 * Since {@link OutcomeReceiver}s cannot be passed via AIDL, a ResultReceiver (which can) must
286 * wrap the Clients {@link OutcomeReceiver} passed in and await for the Telecom Server side
287 * response in {@link ResultReceiver#onReceiveResult(int, Bundle)}.
Thomas Stuartc2427a72023-01-27 17:11:02 -0800288 *
289 * @hide
290 */
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700291 private class CallControlResultReceiver extends ResultReceiver {
292 private final String mCallingMethod;
293 private final Executor mExecutor;
294 private final OutcomeReceiver<Void, CallException> mClientCallback;
295
296 CallControlResultReceiver(String method, Executor executor,
297 OutcomeReceiver<Void, CallException> clientCallback) {
298 super(null);
299 mCallingMethod = method;
300 mExecutor = executor;
301 mClientCallback = clientCallback;
302 }
303
304 @Override
305 protected void onReceiveResult(int resultCode, Bundle resultData) {
306 Log.d(CallControl.TAG, "%s: oRR: resultCode=[%s]", mCallingMethod, resultCode);
307 super.onReceiveResult(resultCode, resultData);
308 final long identity = Binder.clearCallingIdentity();
309 try {
310 if (resultCode == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
311 mExecutor.execute(() -> mClientCallback.onResult(null));
312 } else {
313 mExecutor.execute(() ->
314 mClientCallback.onError(getTransactionException(resultData)));
315 }
316 } finally {
317 Binder.restoreCallingIdentity(identity);
318 }
319 }
320
321 }
322
323 /** @hide */
324 private CallException getTransactionException(Bundle resultData) {
325 String message = "unknown error";
326 if (resultData != null && resultData.containsKey(TRANSACTION_EXCEPTION_KEY)) {
327 return resultData.getParcelable(TRANSACTION_EXCEPTION_KEY,
328 CallException.class);
329 }
330 return new CallException(message, CallException.CODE_ERROR_UNKNOWN);
331 }
Thomas Stuartc2427a72023-01-27 17:11:02 -0800332
333 /** @hide */
334 private void validateDisconnectCause(DisconnectCause disconnectCause) {
335 final int code = disconnectCause.getCode();
336 if (code != DisconnectCause.LOCAL && code != DisconnectCause.REMOTE
337 && code != DisconnectCause.MISSED && code != DisconnectCause.REJECTED) {
338 throw new IllegalArgumentException(TextUtils.formatSimple(
339 "The DisconnectCause code provided, %d , is not a valid Disconnect code. Valid "
340 + "DisconnectCause codes are limited to [DisconnectCause.LOCAL, "
341 + "DisconnectCause.REMOTE, DisconnectCause.MISSED, or "
342 + "DisconnectCause.REJECTED]", disconnectCause.getCode()));
343 }
344 }
345
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700346}