blob: 50f2ad4561cc083bc1b20d3d3325bc1ed3e95e74 [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 /**
Thomas Stuartb4153492023-02-07 13:30:37 -080079 * Request Telecom set the call state to active. This method should be called when either an
80 * outgoing call is ready to go active or a held call is ready to go active again. For incoming
81 * calls that are ready to be answered, use
82 * {@link CallControl#answer(int, Executor, OutcomeReceiver)}.
Thomas Stuart9bfb2432022-09-27 15:02:07 -070083 *
84 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
Thomas Stuartc2427a72023-01-27 17:11:02 -080085 * will be called on.
Thomas Stuart9bfb2432022-09-27 15:02:07 -070086 * @param callback that will be completed on the Telecom side that details success or failure
Thomas Stuartc2427a72023-01-27 17:11:02 -080087 * of the requested operation.
Thomas Stuart9bfb2432022-09-27 15:02:07 -070088 *
89 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
90 * switched the call state to active
91 *
92 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
93 * the call state to active. A {@link CallException} will be passed
94 * that details why the operation failed.
95 */
96 public void setActive(@CallbackExecutor @NonNull Executor executor,
97 @NonNull OutcomeReceiver<Void, CallException> callback) {
98 if (mServerInterface != null) {
99 try {
100 mServerInterface.setActive(mCallId,
101 new CallControlResultReceiver("setActive", executor, callback));
102
103 } catch (RemoteException e) {
104 throw e.rethrowAsRuntimeException();
105 }
106 } else {
107 throw new IllegalStateException(INTERFACE_ERROR_MSG);
108 }
109 }
110
111 /**
Thomas Stuartb4153492023-02-07 13:30:37 -0800112 * Request Telecom answer an incoming call. For outgoing calls and calls that have been placed
113 * on hold, use {@link CallControl#setActive(Executor, OutcomeReceiver)}.
114 *
115 * @param videoState to report to Telecom. Telecom will store VideoState in the event another
116 * service/device requests it in order to continue the call on another screen.
117 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
118 * will be called on.
119 * @param callback that will be completed on the Telecom side that details success or failure
120 * of the requested operation.
121 *
122 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
123 * switched the call state to active
124 *
125 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
126 * the call state to active. A {@link CallException} will be passed
127 * that details why the operation failed.
128 */
129 public void answer(@android.telecom.CallAttributes.CallType int videoState,
130 @CallbackExecutor @NonNull Executor executor,
131 @NonNull OutcomeReceiver<Void, CallException> callback) {
132 validateVideoState(videoState);
133 Objects.requireNonNull(executor);
134 Objects.requireNonNull(callback);
135 if (mServerInterface != null) {
136 try {
137 mServerInterface.answer(videoState, mCallId,
138 new CallControlResultReceiver("answer", executor, callback));
139
140 } catch (RemoteException e) {
141 throw e.rethrowAsRuntimeException();
142 }
143 } else {
144 throw new IllegalStateException(INTERFACE_ERROR_MSG);
145 }
146 }
147
148 /**
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700149 * Request Telecom set the call state to inactive. This the same as hold for two call endpoints
150 * but can be extended to setting a meeting to inactive.
151 *
152 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
Thomas Stuartc2427a72023-01-27 17:11:02 -0800153 * will be called on.
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700154 * @param callback that will be completed on the Telecom side that details success or failure
Thomas Stuartc2427a72023-01-27 17:11:02 -0800155 * of the requested operation.
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700156 *
157 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
158 * switched the call state to inactive
159 *
160 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
161 * the call state to inactive. A {@link CallException} will be passed
162 * that details why the operation failed.
163 */
164 public void setInactive(@CallbackExecutor @NonNull Executor executor,
165 @NonNull OutcomeReceiver<Void, CallException> callback) {
166 if (mServerInterface != null) {
167 try {
168 mServerInterface.setInactive(mCallId,
169 new CallControlResultReceiver("setInactive", executor, callback));
170
171 } catch (RemoteException e) {
172 throw e.rethrowAsRuntimeException();
173 }
174 } else {
175 throw new IllegalStateException(INTERFACE_ERROR_MSG);
176 }
177 }
178
179 /**
Thomas Stuartc2427a72023-01-27 17:11:02 -0800180 * Request Telecom disconnect the call and remove the call from telecom tracking.
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700181 *
Thomas Stuartc2427a72023-01-27 17:11:02 -0800182 * @param disconnectCause represents the cause for disconnecting the call. The only valid
183 * codes for the {@link android.telecom.DisconnectCause} passed in are:
184 * <ul>
185 * <li>{@link DisconnectCause#LOCAL}</li>
186 * <li>{@link DisconnectCause#REMOTE}</li>
187 * <li>{@link DisconnectCause#REJECTED}</li>
188 * <li>{@link DisconnectCause#MISSED}</li>
189 * </ul>
Thomas Stuartc2427a72023-01-27 17:11:02 -0800190 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
191 * will be called on.
Thomas Stuartc2427a72023-01-27 17:11:02 -0800192 * @param callback That will be completed on the Telecom side that details success or
193 * failure of the requested operation.
194 *
195 * {@link OutcomeReceiver#onResult} will be called if Telecom has
196 * successfully disconnected the call.
197 *
198 * {@link OutcomeReceiver#onError} will be called if Telecom has failed
199 * to disconnect the call. A {@link CallException} will be passed
200 * that details why the operation failed.
201 *
202 * <p>
203 * Note: After the call has been successfully disconnected, calling any CallControl API will
204 * result in the {@link OutcomeReceiver#onError} with
205 * {@link CallException#CODE_CALL_IS_NOT_BEING_TRACKED}.
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700206 */
207 public void disconnect(@NonNull DisconnectCause disconnectCause,
208 @CallbackExecutor @NonNull Executor executor,
209 @NonNull OutcomeReceiver<Void, CallException> callback) {
Thomas Stuartc2427a72023-01-27 17:11:02 -0800210 Objects.requireNonNull(disconnectCause);
211 Objects.requireNonNull(executor);
212 Objects.requireNonNull(callback);
213 validateDisconnectCause(disconnectCause);
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700214 if (mServerInterface != null) {
215 try {
216 mServerInterface.disconnect(mCallId, disconnectCause,
217 new CallControlResultReceiver("disconnect", executor, callback));
218 } catch (RemoteException e) {
219 throw e.rethrowAsRuntimeException();
220 }
221 } else {
222 throw new IllegalStateException(INTERFACE_ERROR_MSG);
223 }
224 }
225
226 /**
Grace Jiaef5a4cc2022-12-13 11:08:55 -0800227 * Request start a call streaming session. On receiving valid request, telecom will bind to
228 * the {@link CallStreamingService} implemented by a general call streaming sender. So that the
229 * call streaming sender can perform streaming local device audio to another remote device and
230 * control the call during streaming.
231 *
232 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
233 * will be called on.
234 * @param callback that will be completed on the Telecom side that details success or failure
235 * of the requested operation.
236 *
237 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
Grace Jiaacd4dad2023-01-17 18:14:36 -0800238 * started the call streaming.
Grace Jiaef5a4cc2022-12-13 11:08:55 -0800239 *
240 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
Grace Jiaacd4dad2023-01-17 18:14:36 -0800241 * start the call streaming. A {@link CallException} will be passed that
Grace Jiaef5a4cc2022-12-13 11:08:55 -0800242 * details why the operation failed.
243 */
244 public void startCallStreaming(@CallbackExecutor @NonNull Executor executor,
245 @NonNull OutcomeReceiver<Void, CallException> callback) {
246 if (mServerInterface != null) {
247 try {
248 mServerInterface.startCallStreaming(mCallId,
249 new CallControlResultReceiver("startCallStreaming", executor, callback));
250 } catch (RemoteException e) {
251 throw e.rethrowAsRuntimeException();
252 }
253 } else {
254 throw new IllegalStateException(INTERFACE_ERROR_MSG);
255 }
256 }
257
258 /**
Thomas Stuart7aeccde2022-12-21 14:47:21 -0800259 * Request a CallEndpoint change. Clients should not define their own CallEndpoint when
260 * requesting a change. Instead, the new endpoint should be one of the valid endpoints provided
261 * by {@link CallEventCallback#onAvailableCallEndpointsChanged(List)}.
262 *
Thomas Stuartc2427a72023-01-27 17:11:02 -0800263 * @param callEndpoint The {@link CallEndpoint} to change to.
264 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
Thomas Stuart7aeccde2022-12-21 14:47:21 -0800265 * will be called on.
Thomas Stuartc2427a72023-01-27 17:11:02 -0800266 * @param callback The {@link OutcomeReceiver} that will be completed on the Telecom side
Thomas Stuart7aeccde2022-12-21 14:47:21 -0800267 * that details success or failure of the requested operation.
268 *
269 * {@link OutcomeReceiver#onResult} will be called if Telecom has
270 * successfully changed the CallEndpoint that was requested.
271 *
272 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
273 * switch to the requested CallEndpoint. A {@link CallException} will be
274 * passed that details why the operation failed.
275 */
276 public void requestCallEndpointChange(@NonNull CallEndpoint callEndpoint,
277 @CallbackExecutor @NonNull Executor executor,
278 @NonNull OutcomeReceiver<Void, CallException> callback) {
279 Objects.requireNonNull(callEndpoint);
280 Objects.requireNonNull(executor);
281 Objects.requireNonNull(callback);
282 if (mServerInterface != null) {
283 try {
284 mServerInterface.requestCallEndpointChange(callEndpoint,
285 new CallControlResultReceiver("endpointChange", executor, callback));
286 } catch (RemoteException e) {
287 throw e.rethrowAsRuntimeException();
288 }
289 } else {
290 throw new IllegalStateException(INTERFACE_ERROR_MSG);
291 }
292 }
293
294 /**
Thomas Stuart9ace3392023-02-15 15:01:09 -0800295 * Raises an event to the {@link android.telecom.InCallService} implementations tracking this
296 * call via {@link android.telecom.Call.Callback#onConnectionEvent(Call, String, Bundle)}.
Thomas Stuartef584292023-03-02 13:24:46 -0800297 * These events and the associated extra keys for the {@code Bundle} parameter are mutually
298 * defined by a VoIP application and {@link android.telecom.InCallService}. This API is used to
299 * relay additional information about a call other than what is specified in the
300 * {@link android.telecom.CallAttributes} to {@link android.telecom.InCallService}s. This might
301 * include, for example, a change to the list of participants in a meeting, or the name of the
302 * speakers who have their hand raised. Where appropriate, the {@link InCallService}s tracking
303 * this call may choose to render this additional information about the call. An automotive
304 * calling UX, for example may have enough screen real estate to indicate the number of
305 * participants in a meeting, but to prevent distractions could suppress the list of
306 * participants.
Thomas Stuart9ace3392023-02-15 15:01:09 -0800307 *
Thomas Stuartef584292023-03-02 13:24:46 -0800308 * @param event a string event identifier agreed upon between a VoIP application and an
309 * {@link android.telecom.InCallService}
310 * @param extras a {@link android.os.Bundle} containing information about the event, as agreed
311 * upon between a VoIP application and {@link android.telecom.InCallService}.
Thomas Stuart9ace3392023-02-15 15:01:09 -0800312 */
313 public void sendEvent(@NonNull String event, @NonNull Bundle extras) {
314 Objects.requireNonNull(event);
315 Objects.requireNonNull(extras);
316 if (mServerInterface != null) {
317 try {
318 mServerInterface.sendEvent(mCallId, event, extras);
319 } catch (RemoteException e) {
320 throw e.rethrowAsRuntimeException();
321 }
322 } else {
323 throw new IllegalStateException(INTERFACE_ERROR_MSG);
324 }
325 }
326
327 /**
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700328 * Since {@link OutcomeReceiver}s cannot be passed via AIDL, a ResultReceiver (which can) must
329 * wrap the Clients {@link OutcomeReceiver} passed in and await for the Telecom Server side
330 * response in {@link ResultReceiver#onReceiveResult(int, Bundle)}.
Thomas Stuartc2427a72023-01-27 17:11:02 -0800331 *
332 * @hide
333 */
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700334 private class CallControlResultReceiver extends ResultReceiver {
335 private final String mCallingMethod;
336 private final Executor mExecutor;
337 private final OutcomeReceiver<Void, CallException> mClientCallback;
338
339 CallControlResultReceiver(String method, Executor executor,
340 OutcomeReceiver<Void, CallException> clientCallback) {
341 super(null);
342 mCallingMethod = method;
343 mExecutor = executor;
344 mClientCallback = clientCallback;
345 }
346
347 @Override
348 protected void onReceiveResult(int resultCode, Bundle resultData) {
349 Log.d(CallControl.TAG, "%s: oRR: resultCode=[%s]", mCallingMethod, resultCode);
350 super.onReceiveResult(resultCode, resultData);
351 final long identity = Binder.clearCallingIdentity();
352 try {
353 if (resultCode == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
354 mExecutor.execute(() -> mClientCallback.onResult(null));
355 } else {
356 mExecutor.execute(() ->
357 mClientCallback.onError(getTransactionException(resultData)));
358 }
359 } finally {
360 Binder.restoreCallingIdentity(identity);
361 }
362 }
363
364 }
365
366 /** @hide */
367 private CallException getTransactionException(Bundle resultData) {
368 String message = "unknown error";
369 if (resultData != null && resultData.containsKey(TRANSACTION_EXCEPTION_KEY)) {
370 return resultData.getParcelable(TRANSACTION_EXCEPTION_KEY,
371 CallException.class);
372 }
373 return new CallException(message, CallException.CODE_ERROR_UNKNOWN);
374 }
Thomas Stuartc2427a72023-01-27 17:11:02 -0800375
376 /** @hide */
377 private void validateDisconnectCause(DisconnectCause disconnectCause) {
378 final int code = disconnectCause.getCode();
379 if (code != DisconnectCause.LOCAL && code != DisconnectCause.REMOTE
380 && code != DisconnectCause.MISSED && code != DisconnectCause.REJECTED) {
381 throw new IllegalArgumentException(TextUtils.formatSimple(
382 "The DisconnectCause code provided, %d , is not a valid Disconnect code. Valid "
383 + "DisconnectCause codes are limited to [DisconnectCause.LOCAL, "
384 + "DisconnectCause.REMOTE, DisconnectCause.MISSED, or "
385 + "DisconnectCause.REJECTED]", disconnectCause.getCode()));
386 }
387 }
388
Thomas Stuartb4153492023-02-07 13:30:37 -0800389 /** @hide */
390 private void validateVideoState(@android.telecom.CallAttributes.CallType int videoState) {
391 if (videoState != CallAttributes.AUDIO_CALL && videoState != CallAttributes.VIDEO_CALL) {
392 throw new IllegalArgumentException(TextUtils.formatSimple(
393 "The VideoState argument passed in, %d , is not a valid VideoState. The "
394 + "VideoState choices are limited to CallAttributes.AUDIO_CALL or"
395 + "CallAttributes.VIDEO_CALL", videoState));
396 }
397 }
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700398}