From 460612ce2dc9ed7fe8f5c4f540e37a6ffad21267 Mon Sep 17 00:00:00 2001 From: Josiah Campbell <9521010+jocmp@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:49:37 -0500 Subject: [PATCH 1/2] Move refresh scheduler to account init --- app/src/main/java/com/capyreader/app/CommonModule.kt | 2 ++ app/src/main/java/com/capyreader/app/MainApplication.kt | 6 ------ .../java/com/capyreader/app/refresher/RefresherModule.kt | 1 - .../com/capyreader/app/ui/accounts/AddAccountViewModel.kt | 4 ++++ .../main/java/com/capyreader/app/ui/accounts/LoginModule.kt | 4 +++- .../java/com/capyreader/app/ui/accounts/LoginViewModel.kt | 4 ++++ 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/capyreader/app/CommonModule.kt b/app/src/main/java/com/capyreader/app/CommonModule.kt index a45711e2c..07a9b28b2 100644 --- a/app/src/main/java/com/capyreader/app/CommonModule.kt +++ b/app/src/main/java/com/capyreader/app/CommonModule.kt @@ -6,6 +6,7 @@ import com.capyreader.app.common.AndroidClientCertManager import com.capyreader.app.common.AppFaviconPolicy import com.capyreader.app.common.SharedPreferenceStoreProvider import com.capyreader.app.preferences.AppPreferences +import com.capyreader.app.refresher.RefreshScheduler import com.jocmp.capy.AccountManager import com.jocmp.capy.ClientCertManager import com.jocmp.capy.DatabaseProvider @@ -36,6 +37,7 @@ internal val common = module { ) } single { AppPreferences(get()) } + single { RefreshScheduler(get(), get()) } } private fun Locale.toAcceptLanguageTag(): String { diff --git a/app/src/main/java/com/capyreader/app/MainApplication.kt b/app/src/main/java/com/capyreader/app/MainApplication.kt index 52bbd67ab..37ac8d37b 100644 --- a/app/src/main/java/com/capyreader/app/MainApplication.kt +++ b/app/src/main/java/com/capyreader/app/MainApplication.kt @@ -12,7 +12,6 @@ import coil3.svg.SvgDecoder import coil3.video.VideoFrameDecoder import com.capyreader.app.common.AndroidLogging import com.capyreader.app.preferences.AppPreferences -import com.capyreader.app.refresher.RefreshScheduler import com.capyreader.app.ui.widget.HeadlinesWidgetReceiver import com.capyreader.app.ui.widget.SpotlightWidgetReceiver import com.google.android.material.color.DynamicColors @@ -39,16 +38,11 @@ class MainApplication : Application(), SingletonImageLoader.Factory { if (get().isLoggedIn) { loadAccountModules() - initializeRefreshScheduler() } loadWidgetPreview() } - private fun initializeRefreshScheduler() { - get().initialize() - } - override fun newImageLoader(context: PlatformContext): ImageLoader { return ImageLoader.Builder(context) .components { diff --git a/app/src/main/java/com/capyreader/app/refresher/RefresherModule.kt b/app/src/main/java/com/capyreader/app/refresher/RefresherModule.kt index 79cab65c4..eacf25b13 100644 --- a/app/src/main/java/com/capyreader/app/refresher/RefresherModule.kt +++ b/app/src/main/java/com/capyreader/app/refresher/RefresherModule.kt @@ -5,6 +5,5 @@ import org.koin.dsl.module val refresherModule = module { single { FeedRefresher(account = get(), get(), get(), get()) } - single { RefreshScheduler(get(), get()) } worker { RefreshFeedsWorker(get(), get()) } } diff --git a/app/src/main/java/com/capyreader/app/ui/accounts/AddAccountViewModel.kt b/app/src/main/java/com/capyreader/app/ui/accounts/AddAccountViewModel.kt index bff546291..efbbc39b7 100644 --- a/app/src/main/java/com/capyreader/app/ui/accounts/AddAccountViewModel.kt +++ b/app/src/main/java/com/capyreader/app/ui/accounts/AddAccountViewModel.kt @@ -5,10 +5,12 @@ import com.jocmp.capy.AccountManager import com.jocmp.capy.accounts.Source import com.capyreader.app.preferences.AppPreferences import com.capyreader.app.loadAccountModules +import com.capyreader.app.refresher.RefreshScheduler class AddAccountViewModel( private val accountManager: AccountManager, private val appPreferences: AppPreferences, + private val refreshScheduler: RefreshScheduler, ) : ViewModel() { fun addLocalAccount() { val accountID = accountManager.createAccount(source = Source.LOCAL) @@ -16,6 +18,8 @@ class AddAccountViewModel( selectAccount(accountID) loadAccountModules() + + refreshScheduler.initialize() } private fun selectAccount(id: String) { diff --git a/app/src/main/java/com/capyreader/app/ui/accounts/LoginModule.kt b/app/src/main/java/com/capyreader/app/ui/accounts/LoginModule.kt index d407b36a8..db2551a7b 100644 --- a/app/src/main/java/com/capyreader/app/ui/accounts/LoginModule.kt +++ b/app/src/main/java/com/capyreader/app/ui/accounts/LoginModule.kt @@ -7,7 +7,8 @@ val loginModule = module { viewModel { AddAccountViewModel( accountManager = get(), - appPreferences = get() + appPreferences = get(), + refreshScheduler = get(), ) } viewModel { @@ -16,6 +17,7 @@ val loginModule = module { accountManager = get(), appPreferences = get(), clientCertManager = get(), + refreshScheduler = get(), ) } viewModel { diff --git a/app/src/main/java/com/capyreader/app/ui/accounts/LoginViewModel.kt b/app/src/main/java/com/capyreader/app/ui/accounts/LoginViewModel.kt index 4a90c36f3..47ef8802c 100644 --- a/app/src/main/java/com/capyreader/app/ui/accounts/LoginViewModel.kt +++ b/app/src/main/java/com/capyreader/app/ui/accounts/LoginViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import com.capyreader.app.loadAccountModules import com.capyreader.app.preferences.AppPreferences +import com.capyreader.app.refresher.RefreshScheduler import com.capyreader.app.ui.Route import com.jocmp.capy.AccountManager import com.jocmp.capy.ClientCertManager @@ -30,6 +31,7 @@ class LoginViewModel( private val accountManager: AccountManager, private val appPreferences: AppPreferences, private val clientCertManager: ClientCertManager, + private val refreshScheduler: RefreshScheduler, ) : ViewModel() { private var _username by mutableStateOf("") private var _password by mutableStateOf("") @@ -158,6 +160,8 @@ class LoginViewModel( selectAccount(accountID) loadAccountModules() + + refreshScheduler.initialize() } private fun selectAccount(id: String) { From 2e4bffcf145d69efdab84238519c936db2727b8b Mon Sep 17 00:00:00 2001 From: Josiah Campbell <9521010+jocmp@users.noreply.github.com> Date: Sun, 14 Jun 2026 13:06:51 -0500 Subject: [PATCH 2/2] Lazily resolve WebView user agent WebSettings.getDefaultUserAgent loads the WebView provider, which is slow to initialize on some devices. It was evaluated eagerly inside the AccountManager Koin singleton's initializer, so it ran while holding Koin's single-creation lock. A background thread stalling there blocked the main thread resolving the same singleton, causing startup ANRs (discussion 2145). userAgent is only needed at network time (ArticleContent, FaviconFinder), so it's a () -> String invoked at request time, off the main thread. --- CLAUDE.md | 2 +- app/src/main/java/com/capyreader/app/CommonModule.kt | 3 ++- bench/src/main/kotlin/com/jocmp/bench/BenchAccount.kt | 2 +- capy/src/main/java/com/jocmp/capy/Account.kt | 2 +- capy/src/main/java/com/jocmp/capy/AccountManager.kt | 2 +- capy/src/main/java/com/jocmp/capy/UserAgentInterceptor.kt | 4 ++-- capy/src/main/java/com/jocmp/capy/accounts/FaviconFinder.kt | 2 +- capy/src/main/java/com/jocmp/capy/articles/ArticleContent.kt | 2 +- capy/src/test/java/com/jocmp/capy/AccountManagerTest.kt | 2 +- .../test/java/com/jocmp/capy/accounts/ArticleContentTest.kt | 2 +- capy/src/test/java/com/jocmp/capy/fixtures/AccountFixture.kt | 2 +- 11 files changed, 13 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cea3944bd..9d77cfb63 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,7 +3,7 @@ ## Build and Development - `./gradlew assembleFreeDebug` will compile the debug version of the app -- For fast feedback, run single tests i.e. `./gradlew :capy:testDebugUnitTest --tests com.jocmp.capy.persistence.ArticleRecordsTest` replacing the module - `:capy` - and Java package accordingly +- For fast feedback, run single tests i.e. `./gradlew :capy:test --tests com.jocmp.capy.persistence.ArticleRecordsTest` replacing the module - `:capy` - and Java package accordingly. Note `:capy` is a JVM module (use `:capy:test`); Android modules like `:app` use the variant task `testFreeDebugUnitTest` - `make test` will run all tests via Fastlane. - When modifying the `.js` and `.liquid` files, be sure to run `make` to compile those assets, and `make check` to typecheck diff --git a/app/src/main/java/com/capyreader/app/CommonModule.kt b/app/src/main/java/com/capyreader/app/CommonModule.kt index 07a9b28b2..e41cc080d 100644 --- a/app/src/main/java/com/capyreader/app/CommonModule.kt +++ b/app/src/main/java/com/capyreader/app/CommonModule.kt @@ -25,6 +25,7 @@ internal val common = module { single { AndroidDatabaseProvider(context = get()) } single { AndroidClientCertManager(context = get()) } single { + val userAgent = lazy { WebSettings.getDefaultUserAgent(androidContext()) } AccountManager( rootFolder = androidContext().filesDir.toURI(), databaseProvider = get(), @@ -32,7 +33,7 @@ internal val common = module { preferenceStoreProvider = get(), faviconPolicy = AppFaviconPolicy(get()), clientCertManager = get(), - userAgent = WebSettings.getDefaultUserAgent(androidContext()), + userAgent = { userAgent.value }, acceptLanguage = Locale.getDefault().toAcceptLanguageTag(), ) } diff --git a/bench/src/main/kotlin/com/jocmp/bench/BenchAccount.kt b/bench/src/main/kotlin/com/jocmp/bench/BenchAccount.kt index 5453d62ae..0857e6285 100644 --- a/bench/src/main/kotlin/com/jocmp/bench/BenchAccount.kt +++ b/bench/src/main/kotlin/com/jocmp/bench/BenchAccount.kt @@ -52,7 +52,7 @@ fun loadOrCreateAccount(benchDir: File, config: BenchConfig): Pair builder }, - private val userAgent: String, + private val userAgent: () -> String, private val acceptLanguage: String, private val localHttpClient: OkHttpClient = LocalOkHttpClient.forAccount(path = cacheDirectory), val delegate: AccountDelegate = when (source) { diff --git a/capy/src/main/java/com/jocmp/capy/AccountManager.kt b/capy/src/main/java/com/jocmp/capy/AccountManager.kt index 51db22e0c..e959f5d8a 100644 --- a/capy/src/main/java/com/jocmp/capy/AccountManager.kt +++ b/capy/src/main/java/com/jocmp/capy/AccountManager.kt @@ -15,7 +15,7 @@ class AccountManager( private val preferenceStoreProvider: PreferenceStoreProvider, private val faviconPolicy: FaviconPolicy, private val clientCertManager: ClientCertManager = ClientCertManager { builder, _ -> builder }, - private val userAgent: String, + private val userAgent: () -> String, private val acceptLanguage: String, ) { fun findByID( diff --git a/capy/src/main/java/com/jocmp/capy/UserAgentInterceptor.kt b/capy/src/main/java/com/jocmp/capy/UserAgentInterceptor.kt index e2d52a87c..04dd7b2ed 100644 --- a/capy/src/main/java/com/jocmp/capy/UserAgentInterceptor.kt +++ b/capy/src/main/java/com/jocmp/capy/UserAgentInterceptor.kt @@ -19,13 +19,13 @@ class UserAgentInterceptor(private val userAgent: String = USER_AGENT) : Interce } class BrowserHeadersInterceptor( - private val userAgent: String, + private val userAgent: () -> String, private val acceptLanguage: String, ) : Interceptor { override fun intercept(chain: Chain): Response { val originalRequest = chain.request() val request = originalRequest.newBuilder() - .header("User-Agent", userAgent) + .header("User-Agent", userAgent()) .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8") .header("Accept-Language", acceptLanguage) .header("Sec-Fetch-Dest", "document") diff --git a/capy/src/main/java/com/jocmp/capy/accounts/FaviconFinder.kt b/capy/src/main/java/com/jocmp/capy/accounts/FaviconFinder.kt index 99c38ff1d..6e59e72e0 100644 --- a/capy/src/main/java/com/jocmp/capy/accounts/FaviconFinder.kt +++ b/capy/src/main/java/com/jocmp/capy/accounts/FaviconFinder.kt @@ -12,7 +12,7 @@ import java.net.URL class FaviconFinder( private val httpClient: OkHttpClient, private val faviconPolicy: FaviconPolicy, - private val userAgent: String = "", + private val userAgent: () -> String = { "" }, private val acceptLanguage: String = "", ) { suspend fun find(url: URL): String? { diff --git a/capy/src/main/java/com/jocmp/capy/articles/ArticleContent.kt b/capy/src/main/java/com/jocmp/capy/articles/ArticleContent.kt index e12dee99c..e35047fe0 100644 --- a/capy/src/main/java/com/jocmp/capy/articles/ArticleContent.kt +++ b/capy/src/main/java/com/jocmp/capy/articles/ArticleContent.kt @@ -11,7 +11,7 @@ import java.net.URL class ArticleContent( client: OkHttpClient, - userAgent: String, + userAgent: () -> String, acceptLanguage: String, ) { private val httpClient = diff --git a/capy/src/test/java/com/jocmp/capy/AccountManagerTest.kt b/capy/src/test/java/com/jocmp/capy/AccountManagerTest.kt index 40d11b555..f1a3428a7 100644 --- a/capy/src/test/java/com/jocmp/capy/AccountManagerTest.kt +++ b/capy/src/test/java/com/jocmp/capy/AccountManagerTest.kt @@ -22,7 +22,7 @@ class AccountManagerTest { cacheDirectory = rootFolder.newFolder().toURI(), databaseProvider = InMemoryDatabaseProvider, faviconPolicy = FakeFaviconPolicy, - userAgent = "TestUserAgent", + userAgent = { "TestUserAgent" }, acceptLanguage = "en-US", ) } diff --git a/capy/src/test/java/com/jocmp/capy/accounts/ArticleContentTest.kt b/capy/src/test/java/com/jocmp/capy/accounts/ArticleContentTest.kt index 477fdcc95..143753c0d 100644 --- a/capy/src/test/java/com/jocmp/capy/accounts/ArticleContentTest.kt +++ b/capy/src/test/java/com/jocmp/capy/accounts/ArticleContentTest.kt @@ -50,7 +50,7 @@ class ArticleContentTest { fun setup() { extractor = ArticleContent( client = client, - userAgent = "TestUserAgent", + userAgent = { "TestUserAgent" }, acceptLanguage = "en-US", ) } diff --git a/capy/src/test/java/com/jocmp/capy/fixtures/AccountFixture.kt b/capy/src/test/java/com/jocmp/capy/fixtures/AccountFixture.kt index 4d4a8928d..6f3b2d0d9 100644 --- a/capy/src/test/java/com/jocmp/capy/fixtures/AccountFixture.kt +++ b/capy/src/test/java/com/jocmp/capy/fixtures/AccountFixture.kt @@ -29,7 +29,7 @@ object AccountFixture { source = source, delegate = accountDelegate, faviconPolicy = FakeFaviconPolicy, - userAgent = "TestUserAgent", + userAgent = { "TestUserAgent" }, acceptLanguage = "en-US", ) }