blob: 3bda6f4db394fd9d5d11d906b9775a22671b4cbd [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
34import java.util.concurrent.Executor;
35
36/**
37 * CallControl provides client side control of a call. Each Call will get an individual CallControl
38 * instance in which the client can alter the state of the associated call.
39 *
40 * <p>
41 * Each method is Transactional meaning that it can succeed or fail. If a transaction succeeds,
42 * the {@link OutcomeReceiver#onResult} will be called by Telecom. Otherwise, the
43 * {@link OutcomeReceiver#onError} is called and provides a {@link CallException} that details why
44 * the operation failed.
45 */
46public final class CallControl implements AutoCloseable {
47 private static final String TAG = CallControl.class.getSimpleName();
48 private static final String INTERFACE_ERROR_MSG = "Call Control is not available";
49 private final String mCallId;
50 private final ICallControl mServerInterface;
51 private final PhoneAccountHandle mPhoneAccountHandle;
52 private final ClientTransactionalServiceRepository mRepository;
53
54 /** @hide */
55 public CallControl(@NonNull String callId, @Nullable ICallControl serverInterface,
56 @NonNull ClientTransactionalServiceRepository repository,
57 @NonNull PhoneAccountHandle pah) {
58 mCallId = callId;
59 mServerInterface = serverInterface;
60 mRepository = repository;
61 mPhoneAccountHandle = pah;
62 }
63
64 /**
65 * @return the callId Telecom assigned to this CallControl object which should be attached to
66 * an individual call.
67 */
68 @NonNull
69 public ParcelUuid getCallId() {
70 return ParcelUuid.fromString(mCallId);
71 }
72
73 /**
74 * Request Telecom set the call state to active.
75 *
76 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
77 * will be called on.
78 * @param callback that will be completed on the Telecom side that details success or failure
79 * of the requested operation.
80 *
81 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
82 * switched the call state to active
83 *
84 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
85 * the call state to active. A {@link CallException} will be passed
86 * that details why the operation failed.
87 */
88 public void setActive(@CallbackExecutor @NonNull Executor executor,
89 @NonNull OutcomeReceiver<Void, CallException> callback) {
90 if (mServerInterface != null) {
91 try {
92 mServerInterface.setActive(mCallId,
93 new CallControlResultReceiver("setActive", executor, callback));
94
95 } catch (RemoteException e) {
96 throw e.rethrowAsRuntimeException();
97 }
98 } else {
99 throw new IllegalStateException(INTERFACE_ERROR_MSG);
100 }
101 }
102
103 /**
104 * Request Telecom set the call state to inactive. This the same as hold for two call endpoints
105 * but can be extended to setting a meeting to inactive.
106 *
107 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
108 * will be called on.
109 * @param callback that will be completed on the Telecom side that details success or failure
110 * of the requested operation.
111 *
112 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
113 * switched the call state to inactive
114 *
115 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
116 * the call state to inactive. A {@link CallException} will be passed
117 * that details why the operation failed.
118 */
119 public void setInactive(@CallbackExecutor @NonNull Executor executor,
120 @NonNull OutcomeReceiver<Void, CallException> callback) {
121 if (mServerInterface != null) {
122 try {
123 mServerInterface.setInactive(mCallId,
124 new CallControlResultReceiver("setInactive", executor, callback));
125
126 } catch (RemoteException e) {
127 throw e.rethrowAsRuntimeException();
128 }
129 } else {
130 throw new IllegalStateException(INTERFACE_ERROR_MSG);
131 }
132 }
133
134 /**
135 * Request Telecom set the call state to disconnect.
136 *
137 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
138 * will be called on.
139 * @param callback that will be completed on the Telecom side that details success or failure
140 * of the requested operation.
141 *
142 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
143 * disconnected the call.
144 *
145 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
146 * disconnect the call. A {@link CallException} will be passed
147 * that details why the operation failed.
148 */
149 public void disconnect(@NonNull DisconnectCause disconnectCause,
150 @CallbackExecutor @NonNull Executor executor,
151 @NonNull OutcomeReceiver<Void, CallException> callback) {
152 if (mServerInterface != null) {
153 try {
154 mServerInterface.disconnect(mCallId, disconnectCause,
155 new CallControlResultReceiver("disconnect", executor, callback));
156 } catch (RemoteException e) {
157 throw e.rethrowAsRuntimeException();
158 }
159 } else {
160 throw new IllegalStateException(INTERFACE_ERROR_MSG);
161 }
162 }
163
164 /**
165 * Request Telecom reject the incoming call.
166 *
167 * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
168 * will be called on.
169 * @param callback that will be completed on the Telecom side that details success or failure
170 * of the requested operation.
171 *
172 * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
173 * rejected the incoming call.
174 *
175 * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
176 * reject the incoming call. A {@link CallException} will be passed
177 * that details why the operation failed.
178 */
179 public void rejectCall(@CallbackExecutor @NonNull Executor executor,
180 @NonNull OutcomeReceiver<Void, CallException> callback) {
181 if (mServerInterface != null) {
182 try {
183 mServerInterface.rejectCall(mCallId,
184 new CallControlResultReceiver("rejectCall", executor, callback));
185 } catch (RemoteException e) {
186 throw e.rethrowAsRuntimeException();
187 }
188 } else {
189 throw new IllegalStateException(INTERFACE_ERROR_MSG);
190 }
191 }
192
193 /**
194 * This method should be called after
195 * {@link CallControl#disconnect(DisconnectCause, Executor, OutcomeReceiver)} or
196 * {@link CallControl#rejectCall(Executor, OutcomeReceiver)}
197 * to destroy all references of this object and avoid memory leaks.
198 */
199 @Override
200 public void close() {
201 mRepository.removeCallFromServiceWrapper(mPhoneAccountHandle, mCallId);
202 }
203
204 /**
205 * Since {@link OutcomeReceiver}s cannot be passed via AIDL, a ResultReceiver (which can) must
206 * wrap the Clients {@link OutcomeReceiver} passed in and await for the Telecom Server side
207 * response in {@link ResultReceiver#onReceiveResult(int, Bundle)}.
208 * @hide */
209 private class CallControlResultReceiver extends ResultReceiver {
210 private final String mCallingMethod;
211 private final Executor mExecutor;
212 private final OutcomeReceiver<Void, CallException> mClientCallback;
213
214 CallControlResultReceiver(String method, Executor executor,
215 OutcomeReceiver<Void, CallException> clientCallback) {
216 super(null);
217 mCallingMethod = method;
218 mExecutor = executor;
219 mClientCallback = clientCallback;
220 }
221
222 @Override
223 protected void onReceiveResult(int resultCode, Bundle resultData) {
224 Log.d(CallControl.TAG, "%s: oRR: resultCode=[%s]", mCallingMethod, resultCode);
225 super.onReceiveResult(resultCode, resultData);
226 final long identity = Binder.clearCallingIdentity();
227 try {
228 if (resultCode == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
229 mExecutor.execute(() -> mClientCallback.onResult(null));
230 } else {
231 mExecutor.execute(() ->
232 mClientCallback.onError(getTransactionException(resultData)));
233 }
234 } finally {
235 Binder.restoreCallingIdentity(identity);
236 }
237 }
238
239 }
240
241 /** @hide */
242 private CallException getTransactionException(Bundle resultData) {
243 String message = "unknown error";
244 if (resultData != null && resultData.containsKey(TRANSACTION_EXCEPTION_KEY)) {
245 return resultData.getParcelable(TRANSACTION_EXCEPTION_KEY,
246 CallException.class);
247 }
248 return new CallException(message, CallException.CODE_ERROR_UNKNOWN);
249 }
250}