diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index ea8254cb40..e944aef480 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -30,6 +30,14 @@ import java.util.concurrent.TimeUnit import com.github.kr328.clash.design.R as DesignR class MainActivity : BaseActivity() { + + companion object { + @Volatile + @JvmStatic + var pendingExternalRequest: Boolean = false + private set + } + override suspend fun main() { val design = MainDesign(this) @@ -129,13 +137,18 @@ class MainActivity : BaseActivity() { try { if (vpnRequest != null) { - val result = startActivityForResult( - ActivityResultContracts.StartActivityForResult(), - vpnRequest - ) - - if (result.resultCode == RESULT_OK) - startClashService() + pendingExternalRequest = true + try { + val result = startActivityForResult( + ActivityResultContracts.StartActivityForResult(), + vpnRequest + ) + + if (result.resultCode == RESULT_OK) + startClashService() + } finally { + pendingExternalRequest = false + } } } catch (e: Exception) { design?.showToast(DesignR.string.unable_to_start_vpn, ToastDuration.Long) diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 9242d72299..237081db3e 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -2,12 +2,18 @@ package com.github.kr328.clash import android.app.Application import android.content.Context +import android.os.Process import com.github.kr328.clash.common.Global import com.github.kr328.clash.common.compat.currentProcessName import com.github.kr328.clash.common.log.Log +import com.github.kr328.clash.design.store.UiStore import com.github.kr328.clash.remote.Remote import com.github.kr328.clash.service.util.sendServiceRecreated +import com.github.kr328.clash.util.ApplicationObserver import com.github.kr328.clash.util.clashDir +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import java.io.File import java.io.FileOutputStream @@ -29,6 +35,52 @@ class MainApplication : Application() { Log.d("Process $processName started") if (processName == packageName) { + // Setup auto destroy UI observer (must register before Remote.launch + // so our callback is in the listener list before any visibility event) + val uiStore = UiStore(this) + var autoDestroyJob: Job? = null + + // 含未保存编辑或正在等待外部结果(文件选择器、VPN 授权等)的 Activity, + // 存活时不允许杀 UI 进程 + val protectedActivities: Set> = setOf( + NewProfileActivity::class.java, + FilesActivity::class.java, + LogcatActivity::class.java, + MetaFeatureSettingsActivity::class.java, + AccessControlActivity::class.java, + OverrideSettingsActivity::class.java, + PropertiesActivity::class.java, + ) + + ApplicationObserver.onVisibleChanged { visible -> + autoDestroyJob?.cancel() + autoDestroyJob = null + if (!visible && uiStore.autoDestroyUI) { + autoDestroyJob = Global.launch { + // 持续监测:有等待回调/编辑中则等待,无保护条件持续 3 秒才杀 + var idleMs = 0 + while (idleMs < 3000) { + delay(500) + val protectedNow = LogcatService.running || + ApplicationObserver.createdActivities.any { it.javaClass in protectedActivities } || + MainActivity.pendingExternalRequest + if (protectedNow) { + idleMs = 0 + } else { + idleMs += 500 + } + } + Log.d("AutoDestroyUI: Killing UI process to free memory") + try { + File(clashDir, "auto_destroy.log").appendText( + "${java.util.Date()} AutoDestroyUI: Killing UI process\n" + ) + } catch (e: Exception) { } + Process.killProcess(Process.myPid()) + } + } + } + Remote.launch() } else { sendServiceRecreated() diff --git a/app/src/main/java/com/github/kr328/clash/util/Application.kt b/app/src/main/java/com/github/kr328/clash/util/Application.kt index 1fb6336781..dfad2c12da 100644 --- a/app/src/main/java/com/github/kr328/clash/util/Application.kt +++ b/app/src/main/java/com/github/kr328/clash/util/Application.kt @@ -12,14 +12,16 @@ object ApplicationObserver { private val _createdActivities: MutableSet = mutableSetOf() private val _visibleActivities: MutableSet = mutableSetOf() - private var visibleChanged: (Boolean) -> Unit = {} + private val visibleChangedListeners: MutableList<(Boolean) -> Unit> = mutableListOf() private var appVisible = false private set(value) { if (field != value) { field = value - visibleChanged(value) + visibleChangedListeners.forEach { listener -> + listener(value) + } } } @@ -55,7 +57,7 @@ object ApplicationObserver { } fun onVisibleChanged(visibleChanged: (Boolean) -> Unit) { - this.visibleChanged = visibleChanged + visibleChangedListeners.add(visibleChanged) } fun attach(application: Application) { diff --git a/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt b/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt index dc43d00a29..de1ef20c92 100644 --- a/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt +++ b/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt @@ -88,6 +88,13 @@ class AppSettingsDesign( } } + switch( + value = uiStore::autoDestroyUI, + icon = R.drawable.ic_baseline_close, + title = R.string.auto_destroy_ui_title, + summary = R.string.auto_destroy_ui_desc, + ) + category(R.string.service) switch( diff --git a/design/src/main/java/com/github/kr328/clash/design/store/UiStore.kt b/design/src/main/java/com/github/kr328/clash/design/store/UiStore.kt index ddc9d600fd..3eca036234 100644 --- a/design/src/main/java/com/github/kr328/clash/design/store/UiStore.kt +++ b/design/src/main/java/com/github/kr328/clash/design/store/UiStore.kt @@ -41,6 +41,11 @@ class UiStore(context: Context) { defaultValue = false, ) + var autoDestroyUI: Boolean by store.boolean( + key = "auto_destroy_ui", + defaultValue = false, + ) + var proxyExcludeNotSelectable by store.boolean( key = "proxy_exclude_not_selectable", defaultValue = false, diff --git a/design/src/main/res/values-ja-rJP/strings.xml b/design/src/main/res/values-ja-rJP/strings.xml index e38b8bf030..028991524c 100644 --- a/design/src/main/res/values-ja-rJP/strings.xml +++ b/design/src/main/res/values-ja-rJP/strings.xml @@ -264,4 +264,6 @@ Override Destination カメラのアクセスが制限されています。設定から有効にしてください。 システムで予期しない例外が発生しました。 + UI自動破棄 + アプリがバックグラウンドに移行して3秒後にメモリ節約のためUIを自動的に破棄します diff --git a/design/src/main/res/values-ko-rKR/strings.xml b/design/src/main/res/values-ko-rKR/strings.xml index bc22550af8..82e9bf686c 100644 --- a/design/src/main/res/values-ko-rKR/strings.xml +++ b/design/src/main/res/values-ko-rKR/strings.xml @@ -264,4 +264,6 @@ Override Destination 카메라 접근이 제한되었습니다. 설정에서 허용해 주세요. 처리되지 않은 시스템 예외가 발생했습니다. + UI 자동 종료 + 앱이 백그라운드로 전환된 후 3초 후 메모리 절약을 위해 UI를 자동으로 종료합니다 diff --git a/design/src/main/res/values-ru/strings.xml b/design/src/main/res/values-ru/strings.xml index 4f680de2c5..83dcc9dc0f 100644 --- a/design/src/main/res/values-ru/strings.xml +++ b/design/src/main/res/values-ru/strings.xml @@ -328,4 +328,6 @@ Override Destination Доступ к камере ограничен. Разрешите его в настройках. Произошла не обрабатываемая системная ошибка. + Автоуничтожение UI + Автоматически уничтожать UI через 3 секунды после перехода приложения в фон для экономии памяти diff --git a/design/src/main/res/values-vi/strings.xml b/design/src/main/res/values-vi/strings.xml index 2abca48934..befb8f1691 100644 --- a/design/src/main/res/values-vi/strings.xml +++ b/design/src/main/res/values-vi/strings.xml @@ -250,4 +250,6 @@ Nhập từ Mã QR Quyền truy cập camera bị hạn chế. Vui lòng bật trong Cài đặt. Đã xảy ra ngoại lệ hệ thống không xử lý được. + Tự động hủy UI + Tự động hủy UI sau 3 giây khi ứng dụng chuyển sang nền để tiết kiệm bộ nhớ diff --git a/design/src/main/res/values-zh-rHK/strings.xml b/design/src/main/res/values-zh-rHK/strings.xml index a63dff70a4..d87a1bb18e 100644 --- a/design/src/main/res/values-zh-rHK/strings.xml +++ b/design/src/main/res/values-zh-rHK/strings.xml @@ -261,4 +261,6 @@ Override Destination 相機權限受限,請前往設定開啟。 發生系統未知異常,操作失敗。 + 自動銷毀介面 + 應用程式進入背景3秒後自動銷毀介面以節省記憶體 diff --git a/design/src/main/res/values-zh-rTW/strings.xml b/design/src/main/res/values-zh-rTW/strings.xml index 4bc185b77d..14cf639b6a 100644 --- a/design/src/main/res/values-zh-rTW/strings.xml +++ b/design/src/main/res/values-zh-rTW/strings.xml @@ -261,4 +261,6 @@ Override Destination 相機權限受限,請前往設定開啟。 發生系統未知異常,操作失敗。 + 自動銷毀介面 + 應用程式進入背景3秒後自動銷毀介面以節省記憶體 diff --git a/design/src/main/res/values-zh/strings.xml b/design/src/main/res/values-zh/strings.xml index be62f50f54..f94f33d7fa 100644 --- a/design/src/main/res/values-zh/strings.xml +++ b/design/src/main/res/values-zh/strings.xml @@ -41,6 +41,8 @@ 可以在拨号盘输入 *#*#252746382#*#* 打开应用 从最近任务隐藏 在最近任务中隐藏应用 + 自动销毁界面 + 应用进入后台3秒后自动销毁界面以节省内存 历史 从文件导入 从 URL 导入 diff --git a/design/src/main/res/values/strings.xml b/design/src/main/res/values/strings.xml index 2fe54fd226..8a6ee26474 100644 --- a/design/src/main/res/values/strings.xml +++ b/design/src/main/res/values/strings.xml @@ -361,6 +361,8 @@ You can dial *#*#252746382#*#* to open this App Hide from Recents Hide app from the Recent apps screen + Auto Destroy UI + Automatically destroy UI after 3 seconds when app goes to background Camera access is restricted. Please enable it in Settings. An unhandled system exception occurred.