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 =