Skip to content

Commit

Permalink
检测计费网络状态来限制 BT 上传 (#1082)
Browse files Browse the repository at this point in the history
* observe state of metered network.

* use registerNetworkCallback

* observe state of metered network.

* explicitly fallback on macos

* show option only when supported

* fix

* don't hold context

* 在调试设置中增加计费网络信息

* Add AndroidMeteredNetworkDetector logs

* Add NoopMeteredNetworkDetector

* fix ios package name

---------

Co-authored-by: Him188 <[email protected]>
  • Loading branch information
StageGuard and Him188 committed Oct 22, 2024
1 parent f975b9d commit 3c1875a
Show file tree
Hide file tree
Showing 13 changed files with 321 additions and 2 deletions.
1 change: 1 addition & 0 deletions app/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"-->
<!-- tools:ignore="ScopedStorage" />-->

Expand Down
3 changes: 3 additions & 0 deletions app/android/src/main/kotlin/activity/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import me.him188.ani.app.domain.torrent.service.TorrentServiceConnection
import me.him188.ani.app.navigation.AniNavigator
import me.him188.ani.app.navigation.NavRoutes
import me.him188.ani.app.platform.AppStartupTasks
import me.him188.ani.app.platform.MeteredNetworkDetector
import me.him188.ani.app.platform.PlatformWindow
import me.him188.ani.app.platform.notification.AndroidNotifManager
import me.him188.ani.app.platform.notification.AndroidNotifManager.Companion.EXTRA_REQUEST_CODE
Expand All @@ -56,6 +57,8 @@ import org.koin.mp.KoinPlatformTools

class MainActivity : AniComponentActivity() {
private val sessionManager: SessionManager by inject()
private val meteredNetworkDetector: MeteredNetworkDetector by inject()

private val logger = logger(MainActivity::class)

private val aniNavigator = AniNavigator()
Expand Down
1 change: 1 addition & 0 deletions app/desktop/src/main/kotlin/AniDesktop.kt
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ object AniDesktop {
DesktopTorrentManager.create(
coroutineScope.coroutineContext,
get(),
get(),
baseSaveDir = {
val saveDir = runBlocking {
get<SettingsRepository>().mediaCacheSettings.flow.first().saveDir
Expand Down
1 change: 1 addition & 0 deletions app/shared/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
/*
* Copyright (C) 2024 OpenAni and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
*
* https://github.com/open-ani/ani/blob/main/LICENSE
*/

package me.him188.ani.app.data.models.preference

import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import me.him188.ani.datasources.api.topic.FileSize
import me.him188.ani.datasources.api.topic.FileSize.Companion.megaBytes
import me.him188.ani.utils.platform.Platform

@Serializable
data class AnitorrentConfig(
Expand All @@ -19,6 +29,12 @@ data class AnitorrentConfig(
* 种子分享率限制.
*/
val shareRatioLimit: Double = 1.1,
/**
* 在计费网络限制上传速度为 1 KB/s
* * Android 移动流量
* * Windows 计费 Wi-Fi
*/
val limitUploadOnMeteredNetwork: Boolean = true,
@Transient private val _placeholder: Int = 0,
) {
companion object {
Expand All @@ -27,3 +43,7 @@ data class AnitorrentConfig(
val Default = AnitorrentConfig()
}
}

fun Platform.supportsLimitUploadOnMeteredNetwork(): Boolean {
return this is Platform.Android || this is Platform.Windows
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2024 OpenAni and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
*
* https://github.com/open-ani/ani/blob/main/LICENSE
*/

package me.him188.ani.app.platform

import android.annotation.SuppressLint
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import me.him188.ani.utils.logging.logger


@SuppressLint("MissingPermission")
private class AndroidMeteredNetworkDetector(
context: Context
) : MeteredNetworkDetector {
private val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
private val logger by lazy { logger<AndroidMeteredNetworkDetector>() }

private val flow = MutableStateFlow(getCurrentIsMetered())
override val isMeteredNetworkFlow: Flow<Boolean> get() = flow

// Create a NetworkCallback to detect network changes
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { // 连接 WiFi
flow.tryEmit(getCurrentIsMetered())
}

override fun onLost(network: Network) { // 断开 WiFi
flow.tryEmit(getCurrentIsMetered())
}

// WiFi 设置变更 (设置为计费网络)
// 连接/断开 WiFi 不会触发 onCapabilitiesChanged
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
val isMetered = !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
log { "onCapabilitiesChanged: isMetered=$isMetered" }
flow.tryEmit(isMetered)
}
}

init {
// Register the NetworkCallback instead of using BroadcastReceiver
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
.build()
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)

// Emit the first value
flow.tryEmit(getCurrentIsMetered())
}

private fun getCurrentIsMetered(): Boolean {
val activeNetwork = connectivityManager.activeNetwork ?: return false
val activeNetworkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false

// Return whether the network is metered or not
val isMetered = !activeNetworkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
log { "getCurrentIsMetered: isMetered=$isMetered" }
return isMetered
}

override fun dispose() {
// Unregister the network callback when no longer needed
connectivityManager.unregisterNetworkCallback(networkCallback)
}

private inline fun log(message: () -> String) {
if (BuildConfig.DEBUG) {
logger.debug(message())
}
}
}

actual fun createMeteredNetworkDetector(context: Context): MeteredNetworkDetector {
return AndroidMeteredNetworkDetector(context)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2024 OpenAni and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
*
* https://github.com/open-ani/ani/blob/main/LICENSE
*/

package me.him188.ani.app.platform

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

/**
* Platform-dependent network type detector.
*
* Use [createMeteredNetworkDetector] to create instance.
*/
interface MeteredNetworkDetector {
val isMeteredNetworkFlow: Flow<Boolean>

/**
* Dispose listeners or callbacks which may be created at initializing detector.
*/
fun dispose()
}

object NoopMeteredNetworkDetector : MeteredNetworkDetector {
override val isMeteredNetworkFlow: Flow<Boolean> = flowOf(false)

override fun dispose() {
}
}


expect fun createMeteredNetworkDetector(context: ContextMP): MeteredNetworkDetector
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright (C) 2024 OpenAni and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
*
* https://github.com/open-ani/ani/blob/main/LICENSE
*/

package me.him188.ani.app.platform

import com.sun.jna.platform.win32.COM.COMException
import com.sun.jna.platform.win32.COM.COMUtils
import com.sun.jna.platform.win32.COM.Unknown
import com.sun.jna.platform.win32.Guid
import com.sun.jna.platform.win32.Ole32
import com.sun.jna.platform.win32.WTypes
import com.sun.jna.platform.win32.WinNT.HRESULT
import com.sun.jna.ptr.IntByReference
import com.sun.jna.ptr.PointerByReference
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import me.him188.ani.utils.logging.logger
import me.him188.ani.utils.logging.warn
import me.him188.ani.utils.platform.Platform
import me.him188.ani.utils.platform.currentPlatformDesktop


private class WindowsMeteredNetworkDetector : MeteredNetworkDetector {
private val logger = logger<MeteredNetworkDetector>()

private val flow = flow {
while (true) {
emit(getIsMetered())
kotlinx.coroutines.delay(60000)
}
}
override val isMeteredNetworkFlow: Flow<Boolean> = flow

private fun getIsMetered(): Boolean {
var networkCostManager: INetworkCostManager? = null

try {
val coInitializeHResult = Ole32.INSTANCE.CoInitializeEx(null, Ole32.COINIT_MULTITHREADED)
COMUtils.checkRC(coInitializeHResult)

val pNetworkCostManager = PointerByReference()
val coCreateInstanceHResult = Ole32.INSTANCE.CoCreateInstance(
CLSID_NetworkListManager,
null,
WTypes.CLSCTX_ALL,
IID_INetworkCostManager,
pNetworkCostManager,
)
COMUtils.checkRC(coCreateInstanceHResult)

networkCostManager = INetworkCostManager(pNetworkCostManager)
val pCost = IntByReference()

val getCostResult = networkCostManager.GetCost(pCost)
COMUtils.checkRC(getCostResult)

return (pCost.value and NLM_CONNECTION_COST_FIXED) != 0
} catch (ex: COMException) {
logger.warn(ex) { "Failed to get network status." }
return false
} finally {
networkCostManager?.Release()
Ole32.INSTANCE.CoUninitialize()
}
}

override fun dispose() {

}

private class INetworkCostManager(pointer: PointerByReference) : Unknown(pointer.value) {
@Suppress("FunctionName")
fun GetCost(cost: IntByReference): HRESULT {
return _invokeNativeObject(3, arrayOf(pointer, cost, null), HRESULT::class.java) as HRESULT
}
}

@Suppress("unused")
companion object {
private val CLSID_NetworkListManager = Guid.CLSID("{DCB00C01-570F-4A9B-8D69-199FDBA5723B}")
private val IID_INetworkCostManager = Guid.IID("{DCB00008-570F-4A9B-8D69-199FDBA5723B}")

private const val NLM_CONNECTION_COST_UNKNOWN = 0
private const val NLM_CONNECTION_COST_UNRESTRICTED = 0x1
private const val NLM_CONNECTION_COST_FIXED = 0x2
private const val NLM_CONNECTION_COST_VARIABLE = 0x4
private const val NLM_CONNECTION_COST_OVERDATALIMIT = 0x10000
private const val NLM_CONNECTION_COST_CONGESTED = 0x20000
private const val NLM_CONNECTION_COST_ROAMING = 0x40000
private const val NLM_CONNECTION_COST_APPROACHINGDATALIMIT = 0x80000
}
}

actual fun createMeteredNetworkDetector(context: Context): MeteredNetworkDetector {
return when (currentPlatformDesktop()) {
is Platform.Windows -> WindowsMeteredNetworkDetector()
is Platform.MacOS -> NoopMeteredNetworkDetector // macos API 要用 swift 才能实现
is Platform.Linux -> NoopMeteredNetworkDetector
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright (C) 2024 OpenAni and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
*
* https://github.com/open-ani/ani/blob/main/LICENSE
*/

package me.him188.ani.app.platform

actual fun createMeteredNetworkDetector(context: Context): MeteredNetworkDetector {
return NoopMeteredNetworkDetector
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
Expand Down Expand Up @@ -94,6 +93,7 @@ import me.him188.ani.app.videoplayer.ui.state.CacheProgressStateFactoryManager
import me.him188.ani.datasources.bangumi.BangumiClient
import me.him188.ani.datasources.bangumi.DelegateBangumiClient
import me.him188.ani.datasources.bangumi.createBangumiClient
import me.him188.ani.utils.coroutines.IO_
import me.him188.ani.utils.coroutines.childScope
import me.him188.ani.utils.coroutines.childScopeContext
import me.him188.ani.utils.coroutines.onReplacement
Expand Down Expand Up @@ -172,7 +172,7 @@ fun KoinApplication.getCommonKoinModule(getContext: () -> Context, coroutineScop
getContext().createDatabaseBuilder()
.fallbackToDestructiveMigrationOnDowngrade(true)
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.setQueryCoroutineContext(Dispatchers.IO_)
.build()
}

Expand Down Expand Up @@ -262,6 +262,8 @@ fun KoinApplication.getCommonKoinModule(getContext: () -> Context, coroutineScop
CacheProgressStateFactoryManager.register(TorrentVideoData::class) { videoData, state ->
TorrentMediaCacheProgressState(videoData.pieces) { state.value }
}

single<MeteredNetworkDetector> { createMeteredNetworkDetector(getContext()) }
}


Expand Down
1 change: 1 addition & 0 deletions app/shared/application/src/iosMain/kotlin/ios/AniIos.kt
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ fun getIosModules(
DefaultTorrentManager.create(
coroutineScope.coroutineContext,
get(),
meteredNetworkDetector = get(),
baseSaveDir = { defaultTorrentCacheDir },
)
}
Expand Down
Loading

0 comments on commit 3c1875a

Please sign in to comment.