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