This commit is contained in:
lunaticbum 2026-04-21 15:37:32 +09:00
parent fd0ee96c04
commit ac1c0b12b3
10 changed files with 201 additions and 86 deletions

View File

@ -13,6 +13,10 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color 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.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -46,6 +50,7 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import bums.lunatic.launcher.LunaticLauncher.Companion.isWifiConnected
import bums.lunatic.launcher.apps.AppDrawerBottomSheet import bums.lunatic.launcher.apps.AppDrawerBottomSheet
import bums.lunatic.launcher.common.CommonActivity import bums.lunatic.launcher.common.CommonActivity
import bums.lunatic.launcher.databinding.LauncherActivityBinding 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.receiver.SmsReceiver
import bums.lunatic.launcher.settings.SettingsActivity import bums.lunatic.launcher.settings.SettingsActivity
import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.utils.Blog
import bums.lunatic.launcher.workers.BaseGetter
import bums.lunatic.launcher.workers.TorrentService import bums.lunatic.launcher.workers.TorrentService
import bums.lunatic.launcher.workers.UsageLogType import bums.lunatic.launcher.workers.UsageLogType
import bums.lunatic.launcher.workers.UsageUpdateType import bums.lunatic.launcher.workers.UsageUpdateType
@ -580,8 +586,54 @@ open class LauncherActivity : CommonActivity() {
)) ))
handleSharedIntent(intent) 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 private var smsReceiver: SmsReceiver? = null
// 권한 요청 결과 처리기 // 권한 요청 결과 처리기
@ -911,12 +963,15 @@ open class LauncherActivity : CommonActivity() {
appWidgetHost?.stopListening() // [필수] 여기서 리스닝 중지 (onDestroy 대신 여기 추천) appWidgetHost?.stopListening() // [필수] 여기서 리스닝 중지 (onDestroy 대신 여기 추천)
} }
private lateinit var connectivityManager: ConnectivityManager
private var networkCallback: ConnectivityManager.NetworkCallback? = null
override fun onDestroy() { override fun onDestroy() {
smsReceiver?.let { smsReceiver?.let {
unregisterReceiver(it) unregisterReceiver(it)
smsReceiver = null smsReceiver = null
} }
networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) }
super.onDestroy() super.onDestroy()
} }

View File

