[go: nahoru, domu]

Resolve call extension type in Jetpack ICS

With supporting the new VOIP app actions being added, the Jetpack layer
needs to be able to resolve the extension type for the call before
initiating capability exchange. If the v2 APIs are being used (U/V) or
the backwards compat library is being leveraged (on pre-U devices), we
need to ensure that CAPABILITY_EXCHANGE is the resolved extension type.
For apps that are using the v1.5 Connection Service (AUTO), we need to
resolve the extension type as EXTRAS when the call extras contains
EXTRA_VOIP_API_VERSION. If none of these conditions apply, the phone
account will be checked to see if it supports transactional ops. If the
caller does not have the required permissions, the extension type is
resolved as UNKOWN (no capability exchange). If the phone account
doesn't have the required capability, we can simply resolve the
extension as not supporting capability exchange (NONE).

Fixes: 297438037
Test: atest InCallServiceCompat (on pre-U, U, V devices)
Change-Id: I55aa958e75c28029747a52cc5a8537761a0de430
diff --git a/core/core-telecom/src/androidTest/AndroidManifest.xml b/core/core-telecom/src/androidTest/AndroidManifest.xml
index 6e24bd3..4d94ecb2 100644
--- a/core/core-telecom/src/androidTest/AndroidManifest.xml
+++ b/core/core-telecom/src/androidTest/AndroidManifest.xml
@@ -16,6 +16,7 @@
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
     <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
