From 3f858c4a1ade79f991f95c8fe1657159486f1c32 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 00:03:23 +0300 Subject: [PATCH] =?UTF-8?q?feat(app-glasses):=20=D0=BF=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=B0=20?= =?UTF-8?q?=E2=80=94=20=D0=B2=D1=80=D0=B5=D0=BC=D1=8F=20+=20=D0=B7=D0=B0?= =?UTF-8?q?=D1=80=D1=8F=D0=B4=20(=D1=86=D0=B2=D0=B5=D1=82=20=D0=BF=D0=BE?= =?UTF-8?q?=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D1=83)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Поверх видео (GL-оверлей в EglVideoRenderer: bitmap-текстура, правый верхний угол, обе поверхности) и композ-панель на экране соединения. Цвета (из удачной панели pair-view StatusComponent): CHARGING/FULL — зелёный, DISCHARGING — белый, NOT_CHARGING — красный. Данные: StatusPanelProvider (sticky ACTION_BATTERY_CHANGED + тик 1с, HH:mm). Тесты 4 (chargeColorArgb). Железо: «00:02 94%» + зелёный квадрат (очки на зарядке), скрин подтверждён. --- .../pw/binom/viewmate/glasses/MainActivity.kt | 75 ++++++-- .../viewmate/glasses/status/StatusPanel.kt | 94 ++++++++++ .../glasses/ui/BinocularVideoPlayer.kt | 169 +++++++++++++++++- .../viewmate/glasses/ui/GlassesMovieScreen.kt | 17 +- .../glasses/status/StatusPanelTest.kt | 52 ++++++ 5 files changed, 392 insertions(+), 15 deletions(-) create mode 100644 app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/status/StatusPanel.kt create mode 100644 app-glasses/src/test/kotlin/pw/binom/viewmate/glasses/status/StatusPanelTest.kt diff --git a/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/MainActivity.kt b/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/MainActivity.kt index 315126f..eefa502 100644 --- a/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/MainActivity.kt +++ b/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/MainActivity.kt @@ -17,19 +17,29 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -41,6 +51,9 @@ import com.rayneo.arsdk.android.ui.activity.BaseEventActivity import kotlinx.coroutines.launch import pw.binom.viewmate.glasses.gestures.actionName import pw.binom.viewmate.glasses.gestures.templeActionToCommand +import pw.binom.viewmate.glasses.status.CHARGE_COLOR_DISCHARGING +import pw.binom.viewmate.glasses.status.StatusPanelData +import pw.binom.viewmate.glasses.status.StatusPanelProvider import pw.binom.viewmate.glasses.ui.GlassesMovieScreen class MainActivity : BaseEventActivity() { @@ -245,18 +258,58 @@ fun ConnectionScreen() { val status by connection.status.collectAsState() val messages by connection.messages.collectAsState() - Column( - modifier = Modifier.fillMaxSize().background(Color.Black).padding(24.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text("ViewMate Glasses", style = MaterialTheme.typography.headlineMedium) - Text(if (connected) "подключено к телефону" else "нет соединения") - Text(status, style = MaterialTheme.typography.bodyMedium) - Text("Последние сообщения:", style = MaterialTheme.typography.titleMedium) - LazyColumn(modifier = Modifier.fillMaxWidth()) { - items(messages) { msg -> - Text(msg, style = MaterialTheme.typography.bodySmall) + val context = LocalContext.current + var statusData by remember { + mutableStateOf(StatusPanelData("--:--", 0, CHARGE_COLOR_DISCHARGING)) + } + val statusProvider = remember(context) { + StatusPanelProvider(context.applicationContext) { statusData = it } + } + DisposableEffect(statusProvider) { + statusProvider.start() + onDispose { statusProvider.stop() } + } + + Box(modifier = Modifier.fillMaxSize().background(Color.Black)) { + Column( + modifier = Modifier.fillMaxSize().padding(24.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text("ViewMate Glasses", style = MaterialTheme.typography.headlineMedium) + Text(if (connected) "подключено к телефону" else "нет соединения") + Text(status, style = MaterialTheme.typography.bodyMedium) + Text("Последние сообщения:", style = MaterialTheme.typography.titleMedium) + LazyColumn(modifier = Modifier.fillMaxWidth()) { + items(messages) { msg -> + Text(msg, style = MaterialTheme.typography.bodySmall) + } } } + + StatusPanelOverlay( + data = statusData, + modifier = Modifier.align(Alignment.TopEnd).padding(8.dp), + ) + } +} + +/** Маленькая панель статуса (время + цветной квадрат + заряд) в правом верхнем углу. */ +@Composable +private fun StatusPanelOverlay(data: StatusPanelData, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .background(Color(0x80000000.toInt())) + .padding(horizontal = 6.dp, vertical = 3.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + Modifier + .size(12.dp) + .background(Color(data.chargeColorArgb)) + ) + Text(data.time, color = Color.White, style = MaterialTheme.typography.bodySmall) + Text("${data.percent}%", color = Color.White, style = MaterialTheme.typography.bodySmall) } } diff --git a/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/status/StatusPanel.kt b/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/status/StatusPanel.kt new file mode 100644 index 0000000..28df9b4 --- /dev/null +++ b/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/status/StatusPanel.kt @@ -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))) + } +} diff --git a/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/ui/BinocularVideoPlayer.kt b/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/ui/BinocularVideoPlayer.kt index 463fa8d..4617e7d 100644 --- a/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/ui/BinocularVideoPlayer.kt +++ b/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/ui/BinocularVideoPlayer.kt @@ -1,24 +1,30 @@ package pw.binom.viewmate.glasses.ui +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint import android.graphics.SurfaceTexture import android.opengl.GLES20 +import android.text.TextPaint import android.view.Surface import android.view.SurfaceHolder import android.view.SurfaceView import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.video.VideoSize +import java.nio.ByteBuffer +import java.nio.ByteOrder import kotlin.math.roundToInt import pw.binom.viewmate.glasses.log +import pw.binom.viewmate.glasses.render.Drawable2d import pw.binom.viewmate.glasses.render.EglCore import pw.binom.viewmate.glasses.render.FullFrameRect +import pw.binom.viewmate.glasses.render.GlUtil import pw.binom.viewmate.glasses.render.Texture2dProgram import pw.binom.viewmate.glasses.render.WindowSurface @@ -64,7 +70,10 @@ fun BinocularVideoPlayer( * Всё создаётся на главном потоке (SurfaceHolder.Callback и OnFrameAvailableListener), * поэтому защита от гонок — null-проверки: кадр может прийти между созданиями выводов. */ -class EglVideoRenderer(private val player: SimpleExoPlayer) { +class EglVideoRenderer( + private val player: SimpleExoPlayer, + private val density: Float = 1f, +) { private var eglCore: EglCore? = null private var fullFrameBlit: FullFrameRect? = null @@ -75,6 +84,15 @@ class EglVideoRenderer(private val player: SimpleExoPlayer) { private val outputs = mutableListOf() + // Overlay-панель статуса: рисуется в правом верхнем углу поверх видео. + private var overlayText: String? = null + private var overlayColorArgb: Int = 0 + private var overlayBitmap: Bitmap? = null + private var overlayProgram: Texture2dProgram? = null + private var overlayDrawable: Drawable2d? = null + private var overlayTextureId = 0 + private var overlayNeedsUpload = false + // Размер видео (0,0 пока неизвестен) — нужен для letterbox-вьюпорта. private var videoWidth = 0 private var videoHeight = 0 @@ -171,9 +189,134 @@ class EglVideoRenderer(private val player: SimpleExoPlayer) { GLES20.glViewport(rect.x0, rect.y0, rect.w, rect.h) } blit.drawFrame(textureId, transformMatrix) + drawOverlay(viewWidth, viewHeight) windowSurface.swapBuffers() } + /** + * Задаёт текст и цвет индикатора overlay-панели (обычно из StatusPanelProvider, + * вызывается на главном потоке). При неизменных значениях — no-op. + */ + fun setOverlay(text: String?, colorArgb: Int) { + if (text == overlayText && colorArgb == overlayColorArgb) return + overlayText = text + overlayColorArgb = colorArgb + overlayBitmap?.recycle() + overlayBitmap = text?.let { buildOverlayBitmap(it, colorArgb) } + overlayNeedsUpload = true + } + + /** Компактная панель ~110×34px: полупрозрачный чёрный фон, цветной квадрат слева, текст справа. */ + private fun buildOverlayBitmap(text: String, colorArgb: Int): Bitmap { + val panelHeight = OVERLAY_PANEL_HEIGHT + val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply { + color = 0xFFFFFFFF.toInt() + textSize = OVERLAY_TEXT_SIZE_SP * density + isAntiAlias = true + } + val textWidth = kotlin.math.ceil(textPaint.measureText(text).toDouble()).toInt() + val panelWidth = OVERLAY_PADDING + OVERLAY_COLOR_SQUARE + OVERLAY_PADDING + textWidth + OVERLAY_PADDING + + val bitmap = Bitmap.createBitmap(panelWidth, panelHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + val bgPaint = Paint().apply { color = 0x80000000.toInt() } + canvas.drawRect(0f, 0f, panelWidth.toFloat(), panelHeight.toFloat(), bgPaint) + + val squareTop = (panelHeight - OVERLAY_COLOR_SQUARE) / 2f + val squarePaint = Paint().apply { color = colorArgb } + canvas.drawRect( + OVERLAY_PADDING.toFloat(), squareTop, + (OVERLAY_PADDING + OVERLAY_COLOR_SQUARE).toFloat(), squareTop + OVERLAY_COLOR_SQUARE, + squarePaint, + ) + + val baseline = panelHeight / 2f - (textPaint.ascent() + textPaint.descent()) / 2f + canvas.drawText( + text, + (OVERLAY_PADDING + OVERLAY_COLOR_SQUARE + OVERLAY_PADDING).toFloat(), + baseline, + textPaint, + ) + return bitmap + } + + /** Рисует overlay-панель в правом верхнем углу (после блита видео, перед swapBuffers). */ + private fun drawOverlay(viewWidth: Int, viewHeight: Int) { + val bitmap = overlayBitmap ?: return + if (overlayProgram == null) { + overlayProgram = Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_2D) + overlayDrawable = Drawable2d(Drawable2d.Prefab.RECTANGLE) + } + if (overlayTextureId == 0) { + val textures = IntArray(1) + GLES20.glGenTextures(1, textures, 0) + overlayTextureId = textures[0] + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, overlayTextureId) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE) + overlayNeedsUpload = true + } + if (overlayNeedsUpload) { + val w = bitmap.width + val h = bitmap.height + val buffer = ByteBuffer.allocateDirect(w * h * 4).order(ByteOrder.nativeOrder()) + bitmap.copyPixelsToBuffer(buffer) + buffer.position(0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, overlayTextureId) + GLES20.glTexImage2D( + GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, + w, h, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer, + ) + overlayNeedsUpload = false + } + + GLES20.glViewport(0, 0, viewWidth, viewHeight) + GLES20.glEnable(GLES20.GL_BLEND) + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA) + + val program = overlayProgram!! + val drawable = overlayDrawable!! + program.draw( + mvpMatrix = overlayMvp( + viewWidth, viewHeight, bitmap.width, bitmap.height, OVERLAY_MARGIN, + ), + vertexBuffer = drawable.mVertexArray, + firstVertex = 0, + vertexCount = drawable.mVertexCount, + coordsPerVertex = drawable.mCoordsPerVertex, + vertexStride = drawable.mVertexStride, + texMatrix = GlUtil.IDENTITY_MATRIX, + texBuffer = drawable.mTexCoordArray, + textureId = overlayTextureId, + texStride = drawable.mTexCoordStride, + ) + GLES20.glDisable(GLES20.GL_BLEND) + } + + /** + * MVP для квада панели: прямоугольник RECTANGLE (центр в 0, размер 1×1) + * смещаем/масштабируем так, чтобы панель w×h оказалась в правом верхнем + * углу вьюпорта с отступом [margin]. + */ + private fun overlayMvp(viewWidth: Int, viewHeight: Int, w: Int, h: Int, margin: Int): FloatArray { + val left = viewWidth - w - margin + val top = viewHeight - h - margin + val cx = left + w / 2f + val cy = top + h / 2f + val tx = 2f * cx / viewWidth - 1f + val ty = 1f - 2f * cy / viewHeight + val sx = 2f * w / viewWidth + val sy = 2f * h / viewHeight + return floatArrayOf( + sx, 0f, 0f, 0f, + 0f, sy, 0f, 0f, + 0f, 0f, 1f, 0f, + tx, ty, 0f, 1f, + ) + } + fun release() { if (released) return released = true @@ -196,6 +339,18 @@ class EglVideoRenderer(private val player: SimpleExoPlayer) { videoSurfaceTexture?.release() videoSurfaceTexture = null + // Overlay-текстура/программа живут в GL-контексте — удаляем, пока он ещё текущий. + outputs.firstOrNull()?.let { it.surface.makeCurrent() } + if (overlayTextureId != 0) { + GLES20.glDeleteTextures(1, intArrayOf(overlayTextureId), 0) + overlayTextureId = 0 + } + overlayProgram?.release() + overlayProgram = null + overlayDrawable = null + overlayBitmap?.recycle() + overlayBitmap = null + outputs.forEach { it.surface.release() } outputs.clear() @@ -205,6 +360,14 @@ class EglVideoRenderer(private val player: SimpleExoPlayer) { eglCore?.release() eglCore = null } + + companion object { + private const val OVERLAY_PADDING = 6 + private const val OVERLAY_COLOR_SQUARE = 12 + private const val OVERLAY_PANEL_HEIGHT = 34 + private const val OVERLAY_TEXT_SIZE_SP = 18f + private const val OVERLAY_MARGIN = 8 + } } /** Прямоугольник вписывания видео в поверхность (letterbox): левый-нижний угол и размеры. */ diff --git a/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/ui/GlassesMovieScreen.kt b/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/ui/GlassesMovieScreen.kt index 8dea27d..560f8e7 100644 --- a/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/ui/GlassesMovieScreen.kt +++ b/app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/ui/GlassesMovieScreen.kt @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.video.VideoSize import pw.binom.viewmate.glasses.log +import pw.binom.viewmate.glasses.status.StatusPanelProvider /** * Экран кино: один плеер, вывод — дважды (левый/правый глаз) через [Binocular]. @@ -46,7 +47,21 @@ fun GlassesMovieScreen( // Один рендерер на экран: оба экземпляра BinocularVideoPlayer (левый/правый // глаз из Binocular) регистрируют в нём свои SurfaceView. - val renderer = remember { EglVideoRenderer(player) } + val renderer = remember { + EglVideoRenderer(player, density = context.resources.displayMetrics.density) + } + + // Панель статуса (время + заряд) рисуется в GL-рендерере поверх видео — + // на обе поверхности, т.к. рендерер общий. + val statusProvider = remember { + StatusPanelProvider(context.applicationContext) { data -> + renderer.setOverlay("${data.time} ${data.percent}%", data.chargeColorArgb) + } + } + DisposableEffect(Unit) { + statusProvider.start() + onDispose { statusProvider.stop() } + } DisposableEffect(player) { val listener = object : Player.Listener { diff --git a/app-glasses/src/test/kotlin/pw/binom/viewmate/glasses/status/StatusPanelTest.kt b/app-glasses/src/test/kotlin/pw/binom/viewmate/glasses/status/StatusPanelTest.kt new file mode 100644 index 0000000..a12baba --- /dev/null +++ b/app-glasses/src/test/kotlin/pw/binom/viewmate/glasses/status/StatusPanelTest.kt @@ -0,0 +1,52 @@ +package pw.binom.viewmate.glasses.status + +import android.os.BatteryManager +import kotlin.test.Test +import kotlin.test.assertEquals + +class StatusPanelTest { + + @Test + fun chargingIsGreen() { + val color = chargeColorArgb( + plugged = true, + status = BatteryManager.BATTERY_STATUS_CHARGING, + ) + assertEquals(CHARGE_COLOR_CHARGING, color) + } + + @Test + fun notPluggedIsWhiteRegardlessOfStatus() { + for (status in intArrayOf( + BatteryManager.BATTERY_STATUS_CHARGING, + BatteryManager.BATTERY_STATUS_DISCHARGING, + BatteryManager.BATTERY_STATUS_NOT_CHARGING, + BatteryManager.BATTERY_STATUS_FULL, + BatteryManager.BATTERY_STATUS_UNKNOWN, + )) { + assertEquals( + CHARGE_COLOR_DISCHARGING, + chargeColorArgb(plugged = false, status = status), + "status=$status, plugged=false → белый", + ) + } + } + + @Test + fun pluggedNotChargingIsRed() { + val color = chargeColorArgb( + plugged = true, + status = BatteryManager.BATTERY_STATUS_NOT_CHARGING, + ) + assertEquals(CHARGE_COLOR_PLUGGED_NOT_CHARGING, color) + } + + @Test + fun pluggedFullIsGreen() { + val color = chargeColorArgb( + plugged = true, + status = BatteryManager.BATTERY_STATUS_FULL, + ) + assertEquals(CHARGE_COLOR_CHARGING, color) + } +}