feat(app-glasses): панель статуса — время + заряд (цвет по статусу)

Поверх видео (GL-оверлей в EglVideoRenderer: bitmap-текстура, правый
верхний угол, обе поверхности) и композ-панель на экране соединения.
Цвета (из удачной панели pair-view StatusComponent): CHARGING/FULL —
зелёный, DISCHARGING — белый, NOT_CHARGING — красный.
Данные: StatusPanelProvider (sticky ACTION_BATTERY_CHANGED + тик 1с,
HH:mm). Тесты 4 (chargeColorArgb). Железо: «00:02 94%» + зелёный
квадрат (очки на зарядке), скрин подтверждён.
This commit is contained in:
Hermes Agent
2026-08-20 00:03:23 +03:00
parent 2b6fdb7148
commit 3f858c4a1a
5 changed files with 392 additions and 15 deletions
@@ -0,0 +1,94 @@
package pw.binom.viewmate.glasses.status
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
/** Данные панели статуса: время + заряд + цвет индикатора. */
data class StatusPanelData(
val time: String, // "23:51"
val percent: Int, // 0..100
val chargeColorArgb: Int, // цвет индикатора
)
const val CHARGE_COLOR_CHARGING = 0xFF4CAF50.toInt() // зелёный — идёт заряд
const val CHARGE_COLOR_DISCHARGING = 0xFFFFFFFF.toInt() // белый — разряд
const val CHARGE_COLOR_PLUGGED_NOT_CHARGING = 0xFFE53935.toInt() // красный — подключен, но не заряжается
/**
* Цвет индикатора: не подключен=белый, CHARGING=зелёный,
* подключен-не-заряжается (NOT_CHARGING/FULL)=красный.
*/
fun chargeColorArgb(plugged: Boolean, status: Int): Int = when {
!plugged -> CHARGE_COLOR_DISCHARGING
status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL -> CHARGE_COLOR_CHARGING
else -> CHARGE_COLOR_PLUGGED_NOT_CHARGING
}
/** Провайдер: обновляет раз в секунду (время) + батарея из ACTION_BATTERY_CHANGED. */
class StatusPanelProvider(
private val context: Context,
private val onUpdate: (StatusPanelData) -> Unit,
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val timeFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
private var percent = 0
private var plugged = false
private var status = BatteryManager.BATTERY_STATUS_UNKNOWN
private var running = false
fun start() {
if (running) return
running = true
readBattery()
scope.launch {
while (isActive && running) {
emit()
delay(1_000)
readBattery()
}
}
}
fun stop() {
if (!running) return
running = false
scope.coroutineContext.cancel()
}
/** Sticky ACTION_BATTERY_CHANGED: level/scale/status/plugged. */
private fun readBattery() {
val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
?: return
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
status = intent.getIntExtra(
BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN
)
plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0
percent = if (scale > 0 && level >= 0) {
(level * 100f / scale).roundToInt().coerceIn(0, 100)
} else {
level.coerceIn(0, 100)
}
}
private fun emit() {
val time = timeFormat.format(Date())
onUpdate(StatusPanelData(time, percent, chargeColorArgb(plugged, status)))
}
}