+    <uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
 
     <application>
         <service
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2ECallExtensionExtrasTests.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2ECallExtensionExtrasTests.kt
new file mode 100644
index 0000000..458a920
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/E2ECallExtensionExtrasTests.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2023 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 androidx.core.telecom.test
+
+import android.Manifest
+import android.os.Build
+import android.telecom.Call
+import android.telecom.DisconnectCause
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallAttributesCompat
+import androidx.core.telecom.CallsManager
+import androidx.core.telecom.internal.InCallServiceCompat
+import androidx.core.telecom.internal.utils.Utils
+import androidx.core.telecom.test.utils.BaseTelecomTest
+import androidx.core.telecom.test.utils.TestUtils
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.rule.GrantPermissionRule
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * This test class helps verify the E2E behavior for calls added via Jetpack to ensure that the
+ * call details contain the appropriate extension extras that define the support for capability
+ * exchange between the VOIP app and ICS.
+ *
+ * Note: Currently, this test only verifies the presence of [CallsManager.PROPERTY_IS_TRANSACTIONAL]
+ * (only in V) in the call properties, if the phone account supports transactional ops (U+ devices),
+ * or if the [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] key is present in the call
+ * extras (pre-U devices). In the future, this will be expanded to be provide more robust testing
+ * to verify binder functionality as well as supporting the case for auto
+ * ([CallsManager.EXTRA_VOIP_API_VERSION]).
+ */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@RequiresApi(Build.VERSION_CODES.O)
+@RunWith(AndroidJUnit4::class)
+class E2ECallExtensionExtrasTests : BaseTelecomTest() {
+    private lateinit var inCallServiceCompat: InCallServiceCompat
+
+    /**
+     * Grant READ_PHONE_NUMBERS permission as part of testing
+     * [InCallServiceCompat#resolveCallExtensionsType].
+     */
+    @get:Rule
+    val readPhoneNumbersRule: GrantPermissionRule =
+        GrantPermissionRule.grant(Manifest.permission.READ_PHONE_NUMBERS)!!
+
+    @Before
+    fun setUp() {
+        Utils.resetUtils()
+        inCallServiceCompat = InCallServiceCompat(mContext)
+    }
+
+    @After
+    fun onDestroy() {
+        Utils.resetUtils()
+    }
+
+    /***********************************************************************************************
+     *                           V2 APIs (Android U and above) tests
+     *********************************************************************************************/
+
+    /**
+     * For U+ devices using the v2 APIs, assert that the incoming call details either support
+     * the [CallsManager.PROPERTY_IS_TRANSACTIONAL] property (V) or the phone account supports
+     * transactional operations (U+).
+     */
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testCapabilityExchangeIncoming_V2() {
+        setUpV2Test()
+        addAndVerifyCallExtensionTypeE2E(TestUtils.INCOMING_CALL_ATTRIBUTES)
+    }
+
+    /**
+     * For U+ devices using the v2 APIs, assert that the outgoing call details either support
+     * the [CallsManager.PROPERTY_IS_TRANSACTIONAL] property (V) or the phone account supports
+     * transactional operations (U+).
+     */
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testCapabilityExchangeOutgoing_V2() {
+        setUpV2Test()
+        addAndVerifyCallExtensionTypeE2E(TestUtils.OUTGOING_CALL_ATTRIBUTES)
+    }
+
+    /***********************************************************************************************
+     *                           Backwards Compatibility Layer tests
+     *********************************************************************************************/
+
+    /**
+     * For pre-U devices using the backwards compatibility library, assert that the incoming call
+     * details contain the [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] key
+     */
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testCapabilityExchangeIncoming_BackwardsCompat() {
+        setUpBackwardsCompatTest()
+        addAndVerifyCallExtensionTypeE2E(
+            TestUtils.INCOMING_CALL_ATTRIBUTES,
+            waitForCallDetailExtras = true
+        )
+    }
+
+    /**
+     * For pre-U devices using the backwards compatibility library, assert that the outgoing call
+     * details contain the [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] key
+     */
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testCapabilityExchangeOutgoing_BackwardsCompat() {
+        setUpBackwardsCompatTest()
+        addAndVerifyCallExtensionTypeE2E(
+            TestUtils.OUTGOING_CALL_ATTRIBUTES,
+            waitForCallDetailExtras = true
+        )
+    }
+
+    /***********************************************************************************************
+     *                           Helpers
+     *********************************************************************************************/
+
+    /**
+     * Helper to add a call via CallsManager#addCall and block (if needed) until the connection
+     * extras are propagated into the call details.
+     *
+     * @param callAttributesCompat for the call.
+     * @param waitForCallDetailExtras used for waiting on the call details extras to be non-empty.
+     */
+    private fun addAndVerifyCallExtensionTypeE2E(
+        callAttributesCompat: CallAttributesCompat,
+        waitForCallDetailExtras: Boolean = false
+    ) {
+        runBlocking {
+            assertWithinTimeout_addCall(callAttributesCompat) {
+                launch {
+                    val call = TestUtils.waitOnInCallServiceToReachXCalls(1)
+                    Assert.assertNotNull("The returned Call object is <NULL>", call!!)
+
+                    // Enforce waiting logic to ensure that the call details extras are populated.
+                    if (waitForCallDetailExtras) {
+                        TestUtils.waitOnCallExtras(call)
+                    }
+
+                    // Assert the call extra or call property from the details
+                    assertCallExtraOrProperty(call)
+                    // Always send disconnect signal if possible.
+                    assertTrue(disconnect(DisconnectCause(DisconnectCause.LOCAL)))
+                }
+            }
+        }
+    }
+
+    /**
+     * Helper to assert the call extra or property set on the call coming from Telecom.
+     */
+    private fun assertCallExtraOrProperty(call: Call) {
+        // Call details should be present at this point
+        val callDetails = call.details!!
+        if (TestUtils.buildIsAtLeastU()) {
+            if (TestUtils.buildIsAtLeastV()) {
+                assertTrue(callDetails.hasProperty(CallsManager.PROPERTY_IS_TRANSACTIONAL))
+            } else if (Utils.hasPlatformV2Apis()) {
+                // We need to check the phone account, which requires accessing TelecomManager.
+                // Directly resolving the extension type via resolveCallExtensionsType() will
+                // provide that functionality so no need to rewrite it here.
+                assertEquals(
+                    inCallServiceCompat.resolveCallExtensionsType(call),
+                    InCallServiceCompat.CAPABILITY_EXCHANGE)
+            }
+        } else {
+            val containsBackwardsCompatKey = callDetails.extras != null && callDetails.extras
+                .containsKey(CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED)
+            assertTrue(containsBackwardsCompatKey)
+        }
+    }
+}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/InCallServiceCompatTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/InCallServiceCompatTest.kt
new file mode 100644
index 0000000..94f3a2e
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/InCallServiceCompatTest.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2023 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 androidx.core.telecom.test
+
+import android.Manifest
+import android.os.Build
+import android.telecom.Call
+import android.telecom.DisconnectCause
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.CallAttributesCompat
+import androidx.core.telecom.CallsManager
+import androidx.core.telecom.internal.InCallServiceCompat
+import androidx.core.telecom.internal.utils.Utils
+import androidx.core.telecom.test.utils.BaseTelecomTest
+import androidx.core.telecom.test.utils.TestUtils
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.rule.GrantPermissionRule
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * This test class verifies the [InCallServiceCompat] functionality around resolving the call
+ * extension type in order to determine the supported extensions between the VOIP app and the
+ * associated InCallServices. This test constructs calls via TelecomManager and modifies the call
+ * details (if required) to test each scenario. This is explained in more detail at the test level
+ * for each of the applicable cases below.
+ *
+ * Note: [Call] is package-private so we still need to leverage Telecom to create calls on our
+ * behalf for testing. The call properties and extras fields aren't mutable so we need to ensure
+ * that we wait for them to become available before accessing them.
+ */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@RequiresApi(Build.VERSION_CODES.O)
+@RunWith(AndroidJUnit4::class)
+class InCallServiceCompatTest : BaseTelecomTest() {
+    private lateinit var inCallServiceCompat: InCallServiceCompat
+
+    /**
+     * Grant READ_PHONE_NUMBERS permission as part of testing
+     * [InCallServiceCompat#resolveCallExtensionsType].
+     */
+    @get:Rule
+    val readPhoneNumbersRule: GrantPermissionRule =
+        GrantPermissionRule.grant(Manifest.permission.READ_PHONE_NUMBERS)!!
+
+    companion object {
+        /**
+         * Logging for within the test class.
+         */
+        internal val TAG = InCallServiceCompatTest::class.simpleName
+    }
+
+    @Before
+    fun setUp() {
+        Utils.resetUtils()
+        inCallServiceCompat = InCallServiceCompat(mContext)
+    }
+
+    @After
+    fun onDestroy() {
+        Utils.resetUtils()
+    }
+
+    /**
+     * Assert that EXTRAS is the extension type for calls made using the V1.5 ConnectionService +
+     * Extensions Library (Auto). The call should have the [CallsManager.EXTRA_VOIP_API_VERSION]
+     * defined in the extras.
+     *
+     * The contents of the call detail extras need to be modified to test calls using the V1.5
+     * ConnectionService + Extensions library (until E2E testing can be supported for it). This
+     * requires us to manually insert the [CallsManager.EXTRA_VOIP_API_VERSION] key into the bundle.
+     */
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testResolveCallExtension_Extra() {
+        setUpBackwardsCompatTest()
+        val voipApiExtra = Pair(CallsManager.EXTRA_VOIP_API_VERSION, true)
+        addAndVerifyCallExtensionType(
+            TestUtils.OUTGOING_CALL_ATTRIBUTES,
+            InCallServiceCompat.EXTRAS,
+            extraToInclude = voipApiExtra)
+    }
+
+    /**
+     * Assert that CAPABILITY_EXCHANGE is the extension type for calls that either have the
+     * [CallsManager.PROPERTY_IS_TRANSACTIONAL] (V) defined as a property or the phone account
+     * supports transactional ops (U+). For pre-U devices, the call extras would define the
+     * [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] key.
+     *
+     * Note: The version codes for V is not available so we need to enforce a strict manual check
+     * to ensure the V test path is not executed by incompatible devices.
+     */
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testResolveCallExtension_CapabilityExchange() {
+        if (TestUtils.buildIsAtLeastU()) {
+            Log.w(TAG, "Setting up v2 tests for U+ device")
+            setUpV2Test()
+        } else {
+            Log.w(TAG, "Setting up backwards compatibility tests for pre-U device")
+            setUpBackwardsCompatTest()
+        }
+
+        // Add EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED for pre-U testing
+        val backwardsCompatExtra = if (!TestUtils.buildIsAtLeastU())
+            Pair(CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED, true)
+        else null
+        addAndVerifyCallExtensionType(
+            TestUtils.OUTGOING_CALL_ATTRIBUTES,
+            InCallServiceCompat.CAPABILITY_EXCHANGE,
+            // Waiting is not required for U+ testing
+            waitForCallDetailExtras = !TestUtils.buildIsAtLeastU(),
+            extraToInclude = backwardsCompatExtra
+        )
+    }
+
+    /**
+     * Assert that NONE is the extension type for calls with phone accounts that do not support
+     * transactional ops. Note that the caller must have had the read phone numbers permission.
+     *
+     * Note: Ensure that all extras are cleared before asserting extension type so that the phone
+     * account can be checked. For backwards compatibility tests, calls define the
+     * [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] key in the details extras so this
+     * needs to be disregarded.
+     *
+     * We need to ensure that all extras/properties are ignored for testing so that the phone
+     * account can be checked to see if it supports transactional ops. In jetpack, this can only be
+     * verified on pre-U devices as those phone accounts are registered in Telecom without
+     * transactional ops. Keep in mind that because these calls are set up for backwards
+     * compatibility, they will have the [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED]
+     * extra in the details (which will need to be ignored during testing).
+     */
+    @LargeTest
+    @Test(timeout = 10000)
+    fun testResolveCallExtension_TransactionalOpsNotSupported() {
+        // Phone accounts that don't use the v2 APIs don't support transactional ops.
+        setUpBackwardsCompatTest()
+        addAndVerifyCallExtensionType(
+            TestUtils.OUTGOING_CALL_ATTRIBUTES,
+            InCallServiceCompat.NONE,
+            waitForCallDetailExtras = false
+        )
+    }
+
+    /***********************************************************************************************
+     *                           Helpers
+     *********************************************************************************************/
+
+    /**
+     * Helper to add a call via CallsManager#addCall and verify the extension type depending on
+     * the APIs that are leveraged.
+     *
+     * Note: The connection extras are not added into the call until the connection is successfully
+     * created. This is usually the case when the call moves from the CONNECTING state into either
+     * the DIALING/RINGING state. This would be the case for [CallsManager.EXTRA_VOIP_API_VERSION]
+     * (handled by auto) as well as for [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED]
+     * (see JetpackConnectionService#createSelfManagedConnection). Keep in mind that these extras
+     * would not be available in [InCalLService#onCallAdded], but after
+     * [Call#handleCreateConnectionSuccess] is invoked and the connection service extras are
+     * propagated into the call details via [Call#putConnectionServiceExtras].
+     *
+     * @param callAttributesCompat for the call.
+     * @param expectedType for call extension type.
+     * @param waitForCallDetailExtras used for waiting on the call details extras to be non-null.
+     * @param extraToInclude as part of the call extras.
+     */
+    private fun addAndVerifyCallExtensionType(
+        callAttributesCompat: CallAttributesCompat,
+        @InCallServiceCompat.Companion.CapabilityExchangeType expectedType: Int,
+        waitForCallDetailExtras: Boolean = true,
+        extraToInclude: Pair<String, Boolean>? = null
+    ) {
+        runBlocking {
+            assertWithinTimeout_addCall(callAttributesCompat) {
+                launch {
+                    val call = TestUtils.waitOnInCallServiceToReachXCalls(1)
+                    Assert.assertNotNull("The returned Call object is <NULL>", call!!)
+
+                    // Enforce waiting logic to ensure that the call details extras are populated.
+                    if (waitForCallDetailExtras) {
+                        TestUtils.waitOnCallExtras(call)
+                    }
+
+                    val callDetails = call.details
+                    // Clear out extras to isolate the testing scenarios.
+                    call.details.extras?.clear()
+                    // Add extraToInclude for testing.
+                    if (extraToInclude != null) {
+                        callDetails.extras?.putBoolean(extraToInclude.first, extraToInclude.second)
+                    }
+
+                    // Assert call extension type.
+                    assertEquals(expectedType, inCallServiceCompat.resolveCallExtensionsType(call))
+                    // Always send disconnect signal if possible.
+                    Assert.assertTrue(disconnect(DisconnectCause(DisconnectCause.LOCAL)))
+                }
+            }
+        }
+    }
+}
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
index 5aba448..c44414ca 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/utils/TestUtils.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.media.AudioManager
 import android.net.Uri
