Platform Telecom Transactional APIs

Telecom Transactional APIs are defined as APIs that leverage
android.os.OutcomeReceivers.  The receivers are to be completed by
Telecom for CallControl opertaions and the Client for CallEventCallback
operations.  Doing so ensures the client and telecom are in sync with
call states.  Also, these APIs are more lightweight than the
ConnectionService way of starting a self-managed call.

bug: 249779561
Test: atest android.telecom.cts.TransactionalApisTest

Change-Id: Icb09b2874d599a40afca8b7e960b14ca1bca606d
diff --git a/telecomm/java/android/telecom/CallAttributes.aidl b/telecomm/java/android/telecom/CallAttributes.aidl
new file mode 100644
index 0000000..19bada7
--- /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 0000000..6d87981
--- /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.
+ *
+ * <p>
+ * 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.
+     *
+     * <p>
+     * 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<CallAttributes> 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 0000000..0f780e6
--- /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 0000000..3bda6f4
--- /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.
+ *
+ * <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);
+    }
+}
diff --git a/telecomm/java/android/telecom/CallEventCallback.java b/telecomm/java/android/telecom/CallEventCallback.java
new file mode 100644
index 0000000..a26291f
--- /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.
+ *
+ * <p>
+ * 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.
+ *
+ * <p>
+ * 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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 0000000..a16af12
--- /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 0000000..0b0de6b
--- /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<CallException> 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 ec18c6a..047ab3a 100644
--- a/telecomm/java/android/telecom/PhoneAccount.java
+++ b/telecomm/java/android/telecom/PhoneAccount.java
@@ -418,7 +418,26 @@
      */
     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)}.
+     *
+     * <p>
+     * {@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 af37ed5..7c86a75a 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -18,6 +18,7 @@
 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.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.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 @@
 
     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
      */
@@ -2640,6 +2652,92 @@
     }
 
     /**
+     * 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.
+     *
+     * <p>
+     * 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.
+     *
+     * <p>
+     * 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.
+     *
+     * <p>
+     * Usage example:
+     * <pre>
+     *
+     *  // 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());
+     * </pre>
+     *
+     * @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<CallControl, CallException> 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.
      * @param callingPackageProxy The original package that called this before it was trampolined.