...
This commit is contained in:
parent
a7df635f66
commit
10fddc35d3
@ -1,344 +1,179 @@
|
|||||||
package bums.lunatic.launcher.workers
|
package bums.lunatic.launcher.workers
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Binder
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import com.frostwire.jlibtorrent.*
|
|
||||||
import com.frostwire.jlibtorrent.alerts.Alert
|
|
||||||
import com.frostwire.jlibtorrent.alerts.AlertType
|
|
||||||
import com.frostwire.jlibtorrent.alerts.TorrentFinishedAlert
|
|
||||||
import java.io.File
|
|
||||||
import com.frostwire.jlibtorrent.alerts.*
|
|
||||||
|
|
||||||
import android.app.*
|
import android.app.*
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.net.ConnectivityManager
|
import android.net.*
|
||||||
import android.net.Network
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.NetworkRequest
|
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import bums.lunatic.launcher.LunaticLauncher.Companion.toast
|
import androidx.core.app.NotificationCompat
|
||||||
import bums.lunatic.launcher.utils.Blog
|
import com.frostwire.jlibtorrent.*
|
||||||
|
import com.frostwire.jlibtorrent.alerts.*
|
||||||
import com.frostwire.jlibtorrent.swig.error_code
|
import com.frostwire.jlibtorrent.swig.error_code
|
||||||
import com.frostwire.jlibtorrent.swig.libtorrent
|
import com.frostwire.jlibtorrent.swig.libtorrent
|
||||||
import com.frostwire.jlibtorrent.swig.settings_pack
|
import com.frostwire.jlibtorrent.swig.settings_pack
|
||||||
import com.frostwire.jlibtorrent.swig.torrent_status
|
import com.frostwire.jlibtorrent.swig.torrent_status
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import java.io.File
|
||||||
import kotlinx.coroutines.NonCancellable.isActive
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
data class TorrentTask(
|
data class TorrentTask(
|
||||||
val infoHash: String,
|
val infoHash: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val progress: Float,
|
val progress: Float,
|
||||||
val isPaused: Boolean,
|
val isPaused: Boolean,
|
||||||
val isQueued: Boolean, // 대기열 여부
|
val isQueued: Boolean,
|
||||||
val stateText: String
|
val stateText: String
|
||||||
)
|
)
|
||||||
|
|
||||||
class TorrentService : Service() {
|
class TorrentService : Service() {
|
||||||
private lateinit var connectivityManager: ConnectivityManager
|
|
||||||
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
|
||||||
private var isWifiConnected = false
|
|
||||||
|
|
||||||
private fun registerNetworkCallback() {
|
|
||||||
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
||||||
val request = NetworkRequest.Builder()
|
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
networkCallback = object : ConnectivityManager.NetworkCallback() {
|
|
||||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
|
||||||
super.onCapabilitiesChanged(network, networkCapabilities)
|
|
||||||
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
|
||||||
|
|
||||||
if (isWifi) {
|
|
||||||
if (!isWifiConnected) {
|
|
||||||
isWifiConnected = true
|
|
||||||
println("TorrentService: Wi-Fi 연결됨. 전체 다운로드 세션 재개.")
|
|
||||||
session.resume() // 💡 세션 전체 트래픽 재개
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isWifiConnected) {
|
|
||||||
isWifiConnected = false
|
|
||||||
println("TorrentService: Wi-Fi 연결 아님(셀룰러 등). 전체 다운로드 세션 일시정지.")
|
|
||||||
session.pause() // 💡 세션 전체 트래픽 차단
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
|
||||||
super.onLost(network)
|
|
||||||
if (isWifiConnected) {
|
|
||||||
isWifiConnected = false
|
|
||||||
println("TorrentService: 네트워크 완전히 끊김. 전체 다운로드 세션 일시정지.")
|
|
||||||
session.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 초기 상태 체크 (서비스 시작 시점이 Wi-Fi 환경인지 확인)
|
|
||||||
val activeNetwork = connectivityManager.activeNetwork
|
|
||||||
val caps = connectivityManager.getNetworkCapabilities(activeNetwork)
|
|
||||||
isWifiConnected = caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
|
||||||
|
|
||||||
// 처음 켰을 때 Wi-Fi가 아니면 바로 일시정지
|
|
||||||
if (!isWifiConnected) {
|
|
||||||
println("TorrentService: 현재 Wi-Fi가 아닙니다. 세션을 일시정지 상태로 시작합니다.")
|
|
||||||
session.pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
connectivityManager.registerNetworkCallback(request, networkCallback!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var session: SessionManager
|
private lateinit var session: SessionManager
|
||||||
private val binder = TorrentBinder()
|
private val binder = TorrentBinder()
|
||||||
|
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
// 경로 설정
|
// 경로 설정
|
||||||
private val tempDir by lazy { File(getExternalFilesDir(null), "temp_torrents").apply { mkdirs() } }
|
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 val resumeDir by lazy { File(getExternalFilesDir(null), "resume_data").apply { mkdirs() } }
|
||||||
|
|
||||||
|
// 알림 및 상태 관리
|
||||||
private lateinit var notificationManager: NotificationManager
|
private lateinit var notificationManager: NotificationManager
|
||||||
private lateinit var notificationBuilder: NotificationCompat.Builder
|
private lateinit var notificationBuilder: NotificationCompat.Builder
|
||||||
private val NOTIFICATION_ID = 101
|
private val NOTIFICATION_ID = 101
|
||||||
private var hasActiveTorrents = false // 자동 종료를 위한 플래그
|
private var lastProgress = -1
|
||||||
|
|
||||||
|
// 제어 플래그
|
||||||
|
private var isWifiConnected = false
|
||||||
|
private var isCharging = false
|
||||||
|
private lateinit var connectivityManager: ConnectivityManager
|
||||||
|
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
||||||
|
|
||||||
inner class TorrentBinder : Binder() {
|
inner class TorrentBinder : Binder() {
|
||||||
fun getService(): TorrentService = this@TorrentService
|
fun getService(): TorrentService = this@TorrentService
|
||||||
}
|
}
|
||||||
|
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
||||||
|
|
||||||
// UI에서 관찰할 상태 Flow
|
|
||||||
private val _torrentTasks = MutableStateFlow<List<TorrentTask>>(emptyList())
|
|
||||||
val torrentTasks: StateFlow<List<TorrentTask>> = _torrentTasks
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
notificationManager = getSystemService(Context.CONNECTIVITY_SERVICE) as NotificationManager
|
||||||
startForegroundService()
|
startForegroundService()
|
||||||
initLibTorrent()
|
initLibTorrent()
|
||||||
|
|
||||||
|
// 1. 초기 상태 체크 (배터리 및 네트워크)
|
||||||
|
checkInitialStatus()
|
||||||
|
|
||||||
|
// 2. 리시버 및 콜백 등록
|
||||||
|
registerBatteryReceiver()
|
||||||
registerNetworkCallback()
|
registerNetworkCallback()
|
||||||
startPolling() // 폴링 시작
|
|
||||||
|
// 3. 초기 세션 상태 적용
|
||||||
|
updateSessionState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startPolling() {
|
private fun checkInitialStatus() {
|
||||||
serviceScope.launch {
|
// 배터리 상태
|
||||||
while (isActive) {
|
val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
||||||
val vector = session.swig().get_torrents()
|
val batteryStatus = registerReceiver(null, ifilter)
|
||||||
val taskCount = vector.size.toInt()
|
val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
|
||||||
|
isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
|
||||||
|
|
||||||
// 💡 1. 자동 종료 로직
|
// Wi-Fi 상태
|
||||||
if (taskCount > 0) {
|
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
hasActiveTorrents = true // 토렌트가 1개 이상 활성화된 적이 있음을 마킹
|
val activeNetwork = connectivityManager.activeNetwork
|
||||||
} else if (hasActiveTorrents && taskCount == 0) {
|
val caps = connectivityManager.getNetworkCapabilities(activeNetwork)
|
||||||
// 활성화된 적이 있었는데 0개가 되었다면 (모두 완료되어 이동되었거나 삭제됨)
|
isWifiConnected = caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||||
println("TorrentService: 모든 작업이 완료되어 서비스를 자동 종료합니다.")
|
}
|
||||||
|
|
||||||
try {
|
private fun registerBatteryReceiver() {
|
||||||
if (tempDir.exists()) {
|
val filter = IntentFilter().apply {
|
||||||
tempDir.listFiles()?.forEach { it.deleteRecursively() }
|
addAction(Intent.ACTION_POWER_CONNECTED)
|
||||||
println("TorrentService: 임시 다운로드 폴더 정리를 완료했습니다.")
|
addAction(Intent.ACTION_POWER_DISCONNECTED)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
registerReceiver(batteryReceiver, filter)
|
||||||
e.printStackTrace()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
stopForeground(true) // 💡 상단바 알림 즉시 제거
|
private val batteryReceiver = object : BroadcastReceiver() {
|
||||||
stopSelf() // 💡 서비스 스스로 종료 (메모리 해제)
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
break // 무한 루프 탈출
|
isCharging = when (intent?.action) {
|
||||||
|
Intent.ACTION_POWER_CONNECTED -> true
|
||||||
|
Intent.ACTION_POWER_DISCONNECTED -> false
|
||||||
|
else -> isCharging
|
||||||
|
}
|
||||||
|
updateSessionState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerNetworkCallback() {
|
||||||
|
val request = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
|
||||||
|
val isWifi = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|
if (isWifiConnected != isWifi) {
|
||||||
|
isWifiConnected = isWifi
|
||||||
|
updateSessionState()
|
||||||
}
|
}
|
||||||
// 1. SessionManager에서 SWIG 원시 객체(vector)로 핸들 목록을 가져옵니다.
|
|
||||||
val tasks = mutableListOf<TorrentTask>()
|
|
||||||
|
|
||||||
// 2. 각 핸들(TorrentHandle)을 순회하며 데이터를 추출합니다.
|
|
||||||
for (i in 0 until vector.size.toInt()) {
|
|
||||||
val swigHandle = vector.get(i)
|
|
||||||
val handle = com.frostwire.jlibtorrent.TorrentHandle(swigHandle)
|
|
||||||
|
|
||||||
// 핸들이 유효한지 확인
|
|
||||||
if (!handle.isValid) continue
|
|
||||||
|
|
||||||
val status = handle.status()
|
|
||||||
val state = status.state()
|
|
||||||
|
|
||||||
// 1. 현재 다운로드 대기 중(Queued)인지 확인
|
|
||||||
// 'auto_managed'이면서, 실제 'downloading' 상태가 아닌 경우를 체크합니다.
|
|
||||||
val isQueued = status.flags().and_(libtorrent.getAuto_managed()).nonZero() &&
|
|
||||||
(state.swig() == torrent_status.state_t.downloading_metadata.swigValue() ||
|
|
||||||
state.swig() == torrent_status.state_t.checking_files.swigValue() ||
|
|
||||||
state.swig() == torrent_status.state_t.checking_resume_data.swigValue())
|
|
||||||
|
|
||||||
// 2. 더 확실한 방법 (상태값이 'queued_for_checking' 등인 경우 포함)
|
|
||||||
// 실제 데이터 전송이 일어나지 않고 대기 순번을 기다리는 상태인지 확인
|
|
||||||
val isDownloading = (state.swig() == torrent_status.state_t.downloading.swigValue())
|
|
||||||
val isFinished = status.isFinished
|
|
||||||
val isPaused = status.flags().and_(libtorrent.getPaused()).nonZero()
|
|
||||||
val stateText = when {
|
|
||||||
status.flags().and_(libtorrent.getPaused()).nonZero() -> "일시정지"
|
|
||||||
state.swig() == torrent_status.state_t.checking_files.swigValue() -> "파일 검사 중"
|
|
||||||
state.swig() == torrent_status.state_t.downloading_metadata.swigValue() -> "메타데이터 수신 중"
|
|
||||||
state.swig() == torrent_status.state_t.downloading.swigValue() -> "다운로드 중"
|
|
||||||
state.swig() == torrent_status.state_t.finished.swigValue() || state.swig() == torrent_status.state_t.seeding.swigValue() -> "완료"
|
|
||||||
else -> "대기 중" // activeDownloads 제한에 걸려 대기하는 경우 여기로 들어옵니다.
|
|
||||||
}
|
|
||||||
// "일시정지도 아니고, 완료도 아닌데, 현재 다운로드 중도 아니면" -> 대기열 상태로 간주
|
|
||||||
val realIsQueued = !isPaused && !isFinished && !isDownloading
|
|
||||||
val hashStr = status.infoHash().toString()
|
|
||||||
var rawName = status.name()
|
|
||||||
|
|
||||||
// 💡 핵심: TorrentHandle 객체에서 torrentFile()을 호출하여 TorrentInfo를 가져옵니다.
|
|
||||||
if (status.hasMetadata()) {
|
|
||||||
val torrentInfo = handle.torrentFile()
|
|
||||||
// torrentInfo가 null이 아니고 유효하다면 진짜 이름을 추출합니다.
|
|
||||||
if (torrentInfo != null && torrentInfo.isValid) {
|
|
||||||
val realName = torrentInfo.name()
|
|
||||||
if (!realName.isNullOrEmpty()) {
|
|
||||||
rawName = realName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val displayName = if (rawName.isNullOrEmpty() || rawName == hashStr) {
|
|
||||||
if (status.hasMetadata()) {
|
|
||||||
"파일 정보 분석 중..."
|
|
||||||
} else {
|
|
||||||
"메타데이터 수신 중... (${hashStr.take(6)})"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rawName
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.add(
|
|
||||||
TorrentTask(
|
|
||||||
infoHash = hashStr,
|
|
||||||
name = displayName,
|
|
||||||
progress = status.progress() * 100f,
|
|
||||||
isPaused = status.flags().and_(com.frostwire.jlibtorrent.swig.libtorrent.getPaused()).nonZero(),
|
|
||||||
isQueued = isQueued,
|
|
||||||
stateText = stateText
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_torrentTasks.value = tasks
|
|
||||||
|
|
||||||
// 💡 2. UI 알림 갱신 로직
|
|
||||||
if (tasks.isNotEmpty()) {
|
|
||||||
updateNotification(tasks)
|
|
||||||
}
|
|
||||||
delay(15000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
connectivityManager.registerNetworkCallback(request, networkCallback!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
/**
|
||||||
super.onDestroy()
|
* 핵심 제어 로직: 충전 중일 때만 세션을 열고, Wi-Fi 여부에 따라 슬롯 조절
|
||||||
networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) }
|
*/
|
||||||
serviceScope.cancel() // 서비스 종료 시 코루틴 정리
|
private fun updateSessionState() {
|
||||||
}
|
if (isCharging) {
|
||||||
|
if (session.isPaused) session.resume()
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
// Wi-Fi면 5개, 셀룰러면 1개 다운로드 허용
|
||||||
// 인텐트에서 마그넷 주소를 꺼냄
|
val sp = SettingsPack()
|
||||||
val uriString = intent?.getStringExtra("EXTRA_MAGNET_URI") ?: ""
|
if (isWifiConnected) {
|
||||||
Blog.LOGE("uriString >>> $uriString")
|
sp.activeDownloads(5)
|
||||||
if (uriString.startsWith("magnet:?")) {
|
sp.activeLimit(8)
|
||||||
var fixedUri = uriString
|
} else {
|
||||||
if (fixedUri.contains("xt=urn:") && !fixedUri.contains("xt=urn:btih:")) {
|
sp.activeDownloads(1)
|
||||||
fixedUri = fixedUri.replace("xt=urn:", "xt=urn:btih:")
|
sp.activeLimit(2)
|
||||||
println("TorrentService: 누락된 btih: 를 추가하여 마그넷 주소를 수정했습니다 -> $fixedUri")
|
|
||||||
}
|
}
|
||||||
addMagnet(fixedUri)
|
session.applySettings(sp)
|
||||||
}
|
println("TorrentService: 충전 중 - 세션 활성화 (Wi-Fi: $isWifiConnected)")
|
||||||
// 2. .torrent 파일을 가리키는 일반 HTTP 웹 링크인 경우
|
} else {
|
||||||
else if (uriString.startsWith("http://") || uriString.startsWith("https://")) {
|
if (!session.isPaused) session.pause()
|
||||||
println("TorrentService: 마그넷이 아닌 웹 URL이 전달되었습니다 -> $uriString")
|
|
||||||
// 여기서 바로 addMagnet을 호출하면 안 됩니다!
|
|
||||||
// downloadTorrentFileFromWeb(uriString) // 별도의 파일 다운로드 로직 필요
|
|
||||||
}
|
|
||||||
// 3. 알 수 없는 형식인 경우
|
|
||||||
else {
|
|
||||||
println("TorrentService: 잘못된 형식의 URI입니다 -> $uriString")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 서비스가 강제 종료되어도 시스템이 다시 살려내도록 START_STICKY 반환
|
// 💡 충전 중단 시 타이머 즉시 종료 (배터리 소모 0)
|
||||||
return START_STICKY
|
updateJob?.cancel()
|
||||||
}
|
println("TorrentService: 충전 중 아님 - 세션 및 업데이트 타이머 일시정지")
|
||||||
|
}
|
||||||
// --- 기존 제어 기능들 아래에 추가 ---
|
refreshTorrentStats() // 상태 변경 후 즉시 UI 갱신
|
||||||
fun resumeTorrent(infoHash: String) {
|
|
||||||
session.find(Sha1Hash(infoHash))?.resume()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initLibTorrent() {
|
private fun initLibTorrent() {
|
||||||
session = SessionManager()
|
session = SessionManager()
|
||||||
// 💡 속도 최적화를 위한 세팅 적용
|
|
||||||
val sp = SettingsPack()
|
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.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())
|
sp.setInteger(settings_pack.int_types.out_enc_policy.swigValue(), settings_pack.enc_policy.pe_enabled.swigValue())
|
||||||
|
sp.setBoolean(settings_pack.bool_types.enable_dht.swigValue(), true)
|
||||||
|
sp.setBoolean(settings_pack.bool_types.enable_lsd.swigValue(), true)
|
||||||
|
|
||||||
// [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) // 로컬 네트워크 탐색
|
|
||||||
sp.setBoolean(settings_pack.bool_types.dont_count_slow_torrents.swigValue(), true)
|
|
||||||
// 2. '느리다'고 판단할 다운로드 속도 기준 (예: 10KB/s 미만)
|
|
||||||
sp.setInteger(settings_pack.int_types.inactive_down_rate.swigValue(), 10 * 1024)
|
|
||||||
|
|
||||||
// 3. '느리다'고 판단할 업로드 속도 기준 (예: 5KB/s 미만)
|
|
||||||
sp.setInteger(settings_pack.int_types.inactive_up_rate.swigValue(), 5 * 1024)
|
|
||||||
|
|
||||||
// 4. 이 느린 속도가 몇 초간 지속될 때 대기열로 미룰 것인지 판단 (예: 20초)
|
|
||||||
sp.setInteger(settings_pack.int_types.inactivity_timeout.swigValue(), 20)
|
|
||||||
|
|
||||||
// [3] 얼마나 오래 느려야 대기열로 보낼지 결정 (초 단위)
|
|
||||||
sp.setInteger(settings_pack.int_types.auto_manage_interval.swigValue(), 20)
|
|
||||||
// [3] 모바일 맞춤형 연결 최적화
|
|
||||||
// 안드로이드는 데스크탑처럼 수천 개를 뚫으면 공유기가 뻗거나 앱 OOM이 발생할 수 있습니다.
|
|
||||||
sp.connectionsLimit(400) // 글로벌 최대 연결 수 (기본값 보통 200)
|
|
||||||
sp.activeDownloads(4) // 동시에 속도를 끌어올릴 다운로드 개수 (너무 많으면 속도가 분산됨)
|
|
||||||
sp.activeLimit(6) // 활성 상태(업/다운 포함) 유지 최대 개수
|
|
||||||
sp.activeSeeds(2)
|
|
||||||
|
|
||||||
// 세팅을 세션에 반영
|
|
||||||
session.applySettings(sp)
|
session.applySettings(sp)
|
||||||
|
|
||||||
session.addListener(object : AlertListener {
|
session.addListener(object : AlertListener {
|
||||||
override fun types(): IntArray? = intArrayOf(
|
override fun types(): IntArray? = intArrayOf(
|
||||||
AlertType.TORRENT_FINISHED.swig(),
|
AlertType.TORRENT_FINISHED.swig(),
|
||||||
AlertType.METADATA_RECEIVED.swig(),
|
AlertType.METADATA_RECEIVED.swig(),
|
||||||
|
AlertType.STATE_UPDATE.swig(),
|
||||||
AlertType.SAVE_RESUME_DATA.swig()
|
AlertType.SAVE_RESUME_DATA.swig()
|
||||||
)
|
)
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
override fun alert(alert: Alert<*>) {
|
override fun alert(alert: Alert<*>) {
|
||||||
Blog.LOGE("alert ${alert.type()}")
|
|
||||||
when (alert.type()) {
|
when (alert.type()) {
|
||||||
|
AlertType.STATE_UPDATE -> refreshTorrentStats()
|
||||||
AlertType.TORRENT_FINISHED -> {
|
AlertType.TORRENT_FINISHED -> {
|
||||||
val ta = alert as TorrentFinishedAlert
|
moveToPrivateStorage((alert as TorrentFinishedAlert).handle())
|
||||||
// moveToPublicDownload(ta.handle())
|
refreshTorrentStats()
|
||||||
moveToPrivateStorage(ta.handle())
|
|
||||||
}
|
|
||||||
AlertType.METADATA_RECEIVED -> {
|
|
||||||
val ma = alert as MetadataReceivedAlert
|
|
||||||
ma.handle().saveResumeData()
|
|
||||||
}
|
}
|
||||||
|
AlertType.METADATA_RECEIVED -> (alert as MetadataReceivedAlert).handle().saveResumeData()
|
||||||
AlertType.SAVE_RESUME_DATA -> {
|
AlertType.SAVE_RESUME_DATA -> {
|
||||||
val ra = alert as SaveResumeDataAlert
|
val ra = alert as SaveResumeDataAlert
|
||||||
saveResumeFile(ra.handle().infoHash().toString(), ra.params())
|
saveResumeFile(ra.handle().infoHash().toString(), ra.params())
|
||||||
@ -349,318 +184,217 @@ class TorrentService : Service() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
session.start()
|
session.start()
|
||||||
|
|
||||||
restoreExistingDownloads()
|
restoreExistingDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 마그넷 추가 */
|
private var updateJob: Job? = null
|
||||||
fun addMagnet(magnetUri: String) {
|
|
||||||
try {
|
|
||||||
val error = error_code()
|
|
||||||
|
|
||||||
// 1. SessionManager 대신 libtorrent 원시 함수로 마그넷 파싱 시도
|
// ✅ 추가할 함수: 다운로드 중일 때만 2초마다 가볍게 찔러줍니다.
|
||||||
val swigParams = libtorrent.parse_magnet_uri(magnetUri, error)
|
private fun startLightweightUpdater() {
|
||||||
|
updateJob?.cancel()
|
||||||
if (error.value() != 0) {
|
updateJob = serviceScope.launch {
|
||||||
// 파싱 실패 시 어떤 이유로 실패했는지 정확한 에러 메시지 출력
|
while (isActive) {
|
||||||
println("TorrentService: 마그넷 파싱 에러 - ${error.message()} / URI: $magnetUri")
|
// 충전 중이고 세션이 돌아갈 때만 C++ 엔진에 상태 업데이트 요청
|
||||||
return
|
if (isCharging && !session.isPaused) {
|
||||||
|
session.postTorrentUpdates()
|
||||||
|
}
|
||||||
|
delay(2000) // 2초 주기 (원하는 대로 조절 가능)
|
||||||
}
|
}
|
||||||
swigParams.flags = swigParams.flags.or_(libtorrent.getAuto_managed())
|
|
||||||
swigParams.max_connections = Int.MAX_VALUE
|
|
||||||
// 다운로드/업로드 제한 해제 (기본값이 -1이긴 하나 명시적으로 박아줌)
|
|
||||||
swigParams.download_limit = -1
|
|
||||||
swigParams.upload_limit = -1
|
|
||||||
// 참고: 만약 "순차 다운로드(동영상 스트리밍용)"가 필요하다면 아래 주석을 푸세요.
|
|
||||||
// 단, 전체 다운로드 완료 속도는 일반 다운로드보다 느려집니다.
|
|
||||||
// swigParams.flags = swigParams.flags.or_(libtorrent.getSequential_download())
|
|
||||||
swigParams.setSave_path(tempDir.absolutePath)
|
|
||||||
session.swig().async_add_torrent(swigParams)
|
|
||||||
|
|
||||||
println("TorrentService: 마그넷 추가 성공!")
|
|
||||||
toast("마그넷 추가 성공 ${session.torrentHandles.size}개 진행중")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 상태 리스트 반환 */
|
/**
|
||||||
fun getStatusList(): List<TorrentStatus> {
|
* 리스너에 의해 호출되는 UI 및 데이터 갱신 함수
|
||||||
// SessionManager 대신 swig 원시 객체(C++ 레벨)에서 직접 벡터를 가져옵니다.
|
*/
|
||||||
val vector = session.swig().get_torrents()
|
private fun refreshTorrentStats() {
|
||||||
val list = mutableListOf<TorrentStatus>()
|
serviceScope.launch {
|
||||||
|
val vector = session.swig().get_torrents()
|
||||||
|
val tasks = mutableListOf<TorrentTask>()
|
||||||
|
|
||||||
// SWIG vector size는 Long 타입이므로 toInt()로 캐스팅이 필요합니다.
|
for (i in 0 until vector.size.toInt()) {
|
||||||
for (i in 0 until vector.size.toInt()) {
|
val handle = TorrentHandle(vector.get(i))
|
||||||
val handle = com.frostwire.jlibtorrent.TorrentHandle(vector.get(i))
|
if (!handle.isValid) continue
|
||||||
list.add(handle.status())
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 제어 기능 */
|
val status = handle.status()
|
||||||
fun pauseTorrent(infoHash: String) {
|
val state = status.state()
|
||||||
session.find(Sha1Hash(infoHash))?.let {
|
|
||||||
it.pause()
|
|
||||||
it.queuePositionBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
val isPaused = status.flags().and_(libtorrent.getPaused()).nonZero()
|
||||||
|
val isFinished = status.isFinished
|
||||||
|
val isDownloading = (state.swig() == torrent_status.state_t.downloading.swigValue())
|
||||||
|
|
||||||
fun removeTorrent(infoHash: String, deleteFile: Boolean) {
|
val stateText = when {
|
||||||
val handle = session.find(Sha1Hash(infoHash))
|
!isCharging -> "충전 대기 중"
|
||||||
if (handle != null) {
|
isPaused -> "일시정지"
|
||||||
// 1.2.x 버전의 제거 로직
|
isDownloading -> "다운로드 중"
|
||||||
session.remove(handle)
|
isFinished -> "완료"
|
||||||
if (deleteFile) {
|
else -> "대기 중"
|
||||||
// 파일 삭제는 수동으로 처리하거나 handle의 옵션을 확인해야 합니다.
|
}
|
||||||
File(tempDir, handle.status().name()).deleteRecursively()
|
|
||||||
|
tasks.add(TorrentTask(
|
||||||
|
infoHash = status.infoHash().toString(),
|
||||||
|
name = status.name() ?: "알 수 없음",
|
||||||
|
progress = status.progress() * 100f,
|
||||||
|
isPaused = isPaused,
|
||||||
|
isQueued = !isPaused && !isFinished && !isDownloading,
|
||||||
|
stateText = stateText
|
||||||
|
))
|
||||||
}
|
}
|
||||||
File(resumeDir, "$infoHash.resume").delete()
|
|
||||||
|
_torrentTasks.value = tasks
|
||||||
|
updateNotification(tasks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateNotification(tasks: List<TorrentTask>) {
|
||||||
|
if (tasks.isEmpty()) return
|
||||||
|
val activeTask = tasks.firstOrNull { !it.isPaused && it.progress < 100f } ?: tasks.first()
|
||||||
|
val currentProgress = activeTask.progress.toInt()
|
||||||
|
|
||||||
|
// 최적화: 진행률이 변했을 때만 notify 호출
|
||||||
|
if (currentProgress == lastProgress) return
|
||||||
|
lastProgress = currentProgress
|
||||||
|
|
||||||
|
notificationBuilder.setContentTitle(if (tasks.size > 1) "${activeTask.name} 외 ${tasks.size - 1}건" else activeTask.name)
|
||||||
|
.setContentText("${activeTask.stateText}: $currentProgress%")
|
||||||
|
.setProgress(100, currentProgress, false)
|
||||||
|
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 데이터 관리 및 이동 로직 (기존 유지) ---
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
private fun moveToPrivateStorage(handle: TorrentHandle) {
|
private fun moveToPrivateStorage(handle: TorrentHandle) {
|
||||||
try {
|
try {
|
||||||
val status = handle.status()
|
val status = handle.status()
|
||||||
val infoHash = status.infoHash().toString()
|
val torrentName = if (status.hasMetadata()) handle.torrentFile()?.name() ?: status.name() else status.name()
|
||||||
var torrentName = status.name()
|
|
||||||
|
|
||||||
if (status.hasMetadata()) {
|
|
||||||
val torrentInfo = handle.torrentFile()
|
|
||||||
if (torrentInfo != null && torrentInfo.isValid) {
|
|
||||||
val realName = torrentInfo.name()
|
|
||||||
if (!realName.isNullOrEmpty()) {
|
|
||||||
torrentName = realName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (torrentName.isNullOrEmpty() || torrentName == infoHash) {
|
|
||||||
println("TorrentService: 이동할 유효한 파일/폴더 이름을 찾지 못했습니다.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val sourcePath = File(tempDir, torrentName)
|
val sourcePath = File(tempDir, torrentName)
|
||||||
if (!sourcePath.exists()) {
|
val destPath = File(File(getExternalFilesDir(null), "completed_torrents").apply { mkdirs() }, torrentName)
|
||||||
println("TorrentService: 원본 파일/폴더가 존재하지 않습니다 -> ${sourcePath.absolutePath}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 💡 앱 전용 프라이빗 폴더 설정 (다른 앱 접근 불가)
|
if (sourcePath.exists()) {
|
||||||
// 안드로이드/data/bums.lunatic.launcher/files/completed_torrents 에 저장됩니다.
|
if (!sourcePath.renameTo(destPath)) {
|
||||||
val privateCompletedDir = File(getExternalFilesDir(null), "completed_torrents")
|
sourcePath.copyRecursively(destPath, overwrite = true)
|
||||||
if (!privateCompletedDir.exists()) {
|
sourcePath.deleteRecursively()
|
||||||
privateCompletedDir.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
val destPath = File(privateCompletedDir, torrentName)
|
|
||||||
|
|
||||||
// 1. renameTo로 빠른 파일/폴더 이동 시도 (같은 파티션 내 이동 시 즉시 완료됨)
|
|
||||||
val success = sourcePath.renameTo(destPath)
|
|
||||||
|
|
||||||
// 2. 만약 renameTo가 실패할 경우 (파티션이 다르거나 권한 문제 등), 직접 복사 후 삭제
|
|
||||||
if (!success) {
|
|
||||||
sourcePath.copyRecursively(destPath, overwrite = true)
|
|
||||||
sourcePath.deleteRecursively()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 세션에서 제거 및 리쥼 파일 삭제
|
|
||||||
session.remove(handle)
|
|
||||||
|
|
||||||
val resumeFile = File(resumeDir, "$infoHash.resume")
|
|
||||||
if (resumeFile.exists()) {
|
|
||||||
resumeFile.delete()
|
|
||||||
println("TorrentService: 리쥼 파일 삭제 완료 ($infoHash.resume)")
|
|
||||||
}
|
|
||||||
|
|
||||||
println("TorrentService: 다운로드 완료 및 프라이빗 폴더 이동 성공 ($torrentName)")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
println("TorrentService: 파일 이동 중 에러 발생 - ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
|
||||||
private fun moveToPublicDownload(handle: TorrentHandle) {
|
|
||||||
try {
|
|
||||||
val status = handle.status()
|
|
||||||
val infoHash = status.infoHash().toString() // 💡 리쥼 파일 삭제용 해시값 미리 저장
|
|
||||||
var torrentName = status.name()
|
|
||||||
|
|
||||||
// 💡 날카로운 지적 반영: 메타데이터(TorrentInfo)에서 진짜 파일/폴더명을 확실하게 가져옵니다!
|
|
||||||
if (status.hasMetadata()) {
|
|
||||||
val torrentInfo = handle.torrentFile()
|
|
||||||
if (torrentInfo != null && torrentInfo.isValid) {
|
|
||||||
val realName = torrentInfo.name()
|
|
||||||
if (!realName.isNullOrEmpty()) {
|
|
||||||
torrentName = realName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
session.remove(handle)
|
||||||
|
File(resumeDir, "${status.infoHash()}.resume").delete()
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) { e.printStackTrace() }
|
||||||
// 진짜 이름도 없고 해시값과 동일하다면 비정상 상태로 간주
|
|
||||||
if (torrentName.isNullOrEmpty() || torrentName == status.infoHash().toString()) {
|
|
||||||
println("TorrentService: 이동할 유효한 파일/폴더 이름을 찾지 못했습니다.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 진짜 이름으로 임시 폴더 내의 실제 경로를 찾음
|
|
||||||
val sourcePath = File(tempDir, torrentName)
|
|
||||||
if (!sourcePath.exists()) {
|
|
||||||
println("TorrentService: 원본 파일/폴더가 존재하지 않습니다 -> ${sourcePath.absolutePath}")
|
|
||||||
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, baseDownloadPath)
|
|
||||||
}
|
|
||||||
// 2. 다중 파일(폴더)인 경우
|
|
||||||
else if (sourcePath.isDirectory) {
|
|
||||||
sourcePath.walkTopDown().filter { it.isFile }.forEach { file ->
|
|
||||||
val relativeSubPath = file.parentFile?.absolutePath?.substringAfter(sourcePath.absolutePath) ?: ""
|
|
||||||
val destRelativePath = "$baseDownloadPath/$torrentName$relativeSubPath"
|
|
||||||
|
|
||||||
copySingleFileToDownloads(file, destRelativePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 복사가 모두 끝난 후 임시 파일(폴더) 삭제 및 세션에서 제거
|
|
||||||
sourcePath.deleteRecursively()
|
|
||||||
session.remove(handle)
|
|
||||||
|
|
||||||
val resumeFile = File(resumeDir, "$infoHash.resume")
|
|
||||||
if (resumeFile.exists()) {
|
|
||||||
resumeFile.delete()
|
|
||||||
println("TorrentService: 리쥼 파일 삭제 완료 ($infoHash.resume)")
|
|
||||||
}
|
|
||||||
|
|
||||||
println("TorrentService: 다운로드 완료 및 Public 폴더 이동 성공 ($torrentName)")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
println("TorrentService: 파일 이동 중 에러 발생 - ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
|
||||||
private fun copySingleFileToDownloads(sourceFile: File, destRelativePath: String) {
|
|
||||||
val contentValues = ContentValues().apply {
|
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, sourceFile.name)
|
|
||||||
put(MediaStore.MediaColumns.RELATIVE_PATH, destRelativePath)
|
|
||||||
// 파일을 복사하는 동안 다른 앱이 접근하지 못하도록 IS_PENDING 설정
|
|
||||||
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
|
||||||
uri?.let { destUri ->
|
|
||||||
contentResolver.openOutputStream(destUri)?.use { output ->
|
|
||||||
sourceFile.inputStream().use { input ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 복사 완료 후 IS_PENDING 해제
|
|
||||||
contentValues.clear()
|
|
||||||
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
|
||||||
contentResolver.update(destUri, contentValues, null, null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveResumeFile(hash: String, params: AddTorrentParams) {
|
private fun saveResumeFile(hash: String, params: AddTorrentParams) {
|
||||||
try {
|
try {
|
||||||
val file = File(resumeDir, "$hash.resume")
|
val data = Vectors.byte_vector2bytes(libtorrent.write_resume_data_buf_ex(params.swig()))
|
||||||
|
File(resumeDir, "$hash.resume").writeBytes(data)
|
||||||
// 1.2.x의 bencode() 대신 2.0.x 규격의 버퍼(buf_ex) 쓰기 함수 사용
|
} catch (e: Exception) { e.printStackTrace() }
|
||||||
val byteVector = libtorrent.write_resume_data_buf_ex(params.swig())
|
|
||||||
val data = Vectors.byte_vector2bytes(byteVector)
|
|
||||||
|
|
||||||
file.writeBytes(data)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreExistingDownloads() {
|
private fun restoreExistingDownloads() {
|
||||||
resumeDir.listFiles()?.filter { it.extension == "resume" }?.forEach { file ->
|
resumeDir.listFiles()?.filter { it.extension == "resume" }?.forEach { file ->
|
||||||
try {
|
try {
|
||||||
val data = file.readBytes()
|
val data = file.readBytes()
|
||||||
|
val swigParams = libtorrent.read_resume_data_ex(Vectors.bytes2byte_vector(data), error_code())
|
||||||
// 1. 바이트 배열을 SWIG byte_vector로 변환
|
|
||||||
val byteVector = Vectors.bytes2byte_vector(data)
|
|
||||||
val error = error_code()
|
|
||||||
|
|
||||||
// 2. 올려주신 클래스에 있는 read_resume_data_ex 사용!
|
|
||||||
val swigParams = libtorrent.read_resume_data_ex(byteVector, error)
|
|
||||||
|
|
||||||
if (error.value() != 0) {
|
|
||||||
println("Resume data error: ${error.message()}")
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 다운로드 경로 재지정
|
|
||||||
// (SWIG는 스네이크 케이스 변수를 카멜 케이스 setter로 자동 변환합니다)
|
|
||||||
swigParams.setSave_path(tempDir.absolutePath)
|
swigParams.setSave_path(tempDir.absolutePath)
|
||||||
|
|
||||||
// 4. SessionManager의 C++ 원시 핸들에 직접 접근하여 토렌트 추가 (비동기 권장)
|
|
||||||
session.swig().async_add_torrent(swigParams)
|
session.swig().async_add_torrent(swigParams)
|
||||||
|
} catch (e: Exception) { e.printStackTrace() }
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder = binder
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
intent?.getStringExtra("EXTRA_MAGNET_URI")?.let { addMagnet(it) }
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateNotification(tasks: List<TorrentTask>) {
|
fun addMagnet(magnetUri: String) {
|
||||||
if (tasks.isEmpty()) return
|
try {
|
||||||
|
val error = error_code()
|
||||||
// 다운로드 중인 첫 번째 항목 찾기
|
val swigParams = libtorrent.parse_magnet_uri(magnetUri, error)
|
||||||
val activeTask = tasks.firstOrNull { !it.isPaused && it.progress < 100f } ?: tasks.first()
|
if (error.value() == 0) {
|
||||||
|
swigParams.setSave_path(tempDir.absolutePath)
|
||||||
// 여러 개를 다운 중일 경우 텍스트 처리
|
swigParams.flags = swigParams.flags.or_(libtorrent.getAuto_managed())
|
||||||
val title = if (tasks.size > 1) {
|
session.swig().async_add_torrent(swigParams)
|
||||||
"${activeTask.name} 외 ${tasks.size - 1}건"
|
}
|
||||||
} else {
|
} catch (e: Exception) { e.printStackTrace() }
|
||||||
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() {
|
private fun startForegroundService() {
|
||||||
val channelId = "torrent_channel"
|
val channelId = "torrent_channel"
|
||||||
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val channel = NotificationChannel(
|
notificationManager.createNotificationChannel(NotificationChannel(channelId, "Torrent Service", NotificationManager.IMPORTANCE_LOW))
|
||||||
channelId,
|
|
||||||
"Torrent Service",
|
|
||||||
// 💡 중요: 진행률이 1초마다 갱신되므로 소리가 나지 않게 LOW로 설정합니다.
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
)
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationBuilder = NotificationCompat.Builder(this, channelId)
|
notificationBuilder = NotificationCompat.Builder(this, channelId)
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download) // 다운로드 아이콘
|
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
.setContentTitle("토렌트 엔진 준비 중...")
|
.setOnlyAlertOnce(true)
|
||||||
.setOnlyAlertOnce(true) // 💡 갱신될 때마다 알림음/진동이 울리지 않게 설정
|
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
|
|
||||||
startForeground(NOTIFICATION_ID, notificationBuilder.build())
|
startForeground(NOTIFICATION_ID, notificationBuilder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _torrentTasks = kotlinx.coroutines.flow.MutableStateFlow<List<TorrentTask>>(emptyList())
|
||||||
|
val torrentTasks: kotlinx.coroutines.flow.StateFlow<List<TorrentTask>> = _torrentTasks
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder = binder
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
unregisterReceiver(batteryReceiver)
|
||||||
|
networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) }
|
||||||
|
serviceScope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun resumeTorrent(infoHash: String) {
|
||||||
|
try {
|
||||||
|
session.find(Sha1Hash(infoHash))?.resume()
|
||||||
|
// 💡 UI 즉시 갱신: 수동 조작 시 답답하지 않게 바로 상태를 업데이트합니다.
|
||||||
|
refreshTorrentStats()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pauseTorrent(infoHash: String) {
|
||||||
|
try {
|
||||||
|
session.find(Sha1Hash(infoHash))?.let {
|
||||||
|
it.pause()
|
||||||
|
// 대기열 맨 뒤로 보내기 (선택 사항)
|
||||||
|
// it.queuePositionBottom()
|
||||||
|
}
|
||||||
|
refreshTorrentStats()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeTorrent(infoHash: String, deleteFile: Boolean) {
|
||||||
|
try {
|
||||||
|
val handle = session.find(Sha1Hash(infoHash))
|
||||||
|
if (handle != null) {
|
||||||
|
// 1. 실제 파일 삭제 (요청 시)
|
||||||
|
if (deleteFile) {
|
||||||
|
val status = handle.status()
|
||||||
|
val torrentName = if (status.hasMetadata()) handle.torrentFile()?.name() ?: status.name() else status.name()
|
||||||
|
val targetFile = File(tempDir, torrentName)
|
||||||
|
if (targetFile.exists()) {
|
||||||
|
targetFile.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 엔진에서 세션 제거
|
||||||
|
session.remove(handle)
|
||||||
|
|
||||||
|
// 3. 리쥼(이어받기) 데이터 파일 삭제
|
||||||
|
val resumeFile = File(resumeDir, "$infoHash.resume")
|
||||||
|
if (resumeFile.exists()) {
|
||||||
|
resumeFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 💡 삭제 후 목록에서 즉시 사라지도록 UI 갱신
|
||||||
|
refreshTorrentStats()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user