From 33a4674a825d52076c7f067fd79e8762179b4af7 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Tue, 24 Feb 2026 19:13:34 +0900 Subject: [PATCH] ... --- .../extensions/my_extension/messaging.js | 2 +- .../bums/lunatic/launcher/LauncherActivity.kt | 12 +- .../launcher/helpers/ServiceWatchdogWorker.kt | 72 +++++------ .../bums/lunatic/launcher/home/GeckoWeb.kt | 10 +- .../launcher/home/TorrentListFragment.kt | 14 ++- .../launcher/workers/TorrentManager.kt | 115 +++++++++++++++--- 6 files changed, 163 insertions(+), 62 deletions(-) diff --git a/app/src/main/assets/extensions/my_extension/messaging.js b/app/src/main/assets/extensions/my_extension/messaging.js index 9ccf19c3..9ba54370 100644 --- a/app/src/main/assets/extensions/my_extension/messaging.js +++ b/app/src/main/assets/extensions/my_extension/messaging.js @@ -379,7 +379,7 @@ document.addEventListener('DOMContentLoaded', function () { const shouldExclude = excludedStrings.some(str => currentUrl.includes(str)); if (port && !shouldExclude) { - toast("connect port on " + location.href); +// toast("connect port on " + location.href); time1 = setTimeout(autoScrollAndSave(false), 3500); } }) diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt index 55c6f550..01c8ce43 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt @@ -59,6 +59,7 @@ import bums.lunatic.launcher.receiver.NLService import bums.lunatic.launcher.receiver.SmsReceiver import bums.lunatic.launcher.settings.SettingsActivity import bums.lunatic.launcher.utils.Blog +import bums.lunatic.launcher.workers.TorrentService import bums.lunatic.launcher.workers.UsageLogType import bums.lunatic.launcher.workers.UsageUpdateType import bums.lunatic.launcher.workers.WorkersDb @@ -75,6 +76,7 @@ import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoRuntimeSettings import java.util.Calendar import java.util.Date +import kotlin.jvm.java open class LauncherActivity : CommonActivity() { @@ -379,8 +381,14 @@ open class LauncherActivity : CommonActivity() { // Blog.LOGE("failed to initialize youtubedl-android", e) // } - val intent = Intent(this, ForeGroundService::class.java) - this.startForegroundService(intent) + Intent(this, ForeGroundService::class.java).apply { + startForegroundService(intent) + } + + Intent(this, TorrentService::class.java).apply { + startForegroundService(intent) + } + WindowCompat.setDecorFitsSystemWindows(window, false) diff --git a/app/src/main/kotlin/bums/lunatic/launcher/helpers/ServiceWatchdogWorker.kt b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ServiceWatchdogWorker.kt index 64e17ff8..c73782c5 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/helpers/ServiceWatchdogWorker.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ServiceWatchdogWorker.kt @@ -1,36 +1,36 @@ -package bums.lunatic.launcher.helpers - -import android.app.ActivityManager -import android.content.Context -import android.content.Intent -import android.os.Build -import androidx.work.Worker -import androidx.work.WorkerParameters - -class ServiceWatchdogWorker( - private val context: Context, - workerParams: WorkerParameters -) : Worker(context, workerParams) { - - companion object{ - val TAG = "ServiceWatchdogWorker" - } - - override fun doWork(): Result { - val isServiceRunning = isServiceRunning(ForeGroundService::class.java) - if (!isServiceRunning) { - val intent = Intent(context, ForeGroundService::class.java) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(intent) - } else { - context.startService(intent) - } - } - return Result.success() - } - fun isServiceRunning(serviceClass: Class<*>): Boolean { - val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - return manager.getRunningServices(Int.MAX_VALUE) - .any { it.service.className == serviceClass.name } - } -} +//package bums.lunatic.launcher.helpers +// +//import android.app.ActivityManager +//import android.content.Context +//import android.content.Intent +//import android.os.Build +//import androidx.work.Worker +//import androidx.work.WorkerParameters +// +//class ServiceWatchdogWorker( +// private val context: Context, +// workerParams: WorkerParameters +//) : Worker(context, workerParams) { +// +// companion object{ +// val TAG = "ServiceWatchdogWorker" +// } +// +// override fun doWork(): Result { +// val isServiceRunning = isServiceRunning(ForeGroundService::class.java) +// if (!isServiceRunning) { +// val intent = Intent(context, ForeGroundService::class.java) +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +// context.startForegroundService(intent) +// } else { +// context.startService(intent) +// } +// } +// return Result.success() +// } +// fun isServiceRunning(serviceClass: Class<*>): Boolean { +// val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager +// return manager.getRunningServices(Int.MAX_VALUE) +// .any { it.service.className == serviceClass.name } +// } +//} diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt index 2965d4e2..b71cdb32 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt @@ -618,15 +618,15 @@ open class GeckoWeb @JvmOverloads constructor( var toast : Toast? = null private fun Context.toast(msg: String) { + val biggerText = SpannableStringBuilder(msg) + biggerText.setSpan(RelativeSizeSpan(1.6f), 0, msg.length, 0) + val view: View = inflate(this, R.layout.simple_toast, null) + view.findViewById(R.id.text).text = biggerText if (toast==null) { toast = Toast(this) toast?.duration = Toast.LENGTH_SHORT - val biggerText = SpannableStringBuilder(msg) - biggerText.setSpan(RelativeSizeSpan(1.6f), 0, msg.length, 0) - val view: View = inflate(this, R.layout.simple_toast, null) - view.findViewById(R.id.text).text = biggerText - toast?.view = view } + toast?.view = view toast?.show() } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/TorrentListFragment.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/TorrentListFragment.kt index ebbeec3d..e2fdfced 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/TorrentListFragment.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/TorrentListFragment.kt @@ -4,6 +4,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection +import android.os.Build import android.os.Bundle import android.os.IBinder import android.view.LayoutInflater @@ -70,9 +71,18 @@ class TorrentListFragment : BottomSheetDialogFragment() { override fun onStart() { super.onStart() // 프래그먼트가 보일 때 서비스 바인딩 - Intent(requireContext(), TorrentService::class.java).also { intent -> - requireContext().bindService(intent, connection, Context.BIND_AUTO_CREATE) + val intent = Intent(requireContext(), TorrentService::class.java) + + // 💡 1. 핵심: UI가 닫혀도 서비스가 죽지 않도록 독립적인 생명을 부여합니다. + // (이미 서비스가 켜져 있다면 onStartCommand만 가볍게 한 번 더 호출되므로 안전합니다.) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + requireContext().startForegroundService(intent) + } else { + requireContext().startService(intent) } + + // 💡 2. 그 다음 UI 통신을 위해 바인딩을 맺습니다. + requireContext().bindService(intent, connection, Context.BIND_AUTO_CREATE) } override fun onStop() { 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 7aab5c2f..4df7b0aa 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt @@ -23,9 +23,11 @@ import android.app.* import android.content.* import android.os.* import androidx.annotation.RequiresApi +import bums.lunatic.launcher.home.toast import bums.lunatic.launcher.utils.Blog import com.frostwire.jlibtorrent.swig.error_code import com.frostwire.jlibtorrent.swig.libtorrent +import com.frostwire.jlibtorrent.swig.settings_pack import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable.isActive @@ -51,6 +53,10 @@ class TorrentService : Service() { // 경로 설정 private val tempDir by lazy { File(getExternalFilesDir(null), "temp_torrents").apply { mkdirs() } } private val resumeDir by lazy { File(getExternalFilesDir(null), "resume_data").apply { mkdirs() } } + private lateinit var notificationManager: NotificationManager + private lateinit var notificationBuilder: NotificationCompat.Builder + private val NOTIFICATION_ID = 101 + private var hasActiveTorrents = false // 자동 종료를 위한 플래그 inner class TorrentBinder : Binder() { fun getService(): TorrentService = this@TorrentService @@ -72,8 +78,20 @@ class TorrentService : Service() { private fun startPolling() { serviceScope.launch { while (isActive) { - // 1. SessionManager에서 SWIG 원시 객체(vector)로 핸들 목록을 가져옵니다. val vector = session.swig().get_torrents() + val taskCount = vector.size.toInt() + + // 💡 1. 자동 종료 로직 + if (taskCount > 0) { + hasActiveTorrents = true // 토렌트가 1개 이상 활성화된 적이 있음을 마킹 + } else if (hasActiveTorrents && taskCount == 0) { + // 활성화된 적이 있었는데 0개가 되었다면 (모두 완료되어 이동되었거나 삭제됨) + println("TorrentService: 모든 작업이 완료되어 서비스를 자동 종료합니다.") + stopForeground(true) // 💡 상단바 알림 즉시 제거 + stopSelf() // 💡 서비스 스스로 종료 (메모리 해제) + break // 무한 루프 탈출 + } + // 1. SessionManager에서 SWIG 원시 객체(vector)로 핸들 목록을 가져옵니다. val tasks = mutableListOf() // 2. 각 핸들(TorrentHandle)을 순회하며 데이터를 추출합니다. @@ -119,8 +137,12 @@ class TorrentService : Service() { ) ) } - _torrentTasks.value = tasks + + // 💡 2. UI 알림 갱신 로직 + if (tasks.isNotEmpty()) { + updateNotification(tasks) + } delay(1000) } } @@ -165,6 +187,27 @@ class TorrentService : Service() { private fun initLibTorrent() { session = SessionManager() +// 💡 속도 최적화를 위한 세팅 적용 + val sp = SettingsPack() + + // [1] 통신사(ISP) 토렌트 트래픽 제한 우회 (강력 권장) + // 패킷을 암호화하여 통신사가 토렌트 다운로드 중인지 모르게 만듭니다. + sp.setInteger(settings_pack.int_types.in_enc_policy.swigValue(), settings_pack.enc_policy.pe_enabled.swigValue()) + sp.setInteger(settings_pack.int_types.out_enc_policy.swigValue(), settings_pack.enc_policy.pe_enabled.swigValue()) + + // [2] 마그넷 피어 탐색 극대화 (초기 메타데이터 수신 속도업) + sp.setBoolean(settings_pack.bool_types.enable_dht.swigValue(), true) // DHT 활성화 +// sp.setBoolean(settings_pack.bool_types.enable_pex.swigValue(), true) // Peer Exchange (피어 교환) + sp.setBoolean(settings_pack.bool_types.enable_lsd.swigValue(), true) // 로컬 네트워크 탐색 + + // [3] 모바일 맞춤형 연결 최적화 + // 안드로이드는 데스크탑처럼 수천 개를 뚫으면 공유기가 뻗거나 앱 OOM이 발생할 수 있습니다. + sp.connectionsLimit(400) // 글로벌 최대 연결 수 (기본값 보통 200) + sp.activeDownloads(3) // 동시에 속도를 끌어올릴 다운로드 개수 (너무 많으면 속도가 분산됨) + sp.activeLimit(5) // 활성 상태(업/다운 포함) 유지 최대 개수 + + // 세팅을 세션에 반영 + session.applySettings(sp) session.addListener(object : AlertListener { override fun types(): IntArray? = intArrayOf( @@ -211,15 +254,20 @@ class TorrentService : Service() { println("TorrentService: 마그넷 파싱 에러 - ${error.message()} / URI: $magnetUri") return } + swigParams.max_connections = 150 + // 다운로드/업로드 제한 해제 (기본값이 -1이긴 하나 명시적으로 박아줌) + swigParams.download_limit = -1 + swigParams.upload_limit = -1 + + // 참고: 만약 "순차 다운로드(동영상 스트리밍용)"가 필요하다면 아래 주석을 푸세요. + // 단, 전체 다운로드 완료 속도는 일반 다운로드보다 느려집니다. + // swigParams.flags = swigParams.flags.or_(libtorrent.getSequential_download()) - // 2. 파싱 성공 시, 다운로드 경로 설정 swigParams.setSave_path(tempDir.absolutePath) - - // 3. 원시 세션에 비동기로 토렌트 추가 session.swig().async_add_torrent(swigParams) println("TorrentService: 마그넷 추가 성공!") - + toast("마그넷 추가 성공 ${session.torrentHandles.size}개 진행중") } catch (e: Exception) { e.printStackTrace() } @@ -289,15 +337,21 @@ class TorrentService : Service() { return } + val appFolderName = "Lunatic" + val baseDownloadPath = "${Environment.DIRECTORY_DOWNLOADS}/$appFolderName" + var root = File(baseDownloadPath) + if (root.exists() == false) { + root.mkdirs() + } // 1. 단일 파일인 경우 if (sourcePath.isFile) { - copySingleFileToDownloads(sourcePath, Environment.DIRECTORY_DOWNLOADS) + copySingleFileToDownloads(sourcePath, baseDownloadPath) } // 2. 다중 파일(폴더)인 경우 else if (sourcePath.isDirectory) { sourcePath.walkTopDown().filter { it.isFile }.forEach { file -> val relativeSubPath = file.parentFile?.absolutePath?.substringAfter(sourcePath.absolutePath) ?: "" - val destRelativePath = "${Environment.DIRECTORY_DOWNLOADS}/$torrentName$relativeSubPath" + val destRelativePath = "$baseDownloadPath/$torrentName$relativeSubPath" copySingleFileToDownloads(file, destRelativePath) } @@ -391,17 +445,46 @@ class TorrentService : Service() { override fun onBind(intent: Intent): IBinder = binder + private fun updateNotification(tasks: List) { + if (tasks.isEmpty()) return + + // 다운로드 중인 첫 번째 항목 찾기 + val activeTask = tasks.firstOrNull { !it.isPaused && it.progress < 100f } ?: tasks.first() + + // 여러 개를 다운 중일 경우 텍스트 처리 + val title = if (tasks.size > 1) { + "${activeTask.name} 외 ${tasks.size - 1}건" + } else { + activeTask.name + } + + notificationBuilder.setContentTitle(title) + notificationBuilder.setContentText(String.format("다운로드 중... %.1f%%", activeTask.progress)) + notificationBuilder.setProgress(100, activeTask.progress.toInt(), false) // 프로그레스바 세팅 + + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) + } + private fun startForegroundService() { val channelId = "torrent_channel" + notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel(channelId, "Torrent Service", NotificationManager.IMPORTANCE_LOW) - val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - manager.createNotificationChannel(channel) + val channel = NotificationChannel( + channelId, + "Torrent Service", + // 💡 중요: 진행률이 1초마다 갱신되므로 소리가 나지 않게 LOW로 설정합니다. + NotificationManager.IMPORTANCE_LOW + ) + notificationManager.createNotificationChannel(channel) } - val notification = NotificationCompat.Builder(this, channelId) - .setSmallIcon(android.R.drawable.stat_sys_download) - .setContentTitle("Torrent Manager Active") - .build() - startForeground(101, notification) + + notificationBuilder = NotificationCompat.Builder(this, channelId) + .setSmallIcon(android.R.drawable.stat_sys_download) // 다운로드 아이콘 + .setContentTitle("토렌트 엔진 준비 중...") + .setOnlyAlertOnce(true) // 💡 갱신될 때마다 알림음/진동이 울리지 않게 설정 + .setOngoing(true) + + startForeground(NOTIFICATION_ID, notificationBuilder.build()) } } \ No newline at end of file