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