...
This commit is contained in:
parent
dc19ff5339
commit
8c3bc96417
@ -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
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
19
app/src/main/kotlin/bums/lunatic/launcher/model/QuoteItem.kt
Normal file
19
app/src/main/kotlin/bums/lunatic/launcher/model/QuoteItem.kt
Normal 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()
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
BIN
app/src/main/res/font/cafe24ohsquareair.ttf
Normal file
BIN
app/src/main/res/font/cafe24ohsquareair.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/src/main/res/font/cafe24ssukssukregular.ttf
Normal file
BIN
app/src/main/res/font/cafe24ssukssukregular.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/kyobo_handwriting_2019.ttf
Normal file
BIN
app/src/main/res/font/kyobo_handwriting_2019.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/kyobohandwriting2024psw.ttf
Normal file
BIN
app/src/main/res/font/kyobohandwriting2024psw.ttf
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user