[UI Lib] Add support for LocalAdapter::openSession()

This CL also adds proxy for SessionClient and Session class. I haven't
added proxy for every method yet. More to come in the second CL.

This is the bare minimum to launch our manual test app on Android 10
without any crashes.

Updated our automated tests to run using both kinds of adapter. Since
our test always runs locally without creating an actual Sandbox, we have
to use hack to force the library into using RemoteAdapter.

Bug: 282918647
Test: ./gradlew :privacysandbox:ui:ui-tests:connectedAndroidTest --info
Test: Manual test on Android 10
Change-Id: Ia51c79fd140fa0728db38506742642c7c9476597
diff --git a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
index b917e46..b2761c4 100644
--- a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
+++ b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
@@ -25,7 +25,9 @@
 import android.graphics.Paint
 import android.net.Uri
 import android.os.Bundle
+import android.os.Handler
 import android.os.IBinder
+import android.os.Looper
 import android.provider.Settings
 import android.util.Log
 import android.view.View
@@ -38,6 +40,7 @@
 
 class SdkApi(sdkContext: Context) : ISdkApi.Stub() {
     private var mContext: Context? = null
+    private val handler = Handler(Looper.getMainLooper())
 
     init {
         mContext = sdkContext
@@ -67,28 +70,32 @@
             clientExecutor: Executor,
             client: SandboxedUiAdapter.SessionClient,
         ) {
-            Log.d(TAG, "Session requested")
-            lateinit var adView: View
-            if (isWebView) {
-                // To test error cases.
-                if (isAirplaneModeOn()) {
-                    clientExecutor.execute {
-                        client.onSessionError(Throwable("Cannot load WebView in airplane mode."))
+            handler.post(Runnable lambda@{
+                Log.d(TAG, "Session requested")
+                lateinit var adView: View
+                if (isWebView) {
+                    // To test error cases.
+                    if (isAirplaneModeOn()) {
+                        clientExecutor.execute {
+                            client.onSessionError(
+                                Throwable("Cannot load WebView in airplane mode.")
+                            )
+                        }
+                        return@lambda
                     }
-                    return
+                    val webView = WebView(context)
+                    webView.loadUrl(AD_URL)
+                    webView.layoutParams = ViewGroup.LayoutParams(
+                        initialWidth, initialHeight
+                    )
+                    adView = webView
+                } else {
+                    adView = TestView(context, withSlowDraw, text)
                 }
-                val webView = WebView(context)
-                webView.loadUrl(AD_URL)
-                webView.layoutParams = ViewGroup.LayoutParams(
-                    initialWidth, initialHeight
-                )
-                adView = webView
-            } else {
-                adView = TestView(context, withSlowDraw, text)
-            }
-            clientExecutor.execute {
-                client.onSessionOpened(BannerAdSession(adView))
-            }
+                clientExecutor.execute {
+                    client.onSessionOpened(BannerAdSession(adView))
+                }
+            })
         }
 
         private inner class BannerAdSession(private val adView: View) : SandboxedUiAdapter.Session {
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
index 70ceae1..56c62c6 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
@@ -16,12 +16,14 @@
 
 package androidx.privacysandbox.ui.client
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.content.res.Configuration
 import android.hardware.display.DisplayManager
 import android.os.Build
 import android.os.Bundle
 import android.os.IBinder
+import android.util.Log
 import android.view.Display
 import android.view.SurfaceControlViewHost
 import android.view.SurfaceView
@@ -31,6 +33,9 @@
 import androidx.privacysandbox.ui.core.IRemoteSessionController
 import androidx.privacysandbox.ui.core.ISandboxedUiAdapter
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import java.lang.reflect.InvocationHandler
+import java.lang.reflect.Method
+import java.lang.reflect.Proxy
 import java.util.concurrent.Executor
 
 /**
@@ -40,8 +45,10 @@
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 object SandboxedUiAdapterFactory {
 
+    private const val TAG = "PrivacySandboxUiLib"
     // Bundle key is a binary compatibility requirement
     private const val UI_ADAPTER_BINDER = "uiAdapterBinder"
+    private const val TEST_ONLY_USE_REMOTE_ADAPTER = "testOnlyUseRemoteAdapter"
 
     /**
      * @throws IllegalArgumentException if {@code coreLibInfo} does not contain a Binder with the
@@ -54,9 +61,149 @@
         val adapterInterface = ISandboxedUiAdapter.Stub.asInterface(
             uiAdapterBinder
         )
-        return RemoteAdapter(adapterInterface)
+
+        val forceUseRemoteAdapter = coreLibInfo.getBoolean(TEST_ONLY_USE_REMOTE_ADAPTER)
+        val isLocalBinder = uiAdapterBinder.queryLocalInterface(
+                ISandboxedUiAdapter.DESCRIPTOR) != null
+        val useLocalAdapter = !forceUseRemoteAdapter && isLocalBinder
+        Log.d(TAG, "useLocalAdapter=$useLocalAdapter")
+
+        return if (useLocalAdapter) {
+            LocalAdapter(adapterInterface)
+        } else {
+            RemoteAdapter(adapterInterface)
+        }
     }
 
+    /**
+     * [LocalAdapter] fetches UI from a provider living on same process as the client but on a
+     * different class loader.
+     */
+    private class LocalAdapter(private val adapterInterface: ISandboxedUiAdapter) :
+        SandboxedUiAdapter {
+        private val uiProviderBinder = adapterInterface.asBinder()
+
+        @SuppressLint("BanUncheckedReflection") // using reflection on library classes
+        override fun openSession(
+            context: Context,
+            windowInputToken: IBinder,
+            initialWidth: Int,
+            initialHeight: Int,
+            isZOrderOnTop: Boolean,
+            clientExecutor: Executor,
+            client: SandboxedUiAdapter.SessionClient
+        ) {
+            try {
+                // openSession call needs to be forwarded to uiProvider object instantiated
+                // on a different classloader.
+                val uiProviderClassLoader = uiProviderBinder.javaClass.classLoader
+                val classOnUiProviderClassLoader = Class.forName(uiProviderBinder::class.java.name,
+                        /*initialize=*/false, uiProviderBinder.javaClass.classLoader)
+                val openSessionMethod = classOnUiProviderClassLoader.methods.first {
+                        method -> method.name.equals("openSession")
+                }
+
+                // We can't pass the client object as-is since it's been created on a different
+                // classloader.
+                val targetSessionClientClass = Class.forName(
+                    SandboxedUiAdapter.SessionClient::class.java.name,
+                    /*initialize=*/ false,
+                    uiProviderClassLoader
+                )
+                val sessionClientProxy = Proxy.newProxyInstance(
+                    uiProviderClassLoader,
+                    arrayOf(targetSessionClientClass),
+                    SessionClientProxyHandler(client)
+                )
+
+                openSessionMethod.invoke(uiProviderBinder, context, windowInputToken, initialWidth,
+                        initialHeight, isZOrderOnTop, clientExecutor, sessionClientProxy)
+            } catch (exception: Throwable) {
+                client.onSessionError(exception)
+            }
+        }
+
+        private inner class SessionClientProxyHandler(
+            private val origClient: SandboxedUiAdapter.SessionClient,
+        ) : InvocationHandler {
+
+            @SuppressLint("BanUncheckedReflection") // using reflection on library classes
+            override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any? {
+                return when (method.name) {
+                    "onSessionOpened" -> {
+                        args!! // This method will always have an argument, so safe to !!
+
+                        // We have to forward the call to original client, but it won't
+                        // recognize Session class on targetClassLoader. We need another proxy.
+                        val origSessionClass = Class.forName(
+                            SandboxedUiAdapter.Session::class.java.name,
+                            /*initialize=*/ false,
+                            origClient.javaClass.classLoader
+                        )
+                        val sessionProxy = Proxy.newProxyInstance(
+                            origClient.javaClass.classLoader,
+                            arrayOf(origSessionClass),
+                            SessionProxyHandler(args[0])
+                        )
+
+                        val methodOrig = origClient.javaClass.getMethod("onSessionOpened",
+                        SandboxedUiAdapter.Session::class.java)
+                        methodOrig.invoke(origClient, sessionProxy)
+                    }
+                    "onSessionError" -> {
+                        args!! // This method will always have an argument, so safe to !!
+
+                        val throwable = args[0] as Throwable
+                        val methodOrig = origClient.javaClass.getMethod("onSessionError",
+                        Throwable::class.java)
+                        methodOrig.invoke(origClient, throwable)
+                    }
+                    "toString" -> {
+                        origClient.javaClass.getMethod("toString").invoke(origClient)
+                    }
+                    else -> {
+                        // TODO(b/282918647): Implement other methods required
+                        throw UnsupportedOperationException(
+                            "Unexpected method call object:$proxy, method: $method, args: $args"
+                        )
+                    }
+                }
+            }
+        }
+
+        /**
+         * Create [SandboxedUiAdapter.Session] on [targetClassLoader] that proxies to [origClient]
+         */
+        private inner class SessionProxyHandler(
+                private val origClient: Any,
+            ) : InvocationHandler {
+
+            @SuppressLint("BanUncheckedReflection") // using reflection on library classes
+            override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any? {
+                return when (method.name) {
+                    "close" -> {
+                        origClient.javaClass.getMethod("close").invoke(origClient)
+                    }
+                    "getView" -> {
+                        origClient.javaClass.getMethod("getView").invoke(origClient)
+                    }
+                    "toString" -> {
+                        origClient.javaClass.getMethod("toString").invoke(origClient)
+                    }
+                    else -> {
+                        // TODO(b/282918647): Implement other methods required
+                        throw UnsupportedOperationException(
+                            "Unexpected method call object:$proxy, method: $method, args: $args"
+                        )
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * [RemoteAdapter] fetches content from a provider living on a different process.
+     */
     private class RemoteAdapter(private val adapterInterface: ISandboxedUiAdapter) :
         SandboxedUiAdapter {
 
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
index 49c553f..7f5afe4 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.content.res.Configuration
 import android.graphics.Rect
+import android.os.Binder
 import android.os.Build
 import android.os.IBinder
 import android.util.AttributeSet
@@ -268,7 +269,12 @@
     }
 
     internal fun removeSurfaceViewAndOpenSession() {
-        windowInputToken = surfaceView.hostToken
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            windowInputToken = surfaceView.hostToken
+        } else {
+            // Since there is no SdkSandbox, we don't need windowInputToken for creating SCVH
+            windowInputToken = Binder()
+        }
         super.removeView(surfaceView)
         checkClientOpenSession()
     }
diff --git a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
index 1528e16..e39078f 100644
--- a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
+++ b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
@@ -21,6 +21,7 @@
 import android.content.res.Configuration
 import android.os.Binder
 import android.os.Build
+import android.os.Bundle
 import android.os.IBinder
 import android.os.SystemClock
 import android.view.MotionEvent
@@ -37,11 +38,11 @@
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
 import androidx.privacysandbox.ui.provider.toCoreLibInfo
 import androidx.test.ext.junit.rules.ActivityScenarioRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.testutils.withActivity
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executor
 import java.util.concurrent.TimeUnit
@@ -52,17 +53,26 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-@RunWith(AndroidJUnit4::class)
+@RunWith(Parameterized::class)
 @MediumTest
-class IntegrationTests {
+class IntegrationTests(private val invokeBackwardsCompatFlow: Boolean) {
 
     @get:Rule
     var activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)
 
     companion object {
+        const val TEST_ONLY_USE_REMOTE_ADAPTER = "testOnlyUseRemoteAdapter"
         const val TIMEOUT = 1000.toLong()
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "{index}: invokeBackwardsCompatFlow={0}")
+        fun data(): Array<Any> = arrayOf(
+            arrayOf(true),
+            arrayOf(false),
+        )
     }
 
     private lateinit var context: Context
@@ -74,6 +84,7 @@
     @Before
     fun setup() {
         Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+
         context = InstrumentationRegistry.getInstrumentation().context
         activity = activityScenarioRule.withActivity { this }
         view = SandboxedSdkView(context)
@@ -100,7 +111,7 @@
             null,
             false /* hasFailiningTestSession */
         )
-        val coreLibInfo = adapter.toCoreLibInfo(context)
+        val coreLibInfo = getCoreLibInfoFromAdapter(adapter)
         val userRemoteAdapter = SandboxedUiAdapterFactory.createFromCoreLibInfo(coreLibInfo)
         view.setAdapter(userRemoteAdapter)
 
@@ -145,15 +156,16 @@
     }
 
     @Test
-    fun testSessionOpen() {
+    fun testOpenSession_onSetAdapter() {
         val openSessionLatch = CountDownLatch(1)
         val adapter = TestSandboxedUiAdapter(openSessionLatch, null, false)
-        val coreLibInfo = adapter.toCoreLibInfo(context)
+        val coreLibInfo = getCoreLibInfoFromAdapter(adapter)
         val userRemoteAdapter = SandboxedUiAdapterFactory.createFromCoreLibInfo(coreLibInfo)
         view.setAdapter(userRemoteAdapter)
 
-        openSessionLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)
-        assertTrue(adapter.isOpenSessionCalled)
+        assertTrue(openSessionLatch.await(TIMEOUT, TimeUnit.MILLISECONDS))
+        assertWithMessage("openSession is called on adapter")
+                .that(adapter.isOpenSessionCalled).isTrue()
         var isSessionInitialised = try {
             adapter.session
             true
@@ -164,10 +176,10 @@
     }
 
     @Test
-    fun testOpenSessionFromAdapter() {
+    fun testOpenSession_fromAdapter() {
         val openSessionLatch = CountDownLatch(1)
         val adapter = TestSandboxedUiAdapter(openSessionLatch, null, false)
-        val coreLibInfo = adapter.toCoreLibInfo(context)
+        val coreLibInfo = getCoreLibInfoFromAdapter(adapter)
         val adapterFromCoreLibInfo = SandboxedUiAdapterFactory.createFromCoreLibInfo(coreLibInfo)
         val testSessionClient = TestSandboxedUiAdapter.TestSessionClient()
 
@@ -195,7 +207,7 @@
             configChangedLatch,
             false
         )
-        val coreLibInfo = adapter.toCoreLibInfo(context)
+        val coreLibInfo = getCoreLibInfoFromAdapter(adapter)
         val adapterFromCoreLibInfo = SandboxedUiAdapterFactory.createFromCoreLibInfo(coreLibInfo)
         view.setAdapter(adapterFromCoreLibInfo)
         activity.runOnUiThread {
@@ -264,15 +276,21 @@
         val adapter = TestSandboxedUiAdapter(
             null, null, true
         )
-        val coreLibInfo = adapter.toCoreLibInfo(context)
+        val coreLibInfo = getCoreLibInfoFromAdapter(adapter)
         val adapterThatFailsToCreateUi =
             SandboxedUiAdapterFactory.createFromCoreLibInfo(coreLibInfo)
         view.setAdapter(adapterThatFailsToCreateUi)
-        errorLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)
+        assertThat(errorLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
         assertTrue(stateChangeListener.currentState is SandboxedSdkUiSessionState.Error)
         val errorMessage = (stateChangeListener.currentState as
             SandboxedSdkUiSessionState.Error).throwable.message
-        assertTrue(errorMessage == "Test Session Exception")
+        assertThat(errorMessage).isEqualTo("Test Session Exception")
+    }
+
+    private fun getCoreLibInfoFromAdapter(sdkAdapter: SandboxedUiAdapter): Bundle {
+        val bundle = sdkAdapter.toCoreLibInfo(context)
+        bundle.putBoolean(TEST_ONLY_USE_REMOTE_ADAPTER, !invokeBackwardsCompatFlow)
+        return bundle
     }
 
     private fun openSessionAndWaitToBeActive(initialZOrder: Boolean): TestSandboxedUiAdapter {
@@ -321,10 +339,18 @@
         }
     }
 
+    /**
+     *  TestSandboxedUiAdapter provides content from a fake SDK to show on the host's UI.
+     *
+     *  A [SandboxedUiAdapter] is supposed to fetch the content from SandboxedSdk, but we fake the
+     *  source of content in this class.
+     *
+     *  If [hasFailingTestSession] is true, the fake server side logic returns error.
+     */
     class TestSandboxedUiAdapter(
-        val openSessionLatch: CountDownLatch?,
+        private val openSessionLatch: CountDownLatch?,
         val configChangedLatch: CountDownLatch?,
-        val hasFailingTestSession: Boolean
+        private val hasFailingTestSession: Boolean
     ) : SandboxedUiAdapter {
 
         var isOpenSessionCalled = false
@@ -355,6 +381,9 @@
             openSessionLatch?.countDown()
         }
 
+        /**
+         * A failing session that always sends error notice to the client when content is requested.
+         */
         inner class FailingTestSession(
             private val context: Context
         ) : SandboxedUiAdapter.Session {
@@ -390,10 +419,6 @@
                     }
                 }
 
-            init {
-                internalClient.onSessionOpened(this)
-            }
-
             override fun notifyResized(width: Int, height: Int) {
             }