From ba869e8eb4d088ec62c7c64006f0e0c0c2c91476 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Sun, 5 Apr 2026 09:04:46 +0900 Subject: [PATCH] ... --- .../launcher/helpers/ForeGroundService.kt | 18 +-- .../launcher/home/CompletedFilesFragment.kt | 4 +- .../launcher/wall/MyWallpaperService.kt | 73 +++++++---- .../launcher/workers/TorrentManager.kt | 122 +++++++++--------- 4 files changed, 121 insertions(+), 96 deletions(-) diff --git a/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt index 674061c5..17220f2c 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt @@ -104,9 +104,9 @@ class QuoteFetchWorker(private val context: Context, params: WorkerParameters) : 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() @@ -129,7 +129,7 @@ class QuoteFetchWorker(private val context: Context, params: WorkerParameters) : } } - // 2. Korean Advice (한국어 명언) 호출 - 객체 형태로 응답옴 + val korRequest = Request.Builder().url("https://korean-advice-open-api.vercel.app/api/advice").build() val korResponse = client.newCall(korRequest).execute() @@ -147,14 +147,10 @@ class QuoteFetchWorker(private val context: Context, params: WorkerParameters) : } } } - - // 무료 API Rate Limit(호출 제한) 방어를 위해 1.5초 대기 - delay(1500) - } catch (e: Exception) { - Blog.LOGE("Quote Fetch Iteration $i Failed", e) + Blog.LOGE("Quote Failed", e) } - } + } } } @@ -719,7 +715,7 @@ class ForeGroundService : Service() { val wallpaperRequest = PeriodicWorkRequestBuilder( - 120, java.util.concurrent.TimeUnit.MINUTES // 💡 2시간 간격으로 설정 + 2, TimeUnit.HOURS // 1시간 간격 설정 ) .setConstraints( Constraints.Builder() @@ -735,7 +731,7 @@ class ForeGroundService : Service() { ) val quoteRequest = PeriodicWorkRequestBuilder( - 1, TimeUnit.HOURS // 1시간 간격 설정 + 2, TimeUnit.HOURS // 1시간 간격 설정 ) .setConstraints( Constraints.Builder() diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt index 50fb14ed..f4dab324 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt @@ -36,7 +36,7 @@ import kotlinx.coroutines.withContext import java.io.File enum class FileFilterType(val label : String) { ALL("전체"), IMAGE("이미지"), VIDEO("영상"), DOCUMENT("문서"), OTHER("기타") } -enum class FileSortType(val label : String) { NAME("파일명") , DOWNLOAD_DATE("다운로드"), LAST_USED("최근 사용"), FREQUENTLY_USED("자주 사용"), SIZE("용량") } +enum class FileSortType(val label : String) { LAST_USED("최근 사용"), FREQUENTLY_USED("자주 사용"),NAME("파일명") , DOWNLOAD_DATE("다운로드"), SIZE("용량") } enum class FileViewMode { LIST_TEXT, LIST_THUMB, GRID_LARGE, GRID_SMALL } // 💡 숫자 크기를 인식하는 정렬 비교자 @@ -87,7 +87,7 @@ class CompletedFilesFragment : Fragment() { private var allFiles = listOf() private var currentFilter = FileFilterType.ALL - private var currentSort = FileSortType.NAME + private var currentSort = FileSortType.LAST_USED private var currentViewMode = FileViewMode.LIST_TEXT private var isDescending = true private var searchQuery = "" // 💡 실시간 검색어 저장용 변수 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 eeacf49c..bc7a20ba 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt @@ -308,41 +308,64 @@ class MyWallpaperService : WallpaperService() { val requiredSizeRatio = 0.5 + private fun getVideoSize(file: File): Pair? { + val retriever = android.media.MediaMetadataRetriever() + return try { + retriever.setDataSource(file.absolutePath) + val width = retriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0 + val height = retriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0 + Pair(width, height) + } catch (e: Exception) { + null + } finally { + retriever.release() + } + } + fun loadFiles() { if (!mediaDir.exists()) mediaDir.mkdirs() - Log.d(TAG, "Found ${mediaFiles.size} media files.") - - val allFiles = mediaDir.listFiles().filter { supportedExtensions.contains(it.extension) } + val allFiles = mediaDir.listFiles()?.filter { supportedExtensions.contains(it.extension.lowercase()) } ?: return val trashFolder = File(mediaDir, "low_res_backup") if (!trashFolder.exists()) trashFolder.mkdirs() - val invalidImages = mutableListOf() + val wm = WallpaperManager.getInstance(this@MyWallpaperService) - val minWidth = wm.desiredMinimumWidth - val minHeight = wm.desiredMinimumHeight - val requiredSize = Math.max(minWidth, minHeight).times(requiredSizeRatio) + val requiredSize = Math.max(wm.desiredMinimumWidth, wm.desiredMinimumHeight) * requiredSizeRatio + + val videoExtensions = listOf("mp4", "mkv", "avi", "mov", "webm") + val imageExtensions = listOf("jpg", "jpeg", "png", "bmp", "webp") + for (file in allFiles) { - - if (file.isFile && (file.extension.equals("jpg", true) || - file.extension.equals("png", true) || - file.extension.equals("jpeg", true) || - file.extension.equals("bmp", true) || // BMP 추가 - file.extension.equals("webp", true))) { - - val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeFile(file.absolutePath, options) - if (options.outWidth >= requiredSize && options.outHeight >= requiredSize) { - mediaFiles.add(file) - } else { - invalidImages.add(file) // 조건 미달 + val ext = file.extension.lowercase() + var width = 0 + var height = 0 + var isOk = false + when { + // 이미지 처리 + imageExtensions.contains(ext) -> { + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(file.absolutePath, options) + width = options.outWidth + height = options.outHeight +// Blog.LOGE("loadFiles imageExtensions width $width height $height") + } + // 동영상 처리 + videoExtensions.contains(ext) -> { + val size = getVideoSize(file) + width = size?.first ?: 0 + height = size?.second ?: 0 + isOk = (width >= requiredSize*0.65 && height >= requiredSize*0.65) + Blog.LOGE("loadFiles videoExtensions requiredSize $requiredSize width $width height $height [$isOk]") } } - } - // 2. 부적합 이미지 이동 처리 (Job 밖에서 따로 돌려도 무방) - invalidImages.forEach { file -> - val targetFile = File(trashFolder, file.name) - file.renameTo(targetFile) // 파일 이동 + // 사이즈 검사 및 분류 + if (isOk || (width >= requiredSize && height >= requiredSize)) { + if (!mediaFiles.contains(file)) mediaFiles.add(file) + } else { +// Log.w(TAG, "Low resolution file filtered: ${file.name} (${width}x${height})") + file.renameTo(File(trashFolder, file.name)) + } } } 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 f926f522..c8d3db1c 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt @@ -97,11 +97,11 @@ class TorrentService : Service() { private val batteryReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { - isCharging = when (intent?.action) { - Intent.ACTION_POWER_CONNECTED -> true - Intent.ACTION_POWER_DISCONNECTED -> false - else -> isCharging - } +// isCharging = when (intent?.action) { +// Intent.ACTION_POWER_CONNECTED -> true +// Intent.ACTION_POWER_DISCONNECTED -> false +// else -> isCharging +// } val intent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) @@ -112,7 +112,7 @@ class TorrentService : Service() { val batteryPct = (level / scale.toFloat() * 100).toInt() isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || - status == BatteryManager.BATTERY_STATUS_FULL + status == BatteryManager.BATTERY_STATUS_FULL || (batteryPct > 95) var speedText = "" var detailsText = "" @@ -158,76 +158,82 @@ class TorrentService : Service() { networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) { - val isWifi = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) - isWifiConnected = isWifi + isWifiConnected = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) updateSessionState() } } connectivityManager.registerNetworkCallback(request, networkCallback!!) } + + var lastUpdateTime = -1L /** * 핵심 제어 로직: 충전 중일 때만 세션을 열고, Wi-Fi 여부에 따라 슬롯 조절 */ private fun updateSessionState() { if (session.isPaused) session.resume() + var curentTime = System.currentTimeMillis() + if (curentTime - lastUpdateTime > 5000) { + serviceScope.launch { + lastUpdateTime = curentTime + Blog.LOGE("isWifiConnected $isWifiConnected isCharging $isCharging") + val vector = session.swig().get_torrents() + // (핸들, 계산된 우선순위 점수) + val torrentsWithMetadata = mutableListOf>() + val torrentsWithoutMetadata = mutableListOf() - serviceScope.launch { - val vector = session.swig().get_torrents() - // (핸들, 계산된 우선순위 점수) - val torrentsWithMetadata = mutableListOf>() - val torrentsWithoutMetadata = mutableListOf() + val LOW_SPEED_THRESHOLD = 10 * 1024 // 10 KB/s 미만을 '속도 저조'로 판단 + val PENALTY_SCORE = 1000 // 속도가 낮을 경우 부여할 페널티 (대기열 순서보다 큰 값) - val LOW_SPEED_THRESHOLD = 10 * 1024 // 10 KB/s 미만을 '속도 저조'로 판단 - val PENALTY_SCORE = 1000 // 속도가 낮을 경우 부여할 페널티 (대기열 순서보다 큰 값) + for (i in 0 until vector.size.toInt()) { + val handle = TorrentHandle(vector.get(i)) + if (!handle.isValid) continue - for (i in 0 until vector.size.toInt()) { - val handle = TorrentHandle(vector.get(i)) - if (!handle.isValid) continue + val status = handle.status() + if (!status.hasMetadata()) { + torrentsWithoutMetadata.add(handle) + } else if (!status.isFinished) { + val currentRate = status.downloadPayloadRate() + val basePriority = status.queuePosition() - val status = handle.status() - if (!status.hasMetadata()) { - torrentsWithoutMetadata.add(handle) - } else if (!status.isFinished) { - val currentRate = status.downloadPayloadRate() - val basePriority = status.queuePosition() + // 속도가 너무 낮거나 0인 경우 페널티 부여 + // 단, 막 시작해서 속도가 측정되지 않은 경우를 위해 추가 로직을 넣을 수 있지만 + // 주기적 업데이트(2초)가 되므로 페널티를 받아도 다음 턴에 기회를 잡을 수 있습니다. + val finalScore = if (currentRate < LOW_SPEED_THRESHOLD) { + basePriority + PENALTY_SCORE + } else { + basePriority + } - // 속도가 너무 낮거나 0인 경우 페널티 부여 - // 단, 막 시작해서 속도가 측정되지 않은 경우를 위해 추가 로직을 넣을 수 있지만 - // 주기적 업데이트(2초)가 되므로 페널티를 받아도 다음 턴에 기회를 잡을 수 있습니다. - val finalScore = if (currentRate < LOW_SPEED_THRESHOLD) { - basePriority + PENALTY_SCORE - } else { - basePriority - } - - torrentsWithMetadata.add(handle to finalScore) - } - } - - // 1. 메타데이터 미수신: 무조건 유지 - torrentsWithoutMetadata.forEach { it.swig().resume() } - - // 2. 파일 다운로드: 계산된 점수(finalScore)가 낮은 순으로 정렬 - if (isCharging) { - val maxSlots = if (isWifiConnected) 8 else 1 - val sortedByPriority = torrentsWithMetadata.sortedBy { it.second } - - sortedByPriority.forEachIndexed { index, pair -> - val handle = pair.first - if (index < maxSlots) { - handle.swig().resume() - } else { - handle.pause() + torrentsWithMetadata.add(handle to finalScore) } } - } else { - // 배터리 모드 - torrentsWithMetadata.forEach { it.first.pause() } - } - refreshTorrentStats() + // 1. 메타데이터 미수신: 무조건 유지 + torrentsWithoutMetadata.forEach { it.swig().resume() } + + // 2. 파일 다운로드: 계산된 점수(finalScore)가 낮은 순으로 정렬 + if (isCharging) { + val maxSlots = if (isWifiConnected) 8 else 1 + val sortedByPriority = torrentsWithMetadata.sortedBy { it.second } + + sortedByPriority.forEachIndexed { index, pair -> + val handle = pair.first + if (index < maxSlots) { + handle.swig().resume() + } else { + handle.pause() + } + } + } else { + // 배터리 모드 + torrentsWithMetadata.forEach { it.first.pause() } + } + + refreshTorrentStats() + } } + } private val TRACKER_URL = "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt" @@ -329,12 +335,12 @@ class TorrentService : Service() { while (isActive) { updateSessionState() // 충전 중이고 세션이 돌아갈 때만 C++ 엔진에 상태 업데이트 요청 - var delayTime = 2000L + var delayTime = 30000L if (isCharging) { session.postTorrentUpdates() } else { - delayTime = 10000L + delayTime = 60000L } delay(delayTime) }