From dc19ff5339bef6a191ccf5f4cee5b1147ae993de Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Thu, 2 Apr 2026 18:18:11 +0900 Subject: [PATCH] ... --- app/src/main/cpp/Renderer.cpp | 54 ++- .../launcher/wall/MyWallpaperService.kt | 437 ++++++++++++++---- .../launcher/workers/LocationUpdateService.kt | 14 +- .../launcher/workers/TorrentManager.kt | 47 ++ 4 files changed, 439 insertions(+), 113 deletions(-) diff --git a/app/src/main/cpp/Renderer.cpp b/app/src/main/cpp/Renderer.cpp index 264d5fed..1e7fd68f 100644 --- a/app/src/main/cpp/Renderer.cpp +++ b/app/src/main/cpp/Renderer.cpp @@ -311,27 +311,57 @@ void Renderer::renderFrame(ANativeWindow* window) { { std::lock_guard lock(weatherMutex_); if (!weatherPixels_.empty()) { - // 1. 위치 업데이트 (픽셀 단위 이동) + // ======== ⬇️ 유기적인 움직임(Drift) 추가 ⬇️ ======== + std::uniform_real_distribution 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 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; } } } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt index e7d6681c..8815cbe7 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt @@ -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 { + + 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, 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() - - // 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 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 titleSize = 75f // 날씨 - val dateSize = 55f // 날짜 - val systemSize = 35f // 온도/메모리 - val addressSize = 28f // 주소 - val lineSpacingMultiplier = 0.3f - - var maxWidth = 0f - var totalHeight = 50f - val lineBaselines = mutableListOf() - - finalLines.forEachIndexed { index, line -> - paint.textSize = when (index) { - 0 -> titleSize - 1 -> dateSize - 2 -> systemSize - else -> addressSize + 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 width = paint.measureText(line) - if (width > maxWidth) maxWidth = width - - val metrics = paint.fontMetrics - val fontHeight = metrics.descent - metrics.ascent - val leading = paint.textSize * lineSpacingMultiplier - - lineBaselines.add(totalHeight - metrics.ascent) - totalHeight += (fontHeight + leading) } - totalHeight += 30f - val bitmap = Bitmap.createBitmap((maxWidth + 120).toInt(), totalHeight.toInt(), Bitmap.Config.ARGB_8888) + 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")) + } + } + + fun createHorizontalLayout(): LinearLayout { + return LinearLayout(this@MyWallpaperService).apply { + layoutParams = LinearLayout.LayoutParams(wrapContent, wrapContent) + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER + } + } + + // 3. 각 줄(Row)을 생성하여 리스트에 담기 + val rowsToArrange = mutableListOf() + + // ① 날씨 (아이콘 + 상태) + 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) + } + + // ② 온도 & 습도 + 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(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) @@ -502,4 +663,82 @@ 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 } \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt index d2391130..d75db690 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/LocationUpdateService.kt @@ -266,7 +266,15 @@ open class CurrentWeather { var addr: String? = null var current: Current? = null - fun getSummaryInfo() = listOf("${this.current?.condition?.text}", - "${this.current?.temp_c}℃ ${this.current?.humidity}%" - ,"${addr}") + // 아이콘 URL을 배열의 4번째(index 3) 요소로 추가 전달합니다. + fun getSummaryInfo(): List { + 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 + ) + } } \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt index 795ba4db..2bd12268 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt @@ -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() } }