+import android.os.Build
 import android.os.Build.VERSION_CODES
 import android.os.UserHandle
 import android.os.UserManager
@@ -310,4 +311,39 @@
             )
         }
     }
+
+    /**
+     * Helper to wait on the call detail extras to be populated from the connection service
+     */
+    suspend fun waitOnCallExtras(call: Call) {
+        try {
+            withTimeout(TestUtils.WAIT_ON_CALL_STATE_TIMEOUT) {
+                while (isActive /* aka  within timeout window */ && call.details?.extras == null) {
+                    yield() // another mechanism to stop the while loop if the coroutine is dead
+                    delay(1) // sleep x millisecond(s) instead of spamming check
+                }
+            }
+        } catch (e: TimeoutCancellationException) {
+            Log.i(TestUtils.LOG_TAG, "waitOnCallExtras: timeout reached")
+            TestUtils.dumpTelecom()
+            MockInCallService.destroyAllCalls()
+            throw AssertionError("Expected call detail extras to be non-null.")
+        }
+    }
+
+    /**
+     * Used for testing in V. The build version is not available for referencing so this helper
+     * performs a manual check instead.
+     */
+    fun buildIsAtLeastV(): Boolean {
+        // V is not referencable as a valid build version yet. Enforce strict manual check instead.
+        return Build.VERSION.SDK_INT > 34
+    }
+
+    /**
+     * Determine if the current build supports at least U.
+     */
+    fun buildIsAtLeastU(): Boolean {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+    }
 }
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
index fd67da4..27df20c 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
@@ -82,6 +82,11 @@
         annotation class Capability
 
         /**
+         * Set on Connections that are using ConnectionService+AUTO specific extension layer.
+         */
+        internal const val EXTRA_VOIP_API_VERSION = "android.telecom.extra.VOIP_API_VERSION"
+
+        /**
          * Set on Jetpack Connections that are emulating the transactional APIs using
          * ConnectionService.
          */
