| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.telecom; |
| |
| import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY; |
| |
| import android.annotation.CallbackExecutor; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.OutcomeReceiver; |
| import android.os.ParcelUuid; |
| import android.os.RemoteException; |
| import android.os.ResultReceiver; |
| |
| import com.android.internal.telecom.ClientTransactionalServiceRepository; |
| import com.android.internal.telecom.ICallControl; |
| |
| import java.util.concurrent.Executor; |
| |
| /** |
| * CallControl provides client side control of a call. Each Call will get an individual CallControl |
| * instance in which the client can alter the state of the associated call. |
| * |
| * <p> |
| * Each method is Transactional meaning that it can succeed or fail. If a transaction succeeds, |
| * the {@link OutcomeReceiver#onResult} will be called by Telecom. Otherwise, the |
| * {@link OutcomeReceiver#onError} is called and provides a {@link CallException} that details why |
| * the operation failed. |
| */ |
| public final class CallControl implements AutoCloseable { |
| private static final String TAG = CallControl.class.getSimpleName(); |
| private static final String INTERFACE_ERROR_MSG = "Call Control is not available"; |
| private final String mCallId; |
| private final ICallControl mServerInterface; |
| private final PhoneAccountHandle mPhoneAccountHandle; |
| private final ClientTransactionalServiceRepository mRepository; |
| |
| /** @hide */ |
| public CallControl(@NonNull String callId, @Nullable ICallControl serverInterface, |
| @NonNull ClientTransactionalServiceRepository repository, |
| @NonNull PhoneAccountHandle pah) { |
| mCallId = callId; |
| mServerInterface = serverInterface; |
| mRepository = repository; |
| mPhoneAccountHandle = pah; |
| } |
| |
| /** |
| * @return the callId Telecom assigned to this CallControl object which should be attached to |
| * an individual call. |
| */ |
| @NonNull |
| public ParcelUuid getCallId() { |
| return ParcelUuid.fromString(mCallId); |
| } |
| |
| /** |
| * Request Telecom set the call state to active. |
| * |
| * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback |
| * will be called on. |
| * @param callback that will be completed on the Telecom side that details success or failure |
| * of the requested operation. |
| * |
| * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully |
| * switched the call state to active |
| * |
| * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set |
| * the call state to active. A {@link CallException} will be passed |
| * that details why the operation failed. |
| */ |
| public void setActive(@CallbackExecutor @NonNull Executor executor, |
| @NonNull OutcomeReceiver<Void, CallException> callback) { |
| if (mServerInterface != null) { |
| try { |
| mServerInterface.setActive(mCallId, |
| new CallControlResultReceiver("setActive", executor, callback)); |
| |
| } catch (RemoteException e) { |
| throw e.rethrowAsRuntimeException(); |
| } |
| } else { |
| throw new IllegalStateException(INTERFACE_ERROR_MSG); |
| } |
| } |
| |
| /** |
| * Request Telecom set the call state to inactive. This the same as hold for two call endpoints |
| * but can be extended to setting a meeting to inactive. |
| * |
| * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback |
| * will be called on. |
| * @param callback that will be completed on the Telecom side that details success or failure |
| * of the requested operation. |
| * |
| * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully |
| * switched the call state to inactive |
| * |
| * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set |
| * the call state to inactive. A {@link CallException} will be passed |
| * that details why the operation failed. |
| */ |
| public void setInactive(@CallbackExecutor @NonNull Executor executor, |
| @NonNull OutcomeReceiver<Void, CallException> callback) { |
| if (mServerInterface != null) { |
| try { |
| mServerInterface.setInactive(mCallId, |
| new CallControlResultReceiver("setInactive", executor, callback)); |
| |
| } catch (RemoteException e) { |
| throw e.rethrowAsRuntimeException(); |
| } |
| } else { |
| throw new IllegalStateException(INTERFACE_ERROR_MSG); |
| } |
| } |
| |
| /** |
| * Request Telecom set the call state to disconnect. |
| * |
| * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback |
| * will be called on. |
| * @param callback that will be completed on the Telecom side that details success or failure |
| * of the requested operation. |
| * |
| * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully |
| * disconnected the call. |
| * |
| * {@link OutcomeReceiver#onError} will be called if Telecom has failed to |
| * disconnect the call. A {@link CallException} will be passed |
| * that details why the operation failed. |
| */ |
| public void disconnect(@NonNull DisconnectCause disconnectCause, |
| @CallbackExecutor @NonNull Executor executor, |
| @NonNull OutcomeReceiver<Void, CallException> callback) { |
| if (mServerInterface != null) { |
| try { |
| mServerInterface.disconnect(mCallId, disconnectCause, |
| new CallControlResultReceiver("disconnect", executor, callback)); |
| } catch (RemoteException e) { |
| throw e.rethrowAsRuntimeException(); |
| } |
| } else { |
| throw new IllegalStateException(INTERFACE_ERROR_MSG); |
| } |
| } |
| |
| /** |
| * Request Telecom reject the incoming call. |
| * |
| * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback |
| * will be called on. |
| * @param callback that will be completed on the Telecom side that details success or failure |
| * of the requested operation. |
| * |
| * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully |
| * rejected the incoming call. |
| * |
| * {@link OutcomeReceiver#onError} will be called if Telecom has failed to |
| * reject the incoming call. A {@link CallException} will be passed |
| * that details why the operation failed. |
| */ |
| public void rejectCall(@CallbackExecutor @NonNull Executor executor, |
| @NonNull OutcomeReceiver<Void, CallException> callback) { |
| if (mServerInterface != null) { |
| try { |
| mServerInterface.rejectCall(mCallId, |
| new CallControlResultReceiver("rejectCall", executor, callback)); |
| } catch (RemoteException e) { |
| throw e.rethrowAsRuntimeException(); |
| } |
| } else { |
| throw new IllegalStateException(INTERFACE_ERROR_MSG); |
| } |
| } |
| |
| /** |
| * Request start a call streaming session. On receiving valid request, telecom will bind to |
| * the {@link CallStreamingService} implemented by a general call streaming sender. So that the |
| * call streaming sender can perform streaming local device audio to another remote device and |
| * control the call during streaming. |
| * |
| * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback |
| * will be called on. |
| * @param callback that will be completed on the Telecom side that details success or failure |
| * of the requested operation. |
| * |
| * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully |
| * rejected the incoming call. |
| * |
| * {@link OutcomeReceiver#onError} will be called if Telecom has failed to |
| * reject the incoming call. A {@link CallException} will be passed that |
| * details why the operation failed. |
| */ |
| public void startCallStreaming(@CallbackExecutor @NonNull Executor executor, |
| @NonNull OutcomeReceiver<Void, CallException> callback) { |
| if (mServerInterface != null) { |
| try { |
| mServerInterface.startCallStreaming(mCallId, |
| new CallControlResultReceiver("startCallStreaming", executor, callback)); |
| } catch (RemoteException e) { |
| throw e.rethrowAsRuntimeException(); |
| } |
| } else { |
| throw new IllegalStateException(INTERFACE_ERROR_MSG); |
| } |
| } |
| |
| /** |
| * This method should be called after |
| * {@link CallControl#disconnect(DisconnectCause, Executor, OutcomeReceiver)} or |
| * {@link CallControl#rejectCall(Executor, OutcomeReceiver)} |
| * to destroy all references of this object and avoid memory leaks. |
| */ |
| @Override |
| public void close() { |
| mRepository.removeCallFromServiceWrapper(mPhoneAccountHandle, mCallId); |
| } |
| |
| /** |
| * Since {@link OutcomeReceiver}s cannot be passed via AIDL, a ResultReceiver (which can) must |
| * wrap the Clients {@link OutcomeReceiver} passed in and await for the Telecom Server side |
| * response in {@link ResultReceiver#onReceiveResult(int, Bundle)}. |
| * @hide */ |
| private class CallControlResultReceiver extends ResultReceiver { |
| private final String mCallingMethod; |
| private final Executor mExecutor; |
| private final OutcomeReceiver<Void, CallException> mClientCallback; |
| |
| CallControlResultReceiver(String method, Executor executor, |
| OutcomeReceiver<Void, CallException> clientCallback) { |
| super(null); |
| mCallingMethod = method; |
| mExecutor = executor; |
| mClientCallback = clientCallback; |
| } |
| |
| @Override |
| protected void onReceiveResult(int resultCode, Bundle resultData) { |
| Log.d(CallControl.TAG, "%s: oRR: resultCode=[%s]", mCallingMethod, resultCode); |
| super.onReceiveResult(resultCode, resultData); |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| if (resultCode == TelecomManager.TELECOM_TRANSACTION_SUCCESS) { |
| mExecutor.execute(() -> mClientCallback.onResult(null)); |
| } else { |
| mExecutor.execute(() -> |
| mClientCallback.onError(getTransactionException(resultData))); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| } |
| |
| } |
| |
| /** @hide */ |
| private CallException getTransactionException(Bundle resultData) { |
| String message = "unknown error"; |
| if (resultData != null && resultData.containsKey(TRANSACTION_EXCEPTION_KEY)) { |
| return resultData.getParcelable(TRANSACTION_EXCEPTION_KEY, |
| CallException.class); |
| } |
| return new CallException(message, CallException.CODE_ERROR_UNKNOWN); |
| } |
| } |