From ac1c0b12b35ed7fc5701608d57a6afda6540a642 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Tue, 21 Apr 2026 15:37:32 +0900 Subject: [PATCH] ... --- .../bums/lunatic/launcher/LauncherActivity.kt | 57 +++++++++++- .../bums/lunatic/launcher/LunaticLauncher.kt | 1 + .../launcher/helpers/ForeGroundService.kt | 44 ++++++--- .../launcher/home/CompletedFilesFragment.kt | 7 +- .../bums/lunatic/launcher/home/GeckoWeb.kt | 21 +++-- .../lunatic/launcher/home/NeoRssActivity.kt | 1 + .../lunatic/launcher/player/PlayerActivity.kt | 59 +++++++++--- .../lunatic/launcher/workers/BaseGetter.kt | 3 + .../launcher/workers/TaskAggregator.kt | 3 - .../launcher/workers/TorrentManager.kt | 91 +++++++++++-------- 10 files changed, 201 insertions(+), 86 deletions(-) diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt index b6414728..d94bbf28 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt @@ -13,6 +13,10 @@ import android.content.Intent import android.content.IntentFilter import android.content.res.Configuration import android.graphics.Color +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest import android.net.Uri import android.os.Build import android.os.Bundle @@ -46,6 +50,7 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.core.view.updatePadding +import bums.lunatic.launcher.LunaticLauncher.Companion.isWifiConnected import bums.lunatic.launcher.apps.AppDrawerBottomSheet import bums.lunatic.launcher.common.CommonActivity import bums.lunatic.launcher.databinding.LauncherActivityBinding @@ -64,6 +69,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.BaseGetter import bums.lunatic.launcher.workers.TorrentService import bums.lunatic.launcher.workers.UsageLogType import bums.lunatic.launcher.workers.UsageUpdateType @@ -580,8 +586,54 @@ open class LauncherActivity : CommonActivity() { )) handleSharedIntent(intent) + connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetwork = connectivityManager.activeNetwork + val caps = connectivityManager.getNetworkCapabilities(activeNetwork) + LunaticLauncher.isWifiConnected = caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true + registerNetworkCallback() } + private fun registerNetworkCallback() { + val request = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) { + // 단순히 caps만 보지 말고, 현재 활성화된 기본 네트워크의 상태를 직접 다시 조회합니다. + val activeNet = connectivityManager.activeNetwork + val activeCaps = connectivityManager.getNetworkCapabilities(activeNet) + + val wifiNow = activeCaps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true + + // 상태가 실제로 변했을 때만 업데이트하여 불필요한 로그와 로직 실행을 방지합니다. + if (isWifiConnected != wifiNow) { + isWifiConnected = wifiNow + + } + val intent = Intent(this@LauncherActivity, TorrentService::class.java).apply { + putExtra("WIFI_STATE", isWifiConnected) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } + } + + // 네트워크가 완전히 끊겼을 때도 처리해주는 것이 안전합니다. + override fun onLost(network: Network) { + val intent = Intent(this@LauncherActivity, TorrentService::class.java).apply { + putExtra("WIFI_STATE", false) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } + } + } + connectivityManager.registerNetworkCallback(request, networkCallback!!) + } private var smsReceiver: SmsReceiver? = null // 권한 요청 결과 처리기 @@ -911,12 +963,15 @@ open class LauncherActivity : CommonActivity() { appWidgetHost?.stopListening() // [필수] 여기서 리스닝 중지 (onDestroy 대신 여기 추천) } - + private lateinit var connectivityManager: ConnectivityManager + private var networkCallback: ConnectivityManager.NetworkCallback? = null override fun onDestroy() { smsReceiver?.let { unregisterReceiver(it) smsReceiver = null } + + networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) } super.onDestroy() } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LunaticLauncher.kt b/app/src/main/kotlin/bums/lunatic/launcher/LunaticLauncher.kt index e261ea52..e5dd20e1 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/LunaticLauncher.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/LunaticLauncher.kt @@ -51,6 +51,7 @@ internal class LunaticLauncher : Application() { companion object { var appContext : LunaticLauncher? = null var mHourlyLogWriter : HourlyLogWriter? = null + var isWifiConnected : Boolean = false private var sRuntime: GeckoRuntime? = null fun getRuntime() : GeckoRuntime? { appContext?.initGeckoRuntime() 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 2f7d53ed..0eada38c 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt @@ -77,6 +77,7 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody import java.util.Calendar +import kotlin.math.abs data class ZenQuoteResponse(val q: String, val a: String) data class KorAdviceResponse(val author: String, val message: String) @@ -515,6 +516,7 @@ class ForeGroundService : Service() { } fun addToTargetYtubeUrl(url : String? = null, forMusic: Boolean = false) { + Blog.LOGE("url $url") url?.let { url -> if (url.length > 0) { targetUrls.add(url to forMusic) @@ -537,6 +539,7 @@ class ForeGroundService : Service() { } } + var lastProGress = -1f fun downloadVideo(url: String?, forMusic: Boolean = false) { url?.let { CoroutineScope(Dispatchers.IO).launch { @@ -547,35 +550,42 @@ class ForeGroundService : Service() { val youtubeDLDir = File(baseDir, "Youtube") if (!youtubeDLDir.exists()) youtubeDLDir.mkdirs() val command = YoutubeDLRequest(url).apply { - addOption("-q") // 로그 최소화 + addOption("--newline") // 줄바꿈 단위로 출력 (콜백 트리거 핵심) + addOption("--progress") // 진행 상태 강제 출력 // 로그 최소화 addOption("--no-warnings") // 경고 숨김 val cookieFile = File(this@ForeGroundService.cacheDir, "cookies.txt") addOption("--cookies", "${cookieFile.absolutePath}") // 출력 경로 - addOption("-o", "${youtubeDLDir.absolutePath}/%(title)s.%(ext)s") + addOption("-o", "${youtubeDLDir.absolutePath}/%(title)s [%(id)s].%(ext)s") - if (forMusic) { - // 음악 전용 옵션 - addOption("-x") // 오디오 추출 - addOption("--audio-format", "mp3") - addOption("--extractor-args", "youtube:player_client=web_music,android") - addOption("-f", "bestaudio[ext=m4a]/bestaudio/best") - } else { +// if (forMusic) { +// // 음악 전용 옵션 +// addOption("-x") // 오디오 추출 +// addOption("--audio-format", "mp3") +// addOption("--extractor-args", "youtube:player_client=web_music,android") +// addOption("-f", "bestaudio[ext=m4a]/bestaudio/best") +// } else { // 일반 영상용 addOption("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best") - } - - // 안정성 향상 옵션 (모두에 적용) +// } + addOption("--no-check-certificates") + addOption("--verbose") addOption("--no-mtime") // 파일 수정시간 안 건드림 + addOption("--no-continue") // 이어받기 하지 않음 + addOption("--force-overwrites") // 강제 덮어쓰기 addOption("--restrict-filenames") // 특수문자 제한 } currentProcessId = UUID.randomUUID().toString() - YoutubeDL.getInstance() + var response = YoutubeDL.getInstance() .execute(command, currentProcessId) { progress, est, str -> - startForeGround(100, progress.toInt(), str, false) +// Blog.LOGE("progress $progress est $est str $str" ) + if(progress == 100.0f || progress == 0.0f || abs(lastProGress - progress) > 3) { + startForeGround(100, progress.toInt(), str, false) + lastProGress = progress + } if (progress >= 100) { currentProcessId = null if ((targetUrls?.size ?: 0) > 0) { @@ -583,6 +593,10 @@ class ForeGroundService : Service() { } } } + Blog.LOGE("url $url $currentProcessId") + Blog.LOGE("Exit Code: ${response.exitCode}") + Blog.LOGE("Out: ${response.out}") + Blog.LOGE("Error: ${response.err}") } catch (e: Exception) { Blog.LOGE("Download Error", e) currentProcessId = null @@ -710,7 +724,7 @@ class ForeGroundService : Service() { .build() // 2. 뉴스 피드: 사용자가 설정한 간격 (예: 1시간) - val newsRequest = PeriodicWorkRequestBuilder(PrefLong.shortTimePeriod.get(20L), TimeUnit.MINUTES) + val newsRequest = PeriodicWorkRequestBuilder(PrefLong.longTimePeriod.get(120L), TimeUnit.MINUTES) .build() // 기존의 수많은 enqueue 코드를 이 두 개로 대체 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 fda1b8e4..ecf9884b 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesFragment.kt @@ -891,12 +891,9 @@ class CompletedFilesFragment : Fragment() { val innerFiles = folder.listFiles() ?: return@forEach // 특수 조건(1GB 영상) 확인용 데이터 - val videoFiles = innerFiles.filter { extVideos.contains(it.extension.lowercase()) } + val videoFiles = innerFiles.filter { extVideos.contains(it.extension.lowercase()) }.filter { it.length() >= 1024 * 1024 * 1024 } val potentialSubtitles = innerFiles.filter { subtitleExts.contains(it.extension.lowercase()) } - val hasLargeVideo = videoFiles.any { it.length() >= 1024 * 1024 * 1024 } - val hasTinyText = potentialSubtitles.any { it.length() <= 1024 } - - if (hasLargeVideo) { + if (videoFiles.isNotEmpty()) { // 조건 만족 시 영상+자막 이동 videoFiles.forEach { videoFile -> if (videoFile.renameTo(File(videoTargetDir, videoFile.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 a0b6c056..964921e8 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt @@ -282,17 +282,17 @@ open class GeckoWeb @JvmOverloads constructor( val cookieFile = File(context.cacheDir, "cookies.txt") addOption("--cookies", "${cookieFile.absolutePath}") - if (forMusic) { - addOption("-x") // 오디오 추출 - addOption("--audio-format", "mp3") - // YouTube Music 전용 extractor arg 추가 - addOption("--extractor-args", "youtube:player_client=web_music,android") - // 음악 스트림 우선 선택 - addOption("-f", "bestaudio[ext=m4a]/bestaudio/best") - } else { +// if (forMusic) { +// addOption("-x") // 오디오 추출 +// addOption("--audio-format", "mp3") +// // YouTube Music 전용 extractor arg 추가 +// addOption("--extractor-args", "youtube:player_client=web_music,android") +// // 음악 스트림 우선 선택 +// addOption("-f", "bestaudio[ext=m4a]/bestaudio/best") +// } else { // 일반 영상용 기본 포맷 addOption("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best") - } +// } // 디버깅용 (테스트 후 제거) @@ -476,6 +476,7 @@ open class GeckoWeb @JvmOverloads constructor( override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) { lastSessionState = sessionState onSessionStateChangeCallback?.invoke(sessionState) + saveCurrentSessionState() // Blog.LOGE("onSessionStateChange $sessionState ${session}") } } @@ -661,7 +662,7 @@ open class GeckoWeb @JvmOverloads constructor( } } "COOKIES_REPORT"-> { -// Blog.LOGE("${msg.value} -> ${msg.url}") + Blog.LOGE("${msg.value} -> ${msg.url}") currentCookieString = msg.value ?: "" currentCookieUrlString = msg.url ?: "" CoroutineScope(Dispatchers.IO).launch { diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt index d03a8480..53f6d2ab 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/NeoRssActivity.kt @@ -496,6 +496,7 @@ open class NeoRssActivity : CommonActivity() { super.onCreate(savedInstanceState) try { YoutubeDL.getInstance().init(this) + FFmpeg.getInstance().init(this) CoroutineScope(Dispatchers.IO).launch { try { diff --git a/app/src/main/kotlin/bums/lunatic/launcher/player/PlayerActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/player/PlayerActivity.kt index 2f0f103d..136c60d5 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/player/PlayerActivity.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/player/PlayerActivity.kt @@ -149,8 +149,11 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener { if (allSubtitleTracks.size > 1) { showSubtitleSelectionDialog() } else { - - showSubtitleSearchConfirmDialog() + if (videoPath.contains("Youtube")) { + play() + } else { + showSubtitleSearchConfirmDialog() + } } } } @@ -368,10 +371,28 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener { val center = gestureLayer.getChildAt(1) val right = gestureLayer.getChildAt(2) - center.setOnClickListener { - isPlaying = !isPlaying - if (isPlaying) nativePlayer?.play(Surface(videoTextureView.surfaceTexture!!)) - else nativePlayer?.pause() + val centerDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() { + override fun onSingleTapUp(e: MotionEvent): Boolean { + isPlaying = !isPlaying + if (isPlaying) nativePlayer?.play(Surface(videoTextureView.surfaceTexture!!)) + else nativePlayer?.pause() + return true + } + + // 좌우 스크롤로 자막 싱크 조절 + override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { + if (Math.abs(distanceX) > Math.abs(distanceY)) { + if (distanceX > 0) adjustSubtitleSync(-500) // 왼쪽으로 밀면 자막을 빠르게 + else adjustSubtitleSync(500) // 오른쪽으로 밀면 자막을 느리게 + return true + } + return false + } + }) + + center.setOnTouchListener { _, event -> + centerDetector.onTouchEvent(event) + true } val rightDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() { @@ -411,7 +432,17 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener { true } } + private var subtitleDelayMs: Long = 0L + private fun adjustSubtitleSync(deltaMs: Long) { + subtitleDelayMs += deltaMs + val seconds = subtitleDelayMs / 1000.0 + val sign = if (subtitleDelayMs >= 0) "+" else "" + Toast.makeText(this, "자막 싱크: $sign${String.format("%.1f", seconds)}초", Toast.LENGTH_SHORT).show() + // 싱크가 변경되면 즉시 루프에서 반영되므로 별도의 처리는 필요 없으나, + // 즉각적인 피드백을 위해 lastSubTitle을 초기화하여 강제 갱신 유도 가능 + lastSubTitle = "" + } override fun onConfigurationChanged(newConfig: android.content.res.Configuration) { super.onConfigurationChanged(newConfig) hideSystemUI() @@ -640,23 +671,21 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener { * 💡 인덱스 기반 탐색: 이전 위치부터 찾기 때문에 CPU 부하가 거의 없습니다. */ private fun findSubtitleIndexed(currentSec: Double): SubtitleBlock? { - // 1. 영상이 뒤로 감기 되었을 경우 인덱스 초기화 + // 💡 싱크 오프셋 적용 (초 단위로 변환하여 더함) + val adjustedSec = currentSec - (subtitleDelayMs / 1000.0) + if (currentSubtitleIndex >= externalSubtitles.size || - externalSubtitles[currentSubtitleIndex].startSec > currentSec) { + externalSubtitles[currentSubtitleIndex].startSec > adjustedSec) { currentSubtitleIndex = 0 } - // 2. 마지막으로 찾았던 위치(currentSubtitleIndex)부터 탐색 시작 for (i in currentSubtitleIndex until externalSubtitles.size) { val item = externalSubtitles[i] - - if (currentSec in item.startSec..item.endSec) { - currentSubtitleIndex = i // 현재 위치 저장 + if (adjustedSec in item.startSec..item.endSec) { + currentSubtitleIndex = i return item } - - // 3. 자막이 시간순으로 정렬되어 있다면, 현재 시간보다 시작 시간이 커지는 순간 루프 종료 - if (item.startSec > currentSec) break + if (item.startSec > adjustedSec) break } return null } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/BaseGetter.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/BaseGetter.kt index 58c1d893..16abf4d0 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/BaseGetter.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/BaseGetter.kt @@ -24,6 +24,8 @@ abstract class BaseGetter(internal val context: Context) { } } + + val USAGT = "Mozilla/5.0 (Android 15; Mobile; rv:141.0) Gecko/141.0 Firefox/141.0" val limitDateTime = beforeOneDay() @@ -32,6 +34,7 @@ abstract class BaseGetter(internal val context: Context) { abstract fun realWork() : List open suspend fun fetchData(): List { + return realWork() } } \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/workers/TaskAggregator.kt b/app/src/main/kotlin/bums/lunatic/launcher/workers/TaskAggregator.kt index 6a7f34a0..a4cc6fc7 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/TaskAggregator.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/TaskAggregator.kt @@ -93,11 +93,8 @@ object TaskAggregator { // 병렬로 네트워크 요청 쏘기 (시간 획기적으로 단축됨) val jobs = listOf( -// async { RuliWebGetter(context).fetchData() }, async { TheQooGetter(context).fetchData() }, -// async { YoutubeGetter(context).fetchData() }, async { DCGetter(context).fetchData() }, -// async { FmKoreaGetter(context).fetchData() }, async { NewsFeedsGetter(context).fetchData() }, async { ClienGetter(context).fetchData() }, async { DotaxGetter(context).fetchData() }, 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 d04a94cd..8321f4ab 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/workers/TorrentManager.kt @@ -16,7 +16,7 @@ import com.frostwire.jlibtorrent.swig.settings_pack import com.frostwire.jlibtorrent.swig.torrent_status import kotlinx.coroutines.* import java.io.File - +import android.telephony.TelephonyManager data class TorrentTask( val infoHash: String, val name: String, @@ -45,8 +45,7 @@ class TorrentService : Service() { // 제어 플래그 private var isWifiConnected = false private var isCharging = false - private lateinit var connectivityManager: ConnectivityManager - private var networkCallback: ConnectivityManager.NetworkCallback? = null + inner class TorrentBinder : Binder() { fun getService(): TorrentService = this@TorrentService @@ -56,6 +55,12 @@ class TorrentService : Service() { override fun onCreate() { super.onCreate() notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (!isKoreaRegion()) { + Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.") + stopForeground(true) + stopSelf() + return + } startForegroundService() initLibTorrent() @@ -64,7 +69,7 @@ class TorrentService : Service() { // 2. 리시버 및 콜백 등록 registerBatteryReceiver() - registerNetworkCallback() + // 3. 초기 세션 상태 적용 updateSessionState() @@ -79,11 +84,7 @@ class TorrentService : Service() { val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1 isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL - // Wi-Fi 상태 - connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val activeNetwork = connectivityManager.activeNetwork - val caps = connectivityManager.getNetworkCapabilities(activeNetwork) - isWifiConnected = caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true + } private fun registerBatteryReceiver() { @@ -151,34 +152,7 @@ class TorrentService : Service() { } } - private fun registerNetworkCallback() { - val request = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .build() - networkCallback = object : ConnectivityManager.NetworkCallback() { - override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) { - // 단순히 caps만 보지 말고, 현재 활성화된 기본 네트워크의 상태를 직접 다시 조회합니다. - val activeNet = connectivityManager.activeNetwork - val activeCaps = connectivityManager.getNetworkCapabilities(activeNet) - - val wifiNow = activeCaps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true - - // 상태가 실제로 변했을 때만 업데이트하여 불필요한 로그와 로직 실행을 방지합니다. - if (isWifiConnected != wifiNow) { - isWifiConnected = wifiNow - updateSessionState() - } - } - - // 네트워크가 완전히 끊겼을 때도 처리해주는 것이 안전합니다. - override fun onLost(network: Network) { - checkInitialStatus() // 전체 상태 다시 체크 - updateSessionState() - } - } - connectivityManager.registerNetworkCallback(request, networkCallback!!) - } var lastUpdateTime = -1L @@ -186,6 +160,15 @@ class TorrentService : Service() { * 핵심 제어 로직: 충전 중일 때만 세션을 열고, Wi-Fi 여부에 따라 슬롯 조절 */ private fun updateSessionState() { + if (!isKoreaRegion()) { + Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.") + stopForeground(true) + stopSelf() + return + } + + checkIpAndStop() + if (session.isPaused) session.resume() var curentTime = System.currentTimeMillis() if (curentTime - lastUpdateTime > 5000) { @@ -247,7 +230,38 @@ class TorrentService : Service() { refreshTorrentStats() } } + } + private fun isKoreaRegion(): Boolean { + val tm = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + + // 1. SIM 카드의 국가 코드 확인 (예: "kr") + val simCountry = tm.simCountryIso.lowercase() + + // 2. 네트워크(기지국) 기준 국가 코드 확인 + val networkCountry = tm.networkCountryIso.lowercase() + + // SIM이나 네트워크 중 하나라도 'kr'이면 한국으로 판단 + return simCountry == "kr" || networkCountry == "kr" + } + + private fun checkIpAndStop() { + serviceScope.launch(Dispatchers.IO) { + try { + // 외부 API를 통해 국가 코드 확인 (예: ip-api.com) + val response = java.net.URL("http://ip-api.com/json/").readText() + if (!response.contains("\"countryCode\":\"KR\"")) { + withContext(Dispatchers.Main) { + Blog.LOGE("IP 위치가 대한민국이 아닙니다. 다운로드를 중지합니다.") + // 모든 토렌트 일시정지 또는 서비스 종료 + session.pause() + stopSelf() + } + } + } catch (e: Exception) { + // 네트워크 오류 시 보수적으로 처리 (선택 사항) + } + } } private val TRACKER_URL = "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt" @@ -518,6 +532,10 @@ class TorrentService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { intent?.getStringExtra("EXTRA_MAGNET_URI")?.let { addMagnet(it) } + intent?.getBooleanExtra("WIFI_STATE", false)?.let { + isWifiConnected = it + + } return START_STICKY } @@ -562,7 +580,6 @@ class TorrentService : Service() { override fun onDestroy() { super.onDestroy() unregisterReceiver(batteryReceiver) - networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) } serviceScope.cancel() }