This commit is contained in:
lunaticbum 2026-04-02 18:18:11 +09:00
parent e9e585bad0
commit dc19ff5339
4 changed files with 439 additions and 113 deletions

View File

@ -311,27 +311,57 @@ void Renderer::renderFrame(ANativeWindow* window) {
{
std::lock_guard<std::mutex> lock(weatherMutex_);
if (!weatherPixels_.empty()) {
// 1. 위치 업데이트 (픽셀 단위 이동)
// ======== ⬇️ 유기적인 움직임(Drift) 추가 ⬇️ ========
std::uniform_real_distribution<float> driftDist(-0.3f, 0.3f); // 방향 전환 폭을 살짝 키움
weatherVelX_ += driftDist(randomEngine_);
weatherVelY_ += driftDist(randomEngine_);
// 2. 현재 속도(벡터의 길이) 계산
float speed = std::sqrt(weatherVelX_ * weatherVelX_ + weatherVelY_ * weatherVelY_);
// 💡 여기서 최소/최대 이동 픽셀을 강제로 지정합니다.
float maxSpeed = 3.0f; // 프레임당 최대 5픽셀 이동 (답답하지 않게 상향)
float minSpeed = 1.0f; // 프레임당 최소 2픽셀 이동 보장 (절대 멈추거나 느려지지 않음)
// 속도 보정 (Normalization & Scaling)
if (speed < 0.001f) {
// 완전히 멈췄을 때의 방어 코드
weatherVelX_ = minSpeed;
weatherVelY_ = minSpeed;
} else if (speed > maxSpeed) {
// 한계 속도 초과 시 maxSpeed로 깎아냄
weatherVelX_ = (weatherVelX_ / speed) * maxSpeed;
weatherVelY_ = (weatherVelY_ / speed) * maxSpeed;
} else if (speed < minSpeed) {
// 🚀 속도가 너무 줄어들면 강제로 최소 속도(minSpeed) 픽셀만큼 끌어올림
weatherVelX_ = (weatherVelX_ / speed) * minSpeed;
weatherVelY_ = (weatherVelY_ / speed) * minSpeed;
}
// 3. 위치 업데이트 (최소 2px ~ 최대 5px 이동 보장)
weatherX_ += weatherVelX_;
weatherY_ += weatherVelY_;
// 2. 화면 경계 검사 및 튕기기 (Bounce)
// 4. 화면 경계 검사 및 튕기기 (Bounce)
// 튕길 때 속도가 너무 크게 변하지 않도록 난수 폭을 안정적으로(0.9~1.1) 조절
std::uniform_real_distribution<float> bounceDist(0.9f, 1.1f);
// 가로 경계
if (weatherX_ < 0) {
weatherX_ = 0;
weatherVelX_ *= -1.0f;
weatherVelX_ = std::abs(weatherVelX_) * bounceDist(randomEngine_);
} else if (weatherX_ + weatherWidth_ > surfaceWidth) {
weatherX_ = (float)surfaceWidth - weatherWidth_;
weatherVelX_ *= -1.0f;
weatherVelX_ = -std::abs(weatherVelX_) * bounceDist(randomEngine_);
}
// 세로 경계
if (weatherY_ < 0) {
weatherY_ = 0;
weatherVelY_ *= -1.0f;
weatherVelY_ = std::abs(weatherVelY_) * bounceDist(randomEngine_);
} else if (weatherY_ + weatherHeight_ > surfaceHeight) {
weatherY_ = (float)surfaceHeight - weatherHeight_;
weatherVelY_ *= -1.0f;
weatherVelY_ = -std::abs(weatherVelY_) * bounceDist(randomEngine_);
}
// 3. 그리기 (기존의 블렌딩 로직 사용)
@ -352,16 +382,18 @@ void Renderer::renderFrame(ANativeWindow* window) {
uint32_t* dst = &dstPixels[targetY * dstStride + targetX];
// 단순 합성을 위해 하드코딩된 ARGB/RGBA 순서 주의
uint8_t dr = (*dst >> 16) & 0xFF;
uint8_t dg = (*dst >> 8) & 0xFF;
uint8_t db = (*dst) & 0xFF;
// ======== ⬇️ 이 부분을 수정합니다 ⬇️ ========
// Red와 Blue 채널의 비트 시프트(Shift) 위치를 안드로이드 Bitmap(RGBA) 규격에 맞게 수정
uint8_t dr = (*dst) & 0xFF; // 배경의 Red
uint8_t dg = (*dst >> 8) & 0xFF; // 배경의 Green
uint8_t db = (*dst >> 16) & 0xFF; // 배경의 Blue
// 날씨 아이콘(src)의 R(0), G(1), B(2)와 배경(dst)을 알파 블렌딩
uint8_t r = (src[0] * alpha + dr * (255 - alpha)) / 255;
uint8_t g = (src[1] * alpha + dg * (255 - alpha)) / 255;
uint8_t b = (src[2] * alpha + db * (255 - alpha)) / 255;
*dst = (0xFF << 24) | (r << 16) | (g << 8) | b;
*dst = (0xFF << 24) | (b << 16) | (g << 8) | r;
}
}
}

View File

@ -8,16 +8,33 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PathDashPathEffect
import android.graphics.PixelFormat
import android.graphics.RadialGradient
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Shader
import android.graphics.Typeface
import android.graphics.Typeface.BOLD
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.os.BatteryManager
import android.os.Handler
import android.os.HandlerThread
import android.os.ParcelFileDescriptor
import android.service.wallpaper.WallpaperService
import android.util.Log
import android.view.Gravity
import android.view.SurfaceHolder
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.graphics.alpha
import bums.lunatic.launcher.R
import bums.lunatic.launcher.model.Translation
import bums.lunatic.launcher.model.WallContentGroup
import bums.lunatic.launcher.utils.Blog
@ -39,17 +56,59 @@ class MyWallpaperService : WallpaperService() {
Log.d(TAG, "onCreateEngine: New engine instance created.")
return NativeRenderEngine()
}
private fun getBatteryStatus(): Pair<Int, Boolean> {
data class BatteryInfo(
val percent: Int,
val isCharging: Boolean,
val speedText: String,
val detailsText: String // "9.0V 1.5A (13.5W)" 같은 상세 정보
)
private fun getBatteryStatus(): BatteryInfo {
val intent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
val plugged = intent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
val batteryPct = (level / scale.toFloat() * 100).toInt()
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL
return Pair(batteryPct, isCharging)
var speedText = ""
var detailsText = ""
if (isCharging) {
speedText = when (plugged) {
BatteryManager.BATTERY_PLUGGED_AC -> "고속"
BatteryManager.BATTERY_PLUGGED_USB -> "저속"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "무선"
else -> ""
}
// 1. 전압(Voltage) 가져오기: 기본 단위는 밀리볼트(mV)
val voltageMv = intent?.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0) ?: 0
val voltageV = voltageMv / 1000.0f
// 2. 전류(Current) 가져오기: BatteryManager를 통해 실시간 값 조회
val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager
// 안드로이드 표준 단위는 마이크로암페어(µA) 입니다.
val currentUa = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW)
// 음수(방전)로 나올 수 있으므로 절대값 처리 후 암페어(A)로 변환
val currentA = Math.abs(currentUa) / 1000000.0f
// 3. 전력(Watt) 계산: W = V * A
val wattage = voltageV * currentA
// 값이 정상적으로 읽혔을 때만 텍스트 생성 (가끔 0으로 떨어지는 기기 방어)
if (voltageV > 0f && currentA > 0f) {
detailsText = String.format("%.1fV,%.1fA,(%.1fW)", voltageV, currentA, wattage)
}
}
return BatteryInfo(batteryPct, isCharging, speedText, detailsText)
}
inner class NativeRenderEngine : Engine() {
@ -269,7 +328,6 @@ class MyWallpaperService : WallpaperService() {
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(file.absolutePath, options)
Blog.LOGE("requiredSize ${requiredSize} w :${options.outWidth} , h : ${options.outHeight}")
if (options.outWidth >= requiredSize && options.outHeight >= requiredSize) {
mediaFiles.add(file)
} else {
@ -293,7 +351,7 @@ class MyWallpaperService : WallpaperService() {
val totalMegs = mi.totalMem / 1048576L
val usedMegs = totalMegs - availableMegs
val percent = (usedMegs.toDouble() / totalMegs.toDouble() * 100).toInt()
return "RAM: $usedMegs / ${totalMegs}MB ($percent%)"
return "RAM: $percent%"
}
// 배터리/기기 온도 가져오기 (Celsius)
@ -310,116 +368,218 @@ class MyWallpaperService : WallpaperService() {
}
}
private val materialIconFont: Typeface by lazy {
resources.getFont(R.font.material_symbols)
}
// 날씨 텍스트를 분석하여 Material Symbol 아이콘 문자열로 변환하는 함수
private fun getWeatherIconString(condition: String?): String {
if (condition == null) return "thermostat"
val lower = condition.lowercase()
return when {
// 1. 천둥/번개 (우선순위 최상: 비나 눈이 와도 번개가 치면 뇌우 아이콘)
lower.contains("thunder") -> "thunderstorm"
// 2. 눈, 우박, 진눈깨비, 빙결 (Snow, Ice pellets, Sleet, Blizzard, Freezing)
lower.contains("snow") || lower.contains("blizzard") ||
lower.contains("ice") || lower.contains("sleet") ||
lower.contains("freezing") -> "snowing"
// 3. 비, 소나기, 이슬비 (Rain, Drizzle, Shower)
lower.contains("rain") || lower.contains("drizzle") ||
lower.contains("shower") -> "rainy"
// 4. 안개 (Fog, Mist)
lower.contains("fog") || lower.contains("mist") -> "foggy"
// 5. 구름 (Overcast, Cloudy, Partly cloudy)
lower.contains("partly cloudy") -> "partly_cloudy_day"
lower.contains("cloudy") || lower.contains("overcast") -> "cloud"
// 6. 맑음 (Sunny, Clear)
lower.contains("sunny") || lower.contains("clear") -> "sunny"
// 7. 예외 처리 (매핑되지 않은 기본값)
else -> "thermostat"
}
}
private fun drawBitmapFromText(weatherLines: List<String>, isCharging: Boolean = false): Bitmap? {
// 1. 데이터 준비
val dateFull = SimpleDateFormat("yyyy.MM.dd (E)", Locale.KOREAN).format(Date())
val ramInfo = getRamUsage()
val tempInfo = getDeviceTemperature()
val batteryInfo = getBatteryStatus().let { "${it.first}% ${if(it.second) "⚡" else ""}" }
val finalLines = mutableListOf<String>()
// 1. 날씨 정보 (최상단)
if (weatherLines.isNotEmpty()) finalLines.add(weatherLines[0])
// 2. 날짜 (중간 강조)
finalLines.add(dateFull)
// 3. 시스템 정보 (온도, 메모리, 배터리 통합 한 줄)
finalLines.add("$tempInfo | $ramInfo | $batteryInfo")
// 4. 주소 정보 (최하단 작게)
if (weatherLines.size > 1) {
finalLines.add(weatherLines.last())
val batteryInfo = getBatteryStatus().let { info ->
if (info.isCharging && info.detailsText.isNotEmpty()) {
"${info.percent}% ⚡${info.speedText} [${info.detailsText}]"
} else if (info.isCharging) {
"${info.percent}% ⚡${info.speedText}"
} else {
"${info.percent}%"
}
}
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textAlign = Paint.Align.CENTER
typeface = Typeface.create(Typeface.MONOSPACE, BOLD) // 가독성을 위해 BOLD
setShadowLayer(12f, 4f, 4f, Color.argb(180, 0, 0, 0)) // 가독성을 위한 그림자 강화
val weatherCondition = if (weatherLines.isNotEmpty()) weatherLines[0] else ""
val weatherTemp = if (weatherLines.size > 1) weatherLines[1] else ""
val weatherHum = if (weatherLines.size > 2) weatherLines[2] else ""
val address = if (weatherLines.size > 3) weatherLines[3] else ""
val wrapContent = ViewGroup.LayoutParams.WRAP_CONTENT
val monoBold = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD)
// 2. 뷰 생성 헬퍼
fun createTextView(textToSet: String, textSizeDp: Float, font: Typeface, alphaValue: Float = 1.0f): TextView {
return TextView(this@MyWallpaperService).apply {
layoutParams = LinearLayout.LayoutParams(wrapContent, wrapContent)
text = textToSet
textSize = textSizeDp
typeface = font
setTextColor(Color.WHITE)
alpha = alphaValue
gravity = Gravity.CENTER
setShadowLayer(8f, 2f, 2f, Color.parseColor("#99000000"))
}
}
// 영역별 폰트 사이즈 설정
val titleSize = 75f // 날씨
val dateSize = 55f // 날짜
val systemSize = 35f // 온도/메모리
val addressSize = 28f // 주소
val lineSpacingMultiplier = 0.3f
var maxWidth = 0f
var totalHeight = 50f
val lineBaselines = mutableListOf<Float>()
finalLines.forEachIndexed { index, line ->
paint.textSize = when (index) {
0 -> titleSize
1 -> dateSize
2 -> systemSize
else -> addressSize
fun createHorizontalLayout(): LinearLayout {
return LinearLayout(this@MyWallpaperService).apply {
layoutParams = LinearLayout.LayoutParams(wrapContent, wrapContent)
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER
}
}
val width = paint.measureText(line)
if (width > maxWidth) maxWidth = width
// 3. 각 줄(Row)을 생성하여 리스트에 담기
val rowsToArrange = mutableListOf<View>()
val metrics = paint.fontMetrics
val fontHeight = metrics.descent - metrics.ascent
val leading = paint.textSize * lineSpacingMultiplier
lineBaselines.add(totalHeight - metrics.ascent)
totalHeight += (fontHeight + leading)
// ① 날씨 (아이콘 + 상태)
if (weatherCondition.isNotEmpty()) {
val weatherRow = createHorizontalLayout()
weatherRow.addView(createTextView(getWeatherIconString(weatherCondition), 50f, materialIconFont))
weatherRow.addView(createTextView(weatherCondition, 28f, monoBold).apply { setPadding(20, 0, 0, 0) })
rowsToArrange.add(weatherRow)
}
totalHeight += 30f
val bitmap = Bitmap.createBitmap((maxWidth + 120).toInt(), totalHeight.toInt(), Bitmap.Config.ARGB_8888)
// ② 온도 & 습도
if (weatherTemp.isNotEmpty()) {
val tempRow = createHorizontalLayout()
val tempStr = weatherTemp.trim()
val humStr = weatherHum.trim()
tempRow.addView(createTextView("thermostat", 20f, materialIconFont, 0.9f))
tempRow.addView(createTextView(tempStr, 20f, monoBold, 0.9f).apply { setPadding(8, 0, 30, 0) })
if (humStr.isNotEmpty()) {
tempRow.addView(createTextView("water_drop", 20f, materialIconFont, 0.9f))
tempRow.addView(createTextView(humStr, 20f, monoBold, 0.9f).apply { setPadding(8, 0, 0, 0) })
}
rowsToArrange.add(tempRow)
}
// ③ 주소
if (address.isNotEmpty()) {
rowsToArrange.add(createTextView(address, 16f, monoBold, 0.95f))
}
// ④ 날짜
rowsToArrange.add(createTextView(dateFull, 14f, monoBold, 0.8f))
// ⑤ 시스템 정보 (한 줄로 묶음)
rowsToArrange.add(createTextView("$ramInfo | $tempInfo | Battery $batteryInfo", 12f, monoBold, 0.6f))
// 4. 가로 길이 측정 후 다이아몬드 형태로 재정렬
val unspecifiedSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
rowsToArrange.forEach { it.measure(unspecifiedSpec, unspecifiedSpec) }
val sortedRows = rowsToArrange.sortedBy { it.measuredWidth }
val arrangedRows = arrayOfNulls<View>(sortedRows.size)
var topIndex = 0
var bottomIndex = sortedRows.size - 1
for (i in sortedRows.indices) {
if (i % 2 == 0) arrangedRows[topIndex++] = sortedRows[i]
else arrangedRows[bottomIndex--] = sortedRows[i]
}
// 5. 최종 부모 레이아웃 조립 (원형/타원형 배경)
val rootLayout = LinearLayout(this@MyWallpaperService).apply {
layoutParams = ViewGroup.LayoutParams(wrapContent, wrapContent)
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
// 💡 원형 안에 글씨가 넉넉히 들어가도록 패딩을 충분히 줍니다.
setPadding(20, 20, 20, 20)
background = WavyOvalDrawable(
bgColor = Color.parseColor("#33000000"),
strokeColor = Color.parseColor("#22000000"), // 물결이 잘 보이도록 불투명도를 살짝 올림
strokeThick = 30f
)
}
arrangedRows.forEach { rowView ->
if (rowView != null) {
val lp = LinearLayout.LayoutParams(wrapContent, wrapContent).apply {
setMargins(0, 10, 0, 10)
}
rootLayout.addView(rowView, lp)
}
}
// 6. 💡 완벽한 정원형(Perfect Circle)을 위한 2단계 측정(Measure) 로직
val displayMetrics = resources.displayMetrics
val maxWidthSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, View.MeasureSpec.AT_MOST)
// 1차 측정: 내용물들이 자연스럽게 늘어났을 때의 크기(너비, 높이)를 구합니다.
rootLayout.measure(maxWidthSpec, unspecifiedSpec)
// 너비와 높이 중 '더 큰 값'을 찾아 원의 지름(Size)으로 삼습니다.
val circleSize = Math.max(rootLayout.measuredWidth, rootLayout.measuredHeight)
if (circleSize <= 0) return null
// 2차 측정: 너비와 높이를 원의 지름 크기로 '정확히(EXACTLY)' 덮어씌웁니다.
val exactSpec = View.MeasureSpec.makeMeasureSpec(circleSize, View.MeasureSpec.EXACTLY)
rootLayout.measure(exactSpec, exactSpec)
// 레이아웃 배치
rootLayout.layout(0, 0, circleSize, circleSize)
// 7. 비트맵 캡처
val bitmap = Bitmap.createBitmap(circleSize, circleSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val centerX = bitmap.width / 2f
finalLines.forEachIndexed { index, line ->
paint.textSize = when (index) {
0 -> titleSize
1 -> dateSize
2 -> systemSize
else -> addressSize
}
// 시스템 정보 줄(index 2)은 약간 불투명하게 처리하여 대비를 줄 수도 있습니다.
paint.alpha = if (index == 2) 200 else 255
canvas.drawText(line, centerX, lineBaselines[index], paint)
}
rootLayout.draw(canvas)
return bitmap
}
private val updateWeatherRunnable = object : Runnable {
override fun run() {
// if (!isVisible) return
//
// val (batteryLevel, isCharging) = getBatteryStatus()
//
// // --- ⬇️ 제안하신 조건별 로직 적용 ⬇️ ---
// var nextDelay = 30000L // 기본 20초
//
// if (isCharging) {
// nextDelay = 1000L // 충전 중이거나 70% 이상이면 10초
// nativeRenderer?.setImageRenderFrame(45)
// } else if (batteryLevel >= 80) {
// nativeRenderer?.setImageRenderFrame(70)
// } else {
// handler.removeCallbacks(this)
// return
// }
//
// // 비트맵 생성 및 전달
// val weather = LocationUpdateService.lastWeather
// val bitmap = drawBitmapFromText(weather,isCharging)
// bitmap?.let {
// nativeRenderer?.setWeatherBitmap(it)
// }
//
// Log.d(TAG, "Next update in ${nextDelay / 1000}s (Battery: $batteryLevel%, Charging: $isCharging)")
// handler.postDelayed(this, nextDelay)
if (!isVisible) return
val (batteryLevel, isCharging) = getBatteryStatus()
// --- ⬇️ 제안하신 조건별 로직 적용 ⬇️ ---
var nextDelay = 30000L // 기본 20초
if (isCharging) {
nextDelay = 5000L // 충전 중이거나 70% 이상이면 10초
nativeRenderer?.setImageRenderFrame(45)
} else if (batteryLevel >= 80) {
nativeRenderer?.setImageRenderFrame(70)
} else {
handler.removeCallbacks(this)
return
}
// 비트맵 생성 및 전달
val weather = LocationUpdateService.lastWeather
val bitmap = drawBitmapFromText(weather,isCharging)
bitmap?.let {
nativeRenderer?.setWeatherBitmap(it)
}
handler.postDelayed(this, nextDelay)
}
}
@ -453,9 +613,10 @@ class MyWallpaperService : WallpaperService() {
val (batteryLevel, isCharging) = getBatteryStatus()
if (batteryLevel >= 70) {
if (batteryLevel >= 70 || isCharging) {
val bitmap = drawBitmapFromText(LocationUpdateService.lastWeather)
bitmap?.let { nativeRenderer?.setWeatherBitmap(it) }
nativeRenderer?.setImageRenderFrame(55)
Log.d(TAG, "Battery Low: Weather updated only on media change.")
} else {
nativeRenderer?.setImageRenderFrame(99)
@ -503,3 +664,81 @@ class MyWallpaperService : WallpaperService() {
}
}
}
class WavyOvalDrawable(
private val bgColor: Int,
private val strokeColor: Int,
private val strokeThick: Float
) : Drawable() {
private val waveLength = 40f
private val amplitude = 6f
private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
// 단일 색상이 아닌 Shader(그라데이션)를 사용할 것이므로 style만 지정
style = Paint.Style.FILL
}
private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = strokeColor
style = Paint.Style.STROKE
strokeWidth = strokeThick
strokeCap = Paint.Cap.ROUND
strokeJoin = Paint.Join.ROUND
val wavePath = Path().apply {
moveTo(0f, 0f)
quadTo(waveLength / 4, amplitude, waveLength / 2, 0f)
quadTo(waveLength * 3 / 4, -amplitude, waveLength, 0f)
}
pathEffect = PathDashPathEffect(wavePath, waveLength, 0f, PathDashPathEffect.Style.MORPH)
}
// 💡 뷰의 크기(Bounds)가 결정되거나 변경될 때 호출됩니다.
// 여기서 크기에 맞는 그라데이션을 계산하여 페인트에 입힙니다.
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
val centerX = bounds.exactCenterX()
val centerY = bounds.exactCenterY()
val radius = Math.max(bounds.width(), bounds.height()) / 2f
if (radius > 0) {
// 바깥쪽 색상: 입력받은 bgColor에서 투명도(Alpha)만 0으로 날려버린 색
// 이렇게 해야 검은색->흰색으로 깨지는 현상 없이 자연스럽게 투명해집니다.
val transparentColor = bgColor and 0x11FFFFFF
bgPaint.shader = RadialGradient(
centerX,
centerY,
radius,
intArrayOf(bgColor, transparentColor), // 중앙 -> 바깥 색상 배열
floatArrayOf(0.4f, 1.0f), // 중심에서 40% 지점부터 투명해지기 시작
Shader.TileMode.CLAMP
)
}
}
override fun draw(canvas: Canvas) {
val rectF = RectF(bounds)
// 1. 방사형 그라데이션 배경 채우기
canvas.drawOval(rectF, bgPaint)
// 2. 선 잘림 방지용 여백 계산
val inset = amplitude + 3f
rectF.inset(inset, inset)
// 3. 물결 테두리 선 그리기
canvas.drawOval(rectF, strokePaint)
}
override fun setAlpha(alpha: Int) {
bgPaint.alpha = alpha
strokePaint.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
}

View File

@ -266,7 +266,15 @@ open class CurrentWeather {
var addr: String? = null
var current: Current? = null
fun getSummaryInfo() = listOf<String>("${this.current?.condition?.text}",
"${this.current?.temp_c}${this.current?.humidity}%"
,"${addr}")
// 아이콘 URL을 배열의 4번째(index 3) 요소로 추가 전달합니다.
fun getSummaryInfo(): List<String> {
val iconUrl = current?.condition?.icon?.let { "https:$it" } ?: ""
return listOf(
"${current?.condition?.text}", // 0: 날씨 상태
"${current?.temp_c}", // 1: 온도 및 습도
"${current?.humidity}",
"$addr", // 2: 주소
iconUrl // 3: 아이콘 URL
)
}
}

View File

@ -7,6 +7,7 @@ import android.net.*
import android.os.*
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import bums.lunatic.launcher.utils.Blog
import com.frostwire.jlibtorrent.*
import com.frostwire.jlibtorrent.alerts.*
import com.frostwire.jlibtorrent.swig.error_code
@ -89,6 +90,7 @@ class TorrentService : Service() {
val filter = IntentFilter().apply {
addAction(Intent.ACTION_POWER_CONNECTED)
addAction(Intent.ACTION_POWER_DISCONNECTED)
addAction(Intent.ACTION_BATTERY_CHANGED)
}
registerReceiver(batteryReceiver, filter)
}
@ -100,6 +102,51 @@ class TorrentService : Service() {
Intent.ACTION_POWER_DISCONNECTED -> false
else -> isCharging
}
val intent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
val plugged = intent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
val batteryPct = (level / scale.toFloat() * 100).toInt()
isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL
var speedText = ""
var detailsText = ""
if (isCharging) {
speedText = when (plugged) {
BatteryManager.BATTERY_PLUGGED_AC -> "고속"
BatteryManager.BATTERY_PLUGGED_USB -> "저속"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "무선"
else -> ""
}
// 1. 전압(Voltage) 가져오기: 기본 단위는 밀리볼트(mV)
val voltageMv = intent?.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0) ?: 0
val voltageV = voltageMv / 1000.0f
// 2. 전류(Current) 가져오기: BatteryManager를 통해 실시간 값 조회
val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager
// 안드로이드 표준 단위는 마이크로암페어(µA) 입니다.
val currentUa = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW)
// 음수(방전)로 나올 수 있으므로 절대값 처리 후 암페어(A)로 변환
val currentA = Math.abs(currentUa) / 1000000.0f
// 3. 전력(Watt) 계산: W = V * A
val wattage = voltageV * currentA
// 값이 정상적으로 읽혔을 때만 텍스트 생성 (가끔 0으로 떨어지는 기기 방어)
if (voltageV > 0f && currentA > 0f) {
detailsText = String.format("%.1fV %.1fA (%.1fW)", voltageV, currentA, wattage)
}
}
Blog.LOGE("detailsText >>> $detailsText")
updateSessionState()
}
}