mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 16:57:39 +00:00
fix: make usage insights accurate — impact ranking, honest attribution, missing rules
Accuracy fixes:
- Insights are ranked by an estimated-impact score (rough hours of extra
radio/CPU activity, ranking only, never displayed) before the cut to
three — declaration order no longer decides which consumer gets dropped.
- The notification insight states the measurement ('held connections open
for N relay-hours in the background') instead of an unverified 'largest
background cost' superlative, and only fires when the service's uptime
covers at least half the background window — a service that ran 20
minutes can no longer be blamed for 30 hours of background connections.
- When the measured drain split shows foreground use dominating (>=3x the
background share with enough signal), an informational note leads the
list saying most battery went to screen-on use — so nobody flips a
setting expecting savings it cannot deliver.
Completeness — new rules, each with a deep link:
- PoW mining time -> compose settings (default difficulty), scored as
full-tilt CPU.
- Reconnect churn -> relay list (a flaky relay burns a handshake plus a
radio wake-up per retry) — distinct diagnosis from 'too many relays'.
- Push-processing wakelock time / process-start churn -> notification
settings.
Count strings converted to plurals per res/CLAUDE.md. Insight test suite
extended to 15 cases covering the attribution gate, score ordering, the
honesty note, and each new rule.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
This commit is contained in:
+85
-19
@@ -21,30 +21,47 @@
|
||||
package com.vitorpamplona.amethyst.service.resourceusage
|
||||
|
||||
/**
|
||||
* Turns a week of counters into at most [MAX_INSIGHTS] actionable
|
||||
* Turns a multi-day summary into at most [MAX_INSIGHTS] actionable
|
||||
* recommendations, each mapping to a setting the user can actually change.
|
||||
* The numbers alone make the user the analyst; these rules encode the
|
||||
* analysis (thresholds informed by the 2026-07-12 ping study) and leave the
|
||||
* user only the decision.
|
||||
*
|
||||
* Rules are ordered by typical battery impact; unlike [ResourceUsageAlerts]
|
||||
* (which detects "something is wrong" and interrupts), insights render
|
||||
* passively on the usage screen and use much lower thresholds — "worth
|
||||
* knowing", not "pathological".
|
||||
* Candidates that fire are ranked by an estimated-impact [score] — a rough
|
||||
* conversion of each signal to "hours of extra radio/CPU activity" — so the
|
||||
* cut to [MAX_INSIGHTS] drops the smallest consumers, not whichever rule
|
||||
* happened to be declared last. Scores are order-of-magnitude heuristics for
|
||||
* RANKING ONLY and are never shown to the user.
|
||||
*
|
||||
* When the measured battery split says foreground use dominates, a non-action
|
||||
* [Target.FOREGROUND_INFO] note leads the list: most drain being screen-on
|
||||
* time is the honest headline, and without it users would act on a minor
|
||||
* insight expecting savings the settings can't deliver.
|
||||
*
|
||||
* Unlike [ResourceUsageAlerts] (which detects "something is wrong" and
|
||||
* interrupts), insights render passively on the usage screen and use much
|
||||
* lower thresholds — "worth knowing", not "pathological".
|
||||
*/
|
||||
object UsageInsights {
|
||||
/** Each target names the settings surface that can act on the insight. */
|
||||
enum class Target {
|
||||
/** Informational only — no button. Foreground/screen use dominates the measured drain. */
|
||||
FOREGROUND_INFO,
|
||||
NOTIFICATION_SETTINGS,
|
||||
MEDIA_SETTINGS,
|
||||
RELAY_SETTINGS,
|
||||
RELAY_CHURN,
|
||||
POW_SETTINGS,
|
||||
PUSH_PROCESSING,
|
||||
PRIVACY_SETTINGS,
|
||||
}
|
||||
|
||||
data class Insight(
|
||||
val target: Target,
|
||||
/** ms for time-based insights, bytes for data, count for relays. */
|
||||
/** ms for time-based insights, bytes for data, counts for relays/restarts, percent for FOREGROUND_INFO. */
|
||||
val value: Long,
|
||||
/** Estimated impact in "hours of extra radio/CPU activity" — ranking only, never displayed. */
|
||||
val score: Double,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -53,46 +70,95 @@ object UsageInsights {
|
||||
*/
|
||||
fun evaluate(s: UsageSummary): List<Insight> {
|
||||
val days = s.dayCount.coerceAtLeast(1)
|
||||
val insights = mutableListOf<Insight>()
|
||||
val candidates = mutableListOf<Insight>()
|
||||
|
||||
// Background relay connections dominate drain while the always-on
|
||||
// service is in use — the one consumer with a dedicated off switch.
|
||||
// Background relay connections, attributed to the always-on service
|
||||
// ONLY when its uptime actually covers most of the background window —
|
||||
// a service that ran 20 minutes cannot own 30 hours of background
|
||||
// connections (long background audio sessions also hold relays).
|
||||
val bgConnMs = s.relayConnMsMobileBg + s.relayConnMsWifiBg
|
||||
if (s.alwaysOnMs > 0 && bgConnMs > days * BG_RELAY_HOURS_PER_DAY * MS_PER_HOUR) {
|
||||
insights += Insight(Target.NOTIFICATION_SETTINGS, bgConnMs)
|
||||
val bgWallMs = (days * ResourceUsageAccountant.DAY_MS - s.foregroundMs).coerceAtLeast(1L)
|
||||
val serviceCoversBackground = s.alwaysOnMs * 2 >= bgWallMs
|
||||
if (serviceCoversBackground && bgConnMs > days * BG_RELAY_HOURS_PER_DAY * MS_PER_HOUR) {
|
||||
candidates += Insight(Target.NOTIFICATION_SETTINGS, bgConnMs, score = hours(s.alwaysOnMs))
|
||||
}
|
||||
|
||||
// Cellular media: images/video/previews can be limited to Wi-Fi.
|
||||
// Score: scattered mobile downloads pay radio ramp+tail per burst;
|
||||
// ~60 MB of feed media ≈ an hour of radio-active time.
|
||||
val cellularMediaBytes =
|
||||
(s.mobileBytesPerSubsystem[UsageKeys.ROLE_IMAGE] ?: 0L) +
|
||||
(s.mobileBytesPerSubsystem[UsageKeys.ROLE_VIDEO] ?: 0L) +
|
||||
(s.mobileBytesPerSubsystem[UsageKeys.ROLE_PREVIEW] ?: 0L)
|
||||
if (cellularMediaBytes > days * CELLULAR_MEDIA_BYTES_PER_DAY) {
|
||||
insights += Insight(Target.MEDIA_SETTINGS, cellularMediaBytes)
|
||||
candidates += Insight(Target.MEDIA_SETTINGS, cellularMediaBytes, score = cellularMediaBytes / (60.0 * 1024 * 1024))
|
||||
}
|
||||
|
||||
// Average simultaneous relay connections across the whole period:
|
||||
// every open connection is server-pinged every 30-70s, so the radio
|
||||
// never sleeps while they're up. Fewer relays = less radio time.
|
||||
// Average simultaneous relay connections: every open connection is
|
||||
// server-pinged every 30-70s. Score: marginal — the radio is often up
|
||||
// anyway; extra relays add pings, duplicates, and handshakes.
|
||||
val avgRelays = s.relayConnMs / (days * ResourceUsageAccountant.DAY_MS)
|
||||
if (avgRelays > AVG_RELAYS) {
|
||||
insights += Insight(Target.RELAY_SETTINGS, avgRelays)
|
||||
candidates += Insight(Target.RELAY_SETTINGS, avgRelays, score = hours(s.relayConnMs) / 100.0)
|
||||
}
|
||||
|
||||
// In-app Tor pays circuit crypto + keep-alives for as long as it runs.
|
||||
// Reconnect churn: a flaky relay in the list burns a TCP+TLS
|
||||
// handshake plus a radio burst per retry. Score: ~15s of radio each.
|
||||
val reconnects = s.relayConnects + s.relayConnectFails
|
||||
if (reconnects > days * RECONNECTS_PER_DAY) {
|
||||
candidates += Insight(Target.RELAY_CHURN, reconnects, score = reconnects * 15.0 / 3600.0)
|
||||
}
|
||||
|
||||
// NIP-13 mining: multi-core CPU flat out — roughly 3x the drain rate
|
||||
// of an ordinary radio-active hour.
|
||||
if (s.powMs > days * POW_MINUTES_PER_DAY * MS_PER_MINUTE) {
|
||||
candidates += Insight(Target.POW_SETTINGS, s.powMs, score = hours(s.powMs) * 3.0)
|
||||
}
|
||||
|
||||
// Push processing: wakelock time plus process cold starts (each
|
||||
// restart re-parses, re-connects, and pays a radio burst, ~10s each).
|
||||
val pushScore = hours(s.wakelockMs) + s.appStarts * 10.0 / 3600.0
|
||||
if (s.wakelockMs > days * WAKELOCK_MINUTES_PER_DAY * MS_PER_MINUTE || s.appStarts > days * APP_STARTS_PER_DAY) {
|
||||
candidates += Insight(Target.PUSH_PROCESSING, s.wakelockMs, score = pushScore)
|
||||
}
|
||||
|
||||
// In-app Tor: circuit crypto + keep-alives are overhead ON TOP of
|
||||
// traffic that would exist anyway — count a fraction of its uptime.
|
||||
if (s.torMs > days * TOR_HOURS_PER_DAY * MS_PER_HOUR) {
|
||||
insights += Insight(Target.PRIVACY_SETTINGS, s.torMs)
|
||||
candidates += Insight(Target.PRIVACY_SETTINGS, s.torMs, score = hours(s.torMs) * 0.3)
|
||||
}
|
||||
|
||||
return insights.take(MAX_INSIGHTS)
|
||||
val ranked = candidates.sortedByDescending { it.score }.take(MAX_INSIGHTS)
|
||||
|
||||
// Honesty note: when the measured split says the battery went to
|
||||
// screen-on use, say so first — settings mainly reduce the background
|
||||
// share, and the user deserves to know how big that share is.
|
||||
val fgDominates =
|
||||
s.batteryDrainFg >= MIN_DRAIN_SIGNAL_PCT &&
|
||||
s.batteryDrainFg >= 3 * s.batteryDrainBg.coerceAtLeast(1L)
|
||||
return if (fgDominates) {
|
||||
listOf(Insight(Target.FOREGROUND_INFO, s.batteryDrainFg, score = 0.0)) + ranked
|
||||
} else {
|
||||
ranked
|
||||
}
|
||||
}
|
||||
|
||||
private fun hours(ms: Long): Double = ms / MS_PER_HOUR.toDouble()
|
||||
|
||||
const val MAX_INSIGHTS = 3
|
||||
private const val MS_PER_HOUR = 60L * 60L * 1000L
|
||||
private const val MS_PER_MINUTE = 60L * 1000L
|
||||
|
||||
// Per-day thresholds — deliberately well below the alert levels.
|
||||
const val BG_RELAY_HOURS_PER_DAY = 3L
|
||||
const val CELLULAR_MEDIA_BYTES_PER_DAY = 20L * 1024L * 1024L
|
||||
const val AVG_RELAYS = 25L
|
||||
const val RECONNECTS_PER_DAY = 500L
|
||||
const val POW_MINUTES_PER_DAY = 10L
|
||||
const val WAKELOCK_MINUTES_PER_DAY = 5L
|
||||
const val APP_STARTS_PER_DAY = 15L
|
||||
const val TOR_HOURS_PER_DAY = 4L
|
||||
|
||||
/** Foreground drain below this many percent points is too noisy to call a trend. */
|
||||
const val MIN_DRAIN_SIGNAL_PCT = 5L
|
||||
}
|
||||
|
||||
+31
-8
@@ -253,11 +253,14 @@ private fun InsightsSection(
|
||||
text = insightText(insight),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
TextButton(
|
||||
onClick = { nav.nav(insightRoute(insight.target)) },
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
) {
|
||||
Text(stringRes(insightButton(insight.target)))
|
||||
val route = insightRoute(insight.target)
|
||||
if (route != null) {
|
||||
TextButton(
|
||||
onClick = { nav.nav(route) },
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
) {
|
||||
Text(stringRes(insightButton(insight.target)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,30 +270,50 @@ private fun InsightsSection(
|
||||
@Composable
|
||||
private fun insightText(insight: UsageInsights.Insight): String =
|
||||
when (insight.target) {
|
||||
UsageInsights.Target.FOREGROUND_INFO ->
|
||||
stringRes(R.string.resource_usage_insight_foreground, insight.value.toString())
|
||||
UsageInsights.Target.NOTIFICATION_SETTINGS ->
|
||||
stringRes(R.string.resource_usage_insight_notifications, formatConnHours(insight.value))
|
||||
UsageInsights.Target.MEDIA_SETTINGS ->
|
||||
stringRes(R.string.resource_usage_insight_media, formatBytes(insight.value))
|
||||
UsageInsights.Target.RELAY_SETTINGS ->
|
||||
stringRes(R.string.resource_usage_insight_relays, insight.value.toString())
|
||||
UsageInsights.Target.RELAY_SETTINGS -> {
|
||||
val relays = insight.value.toInt()
|
||||
pluralStringResource(R.plurals.resource_usage_insight_relays, relays, relays)
|
||||
}
|
||||
UsageInsights.Target.RELAY_CHURN -> {
|
||||
val reconnects = insight.value.toInt()
|
||||
pluralStringResource(R.plurals.resource_usage_insight_churn, reconnects, reconnects)
|
||||
}
|
||||
UsageInsights.Target.POW_SETTINGS ->
|
||||
stringRes(R.string.resource_usage_insight_pow, formatDurationMs(insight.value))
|
||||
UsageInsights.Target.PUSH_PROCESSING ->
|
||||
stringRes(R.string.resource_usage_insight_push, formatDurationMs(insight.value))
|
||||
UsageInsights.Target.PRIVACY_SETTINGS ->
|
||||
stringRes(R.string.resource_usage_insight_tor, formatDurationMs(insight.value))
|
||||
}
|
||||
|
||||
private fun insightRoute(target: UsageInsights.Target): Route =
|
||||
private fun insightRoute(target: UsageInsights.Target): Route? =
|
||||
when (target) {
|
||||
UsageInsights.Target.FOREGROUND_INFO -> null
|
||||
UsageInsights.Target.NOTIFICATION_SETTINGS -> Route.NotificationSettings
|
||||
UsageInsights.Target.MEDIA_SETTINGS -> Route.Settings
|
||||
UsageInsights.Target.RELAY_SETTINGS -> Route.EditRelays
|
||||
UsageInsights.Target.RELAY_CHURN -> Route.EditRelays
|
||||
UsageInsights.Target.POW_SETTINGS -> Route.ComposeSettings
|
||||
UsageInsights.Target.PUSH_PROCESSING -> Route.NotificationSettings
|
||||
UsageInsights.Target.PRIVACY_SETTINGS -> Route.PrivacyOptions
|
||||
}
|
||||
|
||||
@StringRes
|
||||
private fun insightButton(target: UsageInsights.Target): Int =
|
||||
when (target) {
|
||||
UsageInsights.Target.FOREGROUND_INFO -> R.string.resource_usage_insights_section
|
||||
UsageInsights.Target.NOTIFICATION_SETTINGS -> R.string.resource_usage_alwayson_settings_button
|
||||
UsageInsights.Target.MEDIA_SETTINGS -> R.string.resource_usage_insight_button_media
|
||||
UsageInsights.Target.RELAY_SETTINGS -> R.string.resource_usage_insight_button_relays
|
||||
UsageInsights.Target.RELAY_CHURN -> R.string.resource_usage_insight_button_relays
|
||||
UsageInsights.Target.POW_SETTINGS -> R.string.compose_settings
|
||||
UsageInsights.Target.PUSH_PROCESSING -> R.string.resource_usage_alwayson_settings_button
|
||||
UsageInsights.Target.PRIVACY_SETTINGS -> R.string.resource_usage_insight_button_privacy
|
||||
}
|
||||
|
||||
|
||||
@@ -3228,9 +3228,19 @@
|
||||
<string name="resource_usage_alwayson_battery">Battery drained meanwhile (whole device)</string>
|
||||
<string name="resource_usage_alwayson_settings_button">Change notification settings</string>
|
||||
<string name="resource_usage_insights_section">What’s using your battery</string>
|
||||
<string name="resource_usage_insight_notifications">Relay connections held while the app was closed are your largest background cost (%1$s).</string>
|
||||
<string name="resource_usage_insight_notifications">The always-on notification service held relay connections open for %1$s while the app was in the background.</string>
|
||||
<string name="resource_usage_insight_foreground">Most of your measured battery use (%1$s%%) happened while you were actively using the app — screen and browsing, not background activity. Settings changes mainly reduce the background share.</string>
|
||||
<plurals name="resource_usage_insight_churn">
|
||||
<item quantity="one">Relays reconnected %1$d time. Frequent reconnections usually mean an unreliable relay in your list — each retry pays a new handshake and a radio wake-up.</item>
|
||||
<item quantity="other">Relays reconnected %1$d times. Frequent reconnections usually mean an unreliable relay in your list — each retry pays a new handshake and a radio wake-up.</item>
|
||||
</plurals>
|
||||
<string name="resource_usage_insight_pow">Proof-of-work mining ran the processor at full speed for %1$s. Lowering the default difficulty reduces this directly.</string>
|
||||
<string name="resource_usage_insight_push">Processing incoming notifications kept the device awake for %1$s. Changing how notifications are delivered can reduce it.</string>
|
||||
<string name="resource_usage_insight_media">%1$s of images, video, and previews were downloaded over cellular. Media loading can be limited to Wi-Fi.</string>
|
||||
<string name="resource_usage_insight_relays">The app kept %1$s relay connections open on average. Each open connection keeps the radio awake — fewer relays means longer battery.</string>
|
||||
<plurals name="resource_usage_insight_relays">
|
||||
<item quantity="one">The app kept %1$d relay connection open on average. Each open connection keeps the radio awake — fewer relays means longer battery.</item>
|
||||
<item quantity="other">The app kept %1$d relay connections open on average. Each open connection keeps the radio awake — fewer relays means longer battery.</item>
|
||||
</plurals>
|
||||
<string name="resource_usage_insight_tor">Built-in Tor ran for %1$s. Tor spends extra battery on encryption and keep-alives for the same traffic.</string>
|
||||
<string name="resource_usage_insight_button_media">Media settings</string>
|
||||
<string name="resource_usage_insight_button_relays">Edit relays</string>
|
||||
|
||||
+83
-2
@@ -676,10 +676,11 @@ class UsageInsightsTest {
|
||||
|
||||
@Test
|
||||
fun backgroundRelayTimeWithAlwaysOnSuggestsNotificationSettings() {
|
||||
// Service ran 100 of the ~168 background hours: it owns the connections.
|
||||
val s =
|
||||
summary(
|
||||
mapOf(
|
||||
UsageKeys.ALWAYS_ON_MS to 24L * 3_600_000L,
|
||||
UsageKeys.ALWAYS_ON_MS to 100L * 3_600_000L,
|
||||
UsageKeys.relayConnMs(mobile = true, foreground = false) to 7L * 4L * 3_600_000L,
|
||||
),
|
||||
days = 7,
|
||||
@@ -688,6 +689,86 @@ class UsageInsightsTest {
|
||||
assertEquals(UsageInsights.Target.NOTIFICATION_SETTINGS, insights.first().target)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun briefServiceUptimeIsNotBlamedForBackgroundConnectionsItCannotOwn() {
|
||||
// Service ran 1h all week; 28h of background connections came from
|
||||
// something else (e.g. background audio) — no notification insight.
|
||||
val s =
|
||||
summary(
|
||||
mapOf(
|
||||
UsageKeys.ALWAYS_ON_MS to 1L * 3_600_000L,
|
||||
UsageKeys.relayConnMs(mobile = true, foreground = false) to 7L * 4L * 3_600_000L,
|
||||
),
|
||||
days = 7,
|
||||
)
|
||||
assertTrue(UsageInsights.evaluate(s).none { it.target == UsageInsights.Target.NOTIFICATION_SETTINGS })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insightsAreRankedByEstimatedImpactNotDeclarationOrder() {
|
||||
// PoW at 100h scores ~300; the notification insight scores ~150 —
|
||||
// PoW must come first even though its rule is declared later.
|
||||
val s =
|
||||
summary(
|
||||
mapOf(
|
||||
UsageKeys.ALWAYS_ON_MS to 150L * 3_600_000L,
|
||||
UsageKeys.relayConnMs(mobile = true, foreground = false) to 7L * 4L * 3_600_000L,
|
||||
UsageKeys.POW_MS to 100L * 3_600_000L,
|
||||
),
|
||||
days = 7,
|
||||
)
|
||||
val insights = UsageInsights.evaluate(s)
|
||||
assertEquals(UsageInsights.Target.POW_SETTINGS, insights[0].target)
|
||||
assertEquals(UsageInsights.Target.NOTIFICATION_SETTINGS, insights[1].target)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun powMiningTimeSuggestsComposeSettings() {
|
||||
val s = summary(mapOf(UsageKeys.POW_MS to 7L * 15L * 60_000L), days = 7)
|
||||
assertTrue(UsageInsights.evaluate(s).any { it.target == UsageInsights.Target.POW_SETTINGS })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reconnectChurnSuggestsReviewingTheRelayList() {
|
||||
val s = summary(mapOf(UsageKeys.relayConnects(mobile = true, foreground = false) to 7L * 600L), days = 7)
|
||||
assertTrue(UsageInsights.evaluate(s).any { it.target == UsageInsights.Target.RELAY_CHURN })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pushProcessingTimeSuggestsNotificationSettings() {
|
||||
val s = summary(mapOf(UsageKeys.WAKELOCK_NOTIF_MS to 7L * 10L * 60_000L), days = 7)
|
||||
assertTrue(UsageInsights.evaluate(s).any { it.target == UsageInsights.Target.PUSH_PROCESSING })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun foregroundDominatedDrainLeadsWithTheHonestyNote() {
|
||||
val s =
|
||||
summary(
|
||||
mapOf(
|
||||
UsageKeys.BATTERY_DRAIN_FG to 20L,
|
||||
UsageKeys.BATTERY_DRAIN_BG to 2L,
|
||||
UsageKeys.TOR_MS to 7L * 5L * 3_600_000L,
|
||||
),
|
||||
days = 7,
|
||||
)
|
||||
val insights = UsageInsights.evaluate(s)
|
||||
assertEquals(UsageInsights.Target.FOREGROUND_INFO, insights.first().target)
|
||||
assertTrue(insights.any { it.target == UsageInsights.Target.PRIVACY_SETTINGS })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun balancedDrainDoesNotAddTheHonestyNote() {
|
||||
val s =
|
||||
summary(
|
||||
mapOf(
|
||||
UsageKeys.BATTERY_DRAIN_FG to 10L,
|
||||
UsageKeys.BATTERY_DRAIN_BG to 8L,
|
||||
),
|
||||
days = 7,
|
||||
)
|
||||
assertTrue(UsageInsights.evaluate(s).none { it.target == UsageInsights.Target.FOREGROUND_INFO })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun backgroundRelayTimeWithoutAlwaysOnDoesNotBlameNotifications() {
|
||||
val s =
|
||||
@@ -745,7 +826,7 @@ class UsageInsightsTest {
|
||||
val s =
|
||||
summary(
|
||||
mapOf(
|
||||
UsageKeys.ALWAYS_ON_MS to 24L * 3_600_000L,
|
||||
UsageKeys.ALWAYS_ON_MS to 168L * 3_600_000L,
|
||||
UsageKeys.relayConnMs(mobile = true, foreground = false) to 40L * 7L * 24L * 3_600_000L,
|
||||
UsageKeys.net(UsageKeys.ROLE_VIDEO, mobile = true, foreground = true, received = true) to 7L * 200L * 1024L * 1024L,
|
||||
UsageKeys.TOR_MS to 7L * 10L * 3_600_000L,
|
||||
|
||||
Reference in New Issue
Block a user