blob: 770a3741b867ccbc97e09239c67bab2533a80b3d [file] [log] [blame]
Thomas Stuart9bfb2432022-09-27 15:02:07 -07001/*
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
17package android.telecom;
18
19import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY;
20
21import android.annotation.CallbackExecutor;
22import android.annotation.NonNull;
23import android.annotation.Nullable;
24import android.os.Binder;
25import android.os.Bundle;
26import android.os.OutcomeReceiver;
27import android.os.ParcelUuid;
28import android.os.RemoteException;
29import android.os.ResultReceiver;
30
31import com.android.internal.telecom.ClientTransactionalServiceRepository;
32import com.android.internal.telecom.ICallControl;
33
Thomas Stuart7aeccde2022-12-21 14:47:21 -080034import java.util.List;
35import java.util.Objects;
Thomas Stuart9bfb2432022-09-27 15:02:07 -070036import java.util.concurrent.Executor;
37
38/**
39 * CallControl provides client side control of a call. Each Call will get an individual CallControl
40 * instance in which the client can alter the state of the associated call.
41 *
42 * <p>
43 * Each method is Transactional meaning that it can succeed or fail. If a transaction succeeds,
44 * the {@link OutcomeReceiver#onResult} will be called by Telecom. Otherwise, the
45 * {@link OutcomeReceiver#onError} is called and provides a {@link CallException} that details why
46 * the operation failed.
47 */
48public final class CallControl implements AutoCloseable {
49 private static final String TAG = CallControl.class.getSimpleName();
50 private static final String INTERFACE_ERROR_MSG = "Call Control is not available";
51 private final String mCallId;
52 private final ICallControl mServerInterface;
53 private final PhoneAccountHandle mPhoneAccountHandle;
54 private final ClientTransactionalServiceRepository mRepository;
55
56 /** @hide */
57 public CallControl(@NonNull String callId, @Nullable ICallControl serverInterface,
58 @NonNull ClientTransactionalServiceRepository repository,
59 @NonNull PhoneAccountHandle pah) {
60 mCallId = callId;
61 mServerInterface = serverInterface;
62 mRepository = repository;
63 mPhoneAccountHandle = pah;
64 }
65
66 /**
67 * @return the callId Telecom assigned to this CallControl object which should be attached to
68 * an individual call.
69 */
70 @NonNull
71 public ParcelUuid getCallId() {
72 return ParcelUuid.fromString(mCallId);
73 }
74
75 /**
76 * Request Telecom set the call state to active.
77 *
78 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
79 * will be called on.
80 * @param callback that will be completed on the Telecom side that details success or failure
81 * of the requested operation.
82 *
83 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
84 * switched the call state to active
85 *
86 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
87 * the call state to active. A {@link CallException} will be passed
88 * that details why the operation failed.
89 */
90 public void setActive(@CallbackExecutor @NonNull Executor executor,
91 @NonNull OutcomeReceiver<Void, CallException> callback) {
92 if (mServerInterface != null) {
93 try {
94 mServerInterface.setActive(mCallId,
95 new CallControlResultReceiver("setActive", executor, callback));
96
97 } catch (RemoteException e) {
98 throw e.rethrowAsRuntimeException();
99 }
100 } else {
101 throw new IllegalStateException(INTERFACE_ERROR_MSG);
102 }
103 }
104
105 /**
106 * Request Telecom set the call state to inactive. This the same as hold for two call endpoints
107 * but can be extended to setting a meeting to inactive.
108 *
109 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
110 * will be called on.
111 * @param callback that will be completed on the Telecom side that details success or failure
112 * of the requested operation.
113 *
114 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
115 * switched the call state to inactive
116 *
117 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
118 * the call state to inactive. A {@link CallException} will be passed
119 * that details why the operation failed.
120 */
121 public void setInactive(@CallbackExecutor @NonNull Executor executor,
122 @NonNull OutcomeReceiver<Void, CallException> callback) {
123 if (mServerInterface != null) {
124 try {
125 mServerInterface.setInactive(mCallId,
126 new CallControlResultReceiver("setInactive", executor, callback));
127
128 } catch (RemoteException e) {
129 throw e.rethrowAsRuntimeException();
130 }
131 } else {
132 throw new IllegalStateException(INTERFACE_ERROR_MSG);
133 }
134 }
135
136 /**
137 * Request Telecom set the call state to disconnect.
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 * disconnected the call.
146 *
147 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
148 * disconnect the call. A {@link CallException} will be passed
149 * that details why the operation failed.
150 */
151 public void disconnect(@NonNull DisconnectCause disconnectCause,
152 @CallbackExecutor @NonNull Executor executor,
153 @NonNull OutcomeReceiver<Void, CallException> callback) {
154 if (mServerInterface != null) {
155 try {
156 mServerInterface.disconnect(mCallId, disconnectCause,
157 new CallControlResultReceiver("disconnect", executor, callback));
158 } catch (RemoteException e) {
159 throw e.rethrowAsRuntimeException();
160 }
161 } else {
162 throw new IllegalStateException(INTERFACE_ERROR_MSG);
163 }
164 }
165
166 /**
167 * Request Telecom reject the incoming call.
168 *
169 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
170 * will be called on.
171 * @param callback that will be completed on the Telecom side that details success or failure
172 * of the requested operation.
173 *
174 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
175 * rejected the incoming call.
176 *
177 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
178 * reject the incoming call. A {@link CallException} will be passed
179 * that details why the operation failed.
180 */
181 public void rejectCall(@CallbackExecutor @NonNull Executor executor,
182 @NonNull OutcomeReceiver<Void, CallException> callback) {
183 if (mServerInterface != null) {
184 try {
185 mServerInterface.rejectCall(mCallId,
186 new CallControlResultReceiver("rejectCall", executor, callback));
187 } catch (RemoteException e) {
188 throw e.rethrowAsRuntimeException();
189 }
190 } else {
191 throw new IllegalStateException(INTERFACE_ERROR_MSG);
192 }
193 }
194
195 /**
Grace Jiaef5a4cc2022-12-13 11:08:55 -0800196 * Request start a call streaming session. On receiving valid request, telecom will bind to
197 * the {@link CallStreamingService} implemented by a general call streaming sender. So that the
198 * call streaming sender can perform streaming local device audio to another remote device and
199 * control the call during streaming.
200 *
201 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
202 * will be called on.
203 * @param callback that will be completed on the Telecom side that details success or failure
204 * of the requested operation.
205 *
206 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
207 * rejected the incoming call.
208 *
209 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
210 * reject the incoming call. A {@link CallException} will be passed that
211 * details why the operation failed.
212 */
213 public void startCallStreaming(@CallbackExecutor @NonNull Executor executor,
214 @NonNull OutcomeReceiver<Void, CallException> callback) {
215 if (mServerInterface != null) {
216 try {
217 mServerInterface.startCallStreaming(mCallId,
218 new CallControlResultReceiver("startCallStreaming", executor, callback));
219 } catch (RemoteException e) {
220 throw e.rethrowAsRuntimeException();
221 }
222 } else {
223 throw new IllegalStateException(INTERFACE_ERROR_MSG);
224 }
225 }
226
227 /**
Thomas Stuart7aeccde2022-12-21 14:47:21 -0800228 * Request a CallEndpoint change. Clients should not define their own CallEndpoint when
229 * requesting a change. Instead, the new endpoint should be one of the valid endpoints provided
230 * by {@link CallEventCallback#onAvailableCallEndpointsChanged(List)}.
231 *
232 * @param callEndpoint ; The {@link CallEndpoint} to change to.
233 * @param executor ; The {@link Executor} on which the {@link OutcomeReceiver} callback
234 * will be called on.
235 * @param callback ; The {@link OutcomeReceiver} that will be completed on the Telecom side
236 * that details success or failure of the requested operation.
237 *
238 * {@link OutcomeReceiver#onResult} will be called if Telecom has
239 * successfully changed the CallEndpoint that was requested.
240 *
241 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
242 * switch to the requested CallEndpoint. A {@link CallException} will be
243 * passed that details why the operation failed.
244 */
245 public void requestCallEndpointChange(@NonNull CallEndpoint callEndpoint,
246 @CallbackExecutor @NonNull Executor executor,
247 @NonNull OutcomeReceiver<Void, CallException> callback) {
248 Objects.requireNonNull(callEndpoint);
249 Objects.requireNonNull(executor);
250 Objects.requireNonNull(callback);
251 if (mServerInterface != null) {
252 try {
253 mServerInterface.requestCallEndpointChange(callEndpoint,
254 new CallControlResultReceiver("endpointChange", executor, callback));
255 } catch (RemoteException e) {
256 throw e.rethrowAsRuntimeException();
257 }
258 } else {
259 throw new IllegalStateException(INTERFACE_ERROR_MSG);
260 }
261 }
262
263 /**
Thomas Stuart9bfb2432022-09-27 15:02:07 -0700264 * This method should be called after
265 * {@link CallControl#disconnect(DisconnectCause, Executor, OutcomeReceiver)} or
266 * {@link CallControl#rejectCall(Executor, OutcomeReceiver)}
267 * to destroy all references of this object and avoid memory leaks.
268 */
269 @Override
270 public void close() {
271 mRepository.removeCallFromServiceWrapper(mPhoneAccountHandle, mCallId);
272 }
273
274 /**
275 * Since {@link OutcomeReceiver}s cannot be passed via AIDL, a ResultReceiver (which can) must
276 * wrap the Clients {@link OutcomeReceiver} passed in and await for the Telecom Server side
277 * response in {@link ResultReceiver#onReceiveResult(int, Bundle)}.
278 * @hide */
279 private class CallControlResultReceiver extends ResultReceiver {
280 private final String mCallingMethod;
281 private final Executor mExecutor;
282 private final OutcomeReceiver<Void, CallException> mClientCallback;
283
284 CallControlResultReceiver(String method, Executor executor,
285 OutcomeReceiver<Void, CallException> clientCallback) {
286 super(null);
287 mCallingMethod = method;
288 mExecutor = executor;
289 mClientCallback = clientCallback;
290 }
291
292 @Override
293 protected void onReceiveResult(int resultCode, Bundle resultData) {
294 Log.d(CallControl.TAG, "%s: oRR: resultCode=[%s]", mCallingMethod, resultCode);
295 super.onReceiveResult(resultCode, resultData);
296 final long identity = Binder.clearCallingIdentity();
297 try {
298 if (resultCode == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
299 mExecutor.execute(() -> mClientCallback.onResult(null));
300 } else {
301 mExecutor.execute(() ->
302 mClientCallback.onError(getTransactionException(resultData)));
303 }
304 } finally {
305 Binder.restoreCallingIdentity(identity);
306 }
307 }
308
309 }
310
311 /** @hide */
312 private CallException getTransactionException(Bundle resultData) {
313 String message = "unknown error";
314 if (resultData != null && resultData.containsKey(TRANSACTION_EXCEPTION_KEY)) {
315 return resultData.getParcelable(TRANSACTION_EXCEPTION_KEY,
316 CallException.class);
317 }
318 return new CallException(message, CallException.CODE_ERROR_UNKNOWN);
319 }
320}