This commit is contained in:
lunaticbum 2026-04-03 16:03:50 +09:00
parent dc19ff5339
commit 8c3bc96417
12 changed files with 393 additions and 112 deletions

View File

@ -65,6 +65,7 @@ import androidx.work.NetworkType
import androidx.work.workDataOf
import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieString
import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieUrlString
import bums.lunatic.launcher.model.QuoteItem
import bums.lunatic.launcher.model.WallContentGroup
import bums.lunatic.launcher.workers.WorkersDb
import com.google.common.reflect.TypeToken
@ -77,6 +78,87 @@ import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.Calendar
data class ZenQuoteResponse(val q: String, val a: String)
data class KorAdviceResponse(val author: String, val message: String)
class QuoteFetchWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
fetchAndSaveQuotes()
Result.success()
} catch (e: Exception) {
Blog.LOGE("QuoteFetchWorker Failed", e)
Result.retry() // 실패 시 재시도
}
}
private suspend fun fetchAndSaveQuotes() {
withContext(Dispatchers.IO) {
Blog.LOGE("fetchAndSaveQuotes")
val client = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build()
val gson = Gson()
val realm = WorkersDb.getRealm()
// 5회 반복 호출
for (i in 1..5) {
try {
// 1. ZenQuotes (영어 명언) 호출 - 배열 형태로 응답옴
val zenRequest = Request.Builder().url("https://zenquotes.io/api/random").build()
val zenResponse = client.newCall(zenRequest).execute()
if (zenResponse.isSuccessful) {
zenResponse.body?.string()?.let { json ->
val listType = object : TypeToken<List<ZenQuoteResponse>>() {}.type
val zenList: List<ZenQuoteResponse> = gson.fromJson(json, listType)
if (zenList.isNotEmpty()) {
val quote = zenList[0]
realm.writeBlocking {
copyToRealm(QuoteItem().apply {
textEn = quote.q
authorEn = quote.a
sourceApi = "ZENQUOTES"
isTranslated = false
timestamp = System.currentTimeMillis()
})
}
}
}
}
// 2. Korean Advice (한국어 명언) 호출 - 객체 형태로 응답옴
val korRequest = Request.Builder().url("https://korean-advice-open-api.vercel.app/api/advice").build()
val korResponse = client.newCall(korRequest).execute()
if (korResponse.isSuccessful) {
korResponse.body?.string()?.let { json ->
val korQuote = gson.fromJson(json, KorAdviceResponse::class.java)
realm.writeBlocking {
copyToRealm(QuoteItem().apply {
textKo = korQuote.message
authorKo = korQuote.author
sourceApi = "KOR_ADVICE"
isTranslated = false
timestamp = System.currentTimeMillis()
})
}
}
}
// 무료 API Rate Limit(호출 제한) 방어를 위해 1.5초 대기
delay(1500)
} catch (e: Exception) {
Blog.LOGE("Quote Fetch Iteration $i Failed", e)
}
}
}
}
}
class WallpaperAutoChangeWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val folderPath = inputData.getString("FOLDER_PATH") ?: return Result.failure()
@ -652,6 +734,22 @@ class ForeGroundService : Service() {
wallpaperRequest
)
val quoteRequest = PeriodicWorkRequestBuilder<QuoteFetchWorker>(
1, TimeUnit.HOURS // 1시간 간격 설정
)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // 네트워크가 연결된 상태에서만 실행
.build()
)
.build()
workManager.enqueueUniquePeriodicWork(
"QuoteFetchWork",
ExistingPeriodicWorkPolicy.KEEP,
quoteRequest
)
}

View File

@ -0,0 +1,19 @@
package bums.lunatic.launcher.model
import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.annotations.PrimaryKey
import org.mongodb.kbson.ObjectId
class QuoteItem : RealmObject {
@PrimaryKey
var _id: ObjectId = ObjectId()
var textEn: String = "" // 영문 텍스트
var textKo: String = "" // 한글 텍스트
var authorEn: String = "" // 영문 작가명
var authorKo: String = "" // 한글 작가명
var sourceApi: String = "" // "ZENQUOTES" 또는 "KOR_ADVICE"
var isTranslated: Boolean = false // AI를 통해 양방향 번역이 완료되었는지 여부
var timestamp: Long = System.currentTimeMillis()
}

View File

