From 5c2cc36e57d2b5dc6a1fa9ac7df84599e7ca9109 Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Fri, 3 Apr 2026 07:50:28 -0400 Subject: [PATCH 1/9] [ADDED] Metro snapshot to work on migration --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c75ae16..5024679 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ app/release/ *.salive app*.aab secret.properties +metro-main* From 7a24df0c3e411a0b29559f982594cae04f1cd54b Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Fri, 3 Apr 2026 08:08:14 -0400 Subject: [PATCH 2/9] [CHANGED] Phase 1: Replace Dagger/Anvil/KAPT with Metro DI - Replace kotlin.kapt and Anvil plugins with Metro Gradle plugin - Remove dagger, dagger-compiler, anvil-annotations dependencies - Add metrox-android for Activity injection support - Update Circuit KSP args: anvil codegen -> circuit.codegen.mode=metro - Bump Kotlin 2.1.10 -> 2.3.20 (JDK 25 compat, Metro 0.10.0+ required) - Bump KSP to 2.3.6 (new independent versioning from KSP 2.3.0+) - Add metro = 0.12.1 to version catalog - Migrate kotlinOptions to kotlin { compilerOptions {} } DSL Closes #272 --- app/build.gradle.kts | 26 +++++++++----------------- build.gradle.kts | 11 +++-------- gradle/libs.versions.toml | 32 +++++++++++++------------------- 3 files changed, 25 insertions(+), 44 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3bff3e0..af63348 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,10 +5,9 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.parcelize) - alias(libs.plugins.kotlin.kapt) alias(libs.plugins.kotlinter) alias(libs.plugins.ksp) - alias(libs.plugins.anvil) + id("dev.zacsweers.metro") } // Load secret.properties file for local development @@ -103,10 +102,6 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" - } - buildFeatures { compose = true buildConfig = true @@ -122,6 +117,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } +} + dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.activity.compose) @@ -147,13 +148,7 @@ dependencies { implementation(libs.core.ktx) ksp(libs.circuit.codegen) - implementation(libs.dagger) - // Dagger KSP support is in Alpha, not available yet. Using KAPT for now. - // https://dagger.dev/dev-guide/ksp.html - kapt(libs.dagger.compiler) - - implementation(libs.anvil.annotations) - implementation(libs.anvil.annotations.optional) + implementation(libs.metrox.android) // Timber implementation(libs.timber) @@ -198,10 +193,7 @@ dependencies { } ksp { - // Anvil-KSP - arg("anvil-ksp-extraContributingAnnotations", "com.slack.circuit.codegen.annotations.CircuitInject") - // kotlin-inject-anvil (requires 0.0.3+) - arg("kotlin-inject-anvil-contributing-annotations", "com.slack.circuit.codegen.annotations.CircuitInject") + arg("circuit.codegen.mode", "metro") } // Enable dynamic agent loading for tests needed by MockK diff --git a/build.gradle.kts b/build.gradle.kts index ff755f5..7e3ba8f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,16 +16,11 @@ plugins { // Project: https://developer.android.com/kotlin/parcelize alias(libs.plugins.kotlin.parcelize) apply false - // Applies the Kotlin KAPT (Kotlin Annotation Processing Tool) plugin. - // Project: https://kotlinlang.org/docs/kapt.html - alias(libs.plugins.kotlin.kapt) apply false - // Applies the Kotlin Symbol Processing (KSP) plugin. // Project: https://github.com/google/ksp alias(libs.plugins.ksp) apply false - // Applies the Anvil plugin for Dagger dependency injection. - // Project: https://github.com/square/anvil - // Also see: https://github.com/ZacSweers/anvil/blob/main/FORK.md - alias(libs.plugins.anvil) apply false + // Applies the Metro dependency injection plugin. + // Project: https://github.com/ZacSweers/metro + alias(libs.plugins.metro) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 95cbf46..2f6ef91 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,11 +3,9 @@ activityCompose = "1.12.1" # https://developer.android.com/jetpack/androidx/releases/compose-material3-adaptive adaptive = "1.2.0" agp = "8.9.2" -anvil = "0.4.1" circuit = "0.31.0" composeBom = "2025.12.00" coreKtx = "1.17.0" -dagger = "2.56.2" espressoCore = "3.7.0" googleFonts = "1.10.0" junit = "4.13.2" @@ -15,9 +13,13 @@ junitVersion = "1.3.0" kotlinxCoroutinesTest = "1.10.2" # Kotlin and KSP versions (must be in sync) -# ⚠️ Kotlin version can't be higher than `2.1.10` due to known issue with Dagger -kotlin = "2.1.10" -ksp = "2.1.10-1.0.31" +# KSP 2.3.0+ uses independent versioning (no longer tied to Kotlin version) +kotlin = "2.3.20" +ksp = "2.3.6" + +# Metro - compile-time DI framework powered by a Kotlin compiler plugin +# https://github.com/ZacSweers/metro +metro = "0.12.1" lifecycleRuntimeKtx = "2.10.0" @@ -92,15 +94,9 @@ circuitx-effects = { group = "com.slack.circuit", name = "circuitx-effects", ver circuitx-gestureNav = { group = "com.slack.circuit", name = "circuitx-gesture-navigation", version.ref = "circuit" } circuitx-overlays = { group = "com.slack.circuit", name = "circuitx-overlays", version.ref = "circuit" } -# https://dagger.dev/ -# https://dagger.dev/dev-guide/ksp -dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } -dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } - -# https://mvnrepository.com/artifact/com.squareup.anvil/annotations-optional -# https://github.com/ZacSweers/anvil/blob/main/FORK.md -anvil-annotations = { group = "dev.zacsweers.anvil", name = "annotations", version.ref = "anvil"} -anvil-annotations-optional = { group = "dev.zacsweers.anvil", name = "annotations-optional", version.ref = "anvil"} +# Metro - compile-time DI framework +# https://github.com/ZacSweers/metro +metrox-android = { group = "dev.zacsweers.metro", name = "metrox-android", version.ref = "metro" } # # https://developer.android.com/develop/ui/compose/text/fonts # https://mvnrepository.com/artifact/androidx.compose.ui/ui-text-google-fonts @@ -157,8 +153,6 @@ test-turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } - # Add @Parcelize support # https://plugins.gradle.org/plugin/org.jetbrains.kotlin.plugin.parcelize kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } @@ -168,9 +162,9 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = # https://github.com/google/ksp/releases ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"} -# Forked Anvil -# https://github.com/ZacSweers/anvil/blob/main/FORK.md -anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" } +# Metro - compile-time DI compiler plugin +# https://github.com/ZacSweers/metro +metro = { id = "dev.zacsweers.metro", version.ref = "metro" } # Kotlin linter with built-in formatter # https://github.com/jeremymailen/kotlinter-gradle From e1c91ba4e25f9d6db582bbbe7cce871c83e29427 Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Fri, 3 Apr 2026 08:27:29 -0400 Subject: [PATCH 3/9] feat: migrate DI from Dagger/Anvil to Metro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Dagger 2 + Anvil DI infrastructure with Metro framework. Changes: - Add AppGraph.kt: Metro @DependencyGraph replacing Dagger AppComponent - Remove AppComponent.kt, ActivityKey.kt, TrmnlAppComponentFactory.kt - Update all DI modules (CircuitModule, NetworkModule, DataStoreModule, WorkerModule) from Dagger @Module objects to Metro @ContributesTo interfaces - Update ApplicationContext qualifier to use dev.zacsweers.metro.Qualifier - Migrate TrmnlDisplayMirrorApp to MetroApplication pattern - Update MainActivity: ContributesMultibinding → ContributesIntoMap - Update all presenter files: dagger.assisted.* → dev.zacsweers.metro.* - Update all data/work files: javax.inject.Inject → dev.zacsweers.metro.Inject - Remove android:appComponentFactory from manifest (MetroX provides it) Closes #273 Closes #274 Closes #275 Closes #276 --- app/src/main/AndroidManifest.xml | 4 +- .../java/ink/trmnl/android/MainActivity.kt | 9 +- .../trmnl/android/TrmnlDisplayMirrorApp.kt | 19 ++-- .../trmnl/android/data/ImageMetadataStore.kt | 2 +- .../data/TrmnlDeviceConfigDataStore.kt | 4 +- .../android/data/TrmnlDisplayRepository.kt | 4 +- .../trmnl/android/data/TrmnlUserRepository.kt | 4 +- .../android/data/log/RefreshLogExporter.kt | 2 +- .../data/log/TrmnlRefreshLogManager.kt | 4 +- .../java/ink/trmnl/android/di/ActivityKey.kt | 12 --- .../java/ink/trmnl/android/di/AppComponent.kt | 34 ------ .../java/ink/trmnl/android/di/AppGraph.kt | 35 ++++++ .../trmnl/android/di/ApplicationContext.kt | 2 +- .../ink/trmnl/android/di/CircuitModule.kt | 22 ++-- .../ink/trmnl/android/di/DataStoreModule.kt | 10 +- .../ink/trmnl/android/di/NetworkModule.kt | 16 ++- .../android/di/TrmnlAppComponentFactory.kt | 100 ------------------ .../java/ink/trmnl/android/di/WorkerModule.kt | 10 +- .../android/ui/aboutapp/AppInfoScreen.kt | 6 +- .../devicemodel/DeviceModelSelectorScreen.kt | 6 +- .../ui/display/TrmnlMirrorDisplayScreen.kt | 6 +- .../ui/refreshlog/DisplayRefreshLogScreen.kt | 6 +- .../android/ui/settings/AppSettingsScreen.kt | 6 +- .../android/util/AndroidDeviceInfoProvider.kt | 4 +- .../android/work/TrmnlImageRefreshWorker.kt | 2 +- .../android/work/TrmnlImageUpdateManager.kt | 4 +- .../trmnl/android/work/TrmnlWorkScheduler.kt | 4 +- .../trmnl/android/work/TrmnlWorkerFactory.kt | 4 +- 28 files changed, 111 insertions(+), 230 deletions(-) delete mode 100644 app/src/main/java/ink/trmnl/android/di/ActivityKey.kt delete mode 100644 app/src/main/java/ink/trmnl/android/di/AppComponent.kt create mode 100644 app/src/main/java/ink/trmnl/android/di/AppGraph.kt delete mode 100644 app/src/main/java/ink/trmnl/android/di/TrmnlAppComponentFactory.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6c78770..403b77f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,6 @@ + android:enableOnBackInvokedCallback="true"> ()) @ActivityKey(MainActivity::class) class MainActivity @Inject diff --git a/app/src/main/java/ink/trmnl/android/TrmnlDisplayMirrorApp.kt b/app/src/main/java/ink/trmnl/android/TrmnlDisplayMirrorApp.kt index dc70568..5ada93a 100644 --- a/app/src/main/java/ink/trmnl/android/TrmnlDisplayMirrorApp.kt +++ b/app/src/main/java/ink/trmnl/android/TrmnlDisplayMirrorApp.kt @@ -3,23 +3,23 @@ package ink.trmnl.android import android.app.Application import android.util.Log import androidx.work.Configuration -import ink.trmnl.android.di.AppComponent -import ink.trmnl.android.work.TrmnlWorkerFactory +import dev.zacsweers.metro.createGraphFactory +import dev.zacsweers.metrox.android.MetroAppComponentProviders +import dev.zacsweers.metrox.android.MetroApplication +import ink.trmnl.android.di.AppGraph import timber.log.Timber -import javax.inject.Inject /** * Application class for the app with key initializations. */ class TrmnlDisplayMirrorApp : Application(), + MetroApplication, Configuration.Provider { - private val appComponent: AppComponent by lazy { AppComponent.create(this) } + private val appGraph by lazy { createGraphFactory().create(this) } - fun appComponent(): AppComponent = appComponent - - @Inject - lateinit var workerFactory: TrmnlWorkerFactory + override val appComponentProviders: MetroAppComponentProviders + get() = appGraph override val workManagerConfiguration: Configuration get() { @@ -27,14 +27,13 @@ class TrmnlDisplayMirrorApp : return Configuration .Builder() .setMinimumLoggingLevel(if (BuildConfig.DEBUG) Log.DEBUG else Log.WARN) - .setWorkerFactory(workerFactory) + .setWorkerFactory(appGraph.workerFactory) .build() } override fun onCreate() { super.onCreate() installLoggingTree() - appComponent.inject(this) } private fun installLoggingTree() { diff --git a/app/src/main/java/ink/trmnl/android/data/ImageMetadataStore.kt b/app/src/main/java/ink/trmnl/android/data/ImageMetadataStore.kt index 0e12621..d62110d 100644 --- a/app/src/main/java/ink/trmnl/android/data/ImageMetadataStore.kt +++ b/app/src/main/java/ink/trmnl/android/data/ImageMetadataStore.kt @@ -8,6 +8,7 @@ import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import dev.zacsweers.metro.Inject import ink.trmnl.android.di.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -15,7 +16,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking import timber.log.Timber import java.time.Instant -import javax.inject.Inject private val Context.imageDataStore: DataStore by preferencesDataStore( name = "trmnl_image_metadata", diff --git a/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt b/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt index 1473a00..126214e 100644 --- a/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt +++ b/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt @@ -8,9 +8,10 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore -import com.squareup.anvil.annotations.optional.SingleIn import com.squareup.moshi.Moshi import com.squareup.moshi.Types +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import ink.trmnl.android.data.AppConfig.DEFAULT_REFRESH_INTERVAL_SEC import ink.trmnl.android.data.AppConfig.TRMNL_API_SERVER_BASE_URL import ink.trmnl.android.di.AppScope @@ -24,7 +25,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking import timber.log.Timber -import javax.inject.Inject /** * DataStore for managing TRMNL device configuration. diff --git a/app/src/main/java/ink/trmnl/android/data/TrmnlDisplayRepository.kt b/app/src/main/java/ink/trmnl/android/data/TrmnlDisplayRepository.kt index b1c2b01..9823305 100644 --- a/app/src/main/java/ink/trmnl/android/data/TrmnlDisplayRepository.kt +++ b/app/src/main/java/ink/trmnl/android/data/TrmnlDisplayRepository.kt @@ -2,7 +2,8 @@ package ink.trmnl.android.data import com.slack.eithernet.ApiResult import com.slack.eithernet.exceptionOrNull -import com.squareup.anvil.annotations.optional.SingleIn +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import ink.trmnl.android.di.AppScope import ink.trmnl.android.model.SupportedDeviceModel import ink.trmnl.android.model.TrmnlDeviceConfig @@ -20,7 +21,6 @@ import ink.trmnl.android.util.AndroidDeviceInfoProvider import ink.trmnl.android.util.HTTP_500 import ink.trmnl.android.util.isHttpOk import timber.log.Timber -import javax.inject.Inject /** * Repository class responsible for fetching and mapping display data. diff --git a/app/src/main/java/ink/trmnl/android/data/TrmnlUserRepository.kt b/app/src/main/java/ink/trmnl/android/data/TrmnlUserRepository.kt index 697e78b..ff74cf1 100644 --- a/app/src/main/java/ink/trmnl/android/data/TrmnlUserRepository.kt +++ b/app/src/main/java/ink/trmnl/android/data/TrmnlUserRepository.kt @@ -2,7 +2,8 @@ package ink.trmnl.android.data import com.slack.eithernet.ApiResult import com.slack.eithernet.exceptionOrNull -import com.squareup.anvil.annotations.optional.SingleIn +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import ink.trmnl.android.di.AppScope import ink.trmnl.android.model.TrmnlDeviceConfig import ink.trmnl.android.network.TrmnlUserApiService @@ -11,7 +12,6 @@ import ink.trmnl.android.network.model.TrmnlUser import ink.trmnl.android.network.util.constructApiUrl import ink.trmnl.android.util.AndroidDeviceInfoProvider import timber.log.Timber -import javax.inject.Inject /** * Repository class for user-level TRMNL API operations. diff --git a/app/src/main/java/ink/trmnl/android/data/log/RefreshLogExporter.kt b/app/src/main/java/ink/trmnl/android/data/log/RefreshLogExporter.kt index 3bb7d8d..964f0ac 100644 --- a/app/src/main/java/ink/trmnl/android/data/log/RefreshLogExporter.kt +++ b/app/src/main/java/ink/trmnl/android/data/log/RefreshLogExporter.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.widget.Toast import androidx.core.content.FileProvider import com.squareup.moshi.Moshi +import dev.zacsweers.metro.Inject import ink.trmnl.android.di.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -13,7 +14,6 @@ import java.io.File import java.text.SimpleDateFormat import java.time.Instant import java.util.Locale -import javax.inject.Inject /** * Handles exporting refresh logs to a JSON file and sharing it via Android's share intent. diff --git a/app/src/main/java/ink/trmnl/android/data/log/TrmnlRefreshLogManager.kt b/app/src/main/java/ink/trmnl/android/data/log/TrmnlRefreshLogManager.kt index 00a466d..87e1d29 100644 --- a/app/src/main/java/ink/trmnl/android/data/log/TrmnlRefreshLogManager.kt +++ b/app/src/main/java/ink/trmnl/android/data/log/TrmnlRefreshLogManager.kt @@ -2,7 +2,8 @@ package ink.trmnl.android.data.log import android.content.Context import androidx.datastore.core.DataStore -import com.squareup.anvil.annotations.optional.SingleIn +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import ink.trmnl.android.data.AppConfig.MAX_LOG_ENTRIES import ink.trmnl.android.data.HttpResponseMetadata import ink.trmnl.android.di.AppScope @@ -12,7 +13,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map import timber.log.Timber -import javax.inject.Inject /** * Manages logs related to terminal refresh operations. diff --git a/app/src/main/java/ink/trmnl/android/di/ActivityKey.kt b/app/src/main/java/ink/trmnl/android/di/ActivityKey.kt deleted file mode 100644 index 0769bb9..0000000 --- a/app/src/main/java/ink/trmnl/android/di/ActivityKey.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ink.trmnl.android.di - -import android.app.Activity -import dagger.MapKey -import kotlin.reflect.KClass - -/** - * A Dagger multi-binding key used for registering a [Activity] into the top level dagger graphs. - */ -@MapKey annotation class ActivityKey( - val value: KClass, -) diff --git a/app/src/main/java/ink/trmnl/android/di/AppComponent.kt b/app/src/main/java/ink/trmnl/android/di/AppComponent.kt deleted file mode 100644 index 5bfb2ea..0000000 --- a/app/src/main/java/ink/trmnl/android/di/AppComponent.kt +++ /dev/null @@ -1,34 +0,0 @@ -package ink.trmnl.android.di - -import android.app.Activity -import android.content.Context -import com.squareup.anvil.annotations.MergeComponent -import com.squareup.anvil.annotations.optional.SingleIn -import dagger.BindsInstance -import ink.trmnl.android.TrmnlDisplayMirrorApp -import javax.inject.Provider - -@MergeComponent( - scope = AppScope::class, - modules = [CircuitModule::class], -) -@SingleIn(AppScope::class) -interface AppComponent { - val activityProviders: Map, @JvmSuppressWildcards Provider> - - /** - * Injects dependencies into [TrmnlDisplayMirrorApp]. - */ - fun inject(app: TrmnlDisplayMirrorApp) - - @MergeComponent.Factory - interface Factory { - fun create( - @ApplicationContext @BindsInstance context: Context, - ): AppComponent - } - - companion object { - fun create(context: Context): AppComponent = DaggerAppComponent.factory().create(context) - } -} diff --git a/app/src/main/java/ink/trmnl/android/di/AppGraph.kt b/app/src/main/java/ink/trmnl/android/di/AppGraph.kt new file mode 100644 index 0000000..3e287d8 --- /dev/null +++ b/app/src/main/java/ink/trmnl/android/di/AppGraph.kt @@ -0,0 +1,35 @@ +package ink.trmnl.android.di + +import android.content.Context +import dev.zacsweers.metro.DependencyGraph +import dev.zacsweers.metro.Provides +import dev.zacsweers.metrox.android.MetroAppComponentProviders +import ink.trmnl.android.work.TrmnlWorkerFactory + +/** + * Metro dependency graph for the application. + * + * - [MetroAppComponentProviders] enables constructor injection of Activities via + * [dev.zacsweers.metrox.android.MetroAppComponentFactory]. + * - [TrmnlWorkerFactory] is exposed so [ink.trmnl.android.TrmnlDisplayMirrorApp] can + * configure WorkManager. + */ +@DependencyGraph(AppScope::class) +interface AppGraph : MetroAppComponentProviders { + val workerFactory: TrmnlWorkerFactory + + /** + * Re-exposes the raw [Context] with the [ApplicationContext] qualifier so that + * module contributors can inject it by qualifier. + */ + @Provides + @ApplicationContext + fun provideApplicationContext(context: Context): Context = context + + @DependencyGraph.Factory + fun interface Factory { + fun create( + @Provides context: Context, + ): AppGraph + } +} diff --git a/app/src/main/java/ink/trmnl/android/di/ApplicationContext.kt b/app/src/main/java/ink/trmnl/android/di/ApplicationContext.kt index 76541c1..8a03379 100644 --- a/app/src/main/java/ink/trmnl/android/di/ApplicationContext.kt +++ b/app/src/main/java/ink/trmnl/android/di/ApplicationContext.kt @@ -1,6 +1,6 @@ package ink.trmnl.android.di -import javax.inject.Qualifier +import dev.zacsweers.metro.Qualifier /** Qualifier to denote a `Context` that is specifically an Application context. */ @Qualifier annotation class ApplicationContext diff --git a/app/src/main/java/ink/trmnl/android/di/CircuitModule.kt b/app/src/main/java/ink/trmnl/android/di/CircuitModule.kt index 56f3965..730eccc 100644 --- a/app/src/main/java/ink/trmnl/android/di/CircuitModule.kt +++ b/app/src/main/java/ink/trmnl/android/di/CircuitModule.kt @@ -3,27 +3,27 @@ package ink.trmnl.android.di import com.slack.circuit.foundation.Circuit import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.ui.Ui -import com.squareup.anvil.annotations.ContributesTo -import com.squareup.anvil.annotations.optional.SingleIn -import dagger.Module -import dagger.Provides -import dagger.multibindings.Multibinds +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Multibinds +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn /** - * Dagger module that provides dependencies for the Circuit framework. + * Metro module that provides dependencies for the Circuit framework. */ @ContributesTo(AppScope::class) -@Module interface CircuitModule { /** - * Dagger multi-binding method that provides a set of Presenter.Factory instances. + * Metro multi-binding declaration for Presenter.Factory instances contributed via + * @ContributesIntoSet. */ - @Multibinds fun presenterFactories(): Set + @Multibinds val presenterFactories: Set /** - * Dagger multi-binding method that provides a set of Ui.Factory instances. + * Metro multi-binding declaration for Ui.Factory instances contributed via + * @ContributesIntoSet. */ - @Multibinds fun viewFactories(): Set + @Multibinds val viewFactories: Set companion object { /** diff --git a/app/src/main/java/ink/trmnl/android/di/DataStoreModule.kt b/app/src/main/java/ink/trmnl/android/di/DataStoreModule.kt index 5dfa3ee..0d34755 100644 --- a/app/src/main/java/ink/trmnl/android/di/DataStoreModule.kt +++ b/app/src/main/java/ink/trmnl/android/di/DataStoreModule.kt @@ -4,19 +4,17 @@ import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile -import com.squareup.anvil.annotations.ContributesTo -import com.squareup.anvil.annotations.optional.SingleIn -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn import ink.trmnl.android.data.log.TrmnlRefreshLogSerializer import ink.trmnl.android.data.log.TrmnlRefreshLogs import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -@Module @ContributesTo(AppScope::class) -object DataStoreModule { +interface DataStoreModule { @Provides @SingleIn(AppScope::class) fun provideActivityLogDataStore( diff --git a/app/src/main/java/ink/trmnl/android/di/NetworkModule.kt b/app/src/main/java/ink/trmnl/android/di/NetworkModule.kt index 27280c1..ada4465 100644 --- a/app/src/main/java/ink/trmnl/android/di/NetworkModule.kt +++ b/app/src/main/java/ink/trmnl/android/di/NetworkModule.kt @@ -4,12 +4,11 @@ import android.content.Context import androidx.core.content.pm.PackageInfoCompat import com.slack.eithernet.integration.retrofit.ApiResultCallAdapterFactory import com.slack.eithernet.integration.retrofit.ApiResultConverterFactory -import com.squareup.anvil.annotations.ContributesTo -import com.squareup.anvil.annotations.optional.SingleIn import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn import ink.trmnl.android.BuildConfig import ink.trmnl.android.network.RateLimitInterceptor import ink.trmnl.android.network.TrmnlApiService @@ -22,12 +21,11 @@ import retrofit2.converter.moshi.MoshiConverterFactory import java.io.File import java.util.concurrent.TimeUnit -@Module -@ContributesTo(AppScope::class) -object NetworkModule { - // Cache size for OkHttp (10 MB) - private const val CACHE_SIZE = 10 * 1024 * 1024L +// Cache size for OkHttp (10 MB) +private const val CACHE_SIZE = 10 * 1024 * 1024L +@ContributesTo(AppScope::class) +interface NetworkModule { @Provides @SingleIn(AppScope::class) fun provideRateLimitInterceptor(): RateLimitInterceptor = RateLimitInterceptor() diff --git a/app/src/main/java/ink/trmnl/android/di/TrmnlAppComponentFactory.kt b/app/src/main/java/ink/trmnl/android/di/TrmnlAppComponentFactory.kt deleted file mode 100644 index 14dbfe1..0000000 --- a/app/src/main/java/ink/trmnl/android/di/TrmnlAppComponentFactory.kt +++ /dev/null @@ -1,100 +0,0 @@ -package ink.trmnl.android.di - -import android.app.Activity -import android.app.Application -import android.content.Intent -import androidx.annotation.Keep -import androidx.core.app.AppComponentFactory -import ink.trmnl.android.TrmnlDisplayMirrorApp -import javax.inject.Provider - -/** - * Custom implementation of [AppComponentFactory] used to inject Android components - * (specifically Activities) via Dagger using constructor injection. This factory - * allows the Android system to delegate activity instantiation to Dagger's dependency - * graph, enabling constructor injection instead of field injection. - * - * This class is referenced in the `AndroidManifest` within the `` tag. - * - * Usage: - * Add the following to your AndroidManifest.xml: - * - * ```xml - * - * ``` - */ -@Keep -class TrmnlAppComponentFactory : AppComponentFactory() { - /** - * Retrieves an instance of the specified class (typically an Activity) from the provided - * Dagger providers map. If a provider exists for the class, it uses that provider to - * obtain the instance; otherwise, it returns null. - * - * @param T The type of the class being retrieved. - * @param classLoader The ClassLoader used to load the class. - * @param className The fully qualified name of the class to be instantiated. - * @param providers A map containing Dagger providers for the available classes. - * @return The instance of the class if found in the providers map, or null if not. - */ - private inline fun getInstance( - classLoader: ClassLoader, - className: String, - providers: Map, @JvmSuppressWildcards Provider>, - ): T? { - // Load the class using the provided ClassLoader and attempt to retrieve the instance. - val clazz = Class.forName(className, false, classLoader).asSubclass(T::class.java) - val modelProvider = providers[clazz] ?: return null - return modelProvider.get() as T - } - - /** - * Called by the Android system to instantiate an activity. This method checks if the - * activity can be provided by the Dagger component. If the Dagger component can provide - * the activity, it returns the injected instance; otherwise, it falls back to the - * default system behavior. - * - * @param classLoader The ClassLoader used to load the activity class. - * @param className The fully qualified name of the activity to be instantiated. - * @param intent The intent that was used to start the activity. - * @return The activity instance, either from Dagger or from the system. - */ - override fun instantiateActivityCompat( - classLoader: ClassLoader, - className: String, - intent: Intent?, - ): Activity = - getInstance(classLoader, className, activityProviders) - ?: super.instantiateActivityCompat(classLoader, className, intent) - - /** - * Called by the Android system to instantiate the Application class. This method - * initializes the Dagger component and retrieves the map of activity providers, - * which are used later for activity injection. - * - * @param classLoader The ClassLoader used to load the Application class. - * @param className The fully qualified name of the Application class. - * @return The Application instance. - */ - override fun instantiateApplicationCompat( - classLoader: ClassLoader, - className: String, - ): Application { - val app = super.instantiateApplicationCompat(classLoader, className) - // Retrieve the Dagger app component and the activity providers from it - activityProviders = (app as TrmnlDisplayMirrorApp).appComponent().activityProviders - return app - } - - /** - * Companion object to store activity providers. This object holds the Dagger-provided - * map of activity classes to their corresponding providers. It's used to inject activities - * upon instantiation. - * - * This map is initialized when the application is created via the Dagger component. - */ - companion object { - private lateinit var activityProviders: Map, Provider> - } -} diff --git a/app/src/main/java/ink/trmnl/android/di/WorkerModule.kt b/app/src/main/java/ink/trmnl/android/di/WorkerModule.kt index 9f1a1e5..41e9034 100644 --- a/app/src/main/java/ink/trmnl/android/di/WorkerModule.kt +++ b/app/src/main/java/ink/trmnl/android/di/WorkerModule.kt @@ -3,15 +3,13 @@ package ink.trmnl.android.di import android.content.Context import androidx.work.Configuration import androidx.work.WorkManager -import com.squareup.anvil.annotations.ContributesTo -import com.squareup.anvil.annotations.optional.SingleIn -import dagger.Module -import dagger.Provides +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn import ink.trmnl.android.work.TrmnlWorkerFactory -@Module @ContributesTo(AppScope::class) -object WorkerModule { +interface WorkerModule { @Provides @SingleIn(AppScope::class) fun provideWorkManager( diff --git a/app/src/main/java/ink/trmnl/android/ui/aboutapp/AppInfoScreen.kt b/app/src/main/java/ink/trmnl/android/ui/aboutapp/AppInfoScreen.kt index 2c7b2c2..286a1f5 100644 --- a/app/src/main/java/ink/trmnl/android/ui/aboutapp/AppInfoScreen.kt +++ b/app/src/main/java/ink/trmnl/android/ui/aboutapp/AppInfoScreen.kt @@ -37,9 +37,9 @@ import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.screen.Screen -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import ink.trmnl.android.BuildConfig import ink.trmnl.android.R import ink.trmnl.android.data.AppConfig.TRMNL_ANDROID_APP_GITHUB_URL diff --git a/app/src/main/java/ink/trmnl/android/ui/devicemodel/DeviceModelSelectorScreen.kt b/app/src/main/java/ink/trmnl/android/ui/devicemodel/DeviceModelSelectorScreen.kt index 45fdc49..6a8742f 100644 --- a/app/src/main/java/ink/trmnl/android/ui/devicemodel/DeviceModelSelectorScreen.kt +++ b/app/src/main/java/ink/trmnl/android/ui/devicemodel/DeviceModelSelectorScreen.kt @@ -43,9 +43,9 @@ import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.screen.PopResult import com.slack.circuit.runtime.screen.Screen -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import ink.trmnl.android.data.AppConfig.TRMNL_API_SERVER_BASE_URL import ink.trmnl.android.data.TrmnlDisplayRepository import ink.trmnl.android.di.AppScope diff --git a/app/src/main/java/ink/trmnl/android/ui/display/TrmnlMirrorDisplayScreen.kt b/app/src/main/java/ink/trmnl/android/ui/display/TrmnlMirrorDisplayScreen.kt index 429be10..d69f454 100644 --- a/app/src/main/java/ink/trmnl/android/ui/display/TrmnlMirrorDisplayScreen.kt +++ b/app/src/main/java/ink/trmnl/android/ui/display/TrmnlMirrorDisplayScreen.kt @@ -47,9 +47,9 @@ import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.screen.Screen -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import ink.trmnl.android.R import ink.trmnl.android.data.AppConfig.AUTO_HIDE_APP_CONFIG_WINDOW_MS import ink.trmnl.android.data.ImageMetadataStore diff --git a/app/src/main/java/ink/trmnl/android/ui/refreshlog/DisplayRefreshLogScreen.kt b/app/src/main/java/ink/trmnl/android/ui/refreshlog/DisplayRefreshLogScreen.kt index 58ee516..ec224f8 100644 --- a/app/src/main/java/ink/trmnl/android/ui/refreshlog/DisplayRefreshLogScreen.kt +++ b/app/src/main/java/ink/trmnl/android/ui/refreshlog/DisplayRefreshLogScreen.kt @@ -55,9 +55,9 @@ import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.screen.Screen -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import ink.trmnl.android.BuildConfig import ink.trmnl.android.R import ink.trmnl.android.data.AppConfig.DEFAULT_REFRESH_INTERVAL_SEC diff --git a/app/src/main/java/ink/trmnl/android/ui/settings/AppSettingsScreen.kt b/app/src/main/java/ink/trmnl/android/ui/settings/AppSettingsScreen.kt index 51dcbfd..367c7ea 100644 --- a/app/src/main/java/ink/trmnl/android/ui/settings/AppSettingsScreen.kt +++ b/app/src/main/java/ink/trmnl/android/ui/settings/AppSettingsScreen.kt @@ -76,9 +76,9 @@ import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuit.runtime.screen.Screen -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import ink.trmnl.android.R import ink.trmnl.android.data.AppConfig.DEFAULT_REFRESH_INTERVAL_SEC import ink.trmnl.android.data.AppConfig.TRMNL_API_SERVER_BASE_URL diff --git a/app/src/main/java/ink/trmnl/android/util/AndroidDeviceInfoProvider.kt b/app/src/main/java/ink/trmnl/android/util/AndroidDeviceInfoProvider.kt index 610a199..3b51598 100644 --- a/app/src/main/java/ink/trmnl/android/util/AndroidDeviceInfoProvider.kt +++ b/app/src/main/java/ink/trmnl/android/util/AndroidDeviceInfoProvider.kt @@ -3,11 +3,11 @@ package ink.trmnl.android.util import android.content.Context import android.net.wifi.WifiManager import android.os.BatteryManager -import com.squareup.anvil.annotations.optional.SingleIn +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import ink.trmnl.android.di.AppScope import ink.trmnl.android.di.ApplicationContext import timber.log.Timber -import javax.inject.Inject /** * Provider class for accessing Android device information. diff --git a/app/src/main/java/ink/trmnl/android/work/TrmnlImageRefreshWorker.kt b/app/src/main/java/ink/trmnl/android/work/TrmnlImageRefreshWorker.kt index bc92059..99bc034 100644 --- a/app/src/main/java/ink/trmnl/android/work/TrmnlImageRefreshWorker.kt +++ b/app/src/main/java/ink/trmnl/android/work/TrmnlImageRefreshWorker.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import androidx.work.workDataOf +import dev.zacsweers.metro.Inject import ink.trmnl.android.MainActivity import ink.trmnl.android.data.ImageMetadataStore import ink.trmnl.android.data.TrmnlDeviceConfigDataStore @@ -21,7 +22,6 @@ import ink.trmnl.android.work.RefreshWorkResult.SUCCESS import ink.trmnl.android.work.TrmnlWorkScheduler.Companion.IMAGE_REFRESH_PERIODIC_WORK_TAG import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber -import javax.inject.Inject /** * Worker to refresh the image displayed on the TRMNL display. diff --git a/app/src/main/java/ink/trmnl/android/work/TrmnlImageUpdateManager.kt b/app/src/main/java/ink/trmnl/android/work/TrmnlImageUpdateManager.kt index 79314d1..f8eafaf 100644 --- a/app/src/main/java/ink/trmnl/android/work/TrmnlImageUpdateManager.kt +++ b/app/src/main/java/ink/trmnl/android/work/TrmnlImageUpdateManager.kt @@ -1,10 +1,10 @@ package ink.trmnl.android.work -import com.squareup.anvil.annotations.optional.SingleIn +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import ink.trmnl.android.data.ImageMetadata import ink.trmnl.android.data.ImageMetadataStore import ink.trmnl.android.di.AppScope -import jakarta.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/app/src/main/java/ink/trmnl/android/work/TrmnlWorkScheduler.kt b/app/src/main/java/ink/trmnl/android/work/TrmnlWorkScheduler.kt index 22714ea..c6f6734 100644 --- a/app/src/main/java/ink/trmnl/android/work/TrmnlWorkScheduler.kt +++ b/app/src/main/java/ink/trmnl/android/work/TrmnlWorkScheduler.kt @@ -13,7 +13,8 @@ import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkQuery import androidx.work.workDataOf -import com.squareup.anvil.annotations.optional.SingleIn +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import ink.trmnl.android.data.TrmnlDeviceConfigDataStore import ink.trmnl.android.di.AppScope import ink.trmnl.android.di.ApplicationContext @@ -23,7 +24,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import timber.log.Timber import java.util.concurrent.TimeUnit -import javax.inject.Inject /** * Manages the scheduling and execution of background work using WorkManager. diff --git a/app/src/main/java/ink/trmnl/android/work/TrmnlWorkerFactory.kt b/app/src/main/java/ink/trmnl/android/work/TrmnlWorkerFactory.kt index 6fa87b3..d0b73b2 100644 --- a/app/src/main/java/ink/trmnl/android/work/TrmnlWorkerFactory.kt +++ b/app/src/main/java/ink/trmnl/android/work/TrmnlWorkerFactory.kt @@ -4,9 +4,9 @@ import android.content.Context import androidx.work.ListenableWorker import androidx.work.WorkerFactory import androidx.work.WorkerParameters -import com.squareup.anvil.annotations.optional.SingleIn +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn import ink.trmnl.android.di.AppScope -import javax.inject.Inject @SingleIn(AppScope::class) class TrmnlWorkerFactory From 6db244eff472cfd1ddc78ff0ce1a465fab0c5234 Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Fri, 3 Apr 2026 08:43:23 -0400 Subject: [PATCH 4/9] [FIXES] Min SDK 16s Run ./gradlew lintKotlin testDebugUnitTest --parallel --daemon Starting a Gradle Daemon (subsequent builds will be faster) Parallel Configuration Cache is an incubating feature. Calculating task graph as no cached configuration is available for tasks: lintKotlin testDebugUnitTest gradle/actions: Writing build results to /home/runner/work/_temp/.gradle-actions/build-results/__run-1775220043424.json [Incubating] Problems report is available at: file:///home/runner/work/trmnl-android/trmnl-android/build/reports/problems/problems-report.html FAILURE: Build failed with an exception. * What went wrong: A problem occurred configuring root project 'TRMNL'. > Could not resolve all artifacts for configuration 'classpath'. > Could not resolve dev.zacsweers.metro:gradle-plugin:0.12.1. Required by: root project : > dev.zacsweers.metro:dev.zacsweers.metro.gradle.plugin:0.12.1 Configuration cache entry stored. > Dependency requires at least JVM runtime version 21. This build uses a Java 17 JVM. --- .github/workflows/android-lint.yml | 2 +- .github/workflows/android-release.yml | 2 +- .github/workflows/android.yml | 2 +- .github/workflows/test-keystore-apk-signing.yml | 2 +- .github/workflows/test-keystore.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/android-lint.yml b/.github/workflows/android-lint.yml index 1763b58..295616c 100644 --- a/.github/workflows/android-lint.yml +++ b/.github/workflows/android-lint.yml @@ -15,7 +15,7 @@ jobs: - name: set up JDK uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' cache: gradle diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index 6de9f65..ceb53dd 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -30,7 +30,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' cache: gradle diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index b3c8b69..b35e7d7 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -15,7 +15,7 @@ jobs: - name: set up JDK uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' cache: gradle diff --git a/.github/workflows/test-keystore-apk-signing.yml b/.github/workflows/test-keystore-apk-signing.yml index c150a3f..5555b72 100644 --- a/.github/workflows/test-keystore-apk-signing.yml +++ b/.github/workflows/test-keystore-apk-signing.yml @@ -17,7 +17,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' - name: Create test directory diff --git a/.github/workflows/test-keystore.yml b/.github/workflows/test-keystore.yml index 2b757ae..0ecb862 100644 --- a/.github/workflows/test-keystore.yml +++ b/.github/workflows/test-keystore.yml @@ -27,7 +27,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v4 with: - java-version: '17' # Using a stable JDK version + java-version: '21' # Using a stable JDK version distribution: 'temurin' - name: Verify Java and Keytool Installation From ca3c35fab631d7baf2acaafde46127b9760da0ab Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Fri, 3 Apr 2026 08:46:30 -0400 Subject: [PATCH 5/9] chore: bump JVM target from 17 to 21 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Metro gradle plugin requires JVM 21+. Update all JDK 17 references: - app/build.gradle.kts: sourceCompatibility, targetCompatibility, jvmTarget - .devcontainer: base image java:1-17-bookworm → java:1-21-bookworm - docs: .github/copilot-instructions.md and CLAUDE.md --- .devcontainer/README.md | 2 +- .devcontainer/devcontainer.json | 2 +- .github/copilot-instructions.md | 4 ++-- CLAUDE.md | 2 +- app/build.gradle.kts | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 31a00d0..b06ca1b 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -4,7 +4,7 @@ This directory contains the development container configuration for TRMNL Androi ## Features -- **Base Image**: Java 17 (Bookworm) +- **Base Image**: Java 21 (Bookworm) - **Android SDK**: Manually installed via post-create script - **Android Command Line Tools**: Version 11076708 - **Platform Tools**: Latest version diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 684ba3a..03214b4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "TRMNL Android", - "image": "mcr.microsoft.com/devcontainers/java:1-17-bookworm", + "image": "mcr.microsoft.com/devcontainers/java:1-21-bookworm", "features": { "ghcr.io/devcontainers/features/git:1": {}, diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 810378e..849b0c2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -55,7 +55,7 @@ This document provides essential information for GitHub Copilot agents working o ## Environment Requirements -- **JDK Version:** 17 (OpenJDK 17 - Temurin distribution recommended) +- **JDK Version:** 21 (OpenJDK 21 - Temurin distribution recommended) - **Gradle:** 8.13 (via wrapper, do NOT install manually) - **Android SDK:** Compile SDK 36 - **Build Tools:** Managed by Gradle plugin (AGP 8.9.2) @@ -199,7 +199,7 @@ See `RELEASE_CHECKLIST.md` for complete release process. ## Trust These Instructions These instructions have been validated by running all commands in a clean repository clone. If you encounter issues not documented here: -1. First verify you're using JDK 17 and the Gradle wrapper (`./gradlew`) +1. First verify you're using JDK 21 and the Gradle wrapper (`./gradlew`) 2. Try `./gradlew clean` then retry the command 3. Check if similar issues exist in closed GitHub issues 4. Only search the codebase if these instructions prove incomplete or incorrect diff --git a/CLAUDE.md b/CLAUDE.md index 2588ddf..b06d392 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,7 @@ TRMNL Android is a native Android app that displays TRMNL e-ink device content o ``` **Notes:** -- JDK: Minimum JDK 17, but JDK 21+ works (backward compatible) +- JDK: Minimum JDK 21 (required by Metro gradle plugin) - ALWAYS use `./gradlew` wrapper, NEVER system gradle - First build: ~1-2 minutes (optimized with G1GC, parallel execution, configuration cache) - Subsequent builds: ~2-8 seconds with Gradle cache and configuration cache diff --git a/app/build.gradle.kts b/app/build.gradle.kts index af63348..7d5e226 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -98,8 +98,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } buildFeatures { @@ -119,7 +119,7 @@ android { kotlin { compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) } } From 74d736652283fd59f8c983892b1a1e76db605d4a Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Fri, 3 Apr 2026 09:12:21 -0400 Subject: [PATCH 6/9] [IGNORE] Lint because API 29+ --- app/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 403b77f..e25a6f1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,7 +20,8 @@ + android:theme="@style/Theme.App" + tools:ignore="Instantiatable"> From fd3d001d7aa12453510dce549d80d93314185633 Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Fri, 3 Apr 2026 09:15:57 -0400 Subject: [PATCH 7/9] Ignore warning w: file:///Users/hossain/dev/repos/android-apps/trmnl-android/app/src/test/java/ink/trmnl/android/util/AndroidDeviceInfoProviderTest.kt:113:29 'val connectionInfo: WifiInfo!' is deprecated. Deprecated in Java. --- .../java/ink/trmnl/android/util/AndroidDeviceInfoProviderTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/test/java/ink/trmnl/android/util/AndroidDeviceInfoProviderTest.kt b/app/src/test/java/ink/trmnl/android/util/AndroidDeviceInfoProviderTest.kt index c59f2e5..f727c1a 100644 --- a/app/src/test/java/ink/trmnl/android/util/AndroidDeviceInfoProviderTest.kt +++ b/app/src/test/java/ink/trmnl/android/util/AndroidDeviceInfoProviderTest.kt @@ -15,6 +15,7 @@ import org.junit.Test /** * Unit tests for [AndroidDeviceInfoProvider]. */ +@Suppress("DEPRECATION") class AndroidDeviceInfoProviderTest { private lateinit var context: Context private lateinit var provider: AndroidDeviceInfoProvider From 4d7685faaf6376d85f445e5d972ce97ac9a67459 Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Fri, 3 Apr 2026 09:21:01 -0400 Subject: [PATCH 8/9] fix: trim access token on save to prevent illegal header chars Trailing newlines (0x0a) in pasted tokens caused OkHttp to throw IllegalArgumentException when setting the access-token header. Trimming on save prevents whitespace from reaching the network layer. --- .../java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt b/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt index 126214e..024168a 100644 --- a/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt +++ b/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt @@ -377,7 +377,7 @@ class TrmnlDeviceConfigDataStore */ suspend fun saveAccessToken(token: String) { context.deviceConfigStore.edit { preferences -> - preferences[ACCESS_TOKEN_KEY] = token + preferences[ACCESS_TOKEN_KEY] = token.trim() } } From 1ecb17a06fe46c845ae2106e1a1b2c5a5b35260a Mon Sep 17 00:00:00 2001 From: Hossain Khan Date: Fri, 3 Apr 2026 09:23:22 -0400 Subject: [PATCH 9/9] docs: update all Dagger/Anvil/KAPT references to Metro Replace stale DI documentation across the codebase: - CONTRIBUTING.md: DI stack description - .github/copilot-instructions.md: architecture, di/ comment, DI pattern, build note - CLAUDE.md: tech stack, build comments, di/ listing, DI patterns, common issues, Kotlin version note, Copilot context section - project-resources/prd/product-requirements-document.md: ARCH-04 row --- .github/copilot-instructions.md | 8 +++--- CLAUDE.md | 25 ++++++++++--------- CONTRIBUTING.md | 2 +- .../prd/product-requirements-document.md | 2 +- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 849b0c2..0c848b1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -11,7 +11,7 @@ This document provides essential information for GitHub Copilot agents working o - Build System: Gradle 8.13 - Min SDK: 28 (Android 9.0 Pie) - Target SDK: 36 (Android 16.0) -- Architecture: Modern Android with Jetpack Compose, Circuit UDF, Dagger DI +- Architecture: Modern Android with Jetpack Compose, Circuit UDF, Metro DI ## Critical Build & Test Commands @@ -70,7 +70,7 @@ app/src/main/java/ink/trmnl/android/ ├── MainActivity.kt # App entry point (138 lines) ├── TrmnlDisplayMirrorApp.kt # Application class (51 lines) ├── data/ # Repositories, DataStore implementations -├── di/ # Dagger dependency injection modules +├── di/ # Metro dependency injection modules ├── model/ # Data models (TrmnlDeviceConfig, etc.) ├── network/ # Retrofit API service, response models ├── ui/ # Jetpack Compose screens (Circuit UDF) @@ -89,7 +89,7 @@ app/src/main/java/ink/trmnl/android/ ### Key Architectural Patterns - **UI Framework:** Jetpack Compose with Circuit (Slack's UDF architecture) -- **DI:** Dagger with Anvil for code generation +- **DI:** Metro (dev.zacsweers.metro) for compile-time code generation - **Background Work:** WorkManager (15 min minimum interval) - **Networking:** Retrofit + OkHttp + Moshi for JSON parsing - **Data Storage:** DataStore (preferences) for settings/tokens @@ -134,7 +134,7 @@ All workflows are in `.github/workflows/`: **Solution:** This is informational only. The file is optional for local dev (used for release keystores). Debug builds work without it. ### Issue: First Build Takes 2-3 Minutes -**Solution:** Expected behavior. Gradle downloads dependencies and builds annotation processors (KSP, KAPT). Subsequent builds are much faster (~1 minute) due to caching. +**Solution:** Expected behavior. Gradle downloads dependencies and runs Metro compiler plugin + KSP. Subsequent builds are much faster (~1 minute) due to caching. ### Issue: Test Warning "Sharing is only supported for boot loader classes" **Solution:** Harmless warning from Robolectric tests. Can be ignored. Tests still pass. diff --git a/CLAUDE.md b/CLAUDE.md index b06d392..4af156f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,7 +11,7 @@ TRMNL Android is a native Android app that displays TRMNL e-ink device content o - Build: Gradle 8.13 with AGP 8.9.2 - Min SDK: 28 (Android 9.0 Pie), Target SDK: 36 (Android 16.0) - UI: Jetpack Compose with Circuit UDF architecture (Slack's unidirectional data flow) -- DI: Dagger 2.56.2 with Anvil 0.4.1 for code generation +- DI: Metro 0.12.1 (dev.zacsweers.metro) with MetroX Android for compile-time DI - Background Work: WorkManager 2.10.1 (15-minute minimum interval limitation) - Networking: Retrofit 2.11.0 + OkHttp 4.12.0 + Moshi 1.15.2 - Image Loading: Coil 3.2.0 with OkHttp integration @@ -25,14 +25,14 @@ TRMNL Android is a native Android app that displays TRMNL e-ink device content o **CRITICAL: On a fresh clone, you MUST build first to generate code:** ```bash -# 1. Build debug APK (generates Dagger/Circuit code via KSP/KAPT) +# 1. Build debug APK (generates Metro/Circuit code via compiler plugin + KSP) ./gradlew assembleDebug --parallel --daemon # 2. Now you can run tests ./gradlew testDebugUnitTest --parallel --daemon ``` -**Why?** Running tests before building will fail with Dagger errors about missing generated modules. The build step generates necessary code via KSP (Circuit) and KAPT (Dagger). +**Why?** Running tests before building will fail with Metro errors about missing generated modules. The build step generates necessary code via Metro compiler plugin (DI) and KSP (Circuit). ### Regular Development (After Initial Build) @@ -72,8 +72,8 @@ app/src/main/java/ink/trmnl/android/ │ ├── TrmnlDeviceConfigDataStore.kt # Device config (token, server) │ ├── ImageMetadataStore.kt # Image state management │ ├── log/ # Refresh log management -├── di/ # Dagger modules -│ ├── AppComponent.kt # Main app component +├── di/ # Metro DI modules +│ ├── AppGraph.kt # Main app dependency graph │ ├── NetworkModule.kt # Retrofit, OkHttp, Moshi │ ├── CircuitModule.kt # Circuit UI setup ├── model/ # Data models @@ -98,15 +98,16 @@ app/src/main/java/ink/trmnl/android/ ### Key Architectural Patterns **Circuit UDF (Unidirectional Data Flow):** -- All screens use `@CircuitInject` annotation for DI with Anvil +- All screens use `@CircuitInject` annotation for DI with Metro - Presenter pattern: State + Event → Presenter → Updated State - Example: `TrmnlMirrorDisplayScreen` with presenter handling events **Dependency Injection:** -- Dagger with Anvil for compile-time code generation +- Metro for compile-time code generation via Kotlin compiler plugin (no KAPT) - `@ContributesTo(AppScope::class)` for module contribution - `@SingleIn(AppScope::class)` for app-scoped singletons -- Circuit integration: `arg("anvil-ksp-extraContributingAnnotations", "com.slack.circuit.codegen.annotations.CircuitInject")` +- `@DependencyGraph(AppScope::class)` on `AppGraph` as the root component +- Circuit integration: `arg("circuit.codegen.mode", "metro")` **Background Work:** - WorkManager with 15-minute minimum interval (Android OS limitation) @@ -198,13 +199,13 @@ The project uses optimized Gradle settings based on best practices from the [Now ## Common Issues -- **Tests fail on fresh checkout**: Must run `./gradlew assembleDebug` first to generate Dagger/Circuit code +- **Tests fail on fresh checkout**: Must run `./gradlew assembleDebug` first to generate Metro/Circuit code - **"secret.properties not found"**: Informational only, debug builds work without it - **First build slow**: Expected for clean builds, ~1-2 minutes with optimized Gradle settings - **"Sharing is only supported for boot loader classes"**: Harmless Robolectric warning, tests still pass - **"INVISIBLE_REFERENCE" in NetworkingTools.kt:56**: Expected, uses internal Kotlin API (documented warning) - **WorkManager 15-min minimum**: Android OS limitation, not a bug -- **Dagger KAPT errors about missing modules**: Run `assembleDebug` first to generate code +- **Metro codegen errors about missing modules**: Run `assembleDebug` first to generate code ## Code Style @@ -243,7 +244,7 @@ The project uses optimized Gradle settings based on best practices from the [Now - **Authentication**: Token-based auth required (TRMNL access token or device ID/MAC address) - **BYOS Support**: Supports custom server URLs for BYOS installations - **Screen Wake Lock**: `FLAG_KEEP_SCREEN_ON` doesn't work reliably on e-Ink tablets due to aggressive battery optimization -- **Kotlin Version**: Capped at 2.1.10 due to Dagger 2.56.2 compatibility (see libs.versions.toml comment) +- **Kotlin Version**: 2.3.20+ (Metro removed the Dagger/KAPT compatibility constraint) - **No Product Flavors**: Only debug/release build types exist ## Copilot Instructions Context @@ -253,5 +254,5 @@ From `.github/copilot-instructions.md`: - Use Circuit's `@CircuitInject` for UI components - WorkManager periodic work has 15-min minimum interval - Image loading handles HTTP 403 auto-refresh for expired URLs -- Dagger KSP support is in Alpha - using KAPT for now +- Metro uses a Kotlin compiler plugin — no KAPT, no KSP for DI itself - Trust the documented build commands - they're CI-validated diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1c1d9db..266614c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ The app uses a modern Android architecture with the following components: - **UI**: Jetpack Compose with Circuit UDF architecture - **Background Processing**: WorkManager for scheduled image (re)loading - **Networking**: Retrofit and OkHttp for API communication -- **DI**: Dagger with Anvil for dependency injection +- **DI**: Metro for dependency injection - **Data Storage**: DataStore for preferences and token storage #### Key Features/Screens diff --git a/project-resources/prd/product-requirements-document.md b/project-resources/prd/product-requirements-document.md index bcfbd56..f284a22 100644 --- a/project-resources/prd/product-requirements-document.md +++ b/project-resources/prd/product-requirements-document.md @@ -190,7 +190,7 @@ This document outlines the requirements for the TRMNL Android application. | ARCH-01 | UI Framework | Jetpack Compose with Circuit UDF architecture | | ARCH-02 | Background Processing | WorkManager for scheduled image refreshes | | ARCH-03 | Networking | Retrofit and OkHttp with EitherNet for API communication | -| ARCH-04 | Dependency Injection | Dagger with Anvil | +| ARCH-04 | Dependency Injection | Metro (dev.zacsweers.metro) | | ARCH-05 | Data Storage | DataStore for preferences and token storage | | ARCH-06 | Image Loading | Coil for image loading and caching | | ARCH-07 | Logging | Timber for debug logging |