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:
@@ -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,8 +258,21 @@ fun ConnectionScreen() {
|
||||
val status by connection.status.collectAsState()
|
||||
val messages by connection.messages.collectAsState()
|
||||
|
||||
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().background(Color.Black).padding(24.dp),
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text("ViewMate Glasses", style = MaterialTheme.typography.headlineMedium)
|
||||
@@ -259,4 +285,31 @@ fun ConnectionScreen() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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<Output>()
|
||||
|
||||
// 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): левый-нижний угол и размеры. */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user