Support java resources for locally loaded SDK.

Path to java resource root (in assets) should be stored in
<java-resources-root-path> tag of compat sdk config.

Bug: 249982004
Test: JavaResourcesLoadingClassLoaderFactoryTest, SdkLoaderTest
Change-Id: I1e23f409c017970a6e6e401b13a26df993abf6fa
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/CompatSdkConfig.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/CompatSdkConfig.xml
index 476b884..dfeca90 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/CompatSdkConfig.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/CompatSdkConfig.xml
@@ -16,5 +16,5 @@
 <compat-config>
     <compat-entrypoint>androidx.privacysandbox.sdkruntime.test.v1.CompatProvider</compat-entrypoint>
     <dex-path>RuntimeEnabledSdks/V1/classes.dex</dex-path>
-    <java-resource-path>RuntimeEnabledSdks/V1/</java-resource-path>
+    <java-resources-root-path>RuntimeEnabledSdks/V1/javaresources</java-resources-root-path>
 </compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/javaresources/test.txt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/javaresources/test.txt
new file mode 100644
index 0000000..30d74d2
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/javaresources/test.txt
@@ -0,0 +1 @@
+test
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatTest.kt
index 3e11549..10f60478 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatTest.kt
@@ -81,6 +81,7 @@
         }
 
         verify(context, Mockito.atLeastOnce()).assets
+        verify(context, Mockito.atLeastOnce()).classLoader
         verifyNoMoreInteractions(context)
     }
 
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParserTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParserTest.kt
index 849e492..af6684e 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParserTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParserTest.kt
@@ -40,7 +40,7 @@
                     <unknown-tag>new inner tag</unknown-tag>
                 </future-version-tag>
                 <dex-path>2.dex</dex-path>
-                <java-resource-path>javaResPath/</java-resource-path>
+                <java-resources-root-path>javaResPath/</java-resources-root-path>
             </compat-config>
         """.trimIndent()
 
@@ -61,7 +61,6 @@
         val xml = """
             <compat-config>
                 <dex-path>1.dex</dex-path>
-                <java-resource-path>path1/</java-resource-path>
             </compat-config>
         """.trimIndent()
 
@@ -79,7 +78,6 @@
                 <compat-entrypoint>compat.sdk.provider</compat-entrypoint>
                 <compat-entrypoint>compat.sdk.provider2</compat-entrypoint>
                 <dex-path>1.dex</dex-path>
-                <java-resource-path>path1/</java-resource-path>
             </compat-config>
         """.trimIndent()
 
@@ -95,7 +93,6 @@
         val xml = """
             <compat-config>
                 <compat-entrypoint>compat.sdk.provider</compat-entrypoint>
-                <java-resource-path>path1/</java-resource-path>
             </compat-config>
         """.trimIndent()
 
@@ -133,15 +130,15 @@
             <compat-config>
                 <compat-entrypoint>compat.sdk.provider</compat-entrypoint>
                 <dex-path>1.dex</dex-path>
