• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package android.telecom;
18 
19 import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY;
20 
21 import android.annotation.CallbackExecutor;
22 import android.annotation.FlaggedApi;
23 import android.annotation.NonNull;
24 import android.annotation.SuppressLint;
25 import android.os.Binder;
26 import android.os.Bundle;
27 import android.os.OutcomeReceiver;
28 import android.os.ParcelUuid;
29 import android.os.RemoteException;
30 import android.os.ResultReceiver;
31 import android.text.TextUtils;
32 
33 import com.android.internal.telecom.ICallControl;
34 import com.android.server.telecom.flags.Flags;
35 
36 import java.util.List;
37 import java.util.Objects;
38 import 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  */
50 @SuppressLint("NotCloseable")
51 public final class CallControl {
52     private static final String TAG = CallControl.class.getSimpleName();
53     private final String mCallId;
54     private final ICallControl mServerInterface;
55 
56     /** @hide */
CallControl(@onNull String callId, @NonNull ICallControl serverInterface)57     public CallControl(@NonNull String callId, @NonNull ICallControl serverInterface) {
58         mCallId = callId;
59         mServerInterface = serverInterface;
60     }
61 
62     /**
63      * @return the callId Telecom assigned to this CallControl object which should be attached to
64      * an individual call.
65      */
66     @NonNull
getCallId()67     public ParcelUuid getCallId() {
68         return ParcelUuid.fromString(mCallId);
69     }
70 
71     /**
72      * Request Telecom set the call state to active. This method should be called when either an
73      * outgoing call is ready to go active or a held call is ready to go active again. For incoming
74      * calls that are ready to be answered, use
75      * {@link CallControl#answer(int, Executor, OutcomeReceiver)}.
76      *
77      * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
78      *                 will be called on.
79      * @param callback that will be completed on the Telecom side that details success or failure
80      *                 of the requested operation.
81      *
82      *                 {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
83      *                 switched the call state to active
84      *
85      *                 {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
86      *                 the call state to active.  A {@link CallException} will be passed
87      *                 that details why the operation failed.
88      */
setActive(@allbackExecutor @onNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)89     public void setActive(@CallbackExecutor @NonNull Executor executor,
90             @NonNull OutcomeReceiver<Void, CallException> callback) {
91         Objects.requireNonNull(executor);
92         Objects.requireNonNull(callback);
93         try {
94             mServerInterface.setActive(mCallId,
95                     new CallControlResultReceiver("setActive", executor, callback));
96 
97         } catch (RemoteException e) {
98             throw e.rethrowAsRuntimeException();
99         }
100     }
101 
102     /**
103      * Request Telecom answer an incoming call.  For outgoing calls and calls that have been placed
104      * on hold, use {@link CallControl#setActive(Executor, OutcomeReceiver)}.
105      *
106      * @param videoState to report to Telecom. Telecom will store VideoState in the event another
107      *                   service/device requests it in order to continue the call on another screen.
108      * @param executor   The {@link Executor} on which the {@link OutcomeReceiver} callback
109      *                   will be called on.
110      * @param callback   that will be completed on the Telecom side that details success or failure
111      *                   of the requested operation.
112      *
113      *                   {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
114      *                   switched the call state to active
115      *
116      *                   {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
117      *                   the call state to active.  A {@link CallException} will be passed
118      *                   that details why the operation failed.
119      */
answer(@ndroid.telecom.CallAttributes.CallType int videoState, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)120     public void answer(@android.telecom.CallAttributes.CallType int videoState,
121             @CallbackExecutor @NonNull Executor executor,
122             @NonNull OutcomeReceiver<Void, CallException> callback) {
123         validateVideoState(videoState);
124         Objects.requireNonNull(executor);
125         Objects.requireNonNull(callback);
126         try {
127             mServerInterface.answer(videoState, mCallId,
128                     new CallControlResultReceiver("answer", executor, callback));
129 
130         } catch (RemoteException e) {
131             throw e.rethrowAsRuntimeException();
132         }
133     }
134 
135     /**
136      * Request Telecom set the call state to inactive. This the same as hold for two call endpoints
137      * but can be extended to setting a meeting to inactive.
138      *
139      * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
140      *                 will be called on.
141      * @param callback that will be completed on the Telecom side that details success or failure
142      *                 of the requested operation.
143      *
144      *                 {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
145      *                 switched the call state to inactive
146      *
147      *                 {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
148      *                 the call state to inactive.  A {@link CallException} will be passed
149      *                 that details why the operation failed.
150      */
setInactive(@allbackExecutor @onNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)151     public void setInactive(@CallbackExecutor @NonNull Executor executor,
152             @NonNull OutcomeReceiver<Void, CallException> callback) {
153         Objects.requireNonNull(executor);
154         Objects.requireNonNull(callback);
155         try {
156             mServerInterface.setInactive(mCallId,
157                     new CallControlResultReceiver("setInactive", executor, callback));
158 
159         } catch (RemoteException e) {
160             throw e.rethrowAsRuntimeException();
161         }
162     }
163 
164     /**
165      * Request Telecom disconnect the call and remove the call from telecom tracking.
166      *
167      * @param disconnectCause represents the cause for disconnecting the call.  The only valid
168      *                        codes for the {@link  android.telecom.DisconnectCause} passed in are:
169      *                        <ul>
170      *                        <li>{@link DisconnectCause#LOCAL}</li>
171      *                        <li>{@link DisconnectCause#REMOTE}</li>
172      *                        <li>{@link DisconnectCause#REJECTED}</li>
173      *                        <li>{@link DisconnectCause#MISSED}</li>
174      *                        </ul>
175      * @param executor        The {@link Executor} on which the {@link OutcomeReceiver} callback
176      *                        will be called on.
177      * @param callback        That will be completed on the Telecom side that details success or
178      *                        failure of the requested operation.
179      *
180      *                        {@link OutcomeReceiver#onResult} will be called if Telecom has
181      *                        successfully disconnected the call.
182      *
183      *                        {@link OutcomeReceiver#onError} will be called if Telecom has failed
184      *                        to disconnect the call.  A {@link CallException} will be passed
185      *                        that details why the operation failed.
186      *
187      * <p>
188      * Note: After the call has been successfully disconnected, calling any CallControl API will
189      * result in the {@link OutcomeReceiver#onError} with
190      * {@link CallException#CODE_CALL_IS_NOT_BEING_TRACKED}.
191      */
disconnect(@onNull DisconnectCause disconnectCause, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)192     public void disconnect(@NonNull DisconnectCause disconnectCause,
193             @CallbackExecutor @NonNull Executor executor,
194             @NonNull OutcomeReceiver<Void, CallException> callback) {
195         Objects.requireNonNull(disconnectCause);
196         Objects.requireNonNull(executor);
197         Objects.requireNonNull(callback);
198         validateDisconnectCause(disconnectCause);
199         try {
200             mServerInterface.disconnect(mCallId, disconnectCause,
201                     new CallControlResultReceiver("disconnect", executor, callback));
202         } catch (RemoteException e) {
203             throw e.rethrowAsRuntimeException();
204         }
205     }
206 
207     /**
208      * Request start a call streaming session. On receiving valid request, telecom will bind to
209      * the {@code CallStreamingService} implemented by a general call streaming sender. So that the
210      * call streaming sender can perform streaming local device audio to another remote device and
211      * control the call during streaming.
212      *
213      * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
214      *                 will be called on.
215      * @param callback that will be completed on the Telecom side that details success or failure
216      *                 of the requested operation.
217      *
218      *                 {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
219      *                 started the call streaming.
220      *
221      *                 {@link OutcomeReceiver#onError} will be called if Telecom has failed to
222      *                 start the call streaming. A {@link CallException} will be passed that
223      *                 details why the operation failed.
224      */
startCallStreaming(@allbackExecutor @onNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)225     public void startCallStreaming(@CallbackExecutor @NonNull Executor executor,
226             @NonNull OutcomeReceiver<Void, CallException> callback) {
227         Objects.requireNonNull(executor);
228         Objects.requireNonNull(callback);
229         try {
230             mServerInterface.startCallStreaming(mCallId,
231                     new CallControlResultReceiver("startCallStreaming", executor, callback));
232         } catch (RemoteException e) {
233             throw e.rethrowAsRuntimeException();
234         }
235     }
236 
237     /**
238      * Request a CallEndpoint change. Clients should not define their own CallEndpoint when
239      * requesting a change. Instead, the new endpoint should be one of the valid endpoints provided
240      * by {@link CallEventCallback#onAvailableCallEndpointsChanged(List)}.
241      *
242      * @param callEndpoint The {@link CallEndpoint} to change to.
243      * @param executor     The {@link Executor} on which the {@link OutcomeReceiver} callback
244      *                     will be called on.
245      * @param callback     The {@link OutcomeReceiver} that will be completed on the Telecom side
246      *                     that details success or failure of the requested operation.
247      *
248      *                     {@link OutcomeReceiver#onResult} will be called if Telecom has
249      *                     successfully changed the CallEndpoint that was requested.
250      *
251      *                     {@link OutcomeReceiver#onError} will be called if Telecom has failed to
252      *                     switch to the requested CallEndpoint.  A {@link CallException} will be
253      *                     passed that details why the operation failed.
254      */
requestCallEndpointChange(@onNull CallEndpoint callEndpoint, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)255     public void requestCallEndpointChange(@NonNull CallEndpoint callEndpoint,
256             @CallbackExecutor @NonNull Executor executor,
257             @NonNull OutcomeReceiver<Void, CallException> callback) {
258         Objects.requireNonNull(callEndpoint);
259         Objects.requireNonNull(executor);
260         Objects.requireNonNull(callback);
261         try {
262             mServerInterface.requestCallEndpointChange(callEndpoint,
263                     new CallControlResultReceiver("requestCallEndpointChange", executor, callback));
264         } catch (RemoteException e) {
265             throw e.rethrowAsRuntimeException();
266         }
267     }
268 
269     /**
270      * Request a new mute state.  Note: {@link CallEventCallback#onMuteStateChanged(boolean)}
271      * will be called every time the mute state is changed and can be used to track the current
272      * mute state.
273      *
274      * @param isMuted  The new mute state.  Passing in a {@link Boolean#TRUE} for the isMuted
275      *                 parameter will mute the call.  {@link Boolean#FALSE} will unmute the call.
276      * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
277      *                 will be called on.
278      * @param callback The {@link OutcomeReceiver} that will be completed on the Telecom side
279      *                 that details success or failure of the requested operation.
280      *
281      *                 {@link OutcomeReceiver#onResult} will be called if Telecom has
282      *                 successfully changed the mute state.
283      *
284      *                 {@link OutcomeReceiver#onError} will be called if Telecom has failed to
285      *                 switch to the mute state.  A {@link CallException} will be
286      *                 passed that details why the operation failed.
287      */
288     @FlaggedApi(Flags.FLAG_SET_MUTE_STATE)
requestMuteState(boolean isMuted, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)289     public void requestMuteState(boolean isMuted, @CallbackExecutor @NonNull Executor executor,
290             @NonNull OutcomeReceiver<Void, CallException> callback) {
291         Objects.requireNonNull(executor);
292         Objects.requireNonNull(callback);
293         try {
294             mServerInterface.setMuteState(isMuted,
295                     new CallControlResultReceiver("requestMuteState", executor, callback));
296         } catch (RemoteException e) {
297             throw e.rethrowAsRuntimeException();
298         }
299     }
300 
301      /**
302      * Request a new video state for the ongoing call. This can only be changed if the application
303      * has registered a {@link PhoneAccount} with the
304      * {@link PhoneAccount#CAPABILITY_SUPPORTS_VIDEO_CALLING} and set the
305      * {@link CallAttributes#SUPPORTS_VIDEO_CALLING} when adding the call via
306      * {@link TelecomManager#addCall(CallAttributes, Executor, OutcomeReceiver,
307      *                                                      CallControlCallback, CallEventCallback)}
308      *
309      * @param videoState to report to Telecom. To see the valid argument to pass,
310       *                   see {@link CallAttributes.CallType}.
311      * @param executor   The {@link Executor} on which the {@link OutcomeReceiver} callback
312      *                   will be called on.
313      * @param callback   that will be completed on the Telecom side that details success or failure
314      *                   of the requested operation.
315      *
316      *                   {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
317      *                   switched the video state.
318      *
319      *                   {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
320      *                   the new video state.  A {@link CallException} will be passed
321      *                   that details why the operation failed.
322      * @throws IllegalArgumentException if the argument passed for videoState is invalid.  To see a
323      * list of valid states, see {@link CallAttributes.CallType}.
324      */
325      @FlaggedApi(Flags.FLAG_TRANSACTIONAL_VIDEO_STATE)
requestVideoState(@allAttributes.CallType int videoState, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)326      public void requestVideoState(@CallAttributes.CallType int videoState,
327              @CallbackExecutor @NonNull Executor executor,
328              @NonNull OutcomeReceiver<Void, CallException> callback) {
329          validateVideoState(videoState);
330          Objects.requireNonNull(executor);
331          Objects.requireNonNull(callback);
332          try {
333              mServerInterface.requestVideoState(videoState, mCallId,
334                      new CallControlResultReceiver("requestVideoState", executor, callback));
335          } catch (RemoteException e) {
336              throw e.rethrowAsRuntimeException();
337          }
338      }
339 
340     /**
341      * Raises an event to the {@link android.telecom.InCallService} implementations tracking this
342      * call via {@link android.telecom.Call.Callback#onConnectionEvent(Call, String, Bundle)}.
343      * These events and the associated extra keys for the {@code Bundle} parameter are mutually
344      * defined by a VoIP application and {@link android.telecom.InCallService}. This API is used to
345      * relay additional information about a call other than what is specified in the
346      * {@link android.telecom.CallAttributes} to {@link android.telecom.InCallService}s. This might
347      * include, for example, a change to the list of participants in a meeting, or the name of the
348      * speakers who have their hand raised. Where appropriate, the {@link InCallService}s tracking
349      * this call may choose to render this additional information about the call. An automotive
350      * calling UX, for example may have enough screen real estate to indicate the number of
351      * participants in a meeting, but to prevent distractions could suppress the list of
352      * participants.
353      *
354      * @param event a string event identifier agreed upon between a VoIP application and an
355      *              {@link android.telecom.InCallService}
356      * @param extras a {@link android.os.Bundle} containing information about the event, as agreed
357      *              upon between a VoIP application and {@link android.telecom.InCallService}.
358      */
sendEvent(@onNull String event, @NonNull Bundle extras)359     public void sendEvent(@NonNull String event, @NonNull Bundle extras) {
360         Objects.requireNonNull(event);
361         Objects.requireNonNull(extras);
362         try {
363             mServerInterface.sendEvent(mCallId, event, extras);
364         } catch (RemoteException e) {
365             throw e.rethrowAsRuntimeException();
366         }
367     }
368 
369     /**
370      * Since {@link OutcomeReceiver}s cannot be passed via AIDL, a ResultReceiver (which can) must
371      * wrap the Clients {@link OutcomeReceiver} passed in and await for the Telecom Server side
372      * response in {@link ResultReceiver#onReceiveResult(int, Bundle)}.
373      *
374      * @hide
375      */
376     private class CallControlResultReceiver extends ResultReceiver {
377         private final String mCallingMethod;
378         private final Executor mExecutor;
379         private final OutcomeReceiver<Void, CallException> mClientCallback;
380 
CallControlResultReceiver(String method, Executor executor, OutcomeReceiver<Void, CallException> clientCallback)381         CallControlResultReceiver(String method, Executor executor,
382                 OutcomeReceiver<Void, CallException> clientCallback) {
383             super(null);
384             mCallingMethod = method;
385             mExecutor = executor;
386             mClientCallback = clientCallback;
387         }
388 
389         @Override
onReceiveResult(int resultCode, Bundle resultData)390         protected void onReceiveResult(int resultCode, Bundle resultData) {
391             Log.d(CallControl.TAG, "%s: oRR: resultCode=[%s]", mCallingMethod, resultCode);
392             super.onReceiveResult(resultCode, resultData);
393             final long identity = Binder.clearCallingIdentity();
394             try {
395                 if (resultCode == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
396                     mExecutor.execute(() -> mClientCallback.onResult(null));
397                 } else {
398                     mExecutor.execute(() ->
399                             mClientCallback.onError(getTransactionException(resultData)));
400                 }
401             } finally {
402                 Binder.restoreCallingIdentity(identity);
403             }
404         }
405 
406     }
407 
408     /** @hide */
getTransactionException(Bundle resultData)409     private CallException getTransactionException(Bundle resultData) {
410         String message = "unknown error";
411         if (resultData != null && resultData.containsKey(TRANSACTION_EXCEPTION_KEY)) {
412             return resultData.getParcelable(TRANSACTION_EXCEPTION_KEY,
413                     CallException.class);
414         }
415         return new CallException(message, CallException.CODE_ERROR_UNKNOWN);
416     }
417 
418     /** @hide */
validateDisconnectCause(DisconnectCause disconnectCause)419     private void validateDisconnectCause(DisconnectCause disconnectCause) {
420         final int code = disconnectCause.getCode();
421         if (code != DisconnectCause.LOCAL && code != DisconnectCause.REMOTE
422                 && code != DisconnectCause.MISSED && code != DisconnectCause.REJECTED) {
423             throw new IllegalArgumentException(TextUtils.formatSimple(
424                     "The DisconnectCause code provided, %d , is not a valid Disconnect code. Valid "
425                             + "DisconnectCause codes are limited to [DisconnectCause.LOCAL, "
426                             + "DisconnectCause.REMOTE, DisconnectCause.MISSED, or "
427                             + "DisconnectCause.REJECTED]", disconnectCause.getCode()));
428         }
429     }
430 
431     /** @hide */
validateVideoState(@ndroid.telecom.CallAttributes.CallType int videoState)432     private void validateVideoState(@android.telecom.CallAttributes.CallType int videoState) {
433         if (videoState != CallAttributes.AUDIO_CALL && videoState != CallAttributes.VIDEO_CALL) {
434             throw new IllegalArgumentException(TextUtils.formatSimple(
435                     "The VideoState argument passed in, %d , is not a valid VideoState. The "
436                             + "VideoState choices are limited to CallAttributes.AUDIO_CALL or"
437                             + "CallAttributes.VIDEO_CALL", videoState));
438         }
439     }
440 }
441