This commit is contained in:
lunaticbum 2026-02-24 19:13:34 +09:00
parent 5052d5cf52
commit 33a4674a82
6 changed files with 163 additions and 62 deletions

View File

@ -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);
}
})

View File

@ -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)

View File

@ -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 }
// }
//}

View File

@ -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<TextView>(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<TextView>(R.id.text).text = biggerText
toast?.view = view
}
toast?.view = view
toast?.show()
}

View File

@ -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() {

View File

@ -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<TorrentTask>()
// 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<TorrentTask>) {
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())
}
}