diff --git a/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt index 9b0834e17..fc6526f59 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt @@ -14,7 +14,7 @@ import kotlin.math.abs * The client reads `/sys/class/power_supply/BAT/...` - a laptop's or a Deck's naming and files * (`energy_now`, `power_now`, `time_to_empty_now`, a `uevent` of `POWER_SUPPLY_*` lines). Android * names its supply `battery` and lays it out differently, and on many phones an app may not read - * it at all, so the client showed no battery. This writes a `BAT0` (and a `BAT1`, the Deck's name) + * it at all, so the client showed no battery. This writes one `BAT1` (the Deck's name) * from Android's own battery API - the same on every device - into a directory the session binds * over `/sys/class/power_supply`, refreshed every few seconds while the session runs. * @@ -31,6 +31,8 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart private var thread: Thread? = null override fun start() { + // Older builds exported the same aggregate battery twice; MangoHud counts both supplies. + File(dir, "BAT0").deleteRecursively() running = true write() thread = Thread({ @@ -39,7 +41,7 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart if (running) write() } }, "battery-sysfs").apply { isDaemon = true; start() } - Log.i(TAG, "battery: BAT0/BAT1 written to ${dir.path} every ${PERIOD_MS / 1000} s") + Log.i(TAG, "battery: BAT1 written to ${dir.path} every ${PERIOD_MS / 1000} s") } override fun stop() { @@ -87,7 +89,7 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart pct <= 15 -> "Low" else -> "Normal" } - for (name in listOf("BAT0", "BAT1")) { + for (name in listOf("BAT1")) { val bat = File(dir, name).apply { mkdirs() } val attrs = linkedMapOf( "type" to "Battery", diff --git a/app/src/test/java/com/droiddeck/launcher/session/BatteryComponentTest.kt b/app/src/test/java/com/droiddeck/launcher/session/BatteryComponentTest.kt new file mode 100644 index 000000000..fd820e22b --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/session/BatteryComponentTest.kt @@ -0,0 +1,71 @@ +package com.droiddeck.launcher.session + +import android.content.Intent +import android.os.BatteryManager +import java.io.File +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28], manifest = Config.NONE) +class BatteryComponentTest { + @get:Rule val tmp = TemporaryFolder() + private var battery: BatteryComponent? = null + + @After fun stop() { battery?.stop() } + + @Test fun oneAggregateBatteryKeepsOverlayPowerAndLifetimeConsistentWithQam() { + val supplies = tmp.newFolder("power_supply") + val vpower = tmp.newFolder("vpower") + start(supplies, vpower, -2_000_000L) + assertTelemetry(supplies, vpower) + } + + @Test fun upgradingRemovesTheDuplicateRatherThanLeavingItForOverlayDiscovery() { + val supplies = tmp.newFolder("power_supply") + val vpower = tmp.newFolder("vpower") + val legacy = File(supplies, "BAT0").apply { mkdirs() } + File(legacy, "power_now").writeText("8000000\n") + File(legacy, "charge_now").writeText("4000000\n") + start(supplies, vpower, 2_000_000L) + assertFalse(legacy.exists()) + assertTelemetry(supplies, vpower) + } + + private fun start(supplies: File, vpower: File, currentUa: Long) { + val context = RuntimeEnvironment.getApplication() + context.sendStickyBroadcast(Intent(Intent.ACTION_BATTERY_CHANGED).apply { + putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_DISCHARGING) + putExtra(BatteryManager.EXTRA_LEVEL, 50) + putExtra(BatteryManager.EXTRA_SCALE, 100) + putExtra(BatteryManager.EXTRA_VOLTAGE, 4000) + }) + shadowOf(context.getSystemService(BatteryManager::class.java)).apply { + setLongProperty(BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER, 4_000_000L) + setLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW, currentUa) + setLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE, currentUa) + } + battery = BatteryComponent(supplies, vpower).also { it.attach(context); it.start() } + } + + private fun assertTelemetry(supplies: File, vpower: File) { + val discovered = supplies.listFiles()!!.filter { it.name.contains("BAT") } + assertEquals(listOf("BAT1"), discovered.map { it.name }) + fun read(name: String) = File(discovered.single(), name).readText().trim().toLong() + // One 4 Ah battery at 4 V drawing 2 A: 8 W and two hours, regardless of vendor current sign. + assertEquals(8_000_000L, read("power_now")) + assertEquals(16_000_000L, read("energy_now")) + assertEquals(7200L, read("time_to_empty_now")) + assertEquals("7200", File(vpower, "secs_until_shutdown_request").readText().trim()) + assertEquals("50.00", File(vpower, "battery_percent").readText().trim()) + } +}