blob: fe699af86f1db8fc73ef76e5b53835c2d7bb96a6 [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;
Thomas Stuartfc93e932023-12-19 10:57:06 -080022import android.annotation.FlaggedApi;
Thomas Stuart9bfb2432022-09-27 15:02:07 -070023import android.annotation.NonNull;
24import android.annotation.Nullable;
Thomas Stuart73a8b872023-01-10 16:21:51 -080025import android.annotation.SuppressLint;
Thomas Stuart9bfb2432022-09-27 15:02:07 -070026import android.os.Binder;
27import android.os.Bundle;
28import android.os.OutcomeReceiver;
29import android.os.ParcelUuid;
30import android.os.RemoteException;
31import android.os.ResultReceiver;
Thomas Stuartc2427a72023-01-27 17:11:02 -080032import android.text.TextUtils;
Thomas Stuart9bfb2432022-09-27 15:02:07 -070033
34import com.android.internal.telecom.ClientTransactionalServiceRepository;
35import com.android.internal.telecom.ICallControl;
Thomas Stuartfc93e932023-12-19 10:57:06 -080036import com.android.server.telecom.flags.Flags;
Thomas Stuart9bfb2432022-09-27 15:02:07 -070037
Thomas Stuart7aeccde2022-12-21 14:47:21 -080038import java.util.List;
39import java.util.Objects;
Thomas Stuart9bfb2432022-09-27 15:02:07 -070040import java.util.concurrent.Executor;
41
42/**
43 * CallControl provides client side control of a call. Each Call will get an individual CallControl
44 * instance in which the client can alter the state of the associated call.
45 *
46 * <p>
47 * Each method is Transactional meaning that it can succeed or fail. If a transaction succeeds,
48 * the {@link OutcomeReceiver#onResult} will be called by Telecom. Otherwise, the
49 * {@link OutcomeReceiver#onError} is called and provides a {@link CallException} that details why
50 * the operation failed.
51 */
Thomas Stuart73a8b872023-01-10 16:21:51 -080052@SuppressLint("NotCloseable")
53public final class CallControl {
Thomas Stuart9bfb2432022-09-27 15:02:07 -070054 private static final String TAG = CallControl.class.getSimpleName();
55 private static final String INTERFACE_ERROR_MSG = "Call Control is not available";
56 private final String mCallId;
57 private final ICallControl mServerInterface;
58 private final PhoneAccountHandle mPhoneAccountHandle;
59 private final ClientTransactionalServiceRepository mRepository;
60
61 /** @hide */
62 public CallControl(@NonNull String callId, @Nullable ICallControl serverInterface,
63 @NonNull ClientTransactionalServiceRepository repository,
64 @NonNull PhoneAccountHandle pah) {
65 mCallId = callId;
66 mServerInterface = serverInterface;
67 mRepository = repository;
68 mPhoneAccountHandle = pah;
69 }
70
71 /**
72 * @return the callId Telecom assigned to this CallControl object which should be attached to
Thomas Stuartc2427a72023-01-27 17:11:02 -080073 * an individual call.
Thomas Stuart9bfb2432022-09-27 15:02:07 -070074 */
75 @NonNull
76 public ParcelUuid getCallId() {
77 return ParcelUuid.fromString(mCallId);
78 }
79
80 /**
Thomas Stuartb4153492023-02-07 13:30:37 -080081 * Request Telecom set the call state to active. This method should be called when either an
82 * outgoing call is ready to go active or a held call is ready to go active again. For incoming
83 * calls that are ready to be answered, use
84 * {@link CallControl#answer(int, Executor, OutcomeReceiver)}.
Thomas Stuart9bfb2432022-09-27 15:02:07 -070085 *
86 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
Thomas Stuartc2427a72023-01-27 17:11:02 -080087 * will be called on.
Thomas Stuart9bfb2432022-09-27 15:02:07 -070088 * @param callback that will be completed on the Telecom side that details success or failure
Thomas Stuartc2427a72023-01-27 17:11:02 -080089 * of the requested operation.
Thomas Stuart9bfb2432022-09-27 15:02:07 -070090 *
91 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
92 * switched the call state to active
93 *
94 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
95 * the call state to active. A {@link CallException} will be passed
96 * that details why the operation failed.
97 */
98 public void setActive(@CallbackExecutor @NonNull Executor executor,
99 @NonNull OutcomeReceiver<Void, CallException> callback) {
100 if (mServerInterface != null) {
101 try {
102 mServerInterface.setActive(mCallId,
103 new CallControlResultReceiver("setActive", executor, callback));
104
105 } catch (RemoteException e) {
106 throw e.rethrowAsRuntimeException();
107 }
108 } else {
109 throw new IllegalStateException(INTERFACE_ERROR_MSG);
110 }
111 }
112
113 /**
Thomas Stuartb4153492023-02-07 13:30:37 -0800114 * Request Telecom answer an incoming call. For outgoing calls and calls that have been placed
115 * on hold, use {@link CallControl#setActive(Executor, OutcomeReceiver)}.
116 *
117 * @param videoState to report to Telecom. Telecom will store VideoState in the event another
118 * service/device requests it in order to continue the call on another screen.
119 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
120 * will be called on.
121 * @param callback that will be completed on the Telecom side that details success or failure
122 * of the requested operation.
123 *
124 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
125 * switched the call state to active
126 *
127 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
128 * the call state to active. A {@link CallException} will be passed
129 * that details why the operation failed.
130 */
131 public void answer(@android.telecom.CallAttributes.CallType int videoState,
132 @CallbackExecutor @NonNull Executor executor,
133 @NonNull OutcomeReceiver<Void, CallException> callback) {
134 validateVideoState(videoState);
135 Objects.requireNonNull(executor);
136 Objects.requireNonNull(callback);
137 if (mServerInterface != null) {
138 try {
139 mServerInterface.answer(videoState, mCallId,
140 new CallControlResultReceiver("answer", executor, callback));
141
142 } catch (RemoteException e) {
143 throw e.rethrowAsRuntimeException();
144 }
145 } else {
146 throw new IllegalStateException(INTERFACE_ERROR_MSG);
147 }
148 }
149
150 /**
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700151 * Request Telecom set the call state to inactive. This the same as hold for two call endpoints
152 * but can be extended to setting a meeting to inactive.
153 *
154 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
Thomas Stuartc2427a72023-01-27 17:11:02 -0800155 * will be called on.
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700156 * @param callback that will be completed on the Telecom side that details success or failure
Thomas Stuartc2427a72023-01-27 17:11:02 -0800157 * of the requested operation.
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700158 *
159 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
160 * switched the call state to inactive
161 *
162 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
163 * the call state to inactive. A {@link CallException} will be passed
164 * that details why the operation failed.
165 */
166 public void setInactive(@CallbackExecutor @NonNull Executor executor,
167 @NonNull OutcomeReceiver<Void, CallException> callback) {
168 if (mServerInterface != null) {
169 try {
170 mServerInterface.setInactive(mCallId,
171 new CallControlResultReceiver("setInactive", executor, callback));
172
173 } catch (RemoteException e) {
174 throw e.rethrowAsRuntimeException();
175 }
176 } else {
177 throw new IllegalStateException(INTERFACE_ERROR_MSG);
178 }
179 }
180
181 /**
Thomas Stuartc2427a72023-01-27 17:11:02 -0800182 * Request Telecom disconnect the call and remove the call from telecom tracking.
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700183 *
Thomas Stuartc2427a72023-01-27 17:11:02 -0800184 * @param disconnectCause represents the cause for disconnecting the call. The only valid
185 * codes for the {@link android.telecom.DisconnectCause} passed in are:
186 * <ul>
187 * <li>{@link DisconnectCause#LOCAL}</li>
188 * <li>{@link DisconnectCause#REMOTE}</li>
189 * <li>{@link DisconnectCause#REJECTED}</li>
190 * <li>{@link DisconnectCause#MISSED}</li>
191 * </ul>
Thomas Stuartc2427a72023-01-27 17:11:02 -0800192 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
193 * will be called on.
Thomas Stuartc2427a72023-01-27 17:11:02 -0800194 * @param callback That will be completed on the Telecom side that details success or
195 * failure of the requested operation.
196 *
197 * {@link OutcomeReceiver#onResult} will be called if Telecom has
198 * successfully disconnected the call.
199 *
200 * {@link OutcomeReceiver#onError} will be called if Telecom has failed
201 * to disconnect the call. A {@link CallException} will be passed
202 * that details why the operation failed.
203 *
204 * <p>
205 * Note: After the call has been successfully disconnected, calling any CallControl API will
206 * result in the {@link OutcomeReceiver#onError} with
207 * {@link CallException#CODE_CALL_IS_NOT_BEING_TRACKED}.
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700208 */
209 public void disconnect(@NonNull DisconnectCause disconnectCause,
210 @CallbackExecutor @NonNull Executor executor,
211 @NonNull OutcomeReceiver<Void, CallException> callback) {
Thomas Stuartc2427a72023-01-27 17:11:02 -0800212 Objects.requireNonNull(disconnectCause);
213 Objects.requireNonNull(executor);
214 Objects.requireNonNull(callback);
215 validateDisconnectCause(disconnectCause);
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700216 if (mServerInterface != null) {
217 try {
218 mServerInterface.disconnect(mCallId, disconnectCause,
219 new CallControlResultReceiver("disconnect", executor, callback));
220 } catch (RemoteException e) {
221 throw e.rethrowAsRuntimeException();
222 }
223 } else {
224 throw new IllegalStateException(INTERFACE_ERROR_MSG);
225 }
226 }
227
228 /**
Grace Jiaef5a4cc2022-12-13 11:08:55 -0800229 * Request start a call streaming session. On receiving valid request, telecom will bind to
Anton Hansson84d6d752023-10-05 09:46:37 +0000230 * the {@code CallStreamingService} implemented by a general call streaming sender. So that the
Grace Jiaef5a4cc2022-12-13 11:08:55 -0800231 * call streaming sender can perform streaming local device audio to another remote device and
232 * control the call during streaming.
233 *
234 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
235 * will be called on.
236 * @param callback that will be completed on the Telecom side that details success or failure
237 * of the requested operation.
238 *
239 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
Grace Jiaacd4dad2023-01-17 18:14:36 -0800240 * started the call streaming.
Grace Jiaef5a4cc2022-12-13 11:08:55 -0800241 *
242 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
Grace Jiaacd4dad2023-01-17 18:14:36 -0800243 * start the call streaming. A {@link CallException} will be passed that
Grace Jiaef5a4cc2022-12-13 11:08:55 -0800244 * details why the operation failed.
245 */
246 public void startCallStreaming(@CallbackExecutor @NonNull Executor executor,
247 @NonNull OutcomeReceiver<Void, CallException> callback) {
248 if (mServerInterface != null) {
249 try {
250 mServerInterface.startCallStreaming(mCallId,
251 new CallControlResultReceiver("startCallStreaming", executor, callback));
252 } catch (RemoteException e) {
253 throw e.rethrowAsRuntimeException();
254 }
255 } else {
256 throw new IllegalStateException(INTERFACE_ERROR_MSG);
257 }
258 }
259
260 /**
Thomas Stuart7aeccde2022-12-21 14:47:21 -0800261 * Request a CallEndpoint change. Clients should not define their own CallEndpoint when
262 * requesting a change. Instead, the new endpoint should be one of the valid endpoints provided
263 * by {@link CallEventCallback#onAvailableCallEndpointsChanged(List)}.
264 *
Thomas Stuartc2427a72023-01-27 17:11:02 -0800265 * @param callEndpoint The {@link CallEndpoint} to change to.
266 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
Thomas Stuart7aeccde2022-12-21 14:47:21 -0800267 * will be called on.
Thomas Stuartc2427a72023-01-27 17:11:02 -0800268 * @param callback The {@link OutcomeReceiver} that will be completed on the Telecom side
Thomas Stuart7aeccde2022-12-21 14:47:21 -0800269 * that details success or failure of the requested operation.
270 *
271 * {@link OutcomeReceiver#onResult} will be called if Telecom has
272 * successfully changed the CallEndpoint that was requested.
273 *
274 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
275 * switch to the requested CallEndpoint. A {@link CallException} will be
276 * passed that details why the operation failed.
277 */
278 public void requestCallEndpointChange(@NonNull CallEndpoint callEndpoint,
279 @CallbackExecutor @NonNull Executor executor,
280 @NonNull OutcomeReceiver<Void, CallException> callback) {
281 Objects.requireNonNull(callEndpoint);
282 Objects.requireNonNull(executor);
283 Objects.requireNonNull(callback);
284 if (mServerInterface != null) {
285 try {
286 mServerInterface.requestCallEndpointChange(callEndpoint,
287 new CallControlResultReceiver("endpointChange", executor, callback));
288 } catch (RemoteException e) {
289 throw e.rethrowAsRuntimeException();
290 }
291 } else {
292 throw new IllegalStateException(INTERFACE_ERROR_MSG);
293 }
294 }
295
296 /**
Thomas Stuartfc93e932023-12-19 10:57:06 -0800297 * Request a new mute state. Note: {@link CallEventCallback#onMuteStateChanged(boolean)}
298 * will be called every time the mute state is changed and can be used to track the current
299 * mute state.
300 *
301 * @param isMuted The new mute state. Passing in a {@link Boolean#TRUE} for the isMuted
302 * parameter will mute the call. {@link Boolean#FALSE} will unmute the call.
303 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
304 * will be called on.
305 * @param callback The {@link OutcomeReceiver} that will be completed on the Telecom side
306 * that details success or failure of the requested operation.
307 *
308 * {@link OutcomeReceiver#onResult} will be called if Telecom has
309 * successfully changed the mute state.
310 *
311 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
312 * switch to the mute state. A {@link CallException} will be
313 * passed that details why the operation failed.
314 */
315 @FlaggedApi(Flags.FLAG_SET_MUTE_STATE)
316 public void setMuteState(boolean isMuted, @CallbackExecutor @NonNull Executor executor,
317 @NonNull OutcomeReceiver<Void, CallException> callback) {
318 Objects.requireNonNull(executor);
319 Objects.requireNonNull(callback);
320 if (mServerInterface != null) {
321 try {
322 mServerInterface.setMuteState(isMuted,
323 new CallControlResultReceiver("setMuteState", executor, callback));
324
325 } catch (RemoteException e) {
326 throw e.rethrowAsRuntimeException();
327 }
328 } else {
329 throw new IllegalStateException(INTERFACE_ERROR_MSG);
330 }
331 }
332
333 /**
Thomas Stuart9ace3392023-02-15 15:01:09 -0800334 * Raises an event to the {@link android.telecom.InCallService} implementations tracking this
335 * call via {@link android.telecom.Call.Callback#onConnectionEvent(Call, String, Bundle)}.
Thomas Stuartef584292023-03-02 13:24:46 -0800336 * These events and the associated extra keys for the {@code Bundle} parameter are mutually
337 * defined by a VoIP application and {@link android.telecom.InCallService}. This API is used to
338 * relay additional information about a call other than what is specified in the
339 * {@link android.telecom.CallAttributes} to {@link android.telecom.InCallService}s. This might
340 * include, for example, a change to the list of participants in a meeting, or the name of the
341 * speakers who have their hand raised. Where appropriate, the {@link InCallService}s tracking
342 * this call may choose to render this additional information about the call. An automotive
343 * calling UX, for example may have enough screen real estate to indicate the number of
344 * participants in a meeting, but to prevent distractions could suppress the list of
345 * participants.
Thomas Stuart9ace3392023-02-15 15:01:09 -0800346 *
Thomas Stuartef584292023-03-02 13:24:46 -0800347 * @param event a string event identifier agreed upon between a VoIP application and an
348 * {@link android.telecom.InCallService}
349 * @param extras a {@link android.os.Bundle} containing information about the event, as agreed
350 * upon between a VoIP application and {@link android.telecom.InCallService}.
Thomas Stuart9ace3392023-02-15 15:01:09 -0800351 */
352 public void sendEvent(@NonNull String event, @NonNull Bundle extras) {
353 Objects.requireNonNull(event);
354 Objects.requireNonNull(extras);
355 if (mServerInterface != null) {
356 try {
357 mServerInterface.sendEvent(mCallId, event, extras);
358 } catch (RemoteException e) {
359 throw e.rethrowAsRuntimeException();
360 }
361 } else {
362 throw new IllegalStateException(INTERFACE_ERROR_MSG);
363 }
364 }
365
366 /**
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700367 * Since {@link OutcomeReceiver}s cannot be passed via AIDL, a ResultReceiver (which can) must
368 * wrap the Clients {@link OutcomeReceiver} passed in and await for the Telecom Server side
369 * response in {@link ResultReceiver#onReceiveResult(int, Bundle)}.
Thomas Stuartc2427a72023-01-27 17:11:02 -0800370 *
371 * @hide
372 */
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700373 private class CallControlResultReceiver extends ResultReceiver {
374 private final String mCallingMethod;
375 private final Executor mExecutor;
376 private final OutcomeReceiver<Void, CallException> mClientCallback;
377
378 CallControlResultReceiver(String method, Executor executor,
379 OutcomeReceiver<Void, CallException> clientCallback) {
380 super(null);
381 mCallingMethod = method;
382 mExecutor = executor;
383 mClientCallback = clientCallback;
384 }
385
386 @Override
387 protected void onReceiveResult(int resultCode, Bundle resultData) {
388 Log.d(CallControl.TAG, "%s: oRR: resultCode=[%s]", mCallingMethod, resultCode);
389 super.onReceiveResult(resultCode, resultData);
390 final long identity = Binder.clearCallingIdentity();
391 try {
392 if (resultCode == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
393 mExecutor.execute(() -> mClientCallback.onResult(null));
394 } else {
395 mExecutor.execute(() ->
396 mClientCallback.onError(getTransactionException(resultData)));
397 }
398 } finally {
399 Binder.restoreCallingIdentity(identity);
400 }
401 }
402
403 }
404
405 /** @hide */
406 private CallException getTransactionException(Bundle resultData) {
407 String message = "unknown error";
408 if (resultData != null && resultData.containsKey(TRANSACTION_EXCEPTION_KEY)) {
409 return resultData.getParcelable(TRANSACTION_EXCEPTION_KEY,
410 CallException.class);
411 }
412 return new CallException(message, CallException.CODE_ERROR_UNKNOWN);
413 }
Thomas Stuartc2427a72023-01-27 17:11:02 -0800414
415 /** @hide */
416 private void validateDisconnectCause(DisconnectCause disconnectCause) {
417 final int code = disconnectCause.getCode();
418 if (code != DisconnectCause.LOCAL && code != DisconnectCause.REMOTE
419 && code != DisconnectCause.MISSED && code != DisconnectCause.REJECTED) {
420 throw new IllegalArgumentException(TextUtils.formatSimple(
421 "The DisconnectCause code provided, %d , is not a valid Disconnect code. Valid "
422 + "DisconnectCause codes are limited to [DisconnectCause.LOCAL, "
423 + "DisconnectCause.REMOTE, DisconnectCause.MISSED, or "
424 + "DisconnectCause.REJECTED]", disconnectCause.getCode()));
425 }
426 }
427
Thomas Stuartb4153492023-02-07 13:30:37 -0800428 /** @hide */
429 private void validateVideoState(@android.telecom.CallAttributes.CallType int videoState) {
430 if (videoState != CallAttributes.AUDIO_CALL && videoState != CallAttributes.VIDEO_CALL) {
431 throw new IllegalArgumentException(TextUtils.formatSimple(
432 "The VideoState argument passed in, %d , is not a valid VideoState. The "
433 + "VideoState choices are limited to CallAttributes.AUDIO_CALL or"
434 + "CallAttributes.VIDEO_CALL", videoState));
435 }
436 }
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700437}