diff --git a/core/api/current.txt b/core/api/current.txt index bdfc2180d9fa2..a8dddcf1e3d0f 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -41038,6 +41038,32 @@ package android.telecom { field public static final int RTT_MODE_VCO = 3; // 0x3 } + public final class CallAttributes implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public android.net.Uri getAddress(); + method public int getCallCapabilities(); + method public int getCallType(); + method public int getDirection(); + method @NonNull public CharSequence getDisplayName(); + method @NonNull public android.telecom.PhoneAccountHandle getPhoneAccountHandle(); + method public void writeToParcel(@Nullable android.os.Parcel, int); + field public static final int AUDIO_CALL = 1; // 0x1 + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + field public static final int DIRECTION_INCOMING = 1; // 0x1 + field public static final int DIRECTION_OUTGOING = 2; // 0x2 + field public static final int SUPPORTS_SET_INACTIVE = 2; // 0x2 + field public static final int SUPPORTS_STREAM = 4; // 0x4 + field public static final int SUPPORTS_TRANSFER = 8; // 0x8 + field public static final int VIDEO_CALL = 2; // 0x2 + } + + public static final class CallAttributes.Builder { + ctor public CallAttributes.Builder(@NonNull android.telecom.PhoneAccountHandle, int, @NonNull CharSequence, @NonNull android.net.Uri); + method @NonNull public android.telecom.CallAttributes build(); + method @NonNull public android.telecom.CallAttributes.Builder setCallCapabilities(int); + method @NonNull public android.telecom.CallAttributes.Builder setCallType(int); + } + public final class CallAudioState implements android.os.Parcelable { ctor public CallAudioState(boolean, int, int); method public static String audioRouteToString(int); @@ -41056,6 +41082,15 @@ package android.telecom { field public static final int ROUTE_WIRED_OR_EARPIECE = 5; // 0x5 } + public final class CallControl implements java.lang.AutoCloseable { + method public void close(); + method public void disconnect(@NonNull android.telecom.DisconnectCause, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); + method @NonNull public android.os.ParcelUuid getCallId(); + method public void rejectCall(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); + method public void setActive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); + method public void setInactive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); + } + public final class CallEndpoint implements android.os.Parcelable { ctor public CallEndpoint(@NonNull CharSequence, int, @NonNull android.os.ParcelUuid); method public int describeContents(); @@ -41086,6 +41121,30 @@ package android.telecom { field public static final int ERROR_UNSPECIFIED = 4; // 0x4 } + public interface CallEventCallback { + method public void onAnswer(int, @NonNull java.util.function.Consumer); + method public void onCallAudioStateChanged(@NonNull android.telecom.CallAudioState); + method public void onDisconnect(@NonNull java.util.function.Consumer); + method public void onReject(@NonNull java.util.function.Consumer); + method public void onSetActive(@NonNull java.util.function.Consumer); + method public void onSetInactive(@NonNull java.util.function.Consumer); + } + + public final class CallException extends java.lang.RuntimeException implements android.os.Parcelable { + ctor public CallException(@Nullable String); + ctor public CallException(@Nullable String, int); + method public int describeContents(); + method public int getCode(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int CODE_CALL_CANNOT_BE_SET_TO_ACTIVE = 4; // 0x4 + field public static final int CODE_CALL_IS_NOT_BEING_TRACKED = 3; // 0x3 + field public static final int CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME = 5; // 0x5 + field public static final int CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL = 2; // 0x2 + field public static final int CODE_ERROR_UNKNOWN = 1; // 0x1 + field public static final int CODE_OPERATION_TIMED_OUT = 6; // 0x6 + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + public abstract class CallRedirectionService extends android.app.Service { ctor public CallRedirectionService(); method public final void cancelCall(); @@ -41595,6 +41654,7 @@ package android.telecom { field public static final int CAPABILITY_RTT = 4096; // 0x1000 field public static final int CAPABILITY_SELF_MANAGED = 2048; // 0x800 field public static final int CAPABILITY_SIM_SUBSCRIPTION = 4; // 0x4 + field public static final int CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS = 262144; // 0x40000 field public static final int CAPABILITY_SUPPORTS_VIDEO_CALLING = 1024; // 0x400 field public static final int CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS = 65536; // 0x10000 field public static final int CAPABILITY_VIDEO_CALLING = 8; // 0x8 @@ -41787,6 +41847,7 @@ package android.telecom { method public void acceptHandover(android.net.Uri, int, android.telecom.PhoneAccountHandle); method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.ANSWER_PHONE_CALLS, android.Manifest.permission.MODIFY_PHONE_STATE}) public void acceptRingingCall(); method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.ANSWER_PHONE_CALLS, android.Manifest.permission.MODIFY_PHONE_STATE}) public void acceptRingingCall(int); + method @RequiresPermission(android.Manifest.permission.MANAGE_OWN_CALLS) public void addCall(@NonNull android.telecom.CallAttributes, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver, @NonNull android.telecom.CallEventCallback); method public void addNewIncomingCall(android.telecom.PhoneAccountHandle, android.os.Bundle); method public void addNewIncomingConference(@NonNull android.telecom.PhoneAccountHandle, @NonNull android.os.Bundle); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void cancelMissedCallsNotification(); diff --git a/telecomm/java/android/telecom/CallAttributes.aidl b/telecomm/java/android/telecom/CallAttributes.aidl new file mode 100644 index 0000000000000..19bada72190f3 --- /dev/null +++ b/telecomm/java/android/telecom/CallAttributes.aidl @@ -0,0 +1,22 @@ +/* + * Copyright 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; + +/** + * {@hide} + */ +parcelable CallAttributes; \ No newline at end of file diff --git a/telecomm/java/android/telecom/CallAttributes.java b/telecomm/java/android/telecom/CallAttributes.java new file mode 100644 index 0000000000000..6d879817b21dd --- /dev/null +++ b/telecomm/java/android/telecom/CallAttributes.java @@ -0,0 +1,344 @@ +/* + * 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 android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.Objects; + +/** + * CallAttributes represents a set of properties that define a new Call. Apps should build an + * instance of this class and use {@link TelecomManager#addCall} to start a new call with Telecom. + * + *

+ * Apps should first register a {@link PhoneAccount} via {@link TelecomManager#registerPhoneAccount} + * and use the same {@link PhoneAccountHandle} registered with Telecom when creating an + * instance of CallAttributes. + */ +public final class CallAttributes implements Parcelable { + + /** PhoneAccountHandle associated with the App managing calls **/ + private final PhoneAccountHandle mPhoneAccountHandle; + + /** Display name of the person on the other end of the call **/ + private final CharSequence mDisplayName; + + /** Address of the call. Note, this can be extended to a meeting link **/ + private final Uri mAddress; + + /** The direction (Outgoing/Incoming) of the new Call **/ + @Direction private final int mDirection; + + /** Information related to data being transmitted (voice, video, etc. ) **/ + @CallType private final int mCallType; + + /** Allows a package to opt into capabilities on the telecom side, on a per-call basis **/ + @CallCapability private final int mCallCapabilities; + + /** @hide **/ + public static final String CALL_CAPABILITIES_KEY = "TelecomCapabilities"; + + private CallAttributes(@NonNull PhoneAccountHandle phoneAccountHandle, + @NonNull CharSequence displayName, + @NonNull Uri address, + int direction, + int callType, + int callCapabilities) { + mPhoneAccountHandle = phoneAccountHandle; + mDisplayName = displayName; + mAddress = address; + mDirection = direction; + mCallType = callType; + mCallCapabilities = callCapabilities; + } + + /** @hide */ + @IntDef(value = {DIRECTION_INCOMING, DIRECTION_OUTGOING}) + public @interface Direction { + } + /** + * Indicates that the call is an incoming call. + */ + public static final int DIRECTION_INCOMING = 1; + /** + * Indicates that the call is an outgoing call. + */ + public static final int DIRECTION_OUTGOING = 2; + + /** @hide */ + @IntDef(value = {AUDIO_CALL, VIDEO_CALL}) + public @interface CallType { + } + /** + * Used when answering or dialing a call to indicate that the call does not have a video + * component + */ + public static final int AUDIO_CALL = 1; + /** + * Indicates video transmission is supported + */ + public static final int VIDEO_CALL = 2; + + /** @hide */ + @IntDef(value = {SUPPORTS_SET_INACTIVE, SUPPORTS_STREAM, SUPPORTS_TRANSFER}, flag = true) + public @interface CallCapability { + } + /** + * The call being created can be set to inactive (traditionally referred to as hold). This + * means that once a new call goes active, if the active call needs to be held in order to + * place or receive an incoming call, the active call will be placed on hold. otherwise, the + * active call may be disconnected. + */ + public static final int SUPPORTS_SET_INACTIVE = 1 << 1; + /** + * The call can be streamed from a root device to another device to continue the call without + * completely transferring it. + */ + public static final int SUPPORTS_STREAM = 1 << 2; + /** + * The call can be completely transferred from one endpoint to another + */ + public static final int SUPPORTS_TRANSFER = 1 << 3; + + /** + * Build an instance of {@link CallAttributes}. In order to build a valid instance, a + * {@link PhoneAccountHandle}, call {@link Direction}, display name, and {@link Uri} address + * are required. + * + *

+ * Note: Pass in the same {@link PhoneAccountHandle} that was used to register a + * {@link PhoneAccount} with Telecom. see {@link TelecomManager#registerPhoneAccount} + */ + public static final class Builder { + // required and final fields + private final PhoneAccountHandle mPhoneAccountHandle; + @Direction private final int mDirection; + private final CharSequence mDisplayName; + private final Uri mAddress; + // optional fields + @CallType private int mCallType = CallAttributes.AUDIO_CALL; + @CallCapability private int mCallCapabilities = SUPPORTS_SET_INACTIVE; + + /** + * Constructor for the CallAttributes.Builder class + * + * @param phoneAccountHandle that belongs to package registered with Telecom + * @param callDirection of the new call that will be added to Telecom + * @param displayName of the caller for incoming calls or initiating user for outgoing calls + * @param address of the caller for incoming calls or destination for outgoing calls + */ + public Builder(@NonNull PhoneAccountHandle phoneAccountHandle, + @Direction int callDirection, @NonNull CharSequence displayName, + @NonNull Uri address) { + if (!isInRange(DIRECTION_INCOMING, DIRECTION_OUTGOING, callDirection)) { + throw new IllegalArgumentException(TextUtils.formatSimple("CallDirection=[%d] is" + + " invalid. CallDirections value should be within [%d, %d]", + callDirection, DIRECTION_INCOMING, DIRECTION_OUTGOING)); + } + Objects.requireNonNull(phoneAccountHandle); + Objects.requireNonNull(displayName); + Objects.requireNonNull(address); + mPhoneAccountHandle = phoneAccountHandle; + mDirection = callDirection; + mDisplayName = displayName; + mAddress = address; + } + + /** + * @param callType see {@link CallType} for valid arguments + * @return Builder + */ + @NonNull + public Builder setCallType(@CallType int callType) { + if (!isInRange(AUDIO_CALL, VIDEO_CALL, callType)) { + throw new IllegalArgumentException(TextUtils.formatSimple("CallType=[%d] is" + + " invalid. CallTypes value should be within [%d, %d]", + callType, AUDIO_CALL, VIDEO_CALL)); + } + mCallType = callType; + return this; + } + + /** + * @param callCapabilities see {@link CallCapability} for valid arguments + * @return Builder + */ + @NonNull + public Builder setCallCapabilities(@CallCapability int callCapabilities) { + mCallCapabilities = callCapabilities; + return this; + } + + /** + * Build an instance of {@link CallAttributes} based on the last values passed to the + * setters or default values. + * + * @return an instance of {@link CallAttributes} + */ + @NonNull + public CallAttributes build() { + return new CallAttributes(mPhoneAccountHandle, mDisplayName, mAddress, mDirection, + mCallType, mCallCapabilities); + } + + /** @hide */ + private boolean isInRange(int floor, int ceiling, int value) { + return value >= floor && value <= ceiling; + } + } + + /** + * The {@link PhoneAccountHandle} that should be registered to Telecom to allow calls. The + * {@link PhoneAccountHandle} should be registered before creating a CallAttributes instance. + * + * @return the {@link PhoneAccountHandle} for this package that allows this call to be created + */ + @NonNull public PhoneAccountHandle getPhoneAccountHandle() { + return mPhoneAccountHandle; + } + + /** + * @return display name of the incoming caller or the person being called for an outgoing call + */ + @NonNull public CharSequence getDisplayName() { + return mDisplayName; + } + + /** + * @return address of the incoming caller + * or the address of the person being called for an outgoing call + */ + @NonNull public Uri getAddress() { + return mAddress; + } + + /** + * @return the direction of the new call. + */ + public @Direction int getDirection() { + return mDirection; + } + + /** + * @return Information related to data being transmitted (voice, video, etc. ) + */ + public @CallType int getCallType() { + return mCallType; + } + + /** + * @return The allowed capabilities of the new call + */ + public @CallCapability int getCallCapabilities() { + return mCallCapabilities; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@Nullable Parcel dest, int flags) { + dest.writeParcelable(mPhoneAccountHandle, flags); + dest.writeCharSequence(mDisplayName); + dest.writeParcelable(mAddress, flags); + dest.writeInt(mDirection); + dest.writeInt(mCallType); + dest.writeInt(mCallCapabilities); + } + + /** + * Responsible for creating CallAttribute objects for deserialized Parcels. + */ + public static final @android.annotation.NonNull + Parcelable.Creator CREATOR = + new Parcelable.Creator<>() { + @Override + public CallAttributes createFromParcel(Parcel source) { + return new CallAttributes(source.readParcelable(getClass().getClassLoader(), + android.telecom.PhoneAccountHandle.class), + source.readCharSequence(), + source.readParcelable(getClass().getClassLoader(), + android.net.Uri.class), + source.readInt(), + source.readInt(), + source.readInt()); + } + + @Override + public CallAttributes[] newArray(int size) { + return new CallAttributes[size]; + } + }; + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append("{ CallAttributes: [phoneAccountHandle: ") + .append(mPhoneAccountHandle) /* PhoneAccountHandle#toString handles PII */ + .append("], [contactName: ") + .append(Log.pii(mDisplayName)) + .append("], [address=") + .append(Log.pii(mAddress)) + .append("], [direction=") + .append(mDirection) + .append("], [callType=") + .append(mCallType) + .append("], [mCallCapabilities=") + .append(mCallCapabilities) + .append("] }"); + + return sb.toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + CallAttributes that = (CallAttributes) obj; + return this.mDirection == that.mDirection + && this.mCallType == that.mCallType + && this.mCallCapabilities == that.mCallCapabilities + && Objects.equals(this.mPhoneAccountHandle, that.mPhoneAccountHandle) + && Objects.equals(this.mAddress, that.mAddress) + && Objects.equals(this.mDisplayName, that.mDisplayName); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hash(mPhoneAccountHandle, mAddress, mDisplayName, + mDirection, mCallType, mCallCapabilities); + } +} diff --git a/telecomm/java/android/telecom/CallControl.aidl b/telecomm/java/android/telecom/CallControl.aidl new file mode 100644 index 0000000000000..0f780e612d5bc --- /dev/null +++ b/telecomm/java/android/telecom/CallControl.aidl @@ -0,0 +1,22 @@ +/* + * Copyright 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; + +/** + * {@hide} + */ +parcelable CallControl; diff --git a/telecomm/java/android/telecom/CallControl.java b/telecomm/java/android/telecom/CallControl.java new file mode 100644 index 0000000000000..3bda6f4db394f --- /dev/null +++ b/telecomm/java/android/telecom/CallControl.java @@ -0,0 +1,250 @@ +/* + * 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. + * + *

+ * 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 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 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 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 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 mClientCallback; + + CallControlResultReceiver(String method, Executor executor, + OutcomeReceiver 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); + } +} diff --git a/telecomm/java/android/telecom/CallEventCallback.java b/telecomm/java/android/telecom/CallEventCallback.java new file mode 100644 index 0000000000000..a26291f7d105e --- /dev/null +++ b/telecomm/java/android/telecom/CallEventCallback.java @@ -0,0 +1,103 @@ +/* + * 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 android.annotation.NonNull; + +import java.util.function.Consumer; + +/** + * CallEventCallback relays updates to a call from the Telecom framework. + * This can include operations which the app must implement on a Call due to the presence of other + * calls on the device, requests relayed from a Bluetooth device, or from another calling surface. + * + *

+ * CallEventCallbacks with {@link Consumer}s are transactional, meaning that a client must + * complete the {@link Consumer} via {@link Consumer#accept(Object)} in order to complete the + * CallEventCallback. If a CallEventCallback can be completed, the + * {@link Consumer#accept(Object)} should be called with {@link Boolean#TRUE}. Otherwise, + * {@link Consumer#accept(Object)} should be called with {@link Boolean#FALSE} to represent the + * CallEventCallback cannot be completed on the client side. + * + *

+ * Note: Each CallEventCallback has a timeout of 5000 milliseconds. Failing to complete the + * {@link Consumer} before the timeout will result in a failed transaction. + */ +public interface CallEventCallback { + /** + * Telecom is informing the client to set the call active + * + * @param wasCompleted The {@link Consumer} to be completed. If the client can set the call + * active on their end, the {@link Consumer#accept(Object)} should be + * called with {@link Boolean#TRUE}. Otherwise, + * {@link Consumer#accept(Object)} should be called with + * {@link Boolean#FALSE}. + */ + void onSetActive(@NonNull Consumer wasCompleted); + + /** + * Telecom is informing the client to set the call inactive. This is the same as holding a call + * for two endpoints but can be extended to setting a meeting inactive. + * + * @param wasCompleted The {@link Consumer} to be completed. If the client can set the call + * inactive on their end, the {@link Consumer#accept(Object)} should be + * called with {@link Boolean#TRUE}. Otherwise, + * {@link Consumer#accept(Object)} should be called with + * {@link Boolean#FALSE}. + */ + void onSetInactive(@NonNull Consumer wasCompleted); + + /** + * Telecom is informing the client to answer an incoming call and set it to active. + * + * @param videoState see {@link android.telecom.CallAttributes.CallType} for valid states + * @param wasCompleted The {@link Consumer} to be completed. If the client can answer the call + * on their end, {@link Consumer#accept(Object)} should be called with + * {@link Boolean#TRUE}. Otherwise, {@link Consumer#accept(Object)} should + * be called with {@link Boolean#FALSE}. + */ + void onAnswer(@android.telecom.CallAttributes.CallType int videoState, + @NonNull Consumer wasCompleted); + + /** + * Telecom is informing the client to reject the incoming call + * + * @param wasCompleted The {@link Consumer} to be completed. If the client can reject the + * incoming call, {@link Consumer#accept(Object)} should be called with + * {@link Boolean#TRUE}. Otherwise, {@link Consumer#accept(Object)} + * should be called with {@link Boolean#FALSE}. + */ + void onReject(@NonNull Consumer wasCompleted); + + /** + * Telecom is informing the client to disconnect the call + * + * @param wasCompleted The {@link Consumer} to be completed. If the client can disconnect the + * call on their end, {@link Consumer#accept(Object)} should be called with + * {@link Boolean#TRUE}. Otherwise, {@link Consumer#accept(Object)} + * should be called with {@link Boolean#FALSE}. + */ + void onDisconnect(@NonNull Consumer wasCompleted); + + /** + * update the client on the new {@link CallAudioState} + * + * @param callAudioState that is currently being used + */ + void onCallAudioStateChanged(@NonNull CallAudioState callAudioState); +} diff --git a/telecomm/java/android/telecom/CallException.aidl b/telecomm/java/android/telecom/CallException.aidl new file mode 100644 index 0000000000000..a16af121145df --- /dev/null +++ b/telecomm/java/android/telecom/CallException.aidl @@ -0,0 +1,22 @@ +/* + * Copyright 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; + +/** + * {@hide} + */ +parcelable CallException; \ No newline at end of file diff --git a/telecomm/java/android/telecom/CallException.java b/telecomm/java/android/telecom/CallException.java new file mode 100644 index 0000000000000..0b0de6bd16606 --- /dev/null +++ b/telecomm/java/android/telecom/CallException.java @@ -0,0 +1,160 @@ +/* + * 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 android.annotation.IntDef; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This class defines exceptions that can be thrown when using Telecom APIs with + * {@link android.os.OutcomeReceiver}s. Most of these exceptions are thrown when changing a call + * state with {@link CallControl}s or {@link CallEventCallback}s. + */ +public final class CallException extends RuntimeException implements Parcelable { + /** @hide **/ + public static final String TRANSACTION_EXCEPTION_KEY = "TelecomTransactionalExceptionKey"; + + /** + * The operation has failed due to an unknown or unspecified error. + */ + public static final int CODE_ERROR_UNKNOWN = 1; + + /** + * The operation has failed due to Telecom failing to hold the current active call for the + * call attempting to become the new active call. The client should end the current active call + * and re-try the failed operation. + */ + public static final int CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL = 2; + + /** + * The operation has failed because Telecom has already removed the call from the server side + * and destroyed all the objects associated with it. The client should re-add the call. + */ + public static final int CODE_CALL_IS_NOT_BEING_TRACKED = 3; + + /** + * The operation has failed because Telecom cannot set the requested call as the current active + * call. The client should end the current active call and re-try the operation. + */ + public static final int CODE_CALL_CANNOT_BE_SET_TO_ACTIVE = 4; + + /** + * The operation has failed because there is either no PhoneAccount registered with Telecom + * for the given operation, or the limit of calls has been reached. The client should end the + * current active call and re-try the failed operation. + */ + public static final int CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME = 5; + + /** + * The operation has failed because the operation failed to complete before the timeout + */ + public static final int CODE_OPERATION_TIMED_OUT = 6; + + private int mCode = CODE_ERROR_UNKNOWN; + private final String mMessage; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mMessage); + dest.writeInt(mCode); + } + + /** + * Responsible for creating CallAttribute objects for deserialized Parcels. + */ + public static final @android.annotation.NonNull + Parcelable.Creator CREATOR = new Parcelable.Creator<>() { + @Override + public CallException createFromParcel(Parcel source) { + return new CallException(source.readString8(), source.readInt()); + } + + @Override + public CallException[] newArray(int size) { + return new CallException[size]; + } + }; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = "CODE_ERROR_", value = { + CODE_ERROR_UNKNOWN, + CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL, + CODE_CALL_IS_NOT_BEING_TRACKED, + CODE_CALL_CANNOT_BE_SET_TO_ACTIVE, + CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME, + CODE_OPERATION_TIMED_OUT + }) + public @interface CallErrorCode { + } + + /** + * Constructor for a new CallException when only message can be specified. + * {@code CODE_ERROR_UNKNOWN} will be default code returned when calling {@code getCode} + * + * @param message related to why the exception was created + */ + public CallException(@Nullable String message) { + super(getMessage(message, CODE_ERROR_UNKNOWN)); + mMessage = message; + } + + /** + * Constructor for a new CallException that has a defined error code in this class + * + * @param message related to why the exception was created + * @param code defined above that caused this exception to be created + */ + public CallException(@Nullable String message, @CallErrorCode int code) { + super(getMessage(message, code)); + mCode = code; + mMessage = message; + } + + /** + * @return one of the error codes defined in this class that was passed into the constructor + */ + public @CallErrorCode int getCode() { + return mCode; + } + + private static String getMessage(String message, int code) { + StringBuilder builder; + if (!TextUtils.isEmpty(message)) { + builder = new StringBuilder(message); + builder.append(" (code: "); + builder.append(code); + builder.append(")"); + return builder.toString(); + } else { + return "code: " + code; + } + } +} diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java index ec18c6a696b84..047ab3ae04549 100644 --- a/telecomm/java/android/telecom/PhoneAccount.java +++ b/telecomm/java/android/telecom/PhoneAccount.java @@ -418,7 +418,26 @@ public final class PhoneAccount implements Parcelable { */ public static final int CAPABILITY_VOICE_CALLING_AVAILABLE = 0x20000; - /* NEXT CAPABILITY: 0x40000 */ + + /** + * Flag indicating that this {@link PhoneAccount} supports the use TelecomManager APIs that + * utilize {@link android.os.OutcomeReceiver}s or {@link java.util.function.Consumer}s. + * Be aware, if this capability is set, {@link #CAPABILITY_SELF_MANAGED} will be amended by + * Telecom when this {@link PhoneAccount} is registered via + * {@link TelecomManager#registerPhoneAccount(PhoneAccount)}. + * + *

+ * {@link android.os.OutcomeReceiver}s and {@link java.util.function.Consumer}s represent + * transactional operations because the operation can succeed or fail. An app wishing to use + * transactional operations should define behavior for a successful and failed TelecomManager + * API call. + * + * @see #CAPABILITY_SELF_MANAGED + * @see #getCapabilities + */ + public static final int CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS = 0x40000; + + /* NEXT CAPABILITY: [0x80000, 0x100000, 0x200000] */ /** * URI scheme for telephone number URIs. diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index af37ed583438f..7c86a75ac0230 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -18,6 +18,7 @@ import static android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE; import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM; import android.Manifest; +import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -38,6 +39,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.IBinder; +import android.os.OutcomeReceiver; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; @@ -49,6 +51,8 @@ import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.GuardedBy; +import com.android.internal.telecom.ClientTransactionalServiceRepository; +import com.android.internal.telecom.ClientTransactionalServiceWrapper; import com.android.internal.telecom.ITelecomService; import java.lang.annotation.Retention; @@ -1056,6 +1060,14 @@ public class TelecomManager { private final ITelecomService mTelecomServiceOverride; + /** @hide **/ + private final ClientTransactionalServiceRepository mTransactionalServiceRepository = + new ClientTransactionalServiceRepository(); + /** @hide **/ + public static final int TELECOM_TRANSACTION_SUCCESS = 0; + /** @hide **/ + public static final String TRANSACTION_CALL_ID_KEY = "TelecomCallId"; + /** * @hide */ @@ -2639,6 +2651,92 @@ public class TelecomManager { } } + /** + * Adds a new call with the specified {@link CallAttributes} to the telecom service. This method + * can be used to add both incoming and outgoing calls. + * + *

+ * The difference between this API call and {@link TelecomManager#placeCall(Uri, Bundle)} or + * {@link TelecomManager#addNewIncomingCall(PhoneAccountHandle, Bundle)} is that this API + * will asynchronously provide an update on whether the new call was added successfully via + * an {@link OutcomeReceiver}. Additionally, callbacks will run on the executor thread that was + * passed in. + * + *

+ * Note: Only packages that register with + * {@link PhoneAccount#CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS} + * can utilize this API. {@link PhoneAccount}s that set the capabilities + * {@link PhoneAccount#CAPABILITY_SIM_SUBSCRIPTION}, + * {@link PhoneAccount#CAPABILITY_CALL_PROVIDER}, + * {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} + * are not supported and will cause an exception to be thrown. + * + *

+ * Usage example: + *

+     *
+     *  // An app should first define their own construct of a Call that overrides all the
+     *  // {@link CallEventCallback}s
+     *  private class MyVoipCall implements CallEventCallback {
+     *    // override all the {@link CallEventCallback}s
+     *  }
+     *
+     * PhoneAccountHandle handle = new PhoneAccountHandle(
+     *                          new ComponentName("com.example.voip.app",
+     *                                            "com.example.voip.app.NewCallActivity"), "123");
+     *
+     * CallAttributes callAttributes = new CallAttributes.Builder(handle,
+     *                                             CallAttributes.DIRECTION_OUTGOING,
+     *                                            "John Smith", Uri.fromParts("tel", "123", null))
+     *                                            .build();
+     *
+     * telecomManager.addCall(callAttributes, Runnable::run, new OutcomeReceiver() {
+     *                              public void onResult(CallControl callControl) {
+     *                                 // The call has been added successfully
+     *                              }
+     *                           }, new MyVoipCall());
+     * 
+ * + * @param callAttributes attributes of the new call (incoming or outgoing, address, etc. ) + * @param executor thread to run background CallEventCallback updates on + * @param pendingControl OutcomeReceiver that receives the result of addCall transaction + * @param callEventCallback object that overrides CallEventCallback + */ + @RequiresPermission(android.Manifest.permission.MANAGE_OWN_CALLS) + @SuppressLint("SamShouldBeLast") + public void addCall(@NonNull CallAttributes callAttributes, + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver pendingControl, + @NonNull CallEventCallback callEventCallback) { + Objects.requireNonNull(callAttributes); + Objects.requireNonNull(executor); + Objects.requireNonNull(pendingControl); + Objects.requireNonNull(callEventCallback); + + ITelecomService service = getTelecomService(); + if (service != null) { + try { + // create or add the new call to a service wrapper w/ the same phoneAccountHandle + ClientTransactionalServiceWrapper transactionalServiceWrapper = + mTransactionalServiceRepository.addNewCallForTransactionalServiceWrapper( + callAttributes.getPhoneAccountHandle()); + + // couple all the args passed by the client + String newCallId = transactionalServiceWrapper.trackCall(callAttributes, executor, + pendingControl, callEventCallback); + + // send args to server to process new call + service.addCall(callAttributes, transactionalServiceWrapper.getCallEventCallback(), + newCallId, mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException addCall: " + e); + e.rethrowFromSystemServer(); + } + } else { + throw new IllegalStateException("Telecom service is not present"); + } + } + /** * Handles {@link Intent#ACTION_CALL} intents trampolined from UserCallActivity. * @param intent The {@link Intent#ACTION_CALL} intent to handle. diff --git a/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceRepository.java b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceRepository.java new file mode 100644 index 0000000000000..2eebbdb35fbbe --- /dev/null +++ b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceRepository.java @@ -0,0 +1,90 @@ +/* + * 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 com.android.internal.telecom; + +import android.telecom.PhoneAccountHandle; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @hide + */ +public class ClientTransactionalServiceRepository { + + private static final Map LOOKUP_TABLE = + new ConcurrentHashMap<>(); + + /** + * creates a new {@link ClientTransactionalServiceWrapper} if this is the first call being + * tracked for a particular package Or adds a new call for an existing + * {@link ClientTransactionalServiceWrapper} + * + * @param phoneAccountHandle for a particular package requesting to create a call + * @return the {@link ClientTransactionalServiceWrapper} that is tied tot the PhoneAccountHandle + */ + public ClientTransactionalServiceWrapper addNewCallForTransactionalServiceWrapper( + PhoneAccountHandle phoneAccountHandle) { + + ClientTransactionalServiceWrapper service = null; + if (!hasExistingServiceWrapper(phoneAccountHandle)) { + service = new ClientTransactionalServiceWrapper(phoneAccountHandle, this); + } else { + service = getTransactionalServiceWrapper(phoneAccountHandle); + } + + LOOKUP_TABLE.put(phoneAccountHandle, service); + + return service; + } + + private ClientTransactionalServiceWrapper getTransactionalServiceWrapper( + PhoneAccountHandle pah) { + return LOOKUP_TABLE.get(pah); + } + + private boolean hasExistingServiceWrapper(PhoneAccountHandle pah) { + return LOOKUP_TABLE.containsKey(pah); + } + + /** + * @param pah that is tied to a particular package with potential tracked calls + * @return if the {@link ClientTransactionalServiceWrapper} was successfully removed + */ + public boolean removeServiceWrapper(PhoneAccountHandle pah) { + if (!hasExistingServiceWrapper(pah)) { + return false; + } + LOOKUP_TABLE.remove(pah); + return true; + } + + /** + * @param pah that is tied to a particular package with potential tracked calls + * @param callId of the TransactionalCall that you want to remove + * @return if the call was successfully removed from the service wrapper + */ + public boolean removeCallFromServiceWrapper(PhoneAccountHandle pah, String callId) { + if (!hasExistingServiceWrapper(pah)) { + return false; + } + ClientTransactionalServiceWrapper service = LOOKUP_TABLE.get(pah); + service.untrackCall(callId); + return true; + } + +} diff --git a/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java new file mode 100644 index 0000000000000..682dba16dca86 --- /dev/null +++ b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java @@ -0,0 +1,263 @@ +/* + * 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 com.android.internal.telecom; + +import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS; + +import android.os.Binder; +import android.os.OutcomeReceiver; +import android.os.ResultReceiver; +import android.telecom.CallAttributes; +import android.telecom.CallAudioState; +import android.telecom.CallControl; +import android.telecom.CallEventCallback; +import android.telecom.CallException; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import android.util.Log; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * wraps {@link CallEventCallback} and {@link CallControl} on a + * per-{@link android.telecom.PhoneAccountHandle} basis to track ongoing calls. + * + * @hide + */ +public class ClientTransactionalServiceWrapper { + + private static final String TAG = ClientTransactionalServiceWrapper.class.getSimpleName(); + private final PhoneAccountHandle mPhoneAccountHandle; + private final ClientTransactionalServiceRepository mRepository; + private final ConcurrentHashMap mCallIdToTransactionalCall = + new ConcurrentHashMap<>(); + private static final String EXECUTOR_FAIL_MSG = + "Telecom hit an exception while handling a CallEventCallback on an executor: "; + + public ClientTransactionalServiceWrapper(PhoneAccountHandle handle, + ClientTransactionalServiceRepository repo) { + mPhoneAccountHandle = handle; + mRepository = repo; + } + + /** + * remove the given call from the class HashMap + * + * @param callId that is tied to TransactionalCall object + */ + public void untrackCall(String callId) { + Log.i(TAG, TextUtils.formatSimple("removeCall: with id=[%s]", callId)); + if (mCallIdToTransactionalCall.containsKey(callId)) { + // remove the call from the hashmap + TransactionalCall call = mCallIdToTransactionalCall.remove(callId); + // null out interface to avoid memory leaks + CallControl control = call.getCallControl(); + if (control != null) { + call.setCallControl(null); + } + } + // possibly cleanup service wrapper if there are no more calls + if (mCallIdToTransactionalCall.size() == 0) { + mRepository.removeServiceWrapper(mPhoneAccountHandle); + } + } + + /** + * start tracking a newly created call for a particular package + * + * @param callAttributes of the new call + * @param executor to run callbacks on + * @param pendingControl that allows telecom to call into the client + * @param callback that overrides the CallEventCallback + * @return the callId of the newly created call + */ + public String trackCall(CallAttributes callAttributes, Executor executor, + OutcomeReceiver pendingControl, + CallEventCallback callback) { + // generate a new id for this new call + String newCallId = UUID.randomUUID().toString(); + + // couple the objects passed from the client side + mCallIdToTransactionalCall.put(newCallId, new TransactionalCall(newCallId, callAttributes, + executor, pendingControl, callback)); + + return newCallId; + } + + public ICallEventCallback getCallEventCallback() { + return mCallEventCallback; + } + + /** + * Consumers that is to be completed by the client and the result relayed back to telecom server + * side via a {@link ResultReceiver}. see com.android.server.telecom.TransactionalServiceWrapper + * for how the response is handled. + */ + private class ReceiverWrapper implements Consumer { + private final ResultReceiver mRepeaterReceiver; + + ReceiverWrapper(ResultReceiver resultReceiver) { + mRepeaterReceiver = resultReceiver; + } + + @Override + public void accept(Boolean clientCompletedCallbackSuccessfully) { + if (clientCompletedCallbackSuccessfully) { + mRepeaterReceiver.send(TELECOM_TRANSACTION_SUCCESS, null); + } else { + mRepeaterReceiver.send(CallException.CODE_ERROR_UNKNOWN, null); + } + } + + @Override + public Consumer andThen(Consumer after) { + return Consumer.super.andThen(after); + } + } + + private final ICallEventCallback mCallEventCallback = new ICallEventCallback.Stub() { + + private static final String ON_SET_ACTIVE = "onSetActive"; + private static final String ON_SET_INACTIVE = "onSetInactive"; + private static final String ON_ANSWER = "onAnswer"; + private static final String ON_REJECT = "onReject"; + private static final String ON_DISCONNECT = "onDisconnect"; + + private void handleCallEventCallback(String action, String callId, int code, + ResultReceiver ackResultReceiver) { + Log.i(TAG, TextUtils.formatSimple("hCEC: id=[%s], action=[%s]", callId, action)); + // lookup the callEventCallback associated with the particular call + TransactionalCall call = mCallIdToTransactionalCall.get(callId); + + if (call != null) { + // Get the CallEventCallback interface + CallEventCallback callback = call.getCallEventCallback(); + // Get Receiver to wait on client ack + ReceiverWrapper outcomeReceiverWrapper = new ReceiverWrapper(ackResultReceiver); + + // wait for the client to complete the CallEventCallback + final long identity = Binder.clearCallingIdentity(); + try { + call.getExecutor().execute(() -> { + switch (action) { + case ON_SET_ACTIVE: + callback.onSetActive(outcomeReceiverWrapper); + break; + case ON_SET_INACTIVE: + callback.onSetInactive(outcomeReceiverWrapper); + break; + case ON_REJECT: + callback.onReject(outcomeReceiverWrapper); + untrackCall(callId); + break; + case ON_DISCONNECT: + callback.onDisconnect(outcomeReceiverWrapper); + untrackCall(callId); + break; + case ON_ANSWER: + callback.onAnswer(code, outcomeReceiverWrapper); + break; + } + }); + } catch (Exception e) { + Log.e(TAG, EXECUTOR_FAIL_MSG + e); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + @Override + public void onAddCallControl(String callId, int resultCode, ICallControl callControl, + CallException transactionalException) { + Log.i(TAG, TextUtils.formatSimple("oACC: id=[%s], code=[%d]", callId, resultCode)); + TransactionalCall call = mCallIdToTransactionalCall.get(callId); + + if (call != null) { + OutcomeReceiver pendingControl = + call.getPendingControl(); + + if (resultCode == TELECOM_TRANSACTION_SUCCESS) { + + // create the interface object that the client will interact with + CallControl control = new CallControl(callId, callControl, mRepository, + mPhoneAccountHandle); + // give the client the object via the OR that was passed into addCall + pendingControl.onResult(control); + + // store for later reference + call.setCallControl(control); + } else { + pendingControl.onError(transactionalException); + mCallIdToTransactionalCall.remove(callId); + } + + } else { + untrackCall(callId); + Log.e(TAG, "oACC: TransactionalCall object not found for call w/ id=" + callId); + } + } + + @Override + public void onSetActive(String callId, ResultReceiver resultReceiver) { + handleCallEventCallback(ON_SET_ACTIVE, callId, 0, resultReceiver); + } + + + @Override + public void onSetInactive(String callId, ResultReceiver resultReceiver) { + handleCallEventCallback(ON_SET_INACTIVE, callId, 0, resultReceiver); + } + + @Override + public void onAnswer(String callId, int videoState, ResultReceiver resultReceiver) { + handleCallEventCallback(ON_ANSWER, callId, videoState, resultReceiver); + } + + @Override + public void onReject(String callId, ResultReceiver resultReceiver) { + handleCallEventCallback(ON_REJECT, callId, 0, resultReceiver); + } + + @Override + public void onDisconnect(String callId, ResultReceiver resultReceiver) { + handleCallEventCallback(ON_DISCONNECT, callId, 0, resultReceiver); + } + + @Override + public void onCallAudioStateChanged(String callId, CallAudioState callAudioState) { + Log.i(TAG, TextUtils.formatSimple("onCallAudioStateChanged: callId=[%s]", callId)); + // lookup the callEventCallback associated with the particular call + TransactionalCall call = mCallIdToTransactionalCall.get(callId); + if (call != null) { + CallEventCallback callback = call.getCallEventCallback(); + Executor executor = call.getExecutor(); + executor.execute(() -> { + callback.onCallAudioStateChanged(callAudioState); + }); + } + } + + @Override + public void removeCallFromTransactionalServiceWrapper(String callId) { + untrackCall(callId); + } + }; +} diff --git a/telecomm/java/com/android/internal/telecom/ICallControl.aidl b/telecomm/java/com/android/internal/telecom/ICallControl.aidl new file mode 100644 index 0000000000000..bf68c5e3b98aa --- /dev/null +++ b/telecomm/java/com/android/internal/telecom/ICallControl.aidl @@ -0,0 +1,31 @@ +/* + * 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 com.android.internal.telecom; + +import android.telecom.CallControl; +import android.telecom.DisconnectCause; +import android.os.ResultReceiver; + +/** + * {@hide} + */ +oneway interface ICallControl { + void setActive(String callId, in ResultReceiver callback); + void setInactive(String callId, in ResultReceiver callback); + void disconnect(String callId, in DisconnectCause disconnectCause, in ResultReceiver callback); + void rejectCall(String callId, in ResultReceiver callback); +} \ No newline at end of file diff --git a/telecomm/java/com/android/internal/telecom/ICallEventCallback.aidl b/telecomm/java/com/android/internal/telecom/ICallEventCallback.aidl new file mode 100644 index 0000000000000..7f5825aba8aa3 --- /dev/null +++ b/telecomm/java/com/android/internal/telecom/ICallEventCallback.aidl @@ -0,0 +1,40 @@ +/* + * 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 com.android.internal.telecom; + +import android.telecom.CallControl; +import com.android.internal.telecom.ICallControl; +import android.os.ResultReceiver; +import android.telecom.CallAudioState; +import android.telecom.CallException; + +/** + * {@hide} + */ +oneway interface ICallEventCallback { + // publicly exposed. Client should override + void onAddCallControl(String callId, int resultCode, in ICallControl callControl, + in CallException exception); + void onSetActive(String callId, in ResultReceiver callback); + void onSetInactive(String callId, in ResultReceiver callback); + void onAnswer(String callId, int videoState, in ResultReceiver callback); + void onReject(String callId, in ResultReceiver callback); + void onDisconnect(String callId, in ResultReceiver callback); + void onCallAudioStateChanged(String callId, in CallAudioState callAudioState); + // hidden methods that help with cleanup + void removeCallFromTransactionalServiceWrapper(String callId); +} \ No newline at end of file diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl index f1a6dd1d769d0..fdcb9749c38e1 100644 --- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl +++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl @@ -25,6 +25,8 @@ import android.os.Bundle; import android.os.UserHandle; import android.telecom.PhoneAccount; import android.content.pm.ParceledListSlice; +import android.telecom.CallAttributes; +import com.android.internal.telecom.ICallEventCallback; /** * Interface used to interact with Telecom. Mostly this is used by TelephonyManager for passing @@ -391,4 +393,10 @@ interface ITelecomService { */ boolean isInSelfManagedCall(String packageName, in UserHandle userHandle, String callingPackage); + + /** + * @see TelecomServiceImpl#addCall + */ + void addCall(in CallAttributes callAttributes, in ICallEventCallback callback, String callId, + String callingPackage); } diff --git a/telecomm/java/com/android/internal/telecom/TransactionalCall.java b/telecomm/java/com/android/internal/telecom/TransactionalCall.java new file mode 100644 index 0000000000000..d9c8210f76040 --- /dev/null +++ b/telecomm/java/com/android/internal/telecom/TransactionalCall.java @@ -0,0 +1,76 @@ +/* + * 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 com.android.internal.telecom; + +import android.os.OutcomeReceiver; +import android.telecom.CallAttributes; +import android.telecom.CallControl; +import android.telecom.CallEventCallback; +import android.telecom.CallException; + +import java.util.concurrent.Executor; + +/** + * @hide + */ +public class TransactionalCall { + + private final String mCallId; + private final CallAttributes mCallAttributes; + private final Executor mExecutor; + private final OutcomeReceiver mPendingControl; + private final CallEventCallback mCallEventCallback; + private CallControl mCallControl; + + public TransactionalCall(String callId, CallAttributes callAttributes, + Executor executor, OutcomeReceiver pendingControl, + CallEventCallback callEventCallback) { + mCallId = callId; + mCallAttributes = callAttributes; + mExecutor = executor; + mPendingControl = pendingControl; + mCallEventCallback = callEventCallback; + } + + public void setCallControl(CallControl callControl) { + mCallControl = callControl; + } + + public CallControl getCallControl() { + return mCallControl; + } + + public String getCallId() { + return mCallId; + } + + public CallAttributes getCallAttributes() { + return mCallAttributes; + } + + public Executor getExecutor() { + return mExecutor; + } + + public OutcomeReceiver getPendingControl() { + return mPendingControl; + } + + public CallEventCallback getCallEventCallback() { + return mCallEventCallback; + } +}