-
Notifications
You must be signed in to change notification settings - Fork 93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
分离 torrent 连接管理逻辑, helps #1606 #1616
Open
StageGuard
wants to merge
10
commits into
main
Choose a base branch
from
sg/rewrite-tsc
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
8376686
extract torrent connection logic to `commonMain`
StageGuard 7a17094
extract interface
StageGuard b0389c0
restart service when app is brought to foreground
StageGuard f80bc74
close scope
StageGuard fd1afab
test state 1
StageGuard 3161c93
Merge branch 'main' into sg/rewrite-tsc
StageGuard a977446
fix Android 34 fgs with type dataSync
StageGuard d9445d1
Add test for LifecycleAwareTorrentServiceConnection
StageGuard 6b2f1e0
reformat
StageGuard 7b1c302
Add separate dispatchers to workaround runBlocking issues
Him188 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
...app-data/src/androidMain/kotlin/domain/torrent/service/AndroidTorrentServiceConnection.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
/* | ||
* Copyright (C) 2024-2025 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.domain.torrent.service | ||
|
||
import android.content.BroadcastReceiver | ||
import android.content.ComponentName | ||
import android.content.Context | ||
import android.content.Intent | ||
import android.content.IntentFilter | ||
import android.content.ServiceConnection | ||
import android.os.IBinder | ||
import androidx.core.content.ContextCompat | ||
import androidx.lifecycle.LifecycleOwner | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.suspendCancellableCoroutine | ||
import me.him188.ani.app.domain.torrent.IRemoteAniTorrentEngine | ||
import me.him188.ani.app.domain.torrent.LifecycleAwareTorrentServiceConnection | ||
import me.him188.ani.app.domain.torrent.TorrentServiceConnection | ||
import me.him188.ani.utils.logging.debug | ||
import me.him188.ani.utils.logging.error | ||
import me.him188.ani.utils.logging.warn | ||
import kotlin.coroutines.CoroutineContext | ||
import kotlin.coroutines.resume | ||
import kotlin.time.Duration.Companion.minutes | ||
|
||
/** | ||
* 管理与 [AniTorrentService] 的连接并获取 [IRemoteAniTorrentEngine] 远程访问接口. | ||
* 通过 [getBinder] 获取服务接口, 再启动完成并绑定之前将挂起协程. | ||
* | ||
* 服务连接控制依赖的 lifecycle 应当尽可能大, 所以应该使用 | ||
* [ProcessLifecycleOwner][androidx.lifecycle.ProcessLifecycleOwner] | ||
* 或其他可以涵盖 app 全局生命周期的自定义 [LifecycleOwner] 来管理服务连接. | ||
* 不能使用 [Activity][android.app.Activity] (例如 [ComponentActivity][androidx.core.app.ComponentActivity]) | ||
* 的生命周期, 因为在屏幕旋转 (例如竖屏转全屏播放) 的时候 Activity 可能会摧毁并重新创建, | ||
* 这会导致 [AndroidTorrentServiceConnection] 错误地重新绑定服务或重启服务. | ||
* | ||
* @see androidx.lifecycle.ProcessLifecycleOwner | ||
* @see ServiceConnection | ||
* @see AniTorrentService.onStartCommand | ||
* @see me.him188.ani.android.AniApplication | ||
*/ | ||
class AndroidTorrentServiceConnection( | ||
private val context: Context, | ||
private val onRequiredRestartService: () -> ComponentName?, | ||
parentCoroutineContext: CoroutineContext = Dispatchers.Default, | ||
) : ServiceConnection, | ||
LifecycleAwareTorrentServiceConnection<IRemoteAniTorrentEngine>(parentCoroutineContext) { | ||
private val startupIntentFilter by lazy { IntentFilter(AniTorrentService.INTENT_STARTUP) } | ||
private val acquireWakeLockIntent by lazy { | ||
Intent(context, AniTorrentService.actualServiceClass).apply { | ||
putExtra("acquireWakeLock", 1.minutes.inWholeMilliseconds) | ||
} | ||
} | ||
|
||
/** | ||
* Android 15 限制了 `dataSync` 和 `mediaProcessing` 的 FGS 后台运行时间限制 | ||
*/ | ||
private var registered = false | ||
private val timeExceedLimitIntentFilter by lazy { IntentFilter(AniTorrentService.INTENT_BACKGROUND_TIMEOUT) } | ||
private val timeExceedLimitReceiver = object : BroadcastReceiver() { | ||
override fun onReceive(context: Context?, intent: Intent?) { | ||
logger.warn { "Service background time exceeded." } | ||
onServiceDisconnected() | ||
} | ||
} | ||
|
||
override suspend fun startService(): TorrentServiceConnection.StartResult { | ||
return suspendCancellableCoroutine { cont -> | ||
val receiver = object : BroadcastReceiver() { | ||
override fun onReceive(c: Context?, intent: Intent?) { | ||
logger.debug { "Received service startup broadcast: $intent, starting bind service." } | ||
context.unregisterReceiver(this) | ||
|
||
if (intent?.getBooleanExtra("success", false) != true) { | ||
cont.resume(TorrentServiceConnection.StartResult.FAILED) | ||
return | ||
} | ||
|
||
val bindResult = context.bindService( | ||
Intent(context, AniTorrentService.actualServiceClass), | ||
this@AndroidTorrentServiceConnection, | ||
Context.BIND_ABOVE_CLIENT, | ||
) | ||
if (!bindResult) { | ||
logger.error { "Failed to bind service, context.bindService returns false." } | ||
cont.resume(TorrentServiceConnection.StartResult.FAILED) | ||
} else { | ||
cont.resume(TorrentServiceConnection.StartResult.STARTED) | ||
} | ||
} | ||
} | ||
|
||
ContextCompat.registerReceiver( | ||
context, | ||
receiver, | ||
startupIntentFilter, | ||
ContextCompat.RECEIVER_NOT_EXPORTED, | ||
) | ||
|
||
val result = onRequiredRestartService() | ||
if (result == null) { | ||
cont.resume(TorrentServiceConnection.StartResult.FAILED) | ||
logger.error { "Failed to start service, context.startForegroundService returns null component info." } | ||
} else { | ||
logger.debug { "Service started, component name: $result" } | ||
} | ||
} | ||
} | ||
|
||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { | ||
if (service == null) { | ||
logger.error { "Service is connected, but got null binder!" } | ||
} | ||
val binder = IRemoteAniTorrentEngine.Stub.asInterface(service) | ||
onServiceConnected(binder) | ||
} | ||
|
||
override fun onPause(owner: LifecycleOwner) { | ||
// app 到后台的时候注册监听 | ||
if (!registered) { | ||
ContextCompat.registerReceiver( | ||
context, | ||
timeExceedLimitReceiver, | ||
timeExceedLimitIntentFilter, | ||
ContextCompat.RECEIVER_NOT_EXPORTED, | ||
) | ||
Comment on lines
+128
to
+133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个没有 unregister, 会有问题吗 |
||
registered = true | ||
} | ||
try { | ||
// 请求 wake lock, 如果在 app 中息屏可以保证 service 正常跑 [acquireWakeLockIntent] 分钟. | ||
context.startService(acquireWakeLockIntent) | ||
} catch (ex: IllegalStateException) { | ||
// 大概率是 ServiceStartForegroundException, 服务已经终止了, 不需要再请求 wakelock. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. catch ForegroundServiceStartNotAllowedException explicitly |
||
logger.warn(ex) { "Failed to acquire wake lock. Service has already died." } | ||
} | ||
super.onPause(owner) | ||
} | ||
|
||
override fun onResume(owner: LifecycleOwner) { | ||
// app 到前台的时候取消监听 | ||
if (registered) { | ||
context.unregisterReceiver(timeExceedLimitReceiver) | ||
registered = false | ||
} | ||
super.onResume(owner) | ||
} | ||
|
||
override fun onServiceDisconnected(name: ComponentName?) { | ||
onServiceDisconnected() | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is lazy really needed here?
Lazy introduces synchornization overhead (which is on main thread).