Set up project skeleton (milestone 1)

- Architecture and feature overview in docs/ARCHITECTURE.md
- Gradle 9.8 / AGP 9.4 / Kotlin 2.4 with version catalog and convention plugins
- Modules :app, :core:*, :device-profiles, :feature:* (placeholder screens)
- Flavors core (no INTERNET, enforced by a build check) and full
- Material You theme, navigation, expert mode and dry-run settings (DataStore)
- Pixel 8-10 device detection, unit tests, GPLv3 license, English README with Obtainium badge

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-15 22:02:14 +02:00
co-authored by Claude Opus 5.5
commit 0a7caf6c5f
117 changed files with 3175 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
plugins {
`kotlin-dsl`
}
group = "org.pixelwall.buildlogic"
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
}
dependencies {
compileOnly(libs.android.gradle.plugin)
compileOnly(libs.kotlin.gradle.plugin)
compileOnly(libs.compose.gradle.plugin)
compileOnly(libs.ksp.gradle.plugin)
compileOnly(libs.hilt.gradle.plugin)
}
gradlePlugin {
plugins {
register("androidApplication") {
id = "pixelwall.android.application"
implementationClass = "AndroidApplicationConventionPlugin"
}
register("androidLibrary") {
id = "pixelwall.android.library"
implementationClass = "AndroidLibraryConventionPlugin"
}
register("androidCompose") {
id = "pixelwall.android.compose"
implementationClass = "AndroidComposeConventionPlugin"
}
register("androidHilt") {
id = "pixelwall.android.hilt"
implementationClass = "AndroidHiltConventionPlugin"
}
register("androidFeature") {
id = "pixelwall.android.feature"
implementationClass = "AndroidFeatureConventionPlugin"
}
register("jvmLibrary") {
id = "pixelwall.jvm.library"
implementationClass = "JvmLibraryConventionPlugin"
}
}
}
@@ -0,0 +1,49 @@
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.register
class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("com.android.application")
extensions.configure<ApplicationExtension> {
compileSdk = libs.int("compileSdk")
defaultConfig {
minSdk = libs.int("minSdk")
targetSdk = libs.int("targetSdk")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JAVA_VERSION
targetCompatibility = JAVA_VERSION
}
lint {
warningsAsErrors = false
abortOnError = true
checkDependencies = true
}
}
configureKotlin()
verifyCoreFlavorHasNoNetwork()
}
/** Varianten des Flavors "core" dürfen nie Netzwerkzugriff anfordern (siehe docs/ARCHITECTURE.md §9). */
private fun Project.verifyCoreFlavorHasNoNetwork() {
val verifyAll = tasks.register("verifyNoNetworkPermission")
tasks.named("check").configure { dependsOn(verifyAll) }
extensions.configure<ApplicationAndroidComponentsExtension> {
onVariants { variant ->
if (variant.productFlavors.none { it.second == "core" }) return@onVariants
val name = variant.name.replaceFirstChar(Char::uppercase)
val task = tasks.register<VerifyNoInternetPermissionTask>("verify${name}NoNetworkPermission") {
mergedManifest.set(variant.artifacts.get(SingleArtifact.MERGED_MANIFEST))
report.set(layout.buildDirectory.file("reports/no-network/${variant.name}.txt"))
}
verifyAll.configure { dependsOn(task) }
}
}
}
}
@@ -0,0 +1,23 @@
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
class AndroidComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("org.jetbrains.kotlin.plugin.compose")
extensions.getByType(CommonExtension::class.java).buildFeatures.compose = true
dependencies {
val bom = platform(libs.lib("compose-bom"))
add("implementation", bom)
add("androidTestImplementation", bom)
add("implementation", libs.lib("compose-ui"))
add("implementation", libs.lib("compose-material3"))
add("implementation", libs.lib("compose-material-icons"))
add("implementation", libs.lib("compose-ui-tooling-preview"))
add("debugImplementation", libs.lib("compose-ui-tooling"))
add("debugImplementation", libs.lib("compose-ui-test-manifest"))
add("androidTestImplementation", libs.lib("compose-ui-test-junit4"))
}
}
}
@@ -0,0 +1,22 @@
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
class AndroidFeatureConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("pixelwall.android.library")
pluginManager.apply("pixelwall.android.compose")
pluginManager.apply("pixelwall.android.hilt")
dependencies {
add("implementation", project(":core:model"))
add("implementation", project(":core:domain"))
add("implementation", project(":core:ui"))
add("implementation", libs.lib("androidx-hilt-navigation-compose"))
add("implementation", libs.lib("androidx-navigation-compose"))
add("implementation", libs.lib("androidx-lifecycle-runtime-compose"))
add("implementation", libs.lib("androidx-lifecycle-viewmodel-compose"))
add("implementation", libs.lib("kotlinx-coroutines-android"))
add("testImplementation", project(":core:testing"))
}
}
}
@@ -0,0 +1,14 @@
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
class AndroidHiltConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("com.google.devtools.ksp")
pluginManager.apply("com.google.dagger.hilt.android")
dependencies {
add("implementation", libs.lib("hilt-android"))
add("ksp", libs.lib("hilt-compiler"))
}
}
}
@@ -0,0 +1,31 @@
import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("com.android.library")
extensions.configure<LibraryExtension> {
compileSdk = libs.int("compileSdk")
defaultConfig {
minSdk = libs.int("minSdk")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
compileOptions {
sourceCompatibility = JAVA_VERSION
targetCompatibility = JAVA_VERSION
}
testOptions.unitTests.isReturnDefaultValues = true
}
configureKotlin()
dependencies {
add("testImplementation", libs.lib("junit"))
add("testImplementation", libs.lib("truth"))
add("testImplementation", libs.lib("kotlinx-coroutines-test"))
add("testImplementation", libs.lib("turbine"))
}
}
}
@@ -0,0 +1,23 @@
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
class JvmLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("org.jetbrains.kotlin.jvm")
pluginManager.apply("com.android.lint")
extensions.configure<JavaPluginExtension> {
sourceCompatibility = JAVA_VERSION
targetCompatibility = JAVA_VERSION
}
configureKotlin()
dependencies {
add("testImplementation", libs.lib("junit"))
add("testImplementation", libs.lib("truth"))
add("testImplementation", libs.lib("kotlinx-coroutines-test"))
add("testImplementation", libs.lib("turbine"))
}
}
}
@@ -0,0 +1,25 @@
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
internal val Project.libs: VersionCatalog
get() = extensions.getByType<VersionCatalogsExtension>().named("libs")
internal fun VersionCatalog.lib(alias: String) = findLibrary(alias).get()
internal fun VersionCatalog.int(alias: String) = findVersion(alias).get().requiredVersion.toInt()
internal val JAVA_VERSION = JavaVersion.VERSION_17
internal fun Project.configureKotlin() {
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
}
@@ -0,0 +1,33 @@
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
/** Bricht den Build ab, wenn das zusammengeführte Manifest Netzwerkberechtigungen enthält. */
abstract class VerifyNoInternetPermissionTask : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val mergedManifest: RegularFileProperty
@get:OutputFile
abstract val report: RegularFileProperty
@TaskAction
fun verify() {
val manifest = mergedManifest.get().asFile.readText()
val found = FORBIDDEN.filter { manifest.contains("\"$it\"") }
if (found.isNotEmpty()) {
throw GradleException("Merged manifest must not request: ${found.joinToString()}")
}
report.get().asFile.writeText("OK: no network permissions\n")
}
private companion object {
val FORBIDDEN = listOf("android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE")
}
}