diff --git a/globallydynamic-android-lib/deps.gradle b/globallydynamic-android-lib/deps.gradle
index e5dfdc5e..5592da14 100644
--- a/globallydynamic-android-lib/deps.gradle
+++ b/globallydynamic-android-lib/deps.gradle
@@ -24,7 +24,7 @@ def versions = [
play : '1.10.3',
dynamicability : '1.0.17.300',
autoservice : '1.0-rc6',
- globallydynamic_gradle: '1.8.0',
+ globallydynamic_gradle: '1.9.0',
uiautomator : '2.2.0',
javadoc : '0.3.0'
]
diff --git a/globallydynamic-android-lib/minimal-sample/build.gradle b/globallydynamic-android-lib/minimal-sample/build.gradle
index 8151b0e2..0bcd011b 100644
--- a/globallydynamic-android-lib/minimal-sample/build.gradle
+++ b/globallydynamic-android-lib/minimal-sample/build.gradle
@@ -5,7 +5,7 @@ buildscript {
}
dependencies {
classpath "com.android.tools.build:gradle:8.6.0"
- classpath "com.jeppeman.globallydynamic.gradle:plugin:1.8.0"
+ classpath "com.jeppeman.globallydynamic.gradle:plugin:1.9.0"
}
}
diff --git a/globallydynamic-gradle-plugin/gradle.properties b/globallydynamic-gradle-plugin/gradle.properties
index 01d57feb..117d5c4b 100644
--- a/globallydynamic-gradle-plugin/gradle.properties
+++ b/globallydynamic-gradle-plugin/gradle.properties
@@ -1,5 +1,5 @@
GROUP=com.jeppeman.globallydynamic.gradle
-VERSION_NAME=1.9.0-SNAPSHOT
+VERSION_NAME=1.10.0-SNAPSHOT
POM_DESCRIPTION=GloballyDynamic - Gradle plugin to facilitate for local dynamic delivery for Android.
diff --git a/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasks.kt b/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasks.kt
index a983f67d..3a11ad01 100644
--- a/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasks.kt
+++ b/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasks.kt
@@ -19,8 +19,8 @@ import com.google.common.util.concurrent.MoreExecutors
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
+import com.jeppeman.globallydynamic.gradle.extensions.*
import com.jeppeman.globallydynamic.gradle.extensions.deleteCompletely
-import com.jeppeman.globallydynamic.gradle.extensions.getTaskName
import com.jeppeman.globallydynamic.gradle.extensions.unzip
import org.gradle.api.DefaultTask
import org.gradle.api.Project
@@ -41,7 +41,8 @@ abstract class ApkProducerTask : DefaultTask() {
private fun JsonObject.getPropertyCompat(propName: String) =
get(propName) ?: get("m${propName.capitalize()}")
- protected open val buildMode: BuildApksCommand.ApkBuildMode = BuildApksCommand.ApkBuildMode.DEFAULT
+ @get:Input
+ protected abstract val buildMode: BuildApksCommand.ApkBuildMode
protected abstract fun processApkSet(apkSet: Path)
@get:InputFiles
@@ -54,7 +55,6 @@ abstract class ApkProducerTask : DefaultTask() {
private set
@get:OutputDirectory
- @get:PathSensitive(PathSensitivity.ABSOLUTE)
lateinit var outputDir: File
protected set
@@ -65,6 +65,7 @@ abstract class ApkProducerTask : DefaultTask() {
@get:Nested
abstract val aapt2: Aapt2Input
+ @get:Input
lateinit var variantName: String
private set
@@ -141,19 +142,8 @@ abstract class ApkProducerTask : DefaultTask() {
override fun execute(task: T) {
task.variantName = applicationVariant.name
task.signed = signed
- task.bundleDir = task.project.buildDir
- .toPath()
- .resolve("intermediates")
- .resolve("intermediary_bundle")
- .resolve(applicationVariant.name)
- .toFile()
- task.signingConfig = task.project.buildDir
- .toPath()
- .resolve("intermediates")
- .resolve("signing_config_data")
- .resolve(applicationVariant.name)
- .resolve("signing-config-data.json")
- .toFile()
+ task.bundleDir = task.project.intermediaryBundleDir(applicationVariant)
+ task.signingConfig = task.project.intermediarySigningConfig(applicationVariant)
createProjectServices(task.project).initializeAapt2Input(task.aapt2)
}
}
@@ -196,6 +186,8 @@ abstract class BuildUniversalApkTask : ApkProducerTask() {
}
abstract class BuildBaseApkTask : ApkProducerTask() {
+ override val buildMode: BuildApksCommand.ApkBuildMode = BuildApksCommand.ApkBuildMode.DEFAULT
+
override fun processApkSet(apkSet: Path) {
val tempDir = Paths.get(outputDir.absolutePath, "temp")
tempDir.deleteCompletely()
diff --git a/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/UploadBundleTask.kt b/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/UploadBundleTask.kt
index 327b6896..e8764368 100644
--- a/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/UploadBundleTask.kt
+++ b/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/UploadBundleTask.kt
@@ -4,7 +4,7 @@ import com.android.build.gradle.api.ApplicationVariant
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
-import com.jeppeman.globallydynamic.gradle.extensions.getTaskName
+import com.jeppeman.globallydynamic.gradle.extensions.*
import com.jeppeman.globallydynamic.gradle.extensions.stackTraceToString
import com.jeppeman.globallydynamic.gradle.extensions.toBase64
import org.apache.http.HttpException
@@ -145,21 +145,8 @@ open class UploadBundleTask : DefaultTask() {
task.applicationId = applicationVariant.applicationId
task.version = applicationVariant.versionCode
task.serverInfo = task.project.resolveServerInfo(extension)
- task.bundleDir = task.project.buildDir
- .toPath()
- .resolve("intermediates")
- .resolve("intermediary_bundle")
- .resolve(applicationVariant.name)
- .resolve(applicationVariant.getTaskName("package", "Bundle"))
- .toFile()
- task.signingConfig = task.project.buildDir
- .toPath()
- .resolve("intermediates")
- .resolve("signing_config_data")
- .resolve(applicationVariant.name)
- .resolve(applicationVariant.getTaskName("signingConfigWriter"))
- .resolve("signing-config-data.json")
- .toFile()
+ task.bundleDir = task.project.intermediaryBundleDir(applicationVariant)
+ task.signingConfig = task.project.intermediarySigningConfig(applicationVariant)
}
}
}
diff --git a/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/extensions/Project.kt b/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/extensions/Project.kt
new file mode 100644
index 00000000..aa823d78
--- /dev/null
+++ b/globallydynamic-gradle-plugin/plugin/src/main/java/com/jeppeman/globallydynamic/gradle/extensions/Project.kt
@@ -0,0 +1,22 @@
+package com.jeppeman.globallydynamic.gradle.extensions
+
+import com.android.build.gradle.api.ApplicationVariant
+import org.gradle.api.Project
+import java.io.File
+
+fun Project.intermediaryBundleDir(variant: ApplicationVariant): File = buildDir
+ .toPath()
+ .resolve("intermediates")
+ .resolve("intermediary_bundle")
+ .resolve(variant.name)
+ .resolve(variant.getTaskName("package", "Bundle"))
+ .toFile()
+
+fun Project.intermediarySigningConfig(variant: ApplicationVariant): File = buildDir
+ .toPath()
+ .resolve("intermediates")
+ .resolve("signing_config_data")
+ .resolve(variant.name)
+ .resolve(variant.getTaskName("signingConfigWriter"))
+ .resolve("signing-config-data.json")
+ .toFile()
\ No newline at end of file
diff --git a/globallydynamic-gradle-plugin/plugin/src/test/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasksTest.kt b/globallydynamic-gradle-plugin/plugin/src/test/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasksTest.kt
new file mode 100644
index 00000000..8f42e548
--- /dev/null
+++ b/globallydynamic-gradle-plugin/plugin/src/test/java/com/jeppeman/globallydynamic/gradle/ApkProducerTasksTest.kt
@@ -0,0 +1,222 @@
+package com.jeppeman.globallydynamic.gradle
+
+import com.google.common.truth.Truth.assertThat
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.TypeSpec
+import org.gradle.testkit.runner.TaskOutcome
+import org.junit.jupiter.api.Test
+import org.junit.platform.runner.JUnitPlatform
+import org.junit.runner.RunWith
+import java.nio.file.Paths
+import java.util.zip.ZipFile
+import kotlin.io.path.exists
+import kotlin.io.path.fileSize
+
+abstract class ApkProducerTasksTest : BaseTaskTest() {
+ override val installTimeFeatureName: String = "installtimefeature"
+ override val onDemandFeatureName: String = "ondemandfeature"
+
+ protected val appModuleOutputsDir get() = appModuleProjectDirPath.resolve("build").resolve("outputs")
+
+ override fun beforeEach() {
+ val testResPath = Paths.get("src", "test", "resources")
+ val keyStoreFile = Paths.get(testResPath.toString(), "test.keystore")
+ val keyStorePath = rootProjectDirPath.resolve("test.keystore").apply {
+ toFile().writeBytes(keyStoreFile.toFile().inputStream().readBytes())
+ }
+ val typeSpec = TypeSpec.classBuilder("GloballyDynamicActivity")
+ .superclass(ClassName.get("androidx.appcompat.app", "AppCompatActivity"))
+ .build()
+
+ val stringsFile = appModuleSourceDir.resolve("res")
+ .resolve("values")
+ .apply { toFile().mkdirs() }
+ .resolve("strings.xml")
+
+ stringsFile.toFile().writeText(
+ """
+
+ Install Time Feature
+ On Demand Feature
+
+ """.trimIndent()
+ )
+
+ JavaFile.builder(BASE_PACKAGE_NAME, typeSpec)
+ .build()
+ .writeTo(appModuleSourceDir)
+
+ appModuleAndroidManifestFilePath.toFile().writeText(
+ """
+
+
+
+ """.trimIndent()
+ )
+
+ appModuleBuildFilePath.toFile().writeText(
+ """
+ plugins {
+ id 'com.android.application'
+ id 'com.jeppeman.globallydynamic'
+ }
+
+ android {
+ namespace '$BASE_PACKAGE_NAME'
+
+ compileSdk 32
+
+ defaultConfig {
+ minSdk 16
+ targetSdk 32
+ versionCode $VERSION_CODE
+ }
+
+ signingConfigs {
+ debug {
+ storeFile file('$keyStorePath')
+ storePassword 'android'
+ keyAlias 'androiddebugkey'
+ keyPassword 'android'
+ }
+ }
+
+ dynamicFeatures = [':$onDemandFeatureName', ':$installTimeFeatureName']
+ }
+
+ dependencies {
+ implementation 'com.jeppeman.globallydynamic.android:gplay:${ANDROID_LIB_VERSION}'
+ implementation 'androidx.appcompat:appcompat:1.4.2'
+ }
+ """.trimIndent()
+ )
+
+ onDemandFeatureAndroidManifestFilePath.toFile().writeText(
+ """
+
+
+
+
+
+
+
+
+
+
+
+ """.trimIndent()
+ )
+
+ installTimeFeatureAndroidManifestFilePath.toFile().writeText(
+ """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """.trimIndent()
+ )
+ }
+}
+
+private const val VERSION_CODE = 1
+private const val VARIANT = "debug"
+
+@RunWith(JUnitPlatform::class)
+class BuildUniversalApkTaskTest : ApkProducerTasksTest() {
+ override val taskName: String = ":app:buildUniversalApkFor${VARIANT.capitalize()}"
+
+ private val producedApk get() = appModuleOutputsDir.resolve("universal_apk")
+ .resolve(VARIANT)
+ .resolve("universal.apk")
+
+ @Test
+ fun `task should produce a universal apk`() {
+ val result = runTask()
+
+ assertThat(result.task(taskName)?.outcome).isEqualTo(TaskOutcome.SUCCESS)
+ assertThat(producedApk.exists()).isTrue()
+ assertThat(producedApk.fileSize()).isGreaterThan(0L)
+ assertThat(ZipFile(producedApk.toFile()).entries().asSequence().any { it.name.matches("META-INF/.+\\.RSA".toRegex()) }).isTrue()
+ }
+}
+
+@RunWith(JUnitPlatform::class)
+class BuildUnsignedUniversalApkTaskTest : ApkProducerTasksTest() {
+ override val taskName: String = ":app:buildUnsignedUniversalApkFor${VARIANT.capitalize()}"
+
+ private val producedApk get() = appModuleOutputsDir.resolve("universal_apk")
+ .resolve(VARIANT)
+ .resolve("universal-unsigned.apk")
+
+ @Test
+ fun `task should produce an unsigned universal apk`() {
+ val result = runTask()
+
+ assertThat(result.task(taskName)?.outcome).isEqualTo(TaskOutcome.SUCCESS)
+ assertThat(producedApk.exists()).isTrue()
+ assertThat(producedApk.fileSize()).isGreaterThan(0L)
+ assertThat(ZipFile(producedApk.toFile()).entries().asSequence().any { it.name.matches("META-INF/.+\\.RSA".toRegex()) }).isFalse()
+ }
+}
+
+@RunWith(JUnitPlatform::class)
+class BuildBaseApkTaskTest : ApkProducerTasksTest() {
+ override val taskName: String = ":app:buildBaseApkFor${VARIANT.capitalize()}"
+
+ private val producedApk get() = appModuleOutputsDir.resolve("base_apk")
+ .resolve(VARIANT)
+ .resolve("base-master.apk")
+
+ @Test
+ fun `task should produce a base apk`() {
+ val result = runTask()
+
+ assertThat(result.task(taskName)?.outcome).isEqualTo(TaskOutcome.SUCCESS)
+ assertThat(producedApk.exists()).isTrue()
+ assertThat(producedApk.fileSize()).isGreaterThan(0L)
+ assertThat(ZipFile(producedApk.toFile()).entries().asSequence().any { it.name.matches("META-INF/.+\\.RSA".toRegex()) }).isTrue()
+ }
+}
+
+@RunWith(JUnitPlatform::class)
+class BuildUnsignedBaseApkTaskTest : ApkProducerTasksTest() {
+ override val taskName: String = ":app:buildUnsignedBaseApkFor${VARIANT.capitalize()}"
+
+ private val producedApk get() = appModuleOutputsDir.resolve("base_apk")
+ .resolve(VARIANT)
+ .resolve("base-master-unsigned.apk")
+
+ @Test
+ fun `task should produce an unsigned base apk`() {
+ val result = runTask()
+
+ assertThat(result.task(taskName)?.outcome).isEqualTo(TaskOutcome.SUCCESS)
+ assertThat(producedApk.exists()).isTrue()
+ assertThat(producedApk.fileSize()).isGreaterThan(0L)
+ assertThat(ZipFile(producedApk.toFile()).entries().asSequence().any { it.name.matches("META-INF/.+\\.RSA".toRegex()) }).isFalse()
+ }
+}