-                <java-resource-path>path1/</java-resource-path>
-                <java-resource-path>path2/</java-resource-path>
+                <java-resources-root-path>path1/</java-resources-root-path>
+                <java-resources-root-path>path2/</java-resources-root-path>
             </compat-config>
         """.trimIndent()
 
         assertThrows<XmlPullParserException> {
             tryParse(xml)
         }.hasMessageThat().isEqualTo(
-            "Duplicate java-resource-path tag found"
+            "Duplicate java-resources-root-path tag found"
         )
     }
 
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt
index 9111585..cc6e081 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt
@@ -50,7 +50,7 @@
         assertThat(result!!.dexPaths)
             .containsExactly("RuntimeEnabledSdks/V1/classes.dex")
         assertThat(result.javaResourcesRoot)
-            .isEqualTo("RuntimeEnabledSdks/V1/")
+            .isEqualTo("RuntimeEnabledSdks/V1/javaresources")
         assertThat(result.entryPoint)
             .isEqualTo("androidx.privacysandbox.sdkruntime.test.v1.CompatProvider")
     }
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/JavaResourcesLoadingClassLoaderFactoryTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/JavaResourcesLoadingClassLoaderFactoryTest.kt
new file mode 100644
index 0000000..1797e6b
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/JavaResourcesLoadingClassLoaderFactoryTest.kt
@@ -0,0 +1,100 @@
+/*
+ * 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 androidx.privacysandbox.sdkruntime.client.loader
+
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class JavaResourcesLoadingClassLoaderFactoryTest {
+
+    private lateinit var appClassloader: ClassLoader
+    private lateinit var factoryUnderTest: JavaResourcesLoadingClassLoaderFactory
+    private lateinit var testSdkConfig: LocalSdkConfig
+
+    @Before
+    fun setUp() {
+        appClassloader = javaClass.classLoader!!
+        factoryUnderTest = JavaResourcesLoadingClassLoaderFactory(
+            appClassloader
+        )
+        testSdkConfig = LocalSdkConfig(
+            listOf("RuntimeEnabledSdks/V1/classes.dex"),
+            "RuntimeEnabledSdks/V1/javaresources",
+            "androidx.privacysandbox.sdkruntime.test.v1.CompatProvider"
+        )
+    }
+
+    @Test
+    fun getResource_delegateToAppClassloaderWithPrefix() {
+        val classLoader = factoryUnderTest.loadSdk(testSdkConfig, appClassloader.parent!!)
+        val resource = classLoader.getResource("test.txt")
+
+        val appResource = appClassloader.getResource(
+            "assets/RuntimeEnabledSdks/V1/javaresources/test.txt"
+        )
+        assertThat(resource).isNotNull()
+        assertThat(resource).isEqualTo(appResource)
+    }
+
+    @Test
+    fun getResource_whenAppResource_returnNull() {
+        val classLoader = factoryUnderTest.loadSdk(testSdkConfig, appClassloader.parent!!)
+
+        val resource = classLoader.getResource("assets/RuntimeEnabledSdkTable.xml")
+        val appResource = appClassloader.getResource("assets/RuntimeEnabledSdkTable.xml")
+
+        assertThat(appResource).isNotNull()
+        assertThat(resource).isNull()
+    }
+
+    @Test
+    fun getResources_delegateToAppClassloaderWithPrefix() {
+        val classLoader = factoryUnderTest.loadSdk(testSdkConfig, appClassloader.parent!!)
+
+        val resources = classLoader.getResources("test.txt")
+        assertThat(resources.hasMoreElements()).isTrue()
+        val resource = resources.nextElement()
+        assertThat(resources.hasMoreElements()).isFalse()
+
+        val appResources = appClassloader.getResources(
+            "assets/RuntimeEnabledSdks/V1/javaresources/test.txt"
+        )
+        assertThat(appResources.hasMoreElements()).isTrue()
+        val appResource = appResources.nextElement()
+        assertThat(appResources.hasMoreElements()).isFalse()
+
+        assertThat(resource).isEqualTo(appResource)
+    }
+
+    @Test
+    fun getResources_whenAppResource_returnEmpty() {
+        val classLoader = factoryUnderTest.loadSdk(testSdkConfig, appClassloader.parent!!)
+
+        val resources = classLoader.getResources("assets/RuntimeEnabledSdkTable.xml")
+        val appResources = appClassloader.getResources("assets/RuntimeEnabledSdkTable.xml")
+
+        assertThat(appResources.hasMoreElements()).isTrue()
+        assertThat(resources.hasMoreElements()).isFalse()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTest.kt
new file mode 100644
index 0000000..e3248d3
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTest.kt
@@ -0,0 +1,228 @@
+/*
+ * 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 androidx.privacysandbox.sdkruntime.client.loader
+
+import android.content.Context
+import android.os.Binder
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import androidx.privacysandbox.sdkruntime.client.loader.impl.SandboxedSdkContextCompat
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
+import androidx.privacysandbox.sdkruntime.core.Versions
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import dalvik.system.InMemoryDexClassLoader
+import java.nio.ByteBuffer
+import java.nio.channels.Channels
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@SmallTest
+@RunWith(Parameterized::class)
+internal class LocalSdkTest(
+    @Suppress("unused") private val sdkPath: String,
+    @Suppress("unused") private val sdkVersion: Int,
+    private val loadedSdk: LocalSdk
+) {
+
+    @Test
+    fun loadSdk_attachCorrectContext() {
+        val sdkContext = loadedSdk.extractSdkContext()
+        assertThat(sdkContext.javaClass.name)
+            .isEqualTo(SandboxedSdkContextCompat::class.java.name)
+    }
+
+    @Test
+    fun onLoadSdk_callOnLoadSdkAndReturnResult() {
+        val params = Bundle()
+
+        val sandboxedSdkCompat = loadedSdk.onLoadSdk(params)
+
+        val expectedBinder = loadedSdk.extractSdkProviderFieldValue<Binder>(
+            fieldName = "onLoadSdkBinder",
+        )
+        assertThat(sandboxedSdkCompat.getInterface()).isEqualTo(expectedBinder)
+
+        val lastParams = loadedSdk.extractSdkProviderFieldValue<Bundle>(
+            fieldName = "lastOnLoadSdkParams",
+        )
+        assertThat(lastParams).isEqualTo(params)
+    }
+
+    @Test
+    fun onLoadSdk_callOnLoadSdkAndThrowException() {
+        val params = Bundle()
+        params.putBoolean("needFail", true)
+
+        val ex = assertThrows(LoadSdkCompatException::class.java) {
+            loadedSdk.onLoadSdk(params)
+        }
+
+        assertThat(ex.extraInformation).isEqualTo(params)
+    }
+
+    @Test
+    fun beforeUnloadSdk_callBeforeUnloadSdk() {
+        loadedSdk.beforeUnloadSdk()
+
+        val isBeforeUnloadSdkCalled = loadedSdk.extractSdkProviderFieldValue<Boolean>(
+            fieldName = "isBeforeUnloadSdkCalled"
+        )
+
+        assertThat(isBeforeUnloadSdkCalled).isTrue()
+    }
+
+    class CurrentVersionProviderLoadTest : SandboxedSdkProviderCompat() {
+        @JvmField
+        val onLoadSdkBinder = Binder()
+
+        @JvmField
+        var lastOnLoadSdkParams: Bundle? = null
+
+        @JvmField
+        var isBeforeUnloadSdkCalled = false
+
+        @Throws(LoadSdkCompatException::class)
+        override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
+            lastOnLoadSdkParams = params
+            if (params.getBoolean("needFail", false)) {
+                throw LoadSdkCompatException(RuntimeException(), params)
+            }
+            return SandboxedSdkCompat.create(onLoadSdkBinder)
+        }
+
+        override fun beforeUnloadSdk() {
+            isBeforeUnloadSdkCalled = true
+        }
+
+        override fun getView(
+            windowContext: Context,
+            params: Bundle,
+            width: Int,
+            height: Int
+        ): View {
+            return View(windowContext)
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    internal class TestClassLoaderFactory : SdkLoader.ClassLoaderFactory {
+        override fun loadSdk(sdkConfig: LocalSdkConfig, parent: ClassLoader): ClassLoader {
+            val assetManager = ApplicationProvider.getApplicationContext<Context>().assets
+            assetManager.open(sdkConfig.dexPaths[0]).use { inputStream ->
+                val byteBuffer = ByteBuffer.allocate(inputStream.available())
+                Channels.newChannel(inputStream).read(byteBuffer)
+                byteBuffer.flip()
+                return InMemoryDexClassLoader(
+                    byteBuffer,
+                    parent
+                )
+            }
+        }
+    }
+
+    internal class TestSdkInfo internal constructor(
+        val apiVersion: Int,
+        dexPath: String,
+        sdkProviderClass: String
+    ) {
+        val mLocalSdkConfig: LocalSdkConfig = LocalSdkConfig(
+            listOf(dexPath), javaResourcesRoot = null,
+            sdkProviderClass
+        )
+    }
+
+    companion object {
+        private val SDKS = arrayOf(
+            TestSdkInfo(
+                1,
+                "RuntimeEnabledSdks/V1/classes.dex",
+                "androidx.privacysandbox.sdkruntime.test.v1.CompatProvider"
+            )
+        )
+
+        @Parameterized.Parameters(name = "sdk: {0}, version: {1}")
+        @JvmStatic
+        fun params(): List<Array<Any>> = buildList {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                assertThat(SDKS.size).isEqualTo(Versions.API_VERSION)
+
+                for (i in SDKS.indices) {
+                    val sdk = SDKS[i]
+                    assertThat(sdk.apiVersion).isEqualTo(i + 1)
+
+                    val loadedSdk = loadTestSdkFromAssets(sdk)
+                    assertThat(loadedSdk.extractApiVersion())
+                        .isEqualTo(sdk.apiVersion)
+
+                    add(
+                        arrayOf(
+                            sdk.mLocalSdkConfig.dexPaths[0],
+                            sdk.apiVersion,
+                            loadedSdk
+                        )
+                    )
+                }
+            }
+
+            // add SDK loaded from test sources
+            add(
+                arrayOf(
+                    "BuiltFromSource",
+                    Versions.API_VERSION,
+                    loadTestSdkFromSource(),
+                )
+            )
+        }
+
+        private fun loadTestSdkFromSource(): LocalSdk {
+            val sdkLoader = SdkLoader(
+                object : SdkLoader.ClassLoaderFactory {
+                    override fun loadSdk(
+                        sdkConfig: LocalSdkConfig,
+                        parent: ClassLoader
+                    ): ClassLoader = javaClass.classLoader!!
+                },
+                ApplicationProvider.getApplicationContext()
+            )
+
+            return sdkLoader.loadSdk(
+                LocalSdkConfig(
+                    emptyList(),
+                    javaResourcesRoot = null,
+                    CurrentVersionProviderLoadTest::class.java.name
+                )
+            )
+        }
+
+        @RequiresApi(Build.VERSION_CODES.O)
+        private fun loadTestSdkFromAssets(sdk: TestSdkInfo): LocalSdk {
+            val sdkLoader = SdkLoader(
+                TestClassLoaderFactory(),
+                ApplicationProvider.getApplicationContext()
+            )
+            return sdkLoader.loadSdk(sdk.mLocalSdkConfig)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTestUtils.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTestUtils.kt
new file mode 100644
index 0000000..0cfa817
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTestUtils.kt
@@ -0,0 +1,72 @@
+/*
+ * 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 androidx.privacysandbox.sdkruntime.client.loader
+
+import android.content.Context
+import androidx.privacysandbox.sdkruntime.core.Versions
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
+import kotlin.reflect.cast
+
+/**
+ * Extract value of [Versions.API_VERSION] from loaded SDK.
+ */
+internal fun LocalSdk.extractApiVersion(): Int =
+    extractVersionValue("API_VERSION")
+
+/**
+ * Extract value of [Versions.CLIENT_VERSION] from loaded SDK.
+ */
+internal fun LocalSdk.extractClientVersion(): Int =
+    extractVersionValue("CLIENT_VERSION")
+
+/**
+ * Extract [SandboxedSdkProviderCompat.context] from loaded SDK.
+ */
+internal fun LocalSdk.extractSdkContext(): Context {
+    val getContextMethod = sdkProvider
+        .javaClass
+        .getMethod("getContext")
+
+    val rawContext = getContextMethod.invoke(sdkProvider)
+
+    return Context::class.cast(rawContext)
+}
+
+/**
+ * Extract field value from [SandboxedSdkProviderCompat]
+ */
+internal inline fun <reified T> LocalSdk.extractSdkProviderFieldValue(fieldName: String): T {
+    return sdkProvider
+        .javaClass
+        .getField(fieldName)
+        .get(sdkProvider)!! as T
+}
+
+/**
+ * Extract classloader that was used for loading of [SandboxedSdkProviderCompat].
+ */
+internal fun LocalSdk.extractSdkProviderClassloader(): ClassLoader =
+    sdkProvider.javaClass.classLoader!!
+
+private fun LocalSdk.extractVersionValue(versionFieldName: String): Int {
+    val versionsClass = Class.forName(
+        Versions::class.java.name,
+        false,
+        extractSdkProviderClassloader()
+    )
+    return versionsClass.getDeclaredField(versionFieldName).get(null) as Int
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
index b418292..6e61a67 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
@@ -15,259 +15,68 @@
  */
 package androidx.privacysandbox.sdkruntime.client.loader
 
-import android.content.Context
-import android.os.Binder
 import android.os.Build
-import android.os.Bundle
-import android.view.View
-import androidx.annotation.RequiresApi
 import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
-import androidx.privacysandbox.sdkruntime.client.loader.impl.SandboxedSdkContextCompat
-import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
-import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
-import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
 import androidx.privacysandbox.sdkruntime.core.Versions
 import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
-import dalvik.system.InMemoryDexClassLoader
-import java.nio.ByteBuffer
-import java.nio.channels.Channels
-import kotlin.reflect.cast
-import org.junit.Assert.assertThrows
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
 
 @SmallTest
-@RunWith(Parameterized::class)
-internal class SdkLoaderTest(
-    @Suppress("unused") private val sdkPath: String,
-    private val loadedSdk: LocalSdk,
-    private val sdkVersion: Int
-) {
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O_MR1)
+class SdkLoaderTest {
 
-    @Test
-    fun create_callVersionsHandShakeAndAttachContext() {
-        val classLoader = loadedSdk.sdkProvider.javaClass.classLoader
+    private lateinit var sdkLoader: SdkLoader
 
-        val apiVersion = extractApiVersion(classLoader)
-        assertThat(apiVersion).isEqualTo(sdkVersion)
+    private lateinit var testSdkConfig: LocalSdkConfig
 
-        val clientVersion = extractClientVersion(classLoader)
-        assertThat(clientVersion).isEqualTo(Versions.API_VERSION)
-
-        val sdkContext = extractSdkContext(loadedSdk.sdkProvider)
-        assertThat(sdkContext.javaClass.name)
-            .isEqualTo(SandboxedSdkContextCompat::class.java.name)
-    }
-
-    @Test
-    fun onLoadSdk_callOnLoadSdkAndReturnResult() {
-        val params = Bundle()
-
-        val sandboxedSdkCompat = loadedSdk.onLoadSdk(params)
-
-        val expectedBinder = extractOnLoadSdkBinder(loadedSdk.sdkProvider)
-        assertThat(sandboxedSdkCompat.getInterface()).isEqualTo(expectedBinder)
-
-        val lastParams = extractLastOnLoadSdkParams(loadedSdk.sdkProvider)
-        assertThat(lastParams).isEqualTo(params)
-    }
-
-    @Test
-    fun onLoadSdk_callOnLoadSdkAndThrowException() {
-        val params = Bundle()
-        params.putBoolean("needFail", true)
-
-        val ex = assertThrows(LoadSdkCompatException::class.java) {
-            loadedSdk.onLoadSdk(params)
-        }
-
-        assertThat(ex.extraInformation).isEqualTo(params)
-    }
-
-    @Test
-    fun beforeUnloadSdk_callBeforeUnloadSdk() {
-        loadedSdk.beforeUnloadSdk()
-
-        val isBeforeUnloadSdkCalled = extractIsBeforeUnloadSdkCalled(loadedSdk.sdkProvider)
-        assertThat(isBeforeUnloadSdkCalled).isTrue()
-    }
-
-    private fun extractApiVersion(classLoader: ClassLoader?): Int =
-        extractVersionValue(classLoader, "API_VERSION")
-
-    private fun extractClientVersion(classLoader: ClassLoader?): Int =
-        extractVersionValue(classLoader, "CLIENT_VERSION")
-
-    private fun extractSdkContext(rawProvider: Any): Context {
-        val getContextMethod = rawProvider
-            .javaClass
-            .getMethod("getContext")
-
-        val rawContext = getContextMethod.invoke(rawProvider)
-
-        return Context::class.cast(rawContext)
-    }
-
-    private fun extractOnLoadSdkBinder(rawProvider: Any): Binder =
-        extractFieldValue(rawProvider, "onLoadSdkBinder", Binder::class.java)
-
-    private fun extractLastOnLoadSdkParams(rawProvider: Any): Bundle =
-        extractFieldValue(rawProvider, "lastOnLoadSdkParams", Bundle::class.java)
-
-    private fun extractVersionValue(classLoader: ClassLoader?, versionFieldName: String): Int {
-        val versionsClass = Class.forName(
-            Versions::class.java.name,
-            false,
-            classLoader
+    @Before
+    fun setUp() {
+        sdkLoader = SdkLoader.create(
+            ApplicationProvider.getApplicationContext()
         )
-        return versionsClass.getDeclaredField(versionFieldName).get(null) as Int
-    }
-
-    private fun <T> extractFieldValue(rawProvider: Any, fieldName: String, clazz: Class<T>): T {
-        return clazz.cast(
-            rawProvider
-                .javaClass
-                .getField(fieldName)[rawProvider]
-        )!!
-    }
-
-    private fun extractIsBeforeUnloadSdkCalled(rawProvider: Any): Boolean {
-        return rawProvider
-            .javaClass
-            .getField("isBeforeUnloadSdkCalled")
-            .getBoolean(rawProvider)
-    }
-
-    class CurrentVersionProviderLoadTest : SandboxedSdkProviderCompat() {
-        @JvmField
-        val onLoadSdkBinder = Binder()
-
-        @JvmField
-        var lastOnLoadSdkParams: Bundle? = null
-
-        @JvmField
-        var isBeforeUnloadSdkCalled = false
-
-        @Throws(LoadSdkCompatException::class)
-        override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
-            lastOnLoadSdkParams = params
-            if (params.getBoolean("needFail", false)) {
-                throw LoadSdkCompatException(RuntimeException(), params)
-            }
-            return SandboxedSdkCompat.create(onLoadSdkBinder)
-        }
-
-        override fun beforeUnloadSdk() {
-            isBeforeUnloadSdkCalled = true
-        }
-
-        override fun getView(
-            windowContext: Context,
-            params: Bundle,
-            width: Int,
-            height: Int
-        ): View {
-            return View(windowContext)
-        }
-    }
-
-    @RequiresApi(Build.VERSION_CODES.O)
-    internal class TestClassLoaderFactory : SdkLoader.ClassLoaderFactory {
-        override fun loadSdk(sdkConfig: LocalSdkConfig, parent: ClassLoader): ClassLoader {
-            val assetManager = ApplicationProvider.getApplicationContext<Context>().assets
-            assetManager.open(sdkConfig.dexPaths[0]).use { inputStream ->
-                val byteBuffer = ByteBuffer.allocate(inputStream.available())
-                Channels.newChannel(inputStream).read(byteBuffer)
-                byteBuffer.flip()
-                return InMemoryDexClassLoader(
-                    byteBuffer,
-                    parent
-                )
-            }
-        }
-    }
-
-    internal class TestSdkInfo internal constructor(
-        val apiVersion: Int,
-        dexPath: String,
-        sdkProviderClass: String
-    ) {
-        val mLocalSdkConfig: LocalSdkConfig = LocalSdkConfig(
-            listOf(dexPath), javaResourcesRoot = null,
-            sdkProviderClass
+        testSdkConfig = LocalSdkConfig(
+            listOf("RuntimeEnabledSdks/V1/classes.dex"),
+            javaResourcesRoot = "RuntimeEnabledSdks/V1/javaresources",
+            "androidx.privacysandbox.sdkruntime.test.v1.CompatProvider"
         )
     }
 
-    companion object {
-        private val SDKS = arrayOf(
-            TestSdkInfo(
-                1,
-                "RuntimeEnabledSdks/V1/classes.dex",
-                "androidx.privacysandbox.sdkruntime.test.v1.CompatProvider"
-            )
-        )
+    @Test
+    fun loadSdk_callVersionsHandShake() {
+        val loadedSdk = sdkLoader.loadSdk(testSdkConfig)
 
-        @Parameterized.Parameters(name = "sdk: {0}, version: {2}")
-        @JvmStatic
-        fun params(): List<Array<Any>> {
-            return mutableListOf<Array<Any>>().apply {
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                    assertThat(SDKS.size).isEqualTo(Versions.API_VERSION)
+        assertThat(loadedSdk.extractClientVersion())
+            .isEqualTo(Versions.API_VERSION)
+    }
 
-                    for (i in SDKS.indices) {
-                        val sdk = SDKS[i]
-                        assertThat(sdk.apiVersion).isEqualTo(i + 1)
+    @Test
+    fun testContextClassloader() {
+        val loadedSdk = sdkLoader.loadSdk(testSdkConfig)
 
-                        add(
-                            arrayOf(
-                                sdk.mLocalSdkConfig.dexPaths[0],
-                                loadTestSdkFromAssets(sdk),
-                                sdk.apiVersion
-                            )
-                        )
-                    }
-                }
+        val classLoader = loadedSdk.extractSdkProviderClassloader()
+        val sdkContext = loadedSdk.extractSdkContext()
 
-                // add SDK loaded from test sources
-                add(
-                    arrayOf(
-                        "BuiltFromSource",
-                        loadTestSdkFromSource(),
-                        Versions.API_VERSION
-                    )
-                )
-            }
+        assertThat(sdkContext.classLoader)
+            .isSameInstanceAs(classLoader)
+    }
+
+    @Test
+    fun testJavaResources() {
+        val loadedSdk = sdkLoader.loadSdk(testSdkConfig)
+
+        val classLoader = loadedSdk.extractSdkProviderClassloader()
+        val content = classLoader.getResourceAsStream("test.txt").use { inputStream ->
+            inputStream.bufferedReader().readLine()
         }
 
-        private fun loadTestSdkFromSource(): LocalSdk {
-            val sdkLoader = SdkLoader(
-                object : SdkLoader.ClassLoaderFactory {
-                    override fun loadSdk(
-                        sdkConfig: LocalSdkConfig,
-                        parent: ClassLoader
-                    ): ClassLoader = javaClass.classLoader!!
-                },
-                ApplicationProvider.getApplicationContext()
-            )
-
-            return sdkLoader.loadSdk(
-                LocalSdkConfig(
-                    emptyList(), javaResourcesRoot = null,
-                    CurrentVersionProviderLoadTest::class.java.name
-                )
-            )
-        }
-
-        @RequiresApi(Build.VERSION_CODES.O)
-        private fun loadTestSdkFromAssets(sdk: TestSdkInfo): LocalSdk {
-            val sdkLoader = SdkLoader(
-                TestClassLoaderFactory(),
-                ApplicationProvider.getApplicationContext()
-            )
-            return sdkLoader.loadSdk(sdk.mLocalSdkConfig)
-        }
+        assertThat(content)
+            .isEqualTo("test")
     }
 }
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParser.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParser.kt
index 4624250..6ed762b 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParser.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParser.kt
@@ -28,9 +28,9 @@
  *
  * The expected XML structure is:
  * <compat-config>
- *     <dex-path>assets/RuntimeEnabledSdk-sdk.package.name/classes.dex</dex-path>
- *     <dex-path>assets/RuntimeEnabledSdk-sdk.package.name/classes2.dex</dex-path>
- *     <java-resource-path>assets/RuntimeEnabledSdk-sdk.package.name/</java-resource-path>
+ *     <dex-path>RuntimeEnabledSdk-sdk.package.name/dex/classes.dex</dex-path>
+ *     <dex-path>RuntimeEnabledSdk-sdk.package.name/dex/classes2.dex</dex-path>
+ *     <java-resources-root-path>RuntimeEnabledSdk-sdk.package.name/res</java-resources-root-path>
  *     <compat-entrypoint>com.sdk.EntryPointClass</compat-entrypoint>
  * </compat-config>
  *
@@ -59,6 +59,7 @@
                     val dexPath = xmlParser.nextText()
                     dexPaths.add(dexPath)
                 }
+
                 RESOURCE_ROOT_ELEMENT_NAME -> {
                     if (javaResourcesRoot != null) {
                         throw XmlPullParserException(
@@ -67,6 +68,7 @@
                     }
                     javaResourcesRoot = xmlParser.nextText()
                 }
+
                 ENTRYPOINT_ELEMENT_NAME -> {
                     if (entryPoint != null) {
                         throw XmlPullParserException(
@@ -75,6 +77,7 @@
                     }
                     entryPoint = xmlParser.nextText()
                 }
+
                 else -> xmlParser.skipCurrentTag()
             }
         }
@@ -94,7 +97,7 @@
         private val NAMESPACE: String? = null // We don't use namespaces
         private const val CONFIG_ELEMENT_NAME = "compat-config"
         private const val DEX_PATH_ELEMENT_NAME = "dex-path"
-        private const val RESOURCE_ROOT_ELEMENT_NAME = "java-resource-path"
+        private const val RESOURCE_ROOT_ELEMENT_NAME = "java-resources-root-path"
         private const val ENTRYPOINT_ELEMENT_NAME = "compat-entrypoint"
 
         fun parse(inputStream: InputStream): LocalSdkConfig {
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/JavaResourcesLoadingClassLoaderFactory.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/JavaResourcesLoadingClassLoaderFactory.kt
new file mode 100644
index 0000000..1baf918
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/JavaResourcesLoadingClassLoaderFactory.kt
@@ -0,0 +1,66 @@
+/*
+ * 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 androidx.privacysandbox.sdkruntime.client.loader
+
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import java.io.File
+import java.io.IOException
+import java.net.URL
+import java.util.Enumeration
+
+/**
+ * Delegate java resources related calls to app classloader.
+ *
+ * Classloaders normally delegate calls to parent classloader first, that's why using this
+ * classloader as parent overrides java resources for all classes loaded by child classloaders.
+ *
+ * Add [LocalSdkConfig.javaResourcesRoot] as prefix to resource names before delegating calls,
+ * thus allowing isolating java resources for different local sdks.
+ */
+internal class JavaResourcesLoadingClassLoaderFactory(
+    private val appClassloader: ClassLoader
+) : SdkLoader.ClassLoaderFactory {
+    override fun loadSdk(sdkConfig: LocalSdkConfig, parent: ClassLoader): ClassLoader {
+        return if (sdkConfig.javaResourcesRoot == null) {
+            parent
+        } else {
+            JavaResourcesLoadingClassLoader(
+                parent,
+                appClassloader,
+                File(ASSETS_DIR, sdkConfig.javaResourcesRoot)
+            )
+        }
+    }
+
+    private class JavaResourcesLoadingClassLoader constructor(
+        parent: ClassLoader,
+        private val appClassloader: ClassLoader,
+        private val javaResourcePrefix: File
+    ) : ClassLoader(parent) {
+        override fun findResource(name: String): URL? {
+            return appClassloader.getResource(File(javaResourcePrefix, name).path)
+        }
+
+        @Throws(IOException::class)
+        override fun findResources(name: String): Enumeration<URL> {
+            return appClassloader.getResources(File(javaResourcePrefix, name).path)
+        }
+    }
+
+    companion object {
+        const val ASSETS_DIR = "assets/"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt
index b21b79e..8f8ad4d 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt
@@ -36,12 +36,21 @@
         fun loadSdk(sdkConfig: LocalSdkConfig, parent: ClassLoader): ClassLoader
     }
 
+    /**
+     * Loading SDK in separate classloader:
+     *  1. Create classloader for sdk;
+     *  2. Performing handshake to determine api version;
+     *  3. Select [LocalSdk] implementation that could work with that api version.
+     *
+     * @param sdkConfig sdk to load
+     * @return LocalSdk implementation for loaded SDK
+     */
     fun loadSdk(sdkConfig: LocalSdkConfig): LocalSdk {
         val classLoader = classLoaderFactory.loadSdk(sdkConfig, getParentClassLoader())
         return createLocalSdk(classLoader, sdkConfig.entryPoint)
     }
 
-    private fun getParentClassLoader(): ClassLoader = javaClass.classLoader!!.parent!!
+    private fun getParentClassLoader(): ClassLoader = appContext.classLoader.parent!!
 
     private fun createLocalSdk(classLoader: ClassLoader?, sdkProviderClassName: String): LocalSdk {
         try {
@@ -64,9 +73,30 @@
     }
 
     companion object {
+        /**
+         * Build chain of [ClassLoaderFactory] that could load SDKs with their resources.
+         * Order is important because classloaders normally delegate calls to parent classloader
+         * first:
+         *  1. [JavaResourcesLoadingClassLoaderFactory] - to provide java resources to classes
+         *  loaded by child classloaders;
+         *  2. [InMemorySdkClassLoaderFactory] - to load SDK classes.
+         *
+         * @return SdkLoader that could load SDKs with their resources.
+         */
         fun create(context: Context): SdkLoader {
-            val classLoaderFactory = InMemorySdkClassLoaderFactory.create(context)
+            val classLoaderFactory = JavaResourcesLoadingClassLoaderFactory(context.classLoader)
+                .andThen(
+                    InMemorySdkClassLoaderFactory.create(context)
+                )
             return SdkLoader(classLoaderFactory, context)
         }
+
+        private fun ClassLoaderFactory.andThen(next: ClassLoaderFactory): ClassLoaderFactory {
+            val prev = this
+            return object : ClassLoaderFactory {
+                override fun loadSdk(sdkConfig: LocalSdkConfig, parent: ClassLoader): ClassLoader =
+                    next.loadSdk(sdkConfig, prev.loadSdk(sdkConfig, parent))
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SdkV1.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SdkV1.kt
index 5877c211..db201d0 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SdkV1.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SdkV1.kt
@@ -23,7 +23,6 @@
 import androidx.privacysandbox.sdkruntime.client.loader.LocalSdk
 import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
 import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
-import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat.Companion.create
 import java.lang.reflect.InvocationTargetException
 import java.lang.reflect.Method
 
@@ -71,7 +70,7 @@
         @SuppressLint("BanUncheckedReflection") // calling method on SandboxedSdkCompat class
         fun build(rawObject: Any): SandboxedSdkCompat {
             val binder = getInterfaceMethod.invoke(rawObject) as IBinder
-            return create(binder)
+            return SandboxedSdkCompat.create(binder)
         }
 
         companion object {
@@ -79,7 +78,7 @@
             fun create(classLoader: ClassLoader?): SandboxedSdkCompatBuilderV1 {
                 val sandboxedSdkCompatClass = Class.forName(
                     SandboxedSdkCompat::class.java.name,
-                    false,
+                    /* initialize = */ false,
                     classLoader
                 )
                 val getInterfaceMethod = sandboxedSdkCompatClass.getMethod("getInterface")
@@ -121,7 +120,7 @@
             fun create(classLoader: ClassLoader?): LoadSdkCompatExceptionBuilderV1 {
                 val loadSdkCompatExceptionClass = Class.forName(
                     LoadSdkCompatException::class.java.name,
-                    false,
+                    /* initialize = */ false,
                     classLoader
                 )
                 val getLoadSdkErrorCodeMethod = loadSdkCompatExceptionClass.getMethod(
@@ -148,7 +147,7 @@
         ): SdkV1 {
             val sdkProviderClass = Class.forName(
                 sdkProviderClassName,
-                false,
+                /* initialize = */ false,
                 classLoader
             )
             val attachContextMethod =