@ -51,6 +51,7 @@ internal class LunaticLauncher : Application() {
companion object { companion object {
var appContext : LunaticLauncher? = null var appContext : LunaticLauncher? = null
var mHourlyLogWriter : HourlyLogWriter? = null var mHourlyLogWriter : HourlyLogWriter? = null
var isWifiConnected : Boolean = false
private var sRuntime: GeckoRuntime? = null private var sRuntime: GeckoRuntime? = null
fun getRuntime() : GeckoRuntime? { fun getRuntime() : GeckoRuntime? {
appContext?.initGeckoRuntime() appContext?.initGeckoRuntime()

View File

@ -77,6 +77,7 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.util.Calendar import java.util.Calendar
import kotlin.math.abs
data class ZenQuoteResponse(val q: String, val a: String) data class ZenQuoteResponse(val q: String, val a: String)
data class KorAdviceResponse(val author: String, val message: 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) { fun addToTargetYtubeUrl(url : String? = null, forMusic: Boolean = false) {
Blog.LOGE("url $url")
url?.let { url -> url?.let { url ->
if (url.length > 0) { if (url.length > 0) {
targetUrls.add(url to forMusic) targetUrls.add(url to forMusic)
@ -537,6 +539,7 @@ class ForeGroundService : Service() {
} }
} }
var lastProGress = -1f
fun downloadVideo(url: String?, forMusic: Boolean = false) { fun downloadVideo(url: String?, forMusic: Boolean = false) {
url?.let { url?.let {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
@ -547,35 +550,42 @@ class ForeGroundService : Service() {
val youtubeDLDir = File(baseDir, "Youtube") val youtubeDLDir = File(baseDir, "Youtube")
if (!youtubeDLDir.exists()) youtubeDLDir.mkdirs() if (!youtubeDLDir.exists()) youtubeDLDir.mkdirs()
val command = YoutubeDLRequest(url).apply { val command = YoutubeDLRequest(url).apply {
addOption("-q") // 로그 최소화 addOption("--newline") // 줄바꿈 단위로 출력 (콜백 트리거 핵심)
addOption("--progress") // 진행 상태 강제 출력 // 로그 최소화
addOption("--no-warnings") // 경고 숨김 addOption("--no-warnings") // 경고 숨김
val cookieFile = File(this@ForeGroundService.cacheDir, "cookies.txt") val cookieFile = File(this@ForeGroundService.cacheDir, "cookies.txt")
addOption("--cookies", "${cookieFile.absolutePath}") addOption("--cookies", "${cookieFile.absolutePath}")
// 출력 경로 // 출력 경로
addOption("-o", "${youtubeDLDir.absolutePath}/%(title)s.%(ext)s") addOption("-o", "${youtubeDLDir.absolutePath}/%(title)s [%(id)s].%(ext)s")
if (forMusic) { // if (forMusic) {
// 음악 전용 옵션 // // 음악 전용 옵션
addOption("-x") // 오디오 추출 // addOption("-x") // 오디오 추출
addOption("--audio-format", "mp3") // addOption("--audio-format", "mp3")
addOption("--extractor-args", "youtube:player_client=web_music,android") // addOption("--extractor-args", "youtube:player_client=web_music,android")
addOption("-f", "bestaudio[ext=m4a]/bestaudio/best") // addOption("-f", "bestaudio[ext=m4a]/bestaudio/best")
} else { // } else {
// 일반 영상용 // 일반 영상용
addOption("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best") addOption("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best")
} // }
addOption("--no-check-certificates")
// 안정성 향상 옵션 (모두에 적용) addOption("--verbose")
addOption("--no-mtime") // 파일 수정시간 안 건드림 addOption("--no-mtime") // 파일 수정시간 안 건드림
addOption("--no-continue") // 이어받기 하지 않음
addOption("--force-overwrites") // 강제 덮어쓰기
addOption("--restrict-filenames") // 특수문자 제한 addOption("--restrict-filenames") // 특수문자 제한
} }
currentProcessId = UUID.randomUUID().toString() currentProcessId = UUID.randomUUID().toString()
YoutubeDL.getInstance() var response = YoutubeDL.getInstance()
.execute(command, currentProcessId) { progress, est, str -> .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) { if (progress >= 100) {
currentProcessId = null currentProcessId = null
if ((targetUrls?.size ?: 0) > 0) { 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) { } catch (e: Exception) {
Blog.LOGE("Download Error", e) Blog.LOGE("Download Error", e)
currentProcessId = null currentProcessId = null
@ -710,7 +724,7 @@ class ForeGroundService : Service() {
.build() .build()
// 2. 뉴스 피드: 사용자가 설정한 간격 (예: 1시간) // 2. 뉴스 피드: 사용자가 설정한 간격 (예: 1시간)
val newsRequest = PeriodicWorkRequestBuilder<AggregatedNewsWorker>(PrefLong.shortTimePeriod.get(20L), TimeUnit.MINUTES) val newsRequest = PeriodicWorkRequestBuilder<AggregatedNewsWorker>(PrefLong.longTimePeriod.get(120L), TimeUnit.MINUTES)
.build() .build()
// 기존의 수많은 enqueue 코드를 이 두 개로 대체 // 기존의 수많은 enqueue 코드를 이 두 개로 대체

View File

@ -891,12 +891,9 @@ class CompletedFilesFragment : Fragment() {
val innerFiles = folder.listFiles() ?: return@forEach val innerFiles = folder.listFiles() ?: return@forEach
// 특수 조건(1GB 영상) 확인용 데이터 // 특수 조건(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 potentialSubtitles = innerFiles.filter { subtitleExts.contains(it.extension.lowercase()) }
val hasLargeVideo = videoFiles.any { it.length() >= 1024 * 1024 * 1024 } if (videoFiles.isNotEmpty()) {
val hasTinyText = potentialSubtitles.any { it.length() <= 1024 }
if (hasLargeVideo) {
// 조건 만족 시 영상+자막 이동 // 조건 만족 시 영상+자막 이동
videoFiles.forEach { videoFile -> videoFiles.forEach { videoFile ->
if (videoFile.renameTo(File(videoTargetDir, videoFile.name))) { if (videoFile.renameTo(File(videoTargetDir, videoFile.name))) {

View File

@ -282,17 +282,17 @@ open class GeckoWeb @JvmOverloads constructor(
val cookieFile = File(context.cacheDir, "cookies.txt") val cookieFile = File(context.cacheDir, "cookies.txt")
addOption("--cookies", "${cookieFile.absolutePath}") addOption("--cookies", "${cookieFile.absolutePath}")
if (forMusic) { // if (forMusic) {
addOption("-x") // 오디오 추출 // addOption("-x") // 오디오 추출
addOption("--audio-format", "mp3") // addOption("--audio-format", "mp3")
// YouTube Music 전용 extractor arg 추가 // // YouTube Music 전용 extractor arg 추가
addOption("--extractor-args", "youtube:player_client=web_music,android") // addOption("--extractor-args", "youtube:player_client=web_music,android")
// 음악 스트림 우선 선택 // // 음악 스트림 우선 선택
addOption("-f", "bestaudio[ext=m4a]/bestaudio/best") // addOption("-f", "bestaudio[ext=m4a]/bestaudio/best")
} else { // } else {
// 일반 영상용 기본 포맷 // 일반 영상용 기본 포맷
addOption("-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best") 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) { override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) {
lastSessionState = sessionState lastSessionState = sessionState
onSessionStateChangeCallback?.invoke(sessionState) onSessionStateChangeCallback?.invoke(sessionState)
saveCurrentSessionState()
// Blog.LOGE("onSessionStateChange $sessionState ${session}") // Blog.LOGE("onSessionStateChange $sessionState ${session}")
} }
} }
@ -661,7 +662,7 @@ open class GeckoWeb @JvmOverloads constructor(
} }
} }
"COOKIES_REPORT"-> { "COOKIES_REPORT"-> {
// Blog.LOGE("${msg.value} -> ${msg.url}") Blog.LOGE("${msg.value} -> ${msg.url}")
currentCookieString = msg.value ?: "" currentCookieString = msg.value ?: ""
currentCookieUrlString = msg.url ?: "" currentCookieUrlString = msg.url ?: ""
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {

View File

@ -496,6 +496,7 @@ open class NeoRssActivity : CommonActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
try { try {
YoutubeDL.getInstance().init(this) YoutubeDL.getInstance().init(this)
FFmpeg.getInstance().init(this) FFmpeg.getInstance().init(this)
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {

View File

@ -149,8 +149,11 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
if (allSubtitleTracks.size > 1) { if (allSubtitleTracks.size > 1) {
showSubtitleSelectionDialog() showSubtitleSelectionDialog()
} else { } else {
if (videoPath.contains("Youtube")) {
showSubtitleSearchConfirmDialog() play()
} else {
showSubtitleSearchConfirmDialog()
}
} }
} }
} }
@ -368,10 +371,28 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
val center = gestureLayer.getChildAt(1) val center = gestureLayer.getChildAt(1)
val right = gestureLayer.getChildAt(2) val right = gestureLayer.getChildAt(2)
center.setOnClickListener { val centerDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
isPlaying = !isPlaying override fun onSingleTapUp(e: MotionEvent): Boolean {
if (isPlaying) nativePlayer?.play(Surface(videoTextureView.surfaceTexture!!)) isPlaying = !isPlaying
else nativePlayer?.pause() 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() { val rightDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
@ -411,7 +432,17 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
true 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) { override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
hideSystemUI() hideSystemUI()
@ -640,23 +671,21 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
* 💡 인덱스 기반 탐색: 이전 위치부터 찾기 때문에 CPU 부하가 거의 없습니다. * 💡 인덱스 기반 탐색: 이전 위치부터 찾기 때문에 CPU 부하가 거의 없습니다.
*/ */
private fun findSubtitleIndexed(currentSec: Double): SubtitleBlock? { private fun findSubtitleIndexed(currentSec: Double): SubtitleBlock? {
// 1. 영상이 뒤로 감기 되었을 경우 인덱스 초기화 // 💡 싱크 오프셋 적용 (초 단위로 변환하여 더함)
val adjustedSec = currentSec - (subtitleDelayMs / 1000.0)
if (currentSubtitleIndex >= externalSubtitles.size || if (currentSubtitleIndex >= externalSubtitles.size ||
externalSubtitles[currentSubtitleIndex].startSec > currentSec) { externalSubtitles[currentSubtitleIndex].startSec > adjustedSec) {
currentSubtitleIndex = 0 currentSubtitleIndex = 0
} }
// 2. 마지막으로 찾았던 위치(currentSubtitleIndex)부터 탐색 시작
for (i in currentSubtitleIndex until externalSubtitles.size) { for (i in currentSubtitleIndex until externalSubtitles.size) {
val item = externalSubtitles[i] val item = externalSubtitles[i]
if (adjustedSec in item.startSec..item.endSec) {
if (currentSec in item.startSec..item.endSec) { currentSubtitleIndex = i
currentSubtitleIndex = i // 현재 위치 저장
return item return item
} }
if (item.startSec > adjustedSec) break
// 3. 자막이 시간순으로 정렬되어 있다면, 현재 시간보다 시작 시간이 커지는 순간 루프 종료
if (item.startSec > currentSec) break
} }
return null return null
} }

View File

@ -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 USAGT = "Mozilla/5.0 (Android 15; Mobile; rv:141.0) Gecko/141.0 Firefox/141.0"
val limitDateTime = beforeOneDay() val limitDateTime = beforeOneDay()
@ -32,6 +34,7 @@ abstract class BaseGetter(internal val context: Context) {
abstract fun realWork() : List<RealmObject> abstract fun realWork() : List<RealmObject>
open suspend fun fetchData(): List<RealmObject> { open suspend fun fetchData(): List<RealmObject> {
return realWork() return realWork()
} }
} }

View File

@ -93,11 +93,8 @@ object TaskAggregator {
// 병렬로 네트워크 요청 쏘기 (시간 획기적으로 단축됨) // 병렬로 네트워크 요청 쏘기 (시간 획기적으로 단축됨)
val jobs = listOf( val jobs = listOf(
// async { RuliWebGetter(context).fetchData() },
async { TheQooGetter(context).fetchData() }, async { TheQooGetter(context).fetchData() },
// async { YoutubeGetter(context).fetchData() },
async { DCGetter(context).fetchData() }, async { DCGetter(context).fetchData() },
// async { FmKoreaGetter(context).fetchData() },
async { NewsFeedsGetter(context).fetchData() }, async { NewsFeedsGetter(context).fetchData() },
async { ClienGetter(context).fetchData() }, async { ClienGetter(context).fetchData() },
async { DotaxGetter(context).fetchData() }, async { DotaxGetter(context).fetchData() },

View File

@ -16,7 +16,7 @@ import com.frostwire.jlibtorrent.swig.settings_pack
import com.frostwire.jlibtorrent.swig.torrent_status import com.frostwire.jlibtorrent.swig.torrent_status
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import android.telephony.TelephonyManager
data class TorrentTask( data class TorrentTask(
val infoHash: String, val infoHash: String,
val name: String, val name: String,
@ -45,8 +45,7 @@ class TorrentService : Service() {
// 제어 플래그 // 제어 플래그
private var isWifiConnected = false private var isWifiConnected = false
private var isCharging = 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
@ -56,6 +55,12 @@ class TorrentService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (!isKoreaRegion()) {
Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.")
stopForeground(true)
stopSelf()
return
}
startForegroundService() startForegroundService()
initLibTorrent() initLibTorrent()
@ -64,7 +69,7 @@ class TorrentService : Service() {
// 2. 리시버 및 콜백 등록 // 2. 리시버 및 콜백 등록
registerBatteryReceiver() registerBatteryReceiver()
registerNetworkCallback()
// 3. 초기 세션 상태 적용 // 3. 초기 세션 상태 적용
updateSessionState() updateSessionState()
@ -79,11 +84,7 @@ class TorrentService : Service() {
val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1 val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL 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() { 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 var lastUpdateTime = -1L
@ -186,6 +160,15 @@ class TorrentService : Service() {
* 핵심 제어 로직: 충전 중일 때만 세션을 열고, Wi-Fi 여부에 따라 슬롯 조절 * 핵심 제어 로직: 충전 중일 때만 세션을 열고, Wi-Fi 여부에 따라 슬롯 조절
*/ */
private fun updateSessionState() { private fun updateSessionState() {
if (!isKoreaRegion()) {
Blog.LOGE("해외 지역 접속 감지: 서비스를 종료합니다.")
stopForeground(true)
stopSelf()
return
}
checkIpAndStop()
if (session.isPaused) session.resume() if (session.isPaused) session.resume()
var curentTime = System.currentTimeMillis() var curentTime = System.currentTimeMillis()
if (curentTime - lastUpdateTime > 5000) { if (curentTime - lastUpdateTime > 5000) {
@ -247,7 +230,38 @@ class TorrentService : Service() {
refreshTorrentStats() 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" 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 { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.getStringExtra("EXTRA_MAGNET_URI")?.let { addMagnet(it) } intent?.getStringExtra("EXTRA_MAGNET_URI")?.let { addMagnet(it) }
intent?.getBooleanExtra("WIFI_STATE", false)?.let {
isWifiConnected = it
}
return START_STICKY return START_STICKY
} }
@ -562,7 +580,6 @@ class TorrentService : Service() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
unregisterReceiver(batteryReceiver) unregisterReceiver(batteryReceiver)
networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) }
serviceScope.cancel() serviceScope.cancel()
} }