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.NonNull;
23 import android.annotation.Nullable;
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.ClientTransactionalServiceRepository;
34 import com.android.internal.telecom.ICallControl;
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 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 */
CallControl(@onNull String callId, @Nullable ICallControl serverInterface, @NonNull ClientTransactionalServiceRepository repository, @NonNull PhoneAccountHandle pah)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
71      * an individual call.
72      */
73     @NonNull
getCallId()74     public ParcelUuid getCallId() {
75         return ParcelUuid.fromString(mCallId);
76     }
77 
78     /**
79      * 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)}.
83      *
84      * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
85      *                 will be called on.
86      * @param callback that will be completed on the Telecom side that details success or failure
87      *                 of the requested operation.
88      *
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      */
setActive(@allbackExecutor @onNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)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     /**
112      * 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      */
answer(@ndroid.telecom.CallAttributes.CallType int videoState, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)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     /**
149      * 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
153      *                 will be called on.
154      * @param callback that will be completed on the Telecom side that details success or failure
155      *                 of the requested operation.
156      *
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      */
setInactive(@allbackExecutor @onNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)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     /**
180      * Request Telecom disconnect the call and remove the call from telecom tracking.
181      *
182      * @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>
190      * @param executor        The {@link Executor} on which the {@link OutcomeReceiver} callback
191      *                        will be called on.
192      * @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}.
206      */
disconnect(@onNull DisconnectCause disconnectCause, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)207     public void disconnect(@NonNull DisconnectCause disconnectCause,
208             @CallbackExecutor @NonNull Executor executor,
209             @NonNull OutcomeReceiver<Void, CallException> callback) {
210         Objects.requireNonNull(disconnectCause);
211         Objects.requireNonNull(executor);
212         Objects.requireNonNull(callback);
213         validateDisconnectCause(disconnectCause);
214         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     /**
227      * 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
238      *                 started the call streaming.
239      *
240      *                 {@link OutcomeReceiver#onError} will be called if Telecom has failed to
241      *                 start the call streaming. A {@link CallException} will be passed that
242      *                 details why the operation failed.
243      */
startCallStreaming(@allbackExecutor @onNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)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     /**
259      * 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      *
263      * @param callEndpoint The {@link CallEndpoint} to change to.
264      * @param executor     The {@link Executor} on which the {@link OutcomeReceiver} callback
265      *                     will be called on.
266      * @param callback     The {@link OutcomeReceiver} that will be completed on the Telecom side
267      *                     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      */
requestCallEndpointChange(@onNull CallEndpoint callEndpoint, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback)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     /**
295      * Raises an event to the {@link android.telecom.InCallService} implementations tracking this
296      * call via {@link android.telecom.Call.Callback#onConnectionEvent(Call, String, Bundle)}.
297      * 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.
307      *
308      * @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}.
312      */
sendEvent(@onNull String event, @NonNull Bundle extras)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     /**
328      * 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)}.
331      *
332      * @hide
333      */
334     private class CallControlResultReceiver extends ResultReceiver {
335         private final String mCallingMethod;
336         private final Executor mExecutor;
337         private final OutcomeReceiver<Void, CallException> mClientCallback;
338 
CallControlResultReceiver(String method, Executor executor, OutcomeReceiver<Void, CallException> clientCallback)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
onReceiveResult(int resultCode, Bundle resultData)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 */
getTransactionException(Bundle resultData)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     }
375 
376     /** @hide */
validateDisconnectCause(DisconnectCause disconnectCause)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 
389     /** @hide */
validateVideoState(@ndroid.telecom.CallAttributes.CallType int videoState)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     }
398 }
399