From 7410c6238de4df17b7a067a8e15b60b2a9cbbdf8 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 29 Jul 2026 09:58:41 +0200 Subject: [PATCH 1/4] Stop the parasitic manager crashing on its first network request OkHttp 5's Android artifact keeps the public suffix list in assets/ and reaches it through a process-static Context that PlatformInitializer sets from androidx.startup. Parasitically the manager's manifest is never installed, so that provider never runs, and DnsOverHttps -- which asks the list whether a host is private before it opens any socket -- died with "Unable to load PublicSuffixDatabase.list" on the very first lookup. Coil was already hand-initialised for this exact reason; OkHttp was not. Initialise it where the one client is built rather than in an activity: that is on the path to every request in both parasitic and standalone runs, and in the debug demo host, which never opens MainActivity. Two things then let a missing asset become a fatal instead of a degraded feature, and both are fixed here: - VectorDns caught only UnknownHostException, so an IllegalStateException escaped it, the latch never closed, and the fallback to the system resolver the class is built around never engaged. - canaryBuilds and frameworkReleases called get() unguarded, unlike every other request in that file. Both are read from a viewModelScope or a LaunchedEffect, and there is no CoroutineExceptionHandler anywhere in the manager, so the throw landed on the main thread. Verified on a Pixel 6: online the store loads with no DoH fallback; offline with the disk cache cleared, DoH falls back once and the release fetch logs "github release list unavailable" instead of taking the process down. --- manager/build.gradle.kts | 3 +++ .../manager/data/github/GitHubRepository.kt | 25 ++++++++++++++----- .../vector/manager/net/HttpClientFactory.kt | 15 +++++++++++ .../matrix/vector/manager/net/VectorDns.kt | 14 +++++++++-- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index 975cddb14..d5e3e762d 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -97,6 +97,9 @@ android { packaging { resources { excludes += "META-INF/**" + // Java resources only, so it is inert against the Android artifact, which ships its + // public suffix list under assets/. Pinning the JVM variant would move that list back + // to okhttp3/internal/publicsuffix/ and this line would then delete it. excludes += "okhttp3/**" excludes += "kotlin/**" excludes += "**.properties" diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/github/GitHubRepository.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/github/GitHubRepository.kt index 715129e3f..c11d68847 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/data/github/GitHubRepository.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/github/GitHubRepository.kt @@ -699,6 +699,23 @@ class GitHubRepository( } ?: CommitPerson(login = name, avatarUrl = null, profileUrl = null) } + /** + * The releases page, or null when GitHub could not be reached. + * + * Both readers of this endpoint are launched from a `viewModelScope` or a `LaunchedEffect`, + * neither of which has a `CoroutineExceptionHandler` anywhere in this app — so a throw here is + * a fatal on the main thread rather than a card that stays empty. Every other request in this + * file already guards itself; these two did not, which is how a resolver failure took the whole + * manager down instead of leaving the update card blank. + * + * One helper for both because it is the same request: fetching it twice under two URLs built + * from the same three constants is how the two lists drift apart. + */ + private fun releaseListJson(freshness: Freshness): String? = + runCatching { get("$API/$REPO/releases?per_page=$CANARY_FETCH", freshness) } + .onFailure { e -> Log.w(Constants.TAG, "update: github release list unavailable", e) } + .getOrNull() + /** * The canary builds, newest first. * @@ -714,9 +731,7 @@ class GitHubRepository( */ suspend fun canaryBuilds(freshness: Freshness = Freshness.Revalidate): List = withContext(Dispatchers.IO) { - val body = - get("$API/$REPO/releases?per_page=$CANARY_FETCH", freshness) - ?: return@withContext emptyList() + val body = releaseListJson(freshness) ?: return@withContext emptyList() runCatching { json.decodeFromString>(body) } .onFailure { e -> Log.e(Constants.TAG, "update: canary release list unreadable", e) } @@ -757,9 +772,7 @@ class GitHubRepository( suspend fun frameworkReleases(freshness: Freshness = Freshness.Revalidate): List = withContext(Dispatchers.IO) { - val body = - get("$API/$REPO/releases?per_page=$CANARY_FETCH", freshness) - ?: return@withContext emptyList() + val body = releaseListJson(freshness) ?: return@withContext emptyList() runCatching { json.decodeFromString>(body) } .onFailure { e -> Log.e(Constants.TAG, "update: release list unreadable", e) } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/net/HttpClientFactory.kt b/manager/src/main/kotlin/org/matrix/vector/manager/net/HttpClientFactory.kt index a55e3f3ba..73873abae 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/net/HttpClientFactory.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/net/HttpClientFactory.kt @@ -4,6 +4,7 @@ import android.content.Context import java.io.File import java.util.concurrent.TimeUnit import okhttp3.Cache +import okhttp3.OkHttp import okhttp3.OkHttpClient import org.matrix.vector.manager.data.repository.SettingsRepository @@ -26,6 +27,20 @@ object HttpClientFactory { private const val CACHE_SIZE_BYTES = 16L * 1024 * 1024 fun create(context: Context, settings: SettingsRepository): OkHttpClient { + // OkHttp's Android artifact ships the public suffix list as an *asset* and reaches it + // through a process-static Context that `PlatformInitializer` sets from `androidx.startup`. + // Parasitically this app's manifest is never installed, so that provider never runs and the + // first DoH lookup — which asks the list whether a host is private before opening any + // socket — dies with "Unable to load PublicSuffixDatabase.list". OkHttp latches that + // failure for the life of the process, so it has to be prevented rather than recovered + // from. + // + // Here rather than in the activity because this is the only place a client is built, which + // puts it on the path to every request in both parasitic and standalone runs and in the + // debug demo host, which never opens MainActivity. Idempotent, so it is a no-op in the + // standalone install, where the Startup initializer did run. + OkHttp.initialize(context) + val cache = Cache(File(context.cacheDir, CACHE_DIR), CACHE_SIZE_BYTES) val base = diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/net/VectorDns.kt b/manager/src/main/kotlin/org/matrix/vector/manager/net/VectorDns.kt index 278fa112a..391ded9ee 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/net/VectorDns.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/net/VectorDns.kt @@ -5,7 +5,6 @@ import android.util.Log import java.net.InetAddress import java.net.Proxy import java.net.ProxySelector -import java.net.UnknownHostException import java.util.concurrent.TimeUnit import okhttp3.Dns import okhttp3.HttpUrl.Companion.toHttpUrl @@ -78,7 +77,18 @@ class VectorDns(private val settings: SettingsRepository, bootstrapClient: OkHtt if (settings.dohEnabled.value && direct && !dohUnavailable) { try { return doh.lookup(hostname) - } catch (e: UnknownHostException) { + } catch (e: Exception) { + // Every way DoH can fail, not only "no such host". A blocked endpoint raises + // UnknownHostException, a slow one raises InterruptedIOException from the timeouts + // above, and a resolver that cannot read its own public suffix list raises + // IllegalStateException before a socket is ever opened — which used to escape this + // method entirely, so the latch never closed and the fallback this class is built + // around never engaged. Anything arriving here means DoH is not usable, which is + // the condition documented above. + // + // Exception and not Throwable: an OutOfMemoryError is not a DNS outcome. There is + // no CancellationException to preserve either — this is a plain blocking call on an + // OkHttp dispatcher thread, with no coroutine in the stack. dohUnavailable = true Log.w( Constants.TAG, From 8039c2a9f0f92803eb1d660ba36541ac122fb2bd Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 29 Jul 2026 10:34:45 +0200 Subject: [PATCH 2/4] Say what DNS over HTTPS is actually doing The switch said what DoH would do; nothing said what it was doing. Three outcomes looked identical from the sheet -- the setting off, a proxy taking the decision away, and a lookup that failed and latched the session onto the system resolver -- and the last one existed only as a log line, on the very networks the setting exists for. VectorDns now records what each lookup did and the sheet renders it under the switch. Observed, never probed: a "test DoH" button would be a second code path, another host at another moment, and it can pass while the client that fetches the module list is failing. The fallback state also offers a way back. The latch is what keeps a blocked endpoint from costing five seconds a name, but the session it belongs to is com.android.shell -- a process nobody can restart on purpose -- so one bad lookup on a captive portal disabled DoH until something else killed the host. Two things this turned up, fixed here: - `direct` was a lazy val, so the proxy check ran once per process. Joining a VPN mid-session was invisible and DoH kept querying Cloudflare when the file says it should stand aside. It is read per lookup now, like the setting beside it. - The failure line named the host being resolved, which is whichever request happened to be in flight -- and UnknownHostException carries that name as its entire message, so it printed twice while the useful part, which way it failed, went missing. What failed is reaching Cloudflare, so that is what it says, with the exception type as the detail. Verified on a Pixel 6 in one process: resolved through Cloudflare, forced to fall back with the radios off, recovered by pressing the action, and back to resolving through Cloudflare with no second fallback. --- .../vector/manager/di/ServiceLocator.kt | 12 ++- .../vector/manager/net/HttpClientFactory.kt | 14 ++- .../matrix/vector/manager/net/VectorDns.kt | 100 ++++++++++++++++-- .../manager/ui/components/SheetParts.kt | 40 +++++++ .../ui/screens/home/HomeAppearanceSheet.kt | 41 +++++++ manager/src/main/res/values-ar/strings.xml | 5 + manager/src/main/res/values-de/strings.xml | 5 + manager/src/main/res/values-es/strings.xml | 5 + manager/src/main/res/values-fa/strings.xml | 5 + manager/src/main/res/values-fr/strings.xml | 5 + manager/src/main/res/values-in/strings.xml | 5 + manager/src/main/res/values-it/strings.xml | 5 + manager/src/main/res/values-iw/strings.xml | 5 + manager/src/main/res/values-ja/strings.xml | 5 + manager/src/main/res/values-ko/strings.xml | 5 + manager/src/main/res/values-pl/strings.xml | 5 + .../src/main/res/values-pt-rBR/strings.xml | 5 + manager/src/main/res/values-ru/strings.xml | 5 + manager/src/main/res/values-tr/strings.xml | 5 + manager/src/main/res/values-uk/strings.xml | 5 + manager/src/main/res/values-vi/strings.xml | 5 + .../src/main/res/values-zh-rCN/strings.xml | 5 + .../src/main/res/values-zh-rTW/strings.xml | 5 + manager/src/main/res/values/strings.xml | 5 + 24 files changed, 291 insertions(+), 11 deletions(-) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/di/ServiceLocator.kt b/manager/src/main/kotlin/org/matrix/vector/manager/di/ServiceLocator.kt index ec924389c..7ab1ddb7a 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/di/ServiceLocator.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/di/ServiceLocator.kt @@ -36,6 +36,7 @@ import org.matrix.vector.manager.data.repository.SettingsRepository import org.matrix.vector.manager.ipc.DaemonClient import org.matrix.vector.manager.ipc.packageEventsFlow import org.matrix.vector.manager.net.HttpClientFactory +import org.matrix.vector.manager.net.VectorDns /** * Hand-rolled service location, deliberately not a DI framework. @@ -77,7 +78,16 @@ object ServiceLocator { val daemon: DaemonClient by lazy { DaemonClient(service) } - val http: OkHttpClient by lazy { HttpClientFactory.create(context, settings) } + private val net: HttpClientFactory.NetStack by lazy { + HttpClientFactory.create(context, settings) + } + + val http: OkHttpClient + get() = net.client + + /** The resolver inside [http], so the settings sheet can report what DoH is actually doing. */ + val dns: VectorDns + get() = net.dns val settings: SettingsRepository by lazy { SettingsRepository(context) } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/net/HttpClientFactory.kt b/manager/src/main/kotlin/org/matrix/vector/manager/net/HttpClientFactory.kt index 73873abae..87e4df483 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/net/HttpClientFactory.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/net/HttpClientFactory.kt @@ -26,7 +26,16 @@ object HttpClientFactory { private const val CACHE_DIR = "http_cache" private const val CACHE_SIZE_BYTES = 16L * 1024 * 1024 - fun create(context: Context, settings: SettingsRepository): OkHttpClient { + /** + * The client and the resolver inside it. + * + * The resolver comes back alongside rather than being fished out of `client.dns` later: it is + * the only thing that knows whether DoH is actually working, the settings sheet reports that, + * and a cast back from the `Dns` interface would be a promise that nothing checks. + */ + class NetStack(val client: OkHttpClient, val dns: VectorDns) + + fun create(context: Context, settings: SettingsRepository): NetStack { // OkHttp's Android artifact ships the public suffix list as an *asset* and reaches it // through a process-static Context that `PlatformInitializer` sets from `androidx.startup`. // Parasitically this app's manifest is never installed, so that provider never runs and the @@ -55,6 +64,7 @@ object HttpClientFactory { // and the shared client — with its connection pool and its disk cache — is never rebuilt. // `base` is passed in as the bootstrap client because a DoH client must not itself resolve // through DoH. - return base.newBuilder().dns(VectorDns(settings, base)).build() + val dns = VectorDns(settings, base) + return NetStack(base.newBuilder().dns(dns).build(), dns) } } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/net/VectorDns.kt b/manager/src/main/kotlin/org/matrix/vector/manager/net/VectorDns.kt index 391ded9ee..9d1e5fbf5 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/net/VectorDns.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/net/VectorDns.kt @@ -6,12 +6,47 @@ import java.net.InetAddress import java.net.Proxy import java.net.ProxySelector import java.util.concurrent.TimeUnit +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import okhttp3.Dns import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.dnsoverhttps.DnsOverHttps import org.matrix.vector.manager.data.repository.SettingsRepository +/** + * What the last name lookup of this session actually did. + * + * Deliberately a record of the real resolver rather than something a probe could produce. A "test + * DoH" button would be a second code path — another host, another moment — and it can pass while + * the client that fetches the module list is failing. Only the shared resolver knows the truth, so + * only the shared resolver reports it. + */ +sealed interface DohStatus { + /** Nothing has been resolved yet, so there is nothing to report. */ + data object Untested : DohStatus + + /** The setting is off; names went to the system resolver. */ + data object Disabled : DohStatus + + /** A proxy is configured, so resolving is its job and DoH was skipped. */ + data object Bypassed : DohStatus + + /** The last lookup went through the DoH endpoint. */ + data class Working(val host: String) : DohStatus + + /** + * DoH failed and the session has fallen back to the system resolver. + * + * No hostname here on purpose. What failed is reaching the DoH endpoint; whichever name was + * being looked up at that moment is incidental — it is simply whatever the app asked for first + * — and naming it reads as though that host were the subject of a test. The log line keeps it + * for anyone diagnosing; the sheet does not need it. + */ + data class FellBack(val reason: String) : DohStatus +} + /** * Name resolution: DNS over HTTPS when it helps, the system resolver when it does not. * @@ -26,15 +61,24 @@ import org.matrix.vector.manager.data.repository.SettingsRepository * - a configured HTTP proxy disables DoH entirely, because the proxy is doing the resolving and * bootstrap IPs are meaningless to it. * - * The setting is read **per lookup** rather than baked into the client at construction. OkHttp - * cannot have its DNS swapped on a live client, and rebuilding the shared client would drop the - * connection pool and orphan the disk cache, so reading it here is what lets the switch take effect - * before the next process start. + * Every one of those branches used to be invisible: off, bypassed and latched all looked like the + * same working switch, and the fallback existed only as a log line. [status] is what each lookup + * did, so the sheet that owns the switch can say which of them is happening. + * + * The setting and the proxy are both read **per lookup** rather than baked into the client at + * construction. OkHttp cannot have its DNS swapped on a live client, and rebuilding the shared + * client would drop the connection pool and orphan the disk cache, so reading them here is what + * lets a switch — or joining a VPN — take effect before the next process start. */ class VectorDns(private val settings: SettingsRepository, bootstrapClient: OkHttpClient) : Dns { private val endpoint = "https://cloudflare-dns.com/dns-query".toHttpUrl() + private val _status = MutableStateFlow(DohStatus.Untested) + + /** What the last lookup did. See [DohStatus] for why this is observed and never probed. */ + val status: StateFlow = _status.asStateFlow() + /** * Latched once the DoH endpoint proves unreachable. * @@ -65,18 +109,43 @@ class VectorDns(private val settings: SettingsRepository, bootstrapClient: OkHtt .build() } - /** True when nothing is proxying our traffic, which is the only case where DoH is ours to do. */ - private val direct: Boolean by lazy { + /** + * True when nothing is proxying our traffic, which is the only case where DoH is ours to do. + * + * Asked on every lookup rather than cached. A proxy can appear mid-session — joining a VPN or a + * work profile does exactly that — and a value read once at startup would keep sending queries + * to Cloudflare long after the answer changed, while [status] claimed a state that was no + * longer true. The call is local and a lookup is about to do network I/O anyway. + */ + private fun direct(): Boolean = runCatching { ProxySelector.getDefault().select(endpoint.toUri()).firstOrNull() == Proxy.NO_PROXY } .getOrDefault(true) + + /** + * Clears the session latch so the next lookup tries DoH again. + * + * The latch is what keeps a blocked endpoint from costing five seconds per name, but "the + * session" here is `com.android.shell`, a process nobody can restart on purpose — so without + * this a single bad lookup on a captive portal disables DoH until something else happens to + * kill the host. This is the way back, and it is offered only once the fallback has happened. + */ + fun retry() { + dohUnavailable = false + _status.value = DohStatus.Untested } override fun lookup(hostname: String): List { - if (settings.dohEnabled.value && direct && !dohUnavailable) { + if (!settings.dohEnabled.value) { + _status.value = DohStatus.Disabled + } else if (!direct()) { + _status.value = DohStatus.Bypassed + } else if (!dohUnavailable) { try { - return doh.lookup(hostname) + val resolved = doh.lookup(hostname) + _status.value = DohStatus.Working(hostname) + return resolved } catch (e: Exception) { // Every way DoH can fail, not only "no such host". A blocked endpoint raises // UnknownHostException, a slow one raises InterruptedIOException from the timeouts @@ -90,6 +159,7 @@ class VectorDns(private val settings: SettingsRepository, bootstrapClient: OkHtt // no CancellationException to preserve either — this is a plain blocking call on an // OkHttp dispatcher thread, with no coroutine in the stack. dohUnavailable = true + _status.value = DohStatus.FellBack(e.describe(hostname)) Log.w( Constants.TAG, "dns: DoH lookup of $hostname failed, using the system resolver for this session", @@ -97,7 +167,21 @@ class VectorDns(private val settings: SettingsRepository, bootstrapClient: OkHtt ) } } + // Latched: the status already says why, and repeating it on every name would only replace + // the host that actually failed with whichever one asked next. return Dns.SYSTEM.lookup(hostname) } + /** + * A line short enough to sit under a switch, and worth the room it takes. + * + * The class name whenever the message would not add anything. `UnknownHostException` carries + * the hostname as its entire message, so using it verbatim printed the name twice — "could not + * resolve example.org (example.org)" — while the one thing a reader wants, *which way* it + * failed, went missing. The class name is jargon, but it is jargon that distinguishes a blocked + * endpoint from a timeout, and it is what a bug report needs to carry anyway. + */ + private fun Throwable.describe(host: String): String = + message?.takeIf { it.isNotBlank() && !it.equals(host, ignoreCase = true) } + ?: javaClass.simpleName } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/SheetParts.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/SheetParts.kt index 2c51b915d..3331ad94b 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/SheetParts.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/components/SheetParts.kt @@ -3,6 +3,7 @@ package org.matrix.vector.manager.ui.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -16,6 +17,7 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -106,6 +108,44 @@ fun ToggleRow( ) } +/** + * What the setting above is currently doing, and — when it is not working — the way back. + * + * Indented to the same column as a [ToggleRow]'s subtitle rather than given a row of its own, + * because it is not another setting: it belongs to the switch above it and has to read as a + * consequence of that switch, not as a sibling of it. + * + * The action is optional and deliberately quiet. A row that always carries a button trains people + * to press it, and most of the states here are the ones where there is nothing to fix. + */ +@Composable +fun StatusNote( + text: String, + modifier: Modifier = Modifier, + tone: Color? = null, + actionLabel: String? = null, + onAction: (() -> Unit)? = null, +) { + Row( + modifier = + modifier.fillMaxWidth().padding(start = 72.dp, end = 24.dp, top = 2.dp, bottom = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = text, + style = MaterialTheme.typography.bodySmall, + color = tone ?: MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f), + ) + if (actionLabel != null && onAction != null) { + Spacer(Modifier.width(8.dp)) + TextButton(onClick = onAction, contentPadding = PaddingValues(horizontal = 12.dp)) { + Text(actionLabel, style = MaterialTheme.typography.labelLarge) + } + } + } +} + /** * One thing the sheet can do. * diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeAppearanceSheet.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeAppearanceSheet.kt index fd4d6e5c8..85497604f 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeAppearanceSheet.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/home/HomeAppearanceSheet.kt @@ -68,7 +68,9 @@ import org.matrix.vector.manager.ui.theme.LocalizedOverlay import org.matrix.vector.manager.R import org.matrix.vector.manager.ui.components.ChoiceRow import org.matrix.vector.manager.ui.components.SheetHeading +import org.matrix.vector.manager.ui.components.StatusNote import org.matrix.vector.manager.ui.components.ToggleRow +import org.matrix.vector.manager.net.DohStatus import org.matrix.vector.manager.di.ServiceLocator import org.matrix.vector.manager.ui.components.ColorWheel import org.matrix.vector.manager.ui.components.ambience.AmbienceKind @@ -107,6 +109,7 @@ fun HomeAppearanceSheet(onDismiss: () -> Unit) { val windowMonths by settings.activityWindowMonths.collectAsStateWithLifecycle() val openExternally by settings.openLinksExternally.collectAsStateWithLifecycle() val doh by settings.dohEnabled.collectAsStateWithLifecycle() + val dohStatus by ServiceLocator.dns.status.collectAsStateWithLifecycle() // The default state, deliberately. `skipPartiallyExpanded` removes the half-height stop, which // is the only thing a drag on a sheet can *do* other than dismiss it, so a sheet taller than @@ -209,6 +212,44 @@ LocalizedOverlay { checked = doh, onCheckedChange = settings::setDohEnabled, ) + // The switch says what was asked for; this says what happened. They come apart more + // often than the switch admits — a proxy takes the decision away entirely, and one + // unreachable lookup latches the fallback for the rest of the session — and until now + // all three cases looked identical from here. + // + // Only while the switch is on. Off, the switch has already said so, and a second line + // repeating it would be the one piece of this that carries no information. + if (doh) { + when (val state = dohStatus) { + is DohStatus.Untested -> + StatusNote(stringResource(R.string.settings_doh_untested)) + + is DohStatus.Bypassed -> + StatusNote(stringResource(R.string.settings_doh_bypassed)) + + is DohStatus.Working -> + StatusNote( + stringResource(R.string.settings_doh_working, state.host), + tone = MaterialTheme.colorScheme.primary, + ) + + // The one state with something to offer. Not an error colour: falling back is + // the designed behaviour and the app is working, so this is the shade the rest + // of the app uses for "worth knowing", not for "something is broken". + is DohStatus.FellBack -> + StatusNote( + stringResource(R.string.settings_doh_fell_back, state.reason), + tone = MaterialTheme.colorScheme.tertiary, + actionLabel = stringResource(R.string.settings_doh_retry), + onAction = ServiceLocator.dns::retry, + ) + + // Reachable only in the gap between flipping the switch on and the next lookup + // recording something newer. + is DohStatus.Disabled -> + StatusNote(stringResource(R.string.settings_doh_untested)) + } + } ToggleRow( title = stringResource(R.string.settings_open_externally), diff --git a/manager/src/main/res/values-ar/strings.xml b/manager/src/main/res/values-ar/strings.xml index 2f9694016..e261d27fa 100644 --- a/manager/src/main/res/values-ar/strings.xml +++ b/manager/src/main/res/values-ar/strings.xml @@ -440,4 +440,9 @@ الشبكة تحليل الأسماء عبر HTTPS يحلّل الأسماء عبر Cloudflare بدلاً من محلّل الشبكة. للشبكات التي تحجب الـ DNS أو تعبث به. + لم يُحلَّل أي اسم بعد. + غير مستخدَم: هناك وكيل مُهيَّأ، وهو الذي يحلّل الأسماء. + يعمل — حُلّل %1$s عبر Cloudflare. + تعذّر الوصول إلى Cloudflare (%1$s)، لذا سيُستخدَم محلّل الشبكة لبقية هذه الجلسة. + إعادة المحاولة diff --git a/manager/src/main/res/values-de/strings.xml b/manager/src/main/res/values-de/strings.xml index 75f7dc999..12834592e 100644 --- a/manager/src/main/res/values-de/strings.xml +++ b/manager/src/main/res/values-de/strings.xml @@ -396,4 +396,9 @@ Netzwerk Namen über HTTPS auflösen Löst Namen über Cloudflare auf statt über den Resolver des Netzwerks. Für Netzwerke, die DNS blockieren oder verfälschen. + Bisher wurden keine Namen aufgelöst. + Nicht in Verwendung: Ein Proxy ist eingerichtet und übernimmt die Auflösung. + Funktioniert — %1$s wurde über Cloudflare aufgelöst. + Cloudflare ist nicht erreichbar (%1$s); für den Rest dieser Sitzung wird der Resolver des Netzwerks verwendet. + Erneut versuchen diff --git a/manager/src/main/res/values-es/strings.xml b/manager/src/main/res/values-es/strings.xml index cbedd0bfd..81b3fc94c 100644 --- a/manager/src/main/res/values-es/strings.xml +++ b/manager/src/main/res/values-es/strings.xml @@ -396,4 +396,9 @@ Red Resolver nombres por HTTPS Resuelve los nombres a través de Cloudflare en lugar del resolutor de la red. Para redes que bloquean o manipulan el DNS. + Todavía no se ha resuelto ningún nombre. + Sin usar: hay un proxy configurado, así que es él quien resuelve los nombres. + Funciona: %1$s se resolvió a través de Cloudflare. + No se pudo contactar con Cloudflare (%1$s), así que se usará el resolutor de la red durante el resto de esta sesión. + Reintentar diff --git a/manager/src/main/res/values-fa/strings.xml b/manager/src/main/res/values-fa/strings.xml index dc6a33b4f..821c9081b 100644 --- a/manager/src/main/res/values-fa/strings.xml +++ b/manager/src/main/res/values-fa/strings.xml @@ -396,4 +396,9 @@ شبکه تحلیل نام‌ها از راه HTTPS نام‌ها را به‌جای تحلیل‌گر شبکه از راه Cloudflare پیدا می‌کند. برای شبکه‌هایی که DNS را مسدود یا دستکاری می‌کنند. + هنوز هیچ نامی تحلیل نشده است. + استفاده نمی‌شود: یک پیشکار پیکربندی شده و نام‌ها را او تحلیل می‌کند. + کار می‌کند — %1$s از راه Cloudflare تحلیل شد. + دسترسی به Cloudflare ممکن نشد (%1$s)، بنابراین تا پایان این نشست از تحلیل‌گر شبکه استفاده می‌شود. + تلاش دوباره diff --git a/manager/src/main/res/values-fr/strings.xml b/manager/src/main/res/values-fr/strings.xml index c64cb3027..8daed06c8 100644 --- a/manager/src/main/res/values-fr/strings.xml +++ b/manager/src/main/res/values-fr/strings.xml @@ -396,4 +396,9 @@ Réseau Résoudre les noms via HTTPS Résout les noms via Cloudflare plutôt que par le résolveur du réseau. Pour les réseaux qui bloquent ou altèrent le DNS. + Aucun nom résolu pour l\'instant. + Inutilisé : un proxy est configuré, c\'est donc lui qui résout les noms. + Fonctionne — %1$s a été résolu via Cloudflare. + Cloudflare est injoignable (%1$s) ; le résolveur du réseau est utilisé pour le reste de cette session. + Réessayer diff --git a/manager/src/main/res/values-in/strings.xml b/manager/src/main/res/values-in/strings.xml index 46c040970..9cc0accb1 100644 --- a/manager/src/main/res/values-in/strings.xml +++ b/manager/src/main/res/values-in/strings.xml @@ -389,4 +389,9 @@ Jaringan Selesaikan nama lewat HTTPS Menyelesaikan nama lewat Cloudflare alih-alih resolver jaringan. Untuk jaringan yang memblokir atau mengubah DNS. + Belum ada nama yang diselesaikan. + Tidak dipakai: proxy telah dikonfigurasi, jadi proxy yang menyelesaikan nama. + Berfungsi — %1$s diselesaikan lewat Cloudflare. + Cloudflare tidak dapat dihubungi (%1$s), jadi resolver jaringan dipakai untuk sisa sesi ini. + Coba lagi diff --git a/manager/src/main/res/values-it/strings.xml b/manager/src/main/res/values-it/strings.xml index 93a367478..e5695f94f 100644 --- a/manager/src/main/res/values-it/strings.xml +++ b/manager/src/main/res/values-it/strings.xml @@ -396,4 +396,9 @@ Rete Risolvere i nomi via HTTPS Risolve i nomi tramite Cloudflare invece che con il resolver della rete. Per reti che bloccano o alterano il DNS. + Nessun nome ancora risolto. + Non in uso: è configurato un proxy, quindi è lui a risolvere i nomi. + Funziona — %1$s è stato risolto tramite Cloudflare. + Cloudflare non è raggiungibile (%1$s), quindi per il resto di questa sessione viene usato il resolver della rete. + Riprova diff --git a/manager/src/main/res/values-iw/strings.xml b/manager/src/main/res/values-iw/strings.xml index da20cea1e..6be53a4e0 100644 --- a/manager/src/main/res/values-iw/strings.xml +++ b/manager/src/main/res/values-iw/strings.xml @@ -422,4 +422,9 @@ רשת פענוח שמות דרך HTTPS מפענח שמות דרך Cloudflare במקום דרך המפענח של הרשת. לרשתות שחוסמות או משבשות DNS. + עדיין לא פוענח אף שם. + לא בשימוש: מוגדר פרוקסי, ולכן הוא מפענח את השמות. + עובד — %1$s פוענח דרך Cloudflare. + לא ניתן היה להגיע ל-Cloudflare (%1$s), ולכן ייעשה שימוש במפענח של הרשת בהמשך ההפעלה הזו. + ניסיון נוסף diff --git a/manager/src/main/res/values-ja/strings.xml b/manager/src/main/res/values-ja/strings.xml index 425ba47cb..509905424 100644 --- a/manager/src/main/res/values-ja/strings.xml +++ b/manager/src/main/res/values-ja/strings.xml @@ -385,4 +385,9 @@ ネットワーク 名前を HTTPS で解決する 名前をネットワークのリゾルバではなく Cloudflare 経由で解決します。DNS を遮断または改変するネットワーク向けです。 + まだ名前を解決していません。 + 未使用: プロキシが設定されているため、名前解決はプロキシが行います。 + 動作中 — %1$s を Cloudflare 経由で解決しました。 + Cloudflare に接続できませんでした (%1$s)。このセッションの残りはネットワークのリゾルバを使用します。 + 再試行 diff --git a/manager/src/main/res/values-ko/strings.xml b/manager/src/main/res/values-ko/strings.xml index 673176c7f..fc068ce4b 100644 --- a/manager/src/main/res/values-ko/strings.xml +++ b/manager/src/main/res/values-ko/strings.xml @@ -385,4 +385,9 @@ 네트워크 HTTPS로 이름 확인 네트워크의 확인자 대신 Cloudflare를 통해 이름을 확인합니다. DNS를 차단하거나 변조하는 네트워크용입니다. + 아직 확인한 이름이 없습니다. + 사용 안 함: 프록시가 설정되어 있어 프록시가 이름을 확인합니다. + 작동 중 — %1$s을(를) Cloudflare로 확인했습니다. + Cloudflare에 연결할 수 없어(%1$s) 이 세션의 나머지 동안 네트워크의 확인자를 사용합니다. + 다시 시도 diff --git a/manager/src/main/res/values-pl/strings.xml b/manager/src/main/res/values-pl/strings.xml index e22057ae9..dbe11afc9 100644 --- a/manager/src/main/res/values-pl/strings.xml +++ b/manager/src/main/res/values-pl/strings.xml @@ -418,4 +418,9 @@ Sieć Rozwiązuj nazwy przez HTTPS Rozwiązuje nazwy przez Cloudflare zamiast przez resolver sieci. Dla sieci, które blokują lub zmieniają DNS. + Nie rozwiązano jeszcze żadnej nazwy. + Nieużywane: skonfigurowano proxy, więc to ono rozwiązuje nazwy. + Działa — %1$s rozwiązano przez Cloudflare. + Nie udało się połączyć z Cloudflare (%1$s), więc przez resztę tej sesji używany jest resolver sieci. + Spróbuj ponownie diff --git a/manager/src/main/res/values-pt-rBR/strings.xml b/manager/src/main/res/values-pt-rBR/strings.xml index 2f97d71cf..e3395cc84 100644 --- a/manager/src/main/res/values-pt-rBR/strings.xml +++ b/manager/src/main/res/values-pt-rBR/strings.xml @@ -396,4 +396,9 @@ Rede Resolver nomes por HTTPS Resolve nomes pela Cloudflare em vez do resolvedor da rede. Para redes que bloqueiam ou adulteram o DNS. + Nenhum nome resolvido ainda. + Sem uso: há um proxy configurado, então é ele que resolve os nomes. + Funcionando — %1$s foi resolvido pela Cloudflare. + Não foi possível alcançar a Cloudflare (%1$s), então o resolvedor da rede será usado no resto desta sessão. + Tentar de novo diff --git a/manager/src/main/res/values-ru/strings.xml b/manager/src/main/res/values-ru/strings.xml index 326d130fd..d6927c64b 100644 --- a/manager/src/main/res/values-ru/strings.xml +++ b/manager/src/main/res/values-ru/strings.xml @@ -399,4 +399,9 @@ Сеть Разрешать имена через HTTPS Разрешает имена через Cloudflare, а не через резолвер сети. Для сетей, которые блокируют или подменяют DNS. + Пока ни одно имя не разрешалось. + Не используется: настроен прокси, и разрешением имён занимается он. + Работает — %1$s разрешено через Cloudflare. + Cloudflare недоступен (%1$s), поэтому до конца этого сеанса используется резолвер сети. + Повторить diff --git a/manager/src/main/res/values-tr/strings.xml b/manager/src/main/res/values-tr/strings.xml index ef4e7cc31..dec883c10 100644 --- a/manager/src/main/res/values-tr/strings.xml +++ b/manager/src/main/res/values-tr/strings.xml @@ -396,4 +396,9 @@ Adları HTTPS üzerinden çöz Adları ağın çözümleyicisi yerine Cloudflare üzerinden çözer. DNS\'i engelleyen veya kurcalayan ağlar için. + Henüz hiçbir ad çözümlenmedi. + Kullanılmıyor: bir proxy yapılandırılmış, adları o çözümlüyor. + Çalışıyor — %1$s Cloudflare üzerinden çözümlendi. + Cloudflare\'a ulaşılamadı (%1$s), bu oturumun kalanında ağın çözümleyicisi kullanılacak. + Yeniden dene diff --git a/manager/src/main/res/values-uk/strings.xml b/manager/src/main/res/values-uk/strings.xml index 0107635e9..41bd5667f 100644 --- a/manager/src/main/res/values-uk/strings.xml +++ b/manager/src/main/res/values-uk/strings.xml @@ -418,4 +418,9 @@ Мережа Розвʼязувати імена через HTTPS Розвʼязує імена через Cloudflare, а не через резолвер мережі. Для мереж, які блокують або підміняють DNS. + Поки жодне імʼя не розвʼязувалося. + Не використовується: налаштовано проксі, і розвʼязуванням імен займається він. + Працює — %1$s розвʼязано через Cloudflare. + Cloudflare недоступний (%1$s), тому до кінця цього сеансу використовується резолвер мережі. + Повторити diff --git a/manager/src/main/res/values-vi/strings.xml b/manager/src/main/res/values-vi/strings.xml index 47eddf2c3..272b3fa77 100644 --- a/manager/src/main/res/values-vi/strings.xml +++ b/manager/src/main/res/values-vi/strings.xml @@ -385,4 +385,9 @@ Mạng Phân giải tên qua HTTPS Phân giải tên qua Cloudflare thay vì trình phân giải của mạng. Dành cho mạng chặn hoặc can thiệp DNS. + Chưa phân giải tên nào. + Không dùng: đã cấu hình proxy nên proxy đang phân giải tên. + Đang hoạt động — %1$s được phân giải qua Cloudflare. + Không kết nối được tới Cloudflare (%1$s), nên trình phân giải của mạng sẽ được dùng trong phần còn lại của phiên này. + Thử lại diff --git a/manager/src/main/res/values-zh-rCN/strings.xml b/manager/src/main/res/values-zh-rCN/strings.xml index 12f2bd372..91b77ad80 100644 --- a/manager/src/main/res/values-zh-rCN/strings.xml +++ b/manager/src/main/res/values-zh-rCN/strings.xml @@ -386,4 +386,9 @@ 网络 通过 HTTPS 解析域名 通过 Cloudflare 而非网络自带的解析器查询域名。适用于屏蔽或篡改 DNS 的网络。 + 尚未解析任何域名。 + 未使用:已配置代理,域名由代理解析。 + 正常 — 已通过 Cloudflare 解析 %1$s。 + 无法连接 Cloudflare(%1$s),本次会话的剩余时间将使用网络自带的解析器。 + 重试 diff --git a/manager/src/main/res/values-zh-rTW/strings.xml b/manager/src/main/res/values-zh-rTW/strings.xml index f69093b14..bd6cb415f 100644 --- a/manager/src/main/res/values-zh-rTW/strings.xml +++ b/manager/src/main/res/values-zh-rTW/strings.xml @@ -386,4 +386,9 @@ 網路 透過 HTTPS 解析網域 透過 Cloudflare 而非網路自帶的解析器查詢網域。適用於封鎖或竄改 DNS 的網路。 + 尚未解析任何網域。 + 未使用:已設定 Proxy,網域由 Proxy 解析。 + 正常 — 已透過 Cloudflare 解析 %1$s。 + 無法連線至 Cloudflare(%1$s),本次工作階段的其餘時間將使用網路自帶的解析器。 + 重試 diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index 05966c55d..7db5e3e12 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -430,4 +430,9 @@ Network Resolve names over HTTPS Looks names up through Cloudflare instead of the network\'s resolver. For networks that block or tamper with DNS. + No names looked up yet. + Not in use: a proxy is configured, so it is resolving names. + Working — %1$s was resolved through Cloudflare. + Cloudflare could not be reached (%1$s), so the network\'s resolver is being used for the rest of this session. + Try again From c511d4c15122e4ffcdfe12fc2955dc78fad2edc2 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 29 Jul 2026 15:17:32 +0200 Subject: [PATCH 3/4] Discard the crash records another build left behind A record outlives the build that wrote it: the directory is this process's cache, which an update does not clear. So after installing a fix the status card still shows the crash that was just fixed, and it stays there until somebody thinks to press clear. That is not a stale line on a screen. It is what a reporter copies into the issue to show the fix did not work -- #799 was answered with five traces from the build before the one that fixed them, which reads as a recurrence and is not one. Matched on the whole build line rather than the hash, because a build from a dirty tree carries the hash of the commit it was built from. Only at install, never at record: this is about what a new build inherits, and a crash loop within one build must keep every record it makes. A file whose second line cannot be read is kept -- losing a trace is the worse of the two failures. --- .../vector/manager/data/log/CrashRecorder.kt | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/log/CrashRecorder.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/log/CrashRecorder.kt index d4d16bb92..fc384379a 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/data/log/CrashRecorder.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/log/CrashRecorder.kt @@ -53,7 +53,7 @@ object CrashRecorder { @Volatile private var installed = false /** - * Takes over the default handler, once per process. + * Takes over the default handler, once per process, and discards what another build left. * * Called from [org.matrix.vector.manager.di.ServiceLocator.attach], early in the activity's * `onCreate` and before anything that could fail, so the handler is in place before any screen @@ -64,6 +64,7 @@ object CrashRecorder { if (installed) return installed = true val application = context.applicationContext ?: context + runCatching { discardOtherBuilds(application) } val previous = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> runCatching { record(application, thread, throwable) } @@ -90,6 +91,29 @@ object CrashRecorder { runCatching { files(context).forEach { it.delete() } } } + /** + * Drops the records that some other build of the manager wrote. + * + * A record outlives the build that made it — the directory is this process's cache, which an + * update does not clear — so the status card goes on showing a crash that the running build + * has already fixed, until somebody thinks to press clear. That is not a stale line on a + * screen. It is what a reporter copies into the issue to show the fix did not work: #799 was + * answered with five traces from the build before the one that fixed them. + * + * Only at [install], never at [record]: this is about what a *new* build inherits, and a crash + * loop within one build must keep every record it makes. + * + * A file whose second line cannot be read is kept. Losing a trace is the worse of the two + * failures, and a record this class cannot parse is exactly the one worth still having. + */ + private fun discardOtherBuilds(context: Context) { + val current = BUILD + files(context).forEach { file -> + val theirs = runCatching { file.useLines { it.drop(1).firstOrNull() } }.getOrNull() + if (theirs != null && !theirs.startsWith(current)) runCatching { file.delete() } + } + } + /** Newest first, which is the order both the card and the clipboard want. */ private fun files(context: Context): List = directory(context) @@ -124,11 +148,22 @@ object CrashRecorder { val parasitic = context.packageName == BuildConfig.INJECTED_PACKAGE_NAME val host = if (parasitic) "parasitic in ${context.packageName}" else "standalone" return "${TIMESTAMP.format(Date(at))}\n" + - "manager ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) " + - "${BuildConfig.VERSION_HASH} · $host · thread ${thread.name} · " + + "$BUILD · $host · thread ${thread.name} · " + "android ${android.os.Build.VERSION.RELEASE} (sdk ${android.os.Build.VERSION.SDK_INT})" } + /** + * How a record says which build wrote it, and so what [discardOtherBuilds] matches on. + * + * The whole line rather than the hash alone: a build from a dirty tree carries the hash of the + * commit it was built from, so two of them can share it while differing by everything that was + * uncommitted at the time. + */ + private val BUILD: String + get() = + "manager ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) " + + BuildConfig.VERSION_HASH + private const val SUFFIX = ".log" private val TIMESTAMP From 70a43cdef8d914474c70f01f0369619e26a6af16 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 29 Jul 2026 15:17:40 +0200 Subject: [PATCH 4/4] Configure Coil where every entry point reaches it Coil is initialised by hand for the same reason OkHttp now is: parasitically this app's manifest is never installed, so nothing that self-registers there ever runs. But it was done in MainActivity, and the debug demo host never opens MainActivity -- which is the argument that moved OkHttp's initialisation out of the activity in the first place, not followed through for the loader beside it. The demo host therefore had no image loader at all, and every avatar in it failed to load. ServiceLocator.attach is where both entry points already meet. The factory is not called until the first image, so this costs nothing at startup and does not build the OkHttp client before something asks for it. --- .../vector/manager/di/ServiceLocator.kt | 19 +++++++++++++++++++ .../matrix/vector/manager/ui/MainActivity.kt | 17 ++--------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/di/ServiceLocator.kt b/manager/src/main/kotlin/org/matrix/vector/manager/di/ServiceLocator.kt index 7ab1ddb7a..5296dfd08 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/di/ServiceLocator.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/di/ServiceLocator.kt @@ -12,6 +12,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.SharingStarted import android.annotation.SuppressLint import android.content.Context +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.SingletonImageLoader +import coil3.network.okhttp.OkHttpNetworkFetcherFactory import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -205,6 +209,21 @@ object ServiceLocator { // Before anything else that could fail. Nothing below is load-bearing for it, and a crash // during startup is exactly the one that is hardest to catch on a cable. CrashRecorder.install(appContext!!) + + // Coil is configured by hand rather than through its manifest hooks, for the same reason + // OkHttp is: parasitically this app's manifest is never installed, so nothing that + // self-registers there ever runs. Here rather than in the activity because every entry + // point comes through `attach` — including the debug demo host, which never opens + // MainActivity and so had no image loader at all while this lived there. + // + // The factory is not called until the first image, so this costs nothing at startup and + // does not build the OkHttp client before something asks for it. + SingletonImageLoader.setSafe { platformContext: PlatformContext -> + ImageLoader.Builder(platformContext) + .components { add(OkHttpNetworkFetcherFactory(callFactory = { http })) } + .build() + } + observePackageChanges() } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/MainActivity.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/MainActivity.kt index 840c5b275..aad3d911b 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/MainActivity.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/MainActivity.kt @@ -5,10 +5,6 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import coil3.ImageLoader -import coil3.PlatformContext -import coil3.SingletonImageLoader -import coil3.network.okhttp.OkHttpNetworkFetcherFactory import org.matrix.vector.manager.di.ServiceLocator import org.matrix.vector.manager.ui.screens.splash.SplashGate import org.matrix.vector.manager.ui.theme.LocalizedContent @@ -33,19 +29,10 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) // Idempotent, and safe whether or not the daemon already called Constants.setBinder. + // Configures Coil, among the rest: it used to be done here, which left the debug demo host + // without it. ServiceLocator.attach(this) - // Coil is configured explicitly rather than through its manifest hooks: parasitically this - // app's manifest is never installed, so nothing that self-registers there ever runs. - // It shares the one OkHttp client, which carries the DoH configuration and the disk cache. - SingletonImageLoader.setSafe { platformContext: PlatformContext -> - ImageLoader.Builder(platformContext) - .components { - add(OkHttpNetworkFetcherFactory(callFactory = { ServiceLocator.http })) - } - .build() - } - // Started here rather than from the panels that need it: the splash is dead time the app // is spending anyway, and these reads are what makes a panel's first visit slower than its // second. By the time the splash has played, most of them have already answered.