@@ -89,6 +94,15 @@
             "android.telecom.extra.VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED"
 
         /**
+         * The connection is using transactional call APIs.
+         *
+         *
+         * The underlying connection was added as a transactional call via the
+         * [TelecomManager.addCall] API.
+         */
+        internal const val PROPERTY_IS_TRANSACTIONAL = 0x00008000
+
+        /**
          * If your VoIP application does not want support any of the capabilities below, then your
          * application can register with [CAPABILITY_BASELINE].
          *
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallCompat.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallCompat.kt
new file mode 100644
index 0000000..890707d
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/CallCompat.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 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 androidx.core.telecom.internal
+
+import android.telecom.Call
+import kotlinx.coroutines.CoroutineScope
+
+internal class CallCompat(call: Call, block: CoroutineScope.() -> Unit) {
+    private val mCall: Call = call
+    private val mBlock: CoroutineScope.() -> Unit = block
+}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/InCallServiceCompat.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/InCallServiceCompat.kt
new file mode 100644
index 0000000..f3063c0
--- /dev/null
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/InCallServiceCompat.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2023 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 androidx.core.telecom.internal
+
+import android.Manifest
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.telecom.Call
+import android.telecom.InCallService
+import android.telecom.PhoneAccount
+import android.telecom.TelecomManager
+import android.util.Log
+import androidx.annotation.IntDef
+import androidx.annotation.RequiresApi
+import androidx.core.content.ContextCompat
+import androidx.core.telecom.CallsManager
+
+/**
+ * This class defines the Jetpack ICS layer which will be leveraged as part of supporting VOIP app
+ * actions.
+ */
+@RequiresApi(Build.VERSION_CODES.M)
+internal class InCallServiceCompat(context: Context) : InCallService() {
+    private val mContext: Context = context
+
+    companion object {
+        /**
+         * Constants used to denote the extension level supported by the VOIP app.
+         */
+        @Retention(AnnotationRetention.SOURCE)
+        @IntDef(NONE, EXTRAS, CAPABILITY_EXCHANGE, UNKNOWN)
+        internal annotation class CapabilityExchangeType
+
+        internal const val NONE = 0
+        internal const val EXTRAS = 1
+        internal const val CAPABILITY_EXCHANGE = 2
+        internal const val UNKNOWN = 3
+
+        private val TAG = InCallServiceCompat::class.simpleName
+    }
+
+    fun onCreateCall(call: Call): CallCompat {
+        Log.d(TAG, "onCreateCall: call = $call")
+        return with(this) {
+            CallCompat(call) {
+            }
+        }
+    }
+
+    fun onRemoveCall(call: CallCompat) {
+        Log.d(TAG, "onRemoveCall: call = $call")
+    }
+
+    /**
+     * Internal helper used by the [InCallService] to help resolve the call extension type. This
+     * is invoked before capability exchange between the [InCallService] and VOIP app starts to
+     * ensure the necessary features are enabled to support it.
+     *
+     * If the call is placed using the V1.5 ConnectionService + Extensions Library (Auto Case), the
+     * call will have the [CallsManager.EXTRA_VOIP_API_VERSION] defined in the extras. The call
+     * extension would be resolved as [InCallServiceCompat.EXTRAS].
+     *
+     * If the call is using the v2 APIs and the phone account associated with the call supports
+     * transactional ops (U+) or the call has the [CallsManager.PROPERTY_IS_TRANSACTIONAL] property
+     * defined (on V devices), then the extension type is [InCallServiceCompat.CAPABILITY_EXCHANGE].
+     *
+     * If the call is added via CallsManager#addCall on pre-U devices and the
+     * [CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED] is present in the call extras,
+     * the extension type also resolves to [InCallServiceCompat.CAPABILITY_EXCHANGE].
+     *
+     * In the case that none of the cases above apply and the phone account is found not to support
+     * transactional ops (assumes that caller has [android.Manifest.permission.READ_PHONE_NUMBERS]
+     * permission), then the extension type is [InCallServiceCompat.NONE].
+     *
+     * If the caller does not have the required permission to retrieve the phone account, then
+     * the extension type will be [InCallServiceCompat.UNKNOWN], until it can be resolved.
+     *
+     * @param call to resolve the extension type for.
+     * @return the extension type [InCallServiceCompat.CapabilityExchangeType] resolved for the
+     * call.
+     */
+    @RequiresApi(Build.VERSION_CODES.O)
+    @CapabilityExchangeType
+    internal fun resolveCallExtensionsType(call: Call): Int {
+        var callDetails = call.details
+        val callExtras = callDetails?.extras ?: Bundle()
+
+        if (callExtras.containsKey(CallsManager.EXTRA_VOIP_API_VERSION)) {
+            return EXTRAS
+        }
+        if (callDetails?.hasProperty(CallsManager.PROPERTY_IS_TRANSACTIONAL) == true || callExtras
+            .containsKey(CallsManager.EXTRA_VOIP_BACKWARDS_COMPATIBILITY_SUPPORTED)) {
+            return CAPABILITY_EXCHANGE
+        }
+        // Verify read phone numbers permission to see if phone account supports transactional ops.
+        if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_PHONE_NUMBERS)
+            == PackageManager.PERMISSION_GRANTED) {
+            var telecomManager = mContext.getSystemService(Context.TELECOM_SERVICE)
+                as TelecomManager
+            var phoneAccount = telecomManager.getPhoneAccount(callDetails?.accountHandle)
+            if (phoneAccount?.hasCapabilities(
+                    PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS) == true) {
+                return CAPABILITY_EXCHANGE
+            } else {
+                return NONE
+            }
+        }
+
+        Log.i(TAG, "Unable to resolve call extension type. Returning $UNKNOWN.")
+        return UNKNOWN
+    }
+}