1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
|
/*
* 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. Outgoing and incoming
* calls should move to active (via {@link CallControl#setActive(Executor, OutcomeReceiver)} or
* answered (via {@link CallControl#answer(int, Executor, OutcomeReceiver)} before 60 seconds. If
* the new call is not moved to active or answered before 60 seconds, the call will be disconnected.
*
* <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));
}
}
}
|