@ -35,12 +35,14 @@ import bums.lunatic.launcher.helpers.Constants.Companion.PREFS_SETTINGS
import bums.lunatic.launcher.helpers.PrefBoolean
import bums.lunatic.launcher.helpers.PrefHelper
import bums.lunatic.launcher.model.AppInfo
import bums.lunatic.launcher.model.QuoteItem
import bums.lunatic.launcher.model.SimpleContact
import bums.lunatic.launcher.settings.childs.Apps
import bums.lunatic.launcher.settings.childs.HomeSettings
import bums.lunatic.launcher.settings.childs.Misc
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.workers.WorkersDb
import bums.lunatic.launcher.workers.WorkersDb.getRealm
import kr.gdrive.bums.lunatic.utils.BackupPayload
import kr.gdrive.bums.lunatic.utils.GDriveBackupTask
import kr.gdrive.bums.lunatic.utils.GDriveLoginManager
@ -175,6 +177,14 @@ internal class SettingsActivity : CommonActivity() {
}
}
fun getUntranslatedQuotesAsJson(): String {
val realm = getRealm()
val untranslatedList = realm.query<QuoteItem>("isTranslated == false").find()
// 리스트를 통째로 Gson을 사용해 JSON String으로 변환 (Logcat이나 파일로 출력)
return Gson().toJson(untranslatedList)
}
private fun updateUiForLoggedIn(email: String) {
binding.btnGoogleLogin.text = "연결 해제 ($email)"
binding.btnManualBackup.isEnabled = true

View File

@ -26,6 +26,7 @@ import android.os.Handler
import android.os.HandlerThread
import android.os.ParcelFileDescriptor
import android.service.wallpaper.WallpaperService
import android.text.SpannableStringBuilder
import android.util.Log
import android.view.Gravity
import android.view.SurfaceHolder
@ -35,10 +36,12 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.graphics.alpha
import bums.lunatic.launcher.R
import bums.lunatic.launcher.model.QuoteItem
import bums.lunatic.launcher.model.Translation
import bums.lunatic.launcher.model.WallContentGroup
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.workers.LocationUpdateService
import bums.lunatic.launcher.workers.WorkersDb
import com.google.common.reflect.TypeToken
import com.google.gson.Gson
import java.io.File
@ -60,7 +63,7 @@ class MyWallpaperService : WallpaperService() {
data class BatteryInfo(
val percent: Int,
val isCharging: Boolean,
val speedText: String,
// val speedText: String,
val detailsText: String // "9.0V 1.5A (13.5W)" 같은 상세 정보
)
@ -76,16 +79,16 @@ class MyWallpaperService : WallpaperService() {
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL
var speedText = ""
// var speedText = ""
var detailsText = ""
if (isCharging) {
speedText = when (plugged) {
BatteryManager.BATTERY_PLUGGED_AC -> "고속"
BatteryManager.BATTERY_PLUGGED_USB -> "저속"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "무선"
else -> ""
}
// 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
@ -108,7 +111,7 @@ class MyWallpaperService : WallpaperService() {
}
}
return BatteryInfo(batteryPct, isCharging, speedText, detailsText)
return BatteryInfo(batteryPct, isCharging, detailsText)
}
inner class NativeRenderEngine : Engine() {
@ -405,40 +408,139 @@ class MyWallpaperService : WallpaperService() {
}
}
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 { info ->
if (info.isCharging && info.detailsText.isNotEmpty()) {
"${info.percent}% ⚡${info.speedText} [${info.detailsText}]"
} else if (info.isCharging) {
"${info.percent}% ⚡${info.speedText}"
} else {
"${info.percent}%"
}
}
private fun getKoreanWeatherCondition(condition: String): String {
val lower = condition.trim().lowercase()
return when {
lower == "sunny" || lower == "clear" -> "맑음"
lower == "partly cloudy" -> "구름 조금"
lower == "cloudy" -> "구름 많음"
lower == "overcast" -> "흐림"
lower.contains("mist") || lower.contains("fog") -> "안개"
lower.contains("blizzard") || lower.contains("blowing snow") -> "눈보라"
// 뇌우(번개) 계열
lower.contains("thunder") && lower.contains("snow") -> "천둥/눈"
lower.contains("thunder") && lower.contains("rain") -> "천둥/비"
lower.contains("thunder") -> "뇌우"
// 얼음 섞인 비/눈 계열
lower.contains("sleet") -> "진눈깨비"
lower.contains("ice pellets") -> "싸락눈"
// 비 계열 (우선순위: 폭우 -> 소나기 -> 강한 비 -> 이슬비 -> 비)
lower.contains("torrential") -> "폭우"
lower.contains("heavy") && lower.contains("shower") -> "강한 소나기"
lower.contains("shower") -> "소나기"
lower.contains("heavy rain") -> "강한 비"
lower.contains("drizzle") -> "이슬비"
lower.contains("rain") -> ""
// 눈 계열 (우선순위: 폭설 -> 눈)
lower.contains("heavy snow") -> "폭설"
lower.contains("snow") -> ""
// 매핑되지 않은 기본값 (혹시 모를 예외 대비)
else -> condition
}
}
val fontz = arrayListOf<Int>(
R.font.cafe24ohsquareair,
R.font.cafe24oneprettynight,
R.font.cafe24ssukssukregular,
R.font.dovemayo,
R.font.ebs_r,
R.font.gabia_solmee,
R.font.godomaum,
R.font.jsarirang_hon,
R.font.jsarirang_ppuri,
R.font.jsdongkang_regular,
R.font.kcc_kimhoon,
R.font.kcc_sonkeechung,
R.font.kccahnjunggeun,
R.font.kotra_bold,
R.font.kotra_songeulssi,
R.font.kyobo_handwriting_2019,
R.font.kyobo_handwriting_2021sjy,
R.font.kyobohandwriting2024psw,
R.font.mapoagape,
R.font.mapobackpacking,
R.font.mapodacapo,
R.font.mapodpp,
R.font.mapodpp_2,
R.font.mapoflowerisland,
R.font.mapogoldenpier,
R.font.mapomaponaru,
R.font.mapopeacefull,
R.font.material_symbols,
R.font.nnsgc_brhp,
R.font.nnsgc_gd_an_gd,
R.font.nnsgc_md,
R.font.nnsgc_wsjidyp,
R.font.nnsgc_yjc,
R.font.on_jsuhl,
R.font.on_jsuhr,
R.font.on_sbsjl,
R.font.on_sbsjr,
R.font.on_treeususimgul,
R.font.on_treeususimgul_r,
R.font.on_wibsr,
R.font.on_wisbl,
R.font.on_ychyuhl,
R.font.on_ychyuhr,
R.font.ssshinb7,
R.font.taebaek_milkyway,
R.font.taefont_tsthlml,
R.font.tvn_jguiyg_light,
R.font.tvn_jguiyg_medium,
R.font.wandohoper,
R.font.ylee_mortal_heart_immortal_memory,
)
var monoBold = resources.getFont(fontz.random())
var quote = resources.getFont(fontz.random())
var local = resources.getFont(fontz.random())
var numbers = resources.getFont(fontz.random())
private fun drawBitmapFromText(weatherLines: List<String>, isCharging: Boolean = false): Bitmap? {
// 💡 1. 원의 크기와 내부 여백 계산
val displayMetrics = resources.displayMetrics
var sizeRate = 1.1f
val circleSize = ((displayMetrics.widthPixels * 0.48f).toInt() * sizeRate).toInt()
val padding = 5
val maxContentWidth = circleSize - (padding * 2)
if (circleSize <= 0) return null
// 💡 2. 텍스트 크기 3단계 변수 정의 (가독성과 감성을 모두 잡은 안정적인 비율)
val iconRate = 1.3f
val sizeLarge = 24f // 메인 날씨 상태
val sizeMedium = 15f // 날짜, 주소, 온도, 명언 본문
val sizeSmall = 13f // 시스템 정보, 명언 번역, 작가
// 3. 데이터 준비
val dateFull = SimpleDateFormat("yyyy.MM.dd(E)", Locale.ENGLISH).format(Date())
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 {
// 4. 뷰 생성 헬퍼
fun createTextView(textToSet: CharSequence, textSizeDp: Float, font: Typeface, alphaValue: Float = 1.0f): TextView {
return TextView(this@MyWallpaperService).apply {
layoutParams = LinearLayout.LayoutParams(wrapContent, wrapContent)
maxWidth = maxContentWidth
text = textToSet
textSize = textSizeDp
typeface = font
setTextColor(Color.WHITE)
alpha = alphaValue
gravity = Gravity.CENTER
setShadowLayer(8f, 2f, 2f, Color.parseColor("#99000000"))
setShadowLayer(6f, 3f, 3f, Color.parseColor("#AA000000"))
}
}
@ -447,51 +549,108 @@ class MyWallpaperService : WallpaperService() {
layoutParams = LinearLayout.LayoutParams(wrapContent, wrapContent)
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER
setPadding(1, 1, 1, 1)
}
}
// 3. 각 줄(Row)을 생성하여 리스트에 담기
val sysAlpha = 0.9f
// 5. 각 줄(Row) 생성
val rowsToArrange = mutableListOf<View>()
// ① 날씨 (아이콘 + 상태)
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) })
// 아이콘은 Large 텍스트보다 살짝 크게 배율 고정
weatherRow.addView(createTextView(getWeatherIconString(weatherCondition), sizeLarge * iconRate, materialIconFont))
weatherRow.addView(createTextView(" " + getKoreanWeatherCondition(weatherCondition), sizeLarge, monoBold))
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) })
tempRow.addView(createTextView("thermostat", sizeMedium * iconRate, materialIconFont, sysAlpha))
tempRow.addView(createTextView(tempStr, sizeMedium, numbers,sysAlpha))
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) })
tempRow.addView(createTextView("water_drop", sizeMedium * iconRate, materialIconFont, sysAlpha))
tempRow.addView(createTextView(humStr, sizeMedium, numbers, sysAlpha))
}
rowsToArrange.add(tempRow)
}
// ③ 주소
if (address.isNotEmpty()) {
rowsToArrange.add(createTextView(address, 16f, monoBold, 0.95f))
rowsToArrange.add(createTextView(address, sizeMedium, local, sysAlpha))
}
// ④ 날짜
rowsToArrange.add(createTextView(dateFull, 14f, monoBold, 0.8f))
rowsToArrange.add(createTextView(dateFull, sizeMedium, local))
// ⑤ 시스템 정보 (한 줄로 묶음)
rowsToArrange.add(createTextView("$ramInfo | $tempInfo | Battery $batteryInfo", 12f, monoBold, 0.6f))
// ⑤ 시스템 정보
val sysInfoRow = createHorizontalLayout()
// 4. 가로 길이 측정 후 다이아몬드 형태로 재정렬
val unspecifiedSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
rowsToArrange.forEach { it.measure(unspecifiedSpec, unspecifiedSpec) }
val ramValue = getRamUsage().replace("RAM: ", "")
sysInfoRow.addView(createTextView("memory", sizeSmall * iconRate, materialIconFont, sysAlpha))
sysInfoRow.addView(createTextView(ramValue, sizeSmall, numbers, sysAlpha).apply { setPadding(0, 0, 15, 0) })
val tempValue = getDeviceTemperature().replace("Temp: ", "")
sysInfoRow.addView(createTextView("device_thermostat", sizeSmall * iconRate, materialIconFont, sysAlpha))
sysInfoRow.addView(createTextView(tempValue, sizeSmall, numbers, sysAlpha).apply { setPadding(0, 0, 15, 0) })
val batteryInfo = getBatteryStatus()
val batteryIcon = if (batteryInfo.isCharging) "battery_charging_full" else "battery_full"
val batteryText = "${batteryInfo.percent}%"
sysInfoRow.addView(createTextView(batteryIcon, sizeSmall * iconRate, materialIconFont, sysAlpha))
sysInfoRow.addView(createTextView(batteryText, sizeSmall, numbers, sysAlpha))
rowsToArrange.add(sysInfoRow)
// ⑥ 명언
randomQuote?.let { randomQuote ->
val builder = SpannableStringBuilder()
fun appendSpanned(text: String, sizeDp: Float, font: Typeface, alpha: Float) {
val start = builder.length
builder.append(text)
val end = builder.length
builder.setSpan(android.text.style.AbsoluteSizeSpan(sizeDp.toInt(), true), start, end, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
val alphaInt = (alpha * 255).toInt()
val color = Color.argb(alphaInt, 255, 255, 255)
builder.setSpan(android.text.style.ForegroundColorSpan(color), start, end, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
builder.setSpan(object : android.text.style.MetricAffectingSpan() {
override fun updateDrawState(tp: android.text.TextPaint) { tp.typeface = font }
override fun updateMeasureState(tp: android.text.TextPaint) { tp.typeface = font }
}, start, end, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
if (randomQuote.isTranslated) {
appendSpanned("\"${randomQuote.textEn}\"", sizeMedium, quote, 1.0f)
builder.append("\n")
appendSpanned(randomQuote.textKo, sizeSmall, monoBold, sysAlpha)
val author = if (randomQuote.authorKo.isNotEmpty()) randomQuote.authorKo else randomQuote.authorEn
appendSpanned(" - $author -", sizeSmall, monoBold, sysAlpha)
} else {
val mainText = if (randomQuote.textKo.isNotEmpty()) randomQuote.textKo else randomQuote.textEn
val author = if (randomQuote.authorKo.isNotEmpty()) randomQuote.authorKo else randomQuote.authorEn
appendSpanned("\"$mainText\"", sizeMedium, quote, 1.0f)
appendSpanned(" - $author -", sizeSmall, monoBold, sysAlpha)
}
val quoteView = createTextView(builder, sizeMedium, quote, 1.0f).apply {
// setLineSpacing(0f, 1.2f)
}
rowsToArrange.add(quoteView)
}
// 💡 6. 정렬 및 다이아몬드 배열
val widthSpec = View.MeasureSpec.makeMeasureSpec(maxContentWidth, View.MeasureSpec.AT_MOST)
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
rowsToArrange.forEach { it.measure(widthSpec, heightSpec) }
val sortedRows = rowsToArrange.sortedBy { it.measuredWidth }
val arrangedRows = arrayOfNulls<View>(sortedRows.size)
@ -503,49 +662,32 @@ class MyWallpaperService : WallpaperService() {
else arrangedRows[bottomIndex--] = sortedRows[i]
}
// 5. 최종 부모 레이아웃 조립 (원형/타원형 배경)
// 7. 35% 고정 크기 원형 틀 생성
val rootLayout = LinearLayout(this@MyWallpaperService).apply {
layoutParams = ViewGroup.LayoutParams(wrapContent, wrapContent)
layoutParams = ViewGroup.LayoutParams(circleSize, circleSize)
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
// 💡 원형 안에 글씨가 넉넉히 들어가도록 패딩을 충분히 줍니다.
setPadding(20, 20, 20, 20)
background = WavyOvalDrawable(
bgColor = Color.parseColor("#33000000"),
strokeColor = Color.parseColor("#22000000"), // 물결이 잘 보이도록 불투명도를 살짝 올림
strokeThick = 30f
bgColor = Color.parseColor("#40000000"),
strokeColor = Color.parseColor("#09000000"),
strokeThick = 40f
)
}
arrangedRows.forEach { rowView ->
if (rowView != null) {
val lp = LinearLayout.LayoutParams(wrapContent, wrapContent).apply {
setMargins(0, 10, 0, 10)
setMargins(0, 2, 0, 2)
}
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)' 덮어씌웁니다.
// 8. 강제 측정, 렌더링 및 캡처
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)
rootLayout.draw(canvas)
@ -598,12 +740,20 @@ class MyWallpaperService : WallpaperService() {
}
val mediaDir = File(File(this@MyWallpaperService.getExternalFilesDir(null), "completed_torrents"), "Images")
val supportedExtensions = listOf("mp4", "mkv", "avi", "mov","webm", "jpg", "jpeg", "png", "bmp", "webp", "gif")
var randomQuote : QuoteItem? = null
private val nextMediaCallback = object : NativeRenderer.NextMediaCallback {
override fun onNextMediaRequested() {
loadFiles()
val nextFile = mediaFiles.random()
Log.d(TAG, "Callback: Preloading next random media: ${nextFile.absolutePath}")
// ⑥ 명언 (DB 랜덤 추출)
val realm = WorkersDb.getRealm()
val quotes = realm.query<QuoteItem>(QuoteItem::class).find().shuffled()
randomQuote = if (quotes.isNotEmpty()) quotes.random() else null
monoBold = resources.getFont(fontz.random())
quote = resources.getFont(fontz.random())
local = resources.getFont(fontz.random())
numbers = resources.getFont(fontz.random())
getFdFromPath(nextFile.absolutePath)?.let { fd ->
nativeRenderer?.startNextPreload(fd)
@ -682,7 +832,7 @@ class WavyOvalDrawable(
private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = strokeColor
style = Paint.Style.STROKE
strokeWidth = strokeThick
strokeWidth = 100f
strokeCap = Paint.Cap.ROUND
strokeJoin = Paint.Join.ROUND
@ -706,14 +856,14 @@ class WavyOvalDrawable(
if (radius > 0) {
// 바깥쪽 색상: 입력받은 bgColor에서 투명도(Alpha)만 0으로 날려버린 색
// 이렇게 해야 검은색->흰색으로 깨지는 현상 없이 자연스럽게 투명해집니다.
val transparentColor = bgColor and 0x11FFFFFF
val transparentColor = bgColor and 0x10FFFFFF
bgPaint.shader = RadialGradient(
centerX,
centerY,
radius,
intArrayOf(bgColor, transparentColor), // 중앙 -> 바깥 색상 배열
floatArrayOf(0.4f, 1.0f), // 중심에서 40% 지점부터 투명해지기 시작
floatArrayOf(0.4f, 1.1f), // 중심에서 40% 지점부터 투명해지기 시작
Shader.TileMode.CLAMP
)
}
@ -722,15 +872,17 @@ class WavyOvalDrawable(
override fun draw(canvas: Canvas) {
val rectF = RectF(bounds)
// 1. 방사형 그라데이션 배경 채우기
canvas.drawOval(rectF, bgPaint)
// 2. 선 잘림 방지용 여백 계산
val inset = amplitude + 3f
val inset = 8f
rectF.inset(inset, inset)
// 3. 물결 테두리 선 그리기
canvas.drawOval(rectF, strokePaint)
// 1. 방사형 그라데이션 배경 채우기
canvas.drawOval(rectF, bgPaint)
}
override fun setAlpha(alpha: Int) {

View File

@ -49,6 +49,7 @@ class LocationUpdateService : Service(), LocationListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
geocoder.getFromLocation(lat, long, 1) { addresses ->
addresses.first()?.let {
addresses.get(0)
WorkersDb.getRealm()?.apply {
LocationLog().let { loc ->
loc.fillData(it)
@ -123,7 +124,9 @@ class LocationUpdateService : Service(), LocationListener {
if (body != null) {
var result = body.string()
var w = Gson().fromJson<CurrentWeather>(result,CurrentWeather::class.java)
w.addr = addresses.first().getAddressLine(0).replace("대한민국", "")
var address =addresses.first()
w.addr = address.getAddressLine(0)
lastWeather.clear()
lastWeather.addAll(w.getSummaryInfo())
Blog.LOGE("Location >>> ${result}\n${lastWeather}")
@ -265,7 +268,7 @@ open class Location {
open class CurrentWeather {
var addr: String? = null
var current: Current? = null
var location : bums.lunatic.launcher.workers.Location? = null
// 아이콘 URL을 배열의 4번째(index 3) 요소로 추가 전달합니다.
fun getSummaryInfo(): List<String> {
val iconUrl = current?.condition?.icon?.let { "https:$it" } ?: ""
@ -273,7 +276,7 @@ open class CurrentWeather {
"${current?.condition?.text}", // 0: 날씨 상태
"${current?.temp_c}", // 1: 온도 및 습도
"${current?.humidity}",
"$addr", // 2: 주소
"${addr}", // 2: 주소
iconUrl // 3: 아이콘 URL
)
}

View File

@ -117,35 +117,35 @@ class TorrentService : Service() {
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")
// 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()
}
@ -159,10 +159,8 @@ class TorrentService : Service() {
networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
val isWifi = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
if (isWifiConnected != isWifi) {
isWifiConnected = isWifi
updateSessionState()
}
isWifiConnected = isWifi
updateSessionState()
}
}
connectivityManager.registerNetworkCallback(request, networkCallback!!)
@ -602,5 +600,5 @@ class TorrentService : Service() {
e.printStackTrace()
}
}
}

View File

@ -36,6 +36,7 @@ import bums.lunatic.launcher.home.tokiz.LastInfo
import bums.lunatic.launcher.home.tokiz.ReaderConfig
import bums.lunatic.launcher.model.ExpressionItem
import bums.lunatic.launcher.model.ExpressionWord
import bums.lunatic.launcher.model.QuoteItem
import bums.lunatic.launcher.model.WallContentGroup
import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.utils.JamoUtils
@ -79,7 +80,7 @@ object WorkersDb {
LastInfo::class, HistoryItem::class, ReaderConfig::class, ContentsCollection::class, ContentsPageInfo::class,
AppUsageLog::class,
WidgetData::class,
ExpressionItem::class,ExpressionWord::class
ExpressionItem::class,ExpressionWord::class,QuoteItem::class
)
//,UserActionModel::class

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.