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 androidx.work.workDataOf
import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieString import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieString
import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieUrlString import bums.lunatic.launcher.home.GeckoWeb.Companion.currentCookieUrlString
import bums.lunatic.launcher.model.QuoteItem
import bums.lunatic.launcher.model.WallContentGroup import bums.lunatic.launcher.model.WallContentGroup
import bums.lunatic.launcher.workers.WorkersDb import bums.lunatic.launcher.workers.WorkersDb
import com.google.common.reflect.TypeToken import com.google.common.reflect.TypeToken
@ -77,6 +78,87 @@ import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.util.Calendar 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) { class WallpaperAutoChangeWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val folderPath = inputData.getString("FOLDER_PATH") ?: return Result.failure() val folderPath = inputData.getString("FOLDER_PATH") ?: return Result.failure()
@ -652,6 +734,22 @@ class ForeGroundService : Service() {
wallpaperRequest 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.PrefBoolean
import bums.lunatic.launcher.helpers.PrefHelper import bums.lunatic.launcher.helpers.PrefHelper
import bums.lunatic.launcher.model.AppInfo import bums.lunatic.launcher.model.AppInfo
import bums.lunatic.launcher.model.QuoteItem
import bums.lunatic.launcher.model.SimpleContact import bums.lunatic.launcher.model.SimpleContact
import bums.lunatic.launcher.settings.childs.Apps import bums.lunatic.launcher.settings.childs.Apps
import bums.lunatic.launcher.settings.childs.HomeSettings import bums.lunatic.launcher.settings.childs.HomeSettings
import bums.lunatic.launcher.settings.childs.Misc import bums.lunatic.launcher.settings.childs.Misc
import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.workers.WorkersDb 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.BackupPayload
import kr.gdrive.bums.lunatic.utils.GDriveBackupTask import kr.gdrive.bums.lunatic.utils.GDriveBackupTask
import kr.gdrive.bums.lunatic.utils.GDriveLoginManager 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) { private fun updateUiForLoggedIn(email: String) {
binding.btnGoogleLogin.text = "연결 해제 ($email)" binding.btnGoogleLogin.text = "연결 해제 ($email)"
binding.btnManualBackup.isEnabled = true binding.btnManualBackup.isEnabled = true

View File

@ -26,6 +26,7 @@ import android.os.Handler
import android.os.HandlerThread import android.os.HandlerThread
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.service.wallpaper.WallpaperService import android.service.wallpaper.WallpaperService
import android.text.SpannableStringBuilder
import android.util.Log import android.util.Log
import android.view.Gravity import android.view.Gravity
import android.view.SurfaceHolder import android.view.SurfaceHolder
@ -35,10 +36,12 @@ import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.graphics.alpha import androidx.core.graphics.alpha
import bums.lunatic.launcher.R import bums.lunatic.launcher.R
import bums.lunatic.launcher.model.QuoteItem
import bums.lunatic.launcher.model.Translation import bums.lunatic.launcher.model.Translation
import bums.lunatic.launcher.model.WallContentGroup import bums.lunatic.launcher.model.WallContentGroup
import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.workers.LocationUpdateService import bums.lunatic.launcher.workers.LocationUpdateService
import bums.lunatic.launcher.workers.WorkersDb
import com.google.common.reflect.TypeToken import com.google.common.reflect.TypeToken
import com.google.gson.Gson import com.google.gson.Gson
import java.io.File import java.io.File
@ -60,7 +63,7 @@ class MyWallpaperService : WallpaperService() {
data class BatteryInfo( data class BatteryInfo(
val percent: Int, val percent: Int,
val isCharging: Boolean, val isCharging: Boolean,
val speedText: String, // val speedText: String,
val detailsText: String // "9.0V 1.5A (13.5W)" 같은 상세 정보 val detailsText: String // "9.0V 1.5A (13.5W)" 같은 상세 정보
) )
@ -76,16 +79,16 @@ class MyWallpaperService : WallpaperService() {
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL status == BatteryManager.BATTERY_STATUS_FULL
var speedText = "" // var speedText = ""
var detailsText = "" var detailsText = ""
if (isCharging) { if (isCharging) {
speedText = when (plugged) { // speedText = when (plugged) {
BatteryManager.BATTERY_PLUGGED_AC -> "고속" // BatteryManager.BATTERY_PLUGGED_AC -> "고속"
BatteryManager.BATTERY_PLUGGED_USB -> "저속" // BatteryManager.BATTERY_PLUGGED_USB -> "저속"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "무선" // BatteryManager.BATTERY_PLUGGED_WIRELESS -> "무선"
else -> "" // else -> ""
} // }
// 1. 전압(Voltage) 가져오기: 기본 단위는 밀리볼트(mV) // 1. 전압(Voltage) 가져오기: 기본 단위는 밀리볼트(mV)
val voltageMv = intent?.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0) ?: 0 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() { inner class NativeRenderEngine : Engine() {
@ -405,40 +408,139 @@ class MyWallpaperService : WallpaperService() {
} }
} }
private fun drawBitmapFromText(weatherLines: List<String>, isCharging: Boolean = false): Bitmap? { private fun getKoreanWeatherCondition(condition: String): String {
// 1. 데이터 준비 val lower = condition.trim().lowercase()
val dateFull = SimpleDateFormat("yyyy.MM.dd (E)", Locale.KOREAN).format(Date()) return when {
val ramInfo = getRamUsage() lower == "sunny" || lower == "clear" -> "맑음"
val tempInfo = getDeviceTemperature() lower == "partly cloudy" -> "구름 조금"
val batteryInfo = getBatteryStatus().let { info -> lower == "cloudy" -> "구름 많음"
if (info.isCharging && info.detailsText.isNotEmpty()) { lower == "overcast" -> "흐림"
"${info.percent}% ⚡${info.speedText} [${info.detailsText}]"
} else if (info.isCharging) {
"${info.percent}% ⚡${info.speedText}"
} else {
"${info.percent}%"
}
}
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 weatherCondition = if (weatherLines.isNotEmpty()) weatherLines[0] else ""
val weatherTemp = if (weatherLines.size > 1) weatherLines[1] else "" val weatherTemp = if (weatherLines.size > 1) weatherLines[1] else ""
val weatherHum = if (weatherLines.size > 2) weatherLines[2] else "" val weatherHum = if (weatherLines.size > 2) weatherLines[2] else ""
val address = if (weatherLines.size > 3) weatherLines[3] else "" val address = if (weatherLines.size > 3) weatherLines[3] else ""
val wrapContent = ViewGroup.LayoutParams.WRAP_CONTENT 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 { return TextView(this@MyWallpaperService).apply {
layoutParams = LinearLayout.LayoutParams(wrapContent, wrapContent) layoutParams = LinearLayout.LayoutParams(wrapContent, wrapContent)
maxWidth = maxContentWidth
text = textToSet text = textToSet
textSize = textSizeDp textSize = textSizeDp
typeface = font typeface = font
setTextColor(Color.WHITE) setTextColor(Color.WHITE)
alpha = alphaValue alpha = alphaValue
gravity = Gravity.CENTER 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) layoutParams = LinearLayout.LayoutParams(wrapContent, wrapContent)
orientation = LinearLayout.HORIZONTAL orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER gravity = Gravity.CENTER
setPadding(1, 1, 1, 1)
} }
} }
val sysAlpha = 0.9f
// 3. 각 줄(Row)을 생성하여 리스트에 담기 // 5. 각 줄(Row) 생성
val rowsToArrange = mutableListOf<View>() val rowsToArrange = mutableListOf<View>()
// ① 날씨 (아이콘 + 상태) // ① 날씨 (아이콘 + 상태)
if (weatherCondition.isNotEmpty()) { if (weatherCondition.isNotEmpty()) {
val weatherRow = createHorizontalLayout() val weatherRow = createHorizontalLayout()
weatherRow.addView(createTextView(getWeatherIconString(weatherCondition), 50f, materialIconFont)) // 아이콘은 Large 텍스트보다 살짝 크게 배율 고정
weatherRow.addView(createTextView(weatherCondition, 28f, monoBold).apply { setPadding(20, 0, 0, 0) }) weatherRow.addView(createTextView(getWeatherIconString(weatherCondition), sizeLarge * iconRate, materialIconFont))
weatherRow.addView(createTextView(" " + getKoreanWeatherCondition(weatherCondition), sizeLarge, monoBold))
rowsToArrange.add(weatherRow) rowsToArrange.add(weatherRow)
} }
// ② 온도 & 습도 // ② 온도 & 습도
if (weatherTemp.isNotEmpty()) { if (weatherTemp.isNotEmpty()) {
val tempRow = createHorizontalLayout() val tempRow = createHorizontalLayout()
val tempStr = weatherTemp.trim() val tempStr = weatherTemp.trim()
val humStr = weatherHum.trim() val humStr = weatherHum.trim()
tempRow.addView(createTextView("thermostat", 20f, materialIconFont, 0.9f)) tempRow.addView(createTextView("thermostat", sizeMedium * iconRate, materialIconFont, sysAlpha))
tempRow.addView(createTextView(tempStr, 20f, monoBold, 0.9f).apply { setPadding(8, 0, 30, 0) }) tempRow.addView(createTextView(tempStr, sizeMedium, numbers,sysAlpha))
if (humStr.isNotEmpty()) { if (humStr.isNotEmpty()) {
tempRow.addView(createTextView("water_drop", 20f, materialIconFont, 0.9f)) tempRow.addView(createTextView("water_drop", sizeMedium * iconRate, materialIconFont, sysAlpha))
tempRow.addView(createTextView(humStr, 20f, monoBold, 0.9f).apply { setPadding(8, 0, 0, 0) }) tempRow.addView(createTextView(humStr, sizeMedium, numbers, sysAlpha))
} }
rowsToArrange.add(tempRow) rowsToArrange.add(tempRow)
} }
// ③ 주소 // ③ 주소
if (address.isNotEmpty()) { 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) val ramValue = getRamUsage().replace("RAM: ", "")
rowsToArrange.forEach { it.measure(unspecifiedSpec, unspecifiedSpec) } 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 sortedRows = rowsToArrange.sortedBy { it.measuredWidth }
val arrangedRows = arrayOfNulls<View>(sortedRows.size) val arrangedRows = arrayOfNulls<View>(sortedRows.size)
@ -503,49 +662,32 @@ class MyWallpaperService : WallpaperService() {
else arrangedRows[bottomIndex--] = sortedRows[i] else arrangedRows[bottomIndex--] = sortedRows[i]
} }
// 5. 최종 부모 레이아웃 조립 (원형/타원형 배경) // 7. 35% 고정 크기 원형 틀 생성
val rootLayout = LinearLayout(this@MyWallpaperService).apply { val rootLayout = LinearLayout(this@MyWallpaperService).apply {
layoutParams = ViewGroup.LayoutParams(wrapContent, wrapContent) layoutParams = ViewGroup.LayoutParams(circleSize, circleSize)
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER gravity = Gravity.CENTER
// 💡 원형 안에 글씨가 넉넉히 들어가도록 패딩을 충분히 줍니다.
setPadding(20, 20, 20, 20)
background = WavyOvalDrawable( background = WavyOvalDrawable(
bgColor = Color.parseColor("#33000000"), bgColor = Color.parseColor("#40000000"),
strokeColor = Color.parseColor("#22000000"), // 물결이 잘 보이도록 불투명도를 살짝 올림 strokeColor = Color.parseColor("#09000000"),
strokeThick = 30f strokeThick = 40f
) )
} }
arrangedRows.forEach { rowView -> arrangedRows.forEach { rowView ->
if (rowView != null) { if (rowView != null) {
val lp = LinearLayout.LayoutParams(wrapContent, wrapContent).apply { val lp = LinearLayout.LayoutParams(wrapContent, wrapContent).apply {
setMargins(0, 10, 0, 10) setMargins(0, 2, 0, 2)
} }
rootLayout.addView(rowView, lp) rootLayout.addView(rowView, lp)
} }
} }
// 6. 💡 완벽한 정원형(Perfect Circle)을 위한 2단계 측정(Measure) 로직 // 8. 강제 측정, 렌더링 및 캡처
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) val exactSpec = View.MeasureSpec.makeMeasureSpec(circleSize, View.MeasureSpec.EXACTLY)
rootLayout.measure(exactSpec, exactSpec) rootLayout.measure(exactSpec, exactSpec)
// 레이아웃 배치
rootLayout.layout(0, 0, circleSize, circleSize) rootLayout.layout(0, 0, circleSize, circleSize)
// 7. 비트맵 캡처
val bitmap = Bitmap.createBitmap(circleSize, circleSize, Bitmap.Config.ARGB_8888) val bitmap = Bitmap.createBitmap(circleSize, circleSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap) val canvas = Canvas(bitmap)
rootLayout.draw(canvas) rootLayout.draw(canvas)
@ -598,12 +740,20 @@ class MyWallpaperService : WallpaperService() {
} }
val mediaDir = File(File(this@MyWallpaperService.getExternalFilesDir(null), "completed_torrents"), "Images") 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") val supportedExtensions = listOf("mp4", "mkv", "avi", "mov","webm", "jpg", "jpeg", "png", "bmp", "webp", "gif")
var randomQuote : QuoteItem? = null
private val nextMediaCallback = object : NativeRenderer.NextMediaCallback { private val nextMediaCallback = object : NativeRenderer.NextMediaCallback {
override fun onNextMediaRequested() { override fun onNextMediaRequested() {
loadFiles() loadFiles()
val nextFile = mediaFiles.random() val nextFile = mediaFiles.random()
Log.d(TAG, "Callback: Preloading next random media: ${nextFile.absolutePath}") 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 -> getFdFromPath(nextFile.absolutePath)?.let { fd ->
nativeRenderer?.startNextPreload(fd) nativeRenderer?.startNextPreload(fd)
@ -682,7 +832,7 @@ class WavyOvalDrawable(
private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = strokeColor color = strokeColor
style = Paint.Style.STROKE style = Paint.Style.STROKE
strokeWidth = strokeThick strokeWidth = 100f
strokeCap = Paint.Cap.ROUND strokeCap = Paint.Cap.ROUND
strokeJoin = Paint.Join.ROUND strokeJoin = Paint.Join.ROUND
@ -706,14 +856,14 @@ class WavyOvalDrawable(
if (radius > 0) { if (radius > 0) {
// 바깥쪽 색상: 입력받은 bgColor에서 투명도(Alpha)만 0으로 날려버린 색 // 바깥쪽 색상: 입력받은 bgColor에서 투명도(Alpha)만 0으로 날려버린 색
// 이렇게 해야 검은색->흰색으로 깨지는 현상 없이 자연스럽게 투명해집니다. // 이렇게 해야 검은색->흰색으로 깨지는 현상 없이 자연스럽게 투명해집니다.
val transparentColor = bgColor and 0x11FFFFFF val transparentColor = bgColor and 0x10FFFFFF
bgPaint.shader = RadialGradient( bgPaint.shader = RadialGradient(
centerX, centerX,
centerY, centerY,
radius, radius,
intArrayOf(bgColor, transparentColor), // 중앙 -> 바깥 색상 배열 intArrayOf(bgColor, transparentColor), // 중앙 -> 바깥 색상 배열
floatArrayOf(0.4f, 1.0f), // 중심에서 40% 지점부터 투명해지기 시작 floatArrayOf(0.4f, 1.1f), // 중심에서 40% 지점부터 투명해지기 시작
Shader.TileMode.CLAMP Shader.TileMode.CLAMP
) )
} }
@ -722,15 +872,17 @@ class WavyOvalDrawable(
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
val rectF = RectF(bounds) val rectF = RectF(bounds)
// 1. 방사형 그라데이션 배경 채우기
canvas.drawOval(rectF, bgPaint)
// 2. 선 잘림 방지용 여백 계산 // 2. 선 잘림 방지용 여백 계산
val inset = amplitude + 3f val inset = 8f
rectF.inset(inset, inset) rectF.inset(inset, inset)
// 3. 물결 테두리 선 그리기 // 3. 물결 테두리 선 그리기
canvas.drawOval(rectF, strokePaint) canvas.drawOval(rectF, strokePaint)
// 1. 방사형 그라데이션 배경 채우기
canvas.drawOval(rectF, bgPaint)
} }
override fun setAlpha(alpha: Int) { override fun setAlpha(alpha: Int) {

View File

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

View File

@ -117,35 +117,35 @@ class TorrentService : Service() {
var speedText = "" var speedText = ""
var detailsText = "" var detailsText = ""
if (isCharging) { // if (isCharging) {
speedText = when (plugged) { // speedText = when (plugged) {
BatteryManager.BATTERY_PLUGGED_AC -> "고속" // BatteryManager.BATTERY_PLUGGED_AC -> "고속"
BatteryManager.BATTERY_PLUGGED_USB -> "저속" // BatteryManager.BATTERY_PLUGGED_USB -> "저속"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "무선" // BatteryManager.BATTERY_PLUGGED_WIRELESS -> "무선"
else -> "" // else -> ""
} // }
//
// 1. 전압(Voltage) 가져오기: 기본 단위는 밀리볼트(mV) // // 1. 전압(Voltage) 가져오기: 기본 단위는 밀리볼트(mV)
val voltageMv = intent?.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0) ?: 0 // val voltageMv = intent?.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0) ?: 0
val voltageV = voltageMv / 1000.0f // val voltageV = voltageMv / 1000.0f
//
// 2. 전류(Current) 가져오기: BatteryManager를 통해 실시간 값 조회 // // 2. 전류(Current) 가져오기: BatteryManager를 통해 실시간 값 조회
val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager // val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager
// 안드로이드 표준 단위는 마이크로암페어(µA) 입니다. // // 안드로이드 표준 단위는 마이크로암페어(µA) 입니다.
val currentUa = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW) // val currentUa = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW)
//
// 음수(방전)로 나올 수 있으므로 절대값 처리 후 암페어(A)로 변환 // // 음수(방전)로 나올 수 있으므로 절대값 처리 후 암페어(A)로 변환
val currentA = Math.abs(currentUa) / 1000000.0f // val currentA = Math.abs(currentUa) / 1000000.0f
//
// 3. 전력(Watt) 계산: W = V * A // // 3. 전력(Watt) 계산: W = V * A
val wattage = voltageV * currentA // val wattage = voltageV * currentA
//
// 값이 정상적으로 읽혔을 때만 텍스트 생성 (가끔 0으로 떨어지는 기기 방어) // // 값이 정상적으로 읽혔을 때만 텍스트 생성 (가끔 0으로 떨어지는 기기 방어)
if (voltageV > 0f && currentA > 0f) { // if (voltageV > 0f && currentA > 0f) {
detailsText = String.format("%.1fV %.1fA (%.1fW)", voltageV, currentA, wattage) // detailsText = String.format("%.1fV %.1fA (%.1fW)", voltageV, currentA, wattage)
} // }
} // }
Blog.LOGE("detailsText >>> $detailsText") // Blog.LOGE("detailsText >>> $detailsText")
updateSessionState() updateSessionState()
} }
@ -159,10 +159,8 @@ class TorrentService : Service() {
networkCallback = object : ConnectivityManager.NetworkCallback() { networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) { override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
val isWifi = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) val isWifi = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
if (isWifiConnected != isWifi) { isWifiConnected = isWifi
isWifiConnected = isWifi updateSessionState()
updateSessionState()
}
} }
} }
connectivityManager.registerNetworkCallback(request, networkCallback!!) connectivityManager.registerNetworkCallback(request, networkCallback!!)
@ -602,5 +600,5 @@ class TorrentService : Service() {
e.printStackTrace() 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.home.tokiz.ReaderConfig
import bums.lunatic.launcher.model.ExpressionItem import bums.lunatic.launcher.model.ExpressionItem
import bums.lunatic.launcher.model.ExpressionWord import bums.lunatic.launcher.model.ExpressionWord
import bums.lunatic.launcher.model.QuoteItem
import bums.lunatic.launcher.model.WallContentGroup import bums.lunatic.launcher.model.WallContentGroup
import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.utils.JamoUtils import bums.lunatic.launcher.utils.JamoUtils
@ -79,7 +80,7 @@ object WorkersDb {
LastInfo::class, HistoryItem::class, ReaderConfig::class, ContentsCollection::class, ContentsPageInfo::class, LastInfo::class, HistoryItem::class, ReaderConfig::class, ContentsCollection::class, ContentsPageInfo::class,
AppUsageLog::class, AppUsageLog::class,
WidgetData::class, WidgetData::class,
ExpressionItem::class,ExpressionWord::class ExpressionItem::class,ExpressionWord::class,QuoteItem::class
) )
//,UserActionModel::class //,UserActionModel::class

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.