|  | /* | 
|  | * 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); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * 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); | 
|  | } | 
|  | } |