From 12a999e53ff7d409ea2b8fef85b5455e3b86dacd Mon Sep 17 00:00:00 2001 From: hawkff <109485367+hawkff@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:03:04 -0400 Subject: [PATCH 1/2] chore: clear deprecated API usage (onBackPressed, getParcelable, launchWhen*) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tier-1 deprecation cleanup, no behavior change except a bug fix noted below: - onBackPressed() overrides -> OnBackPressedDispatcher.addCallback (ProfileSettings, ConfigEdit, RouteSettings, GroupSettings); AssetsActivity's redundant onBackPressed{finish()} removed (default back already finishes). - onSupportNavigateUp() now routes through onBackPressedDispatcher.onBackPressed() so the action-bar up button honors the unsaved-changes guard too (previously it called finish() directly and could silently discard edits) — fixes a real data-loss path. - intent/bundle getParcelable -> IntentCompat/BundleCompat.getParcelable(..., Class) (ProfileSelectActivity, ConfigurationFragment) — typed, non-deprecated on API 33+. - launchWhenStarted/launchWhenCreated -> lifecycleScope.launch (one-shot jobs: ServiceButton delayed animation, AppListActivity loader); whenStarted -> withStarted (StatsBar). Verified on-device: clean back finishes; back/up with unsaved changes both show the save dialog; no crashes. --- .../nekohasekai/sagernet/ui/AppListActivity.kt | 3 ++- .../io/nekohasekai/sagernet/ui/AssetsActivity.kt | 4 ---- .../sagernet/ui/ConfigurationFragment.kt | 3 ++- .../sagernet/ui/GroupSettingsActivity.kt | 16 +++++++++------- .../sagernet/ui/ProfileSelectActivity.kt | 3 ++- .../sagernet/ui/RouteSettingsActivity.kt | 16 +++++++++------- .../sagernet/ui/profile/ConfigEditActivity.kt | 14 ++++++++------ .../ui/profile/ProfileSettingsActivity.kt | 14 ++++++++------ .../nekohasekai/sagernet/widget/ServiceButton.kt | 3 ++- .../io/nekohasekai/sagernet/widget/StatsBar.kt | 4 ++-- 10 files changed, 44 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt index 7f3892430..111ecb675 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt @@ -38,6 +38,7 @@ import io.nekohasekai.sagernet.widget.ListListener import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.coroutines.coroutineContext @@ -174,7 +175,7 @@ class AppListActivity : ThemedActivity() { @UiThread private fun loadApps() { loader?.cancel() - loader = lifecycleScope.launchWhenCreated { + loader = lifecycleScope.launch { loading.crossFadeFrom(binding.list) val adapter = binding.list.adapter as AppsAdapter withContext(Dispatchers.IO) { adapter.reload() } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt index 22d7789ca..aa4995254 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt @@ -384,10 +384,6 @@ class AssetsActivity : ThemedActivity() { return true } - override fun onBackPressed() { - finish() - } - override fun onResume() { super.onResume() diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt index 86a6fdfb5..589898ea4 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt @@ -27,6 +27,7 @@ import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar import androidx.core.graphics.ColorUtils import androidx.core.net.toUri +import androidx.core.os.BundleCompat import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.size @@ -1180,7 +1181,7 @@ class ConfigurationFragment @JvmOverloads constructor( override fun onViewStateRestored(savedInstanceState: Bundle?) { super.onViewStateRestored(savedInstanceState) - savedInstanceState?.getParcelable("proxyGroup")?.also { + savedInstanceState?.let { BundleCompat.getParcelable(it, "proxyGroup", ProxyGroup::class.java) }?.also { proxyGroup = it onViewCreated(requireView(), null) } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt index 5c827eda0..e1bfa029c 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt @@ -10,6 +10,7 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.Toast +import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.LayoutRes import androidx.appcompat.app.AlertDialog @@ -246,6 +247,11 @@ class GroupSettingsActivity( @SuppressLint("CommitTransaction") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + onBackPressedDispatcher.addCallback(this) { + if (needSave()) { + UnsavedChangesDialogFragment().apply { key() }.show(supportFragmentManager, null) + } else finish() + } setSupportActionBar(findViewById(R.id.toolbar)) supportActionBar?.apply { setTitle(R.string.group_settings) @@ -330,14 +336,10 @@ class GroupSettingsActivity( override fun onOptionsItemSelected(item: MenuItem) = child.onOptionsItemSelected(item) - override fun onBackPressed() { - if (needSave()) { - UnsavedChangesDialogFragment().apply { key() }.show(supportFragmentManager, null) - } else super.onBackPressed() - } - override fun onSupportNavigateUp(): Boolean { - if (!super.onSupportNavigateUp()) finish() + // Route the action-bar up button through the back dispatcher so it honors the + // unsaved-changes guard instead of finishing directly (avoids data loss). + onBackPressedDispatcher.onBackPressed() return true } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ProfileSelectActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ProfileSelectActivity.kt index 20edf1a1e..cea8e1093 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ProfileSelectActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ProfileSelectActivity.kt @@ -2,6 +2,7 @@ package io.nekohasekai.sagernet.ui import android.content.Intent import android.os.Bundle +import androidx.core.content.IntentCompat import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.database.ProxyEntity @@ -16,7 +17,7 @@ class ProfileSelectActivity : ThemedActivity(R.layout.layout_empty), override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val selected = intent.getParcelableExtra(EXTRA_SELECTED) + val selected = IntentCompat.getParcelableExtra(intent, EXTRA_SELECTED, ProxyEntity::class.java) supportFragmentManager.beginTransaction() .replace( diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt index 94e0b8f7d..46720f8ea 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt @@ -9,6 +9,7 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.Toast +import androidx.activity.addCallback import androidx.activity.result.component1 import androidx.activity.result.component2 import androidx.activity.result.contract.ActivityResultContracts @@ -221,6 +222,11 @@ class RouteSettingsActivity( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + onBackPressedDispatcher.addCallback(this) { + if (needSave()) { + UnsavedChangesDialogFragment().apply { key() }.show(supportFragmentManager, null) + } else finish() + } setSupportActionBar(findViewById(R.id.toolbar)) supportActionBar?.apply { setTitle(R.string.cag_route) @@ -300,14 +306,10 @@ class RouteSettingsActivity( override fun onOptionsItemSelected(item: MenuItem) = child.onOptionsItemSelected(item) - override fun onBackPressed() { - if (needSave()) { - UnsavedChangesDialogFragment().apply { key() }.show(supportFragmentManager, null) - } else super.onBackPressed() - } - override fun onSupportNavigateUp(): Boolean { - if (!super.onSupportNavigateUp()) finish() + // Route the action-bar up button through the back dispatcher so it honors the + // unsaved-changes guard instead of finishing directly (avoids data loss). + onBackPressedDispatcher.onBackPressed() return true } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt index e144a26ef..504008d88 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt @@ -7,6 +7,7 @@ import android.view.Menu import android.view.MenuItem import android.view.ViewGroup.MarginLayoutParams import android.widget.LinearLayout +import androidx.activity.addCallback import androidx.appcompat.app.AlertDialog import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat @@ -53,6 +54,10 @@ class ConfigEditActivity : ThemedActivity() { @SuppressLint("InlinedApi") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + onBackPressedDispatcher.addCallback(this) { + if (dirty) UnsavedChangesDialogFragment().apply { key() } + .show(supportFragmentManager, null) else finish() + } intent?.extras?.apply { getString("key")?.let { key = it } @@ -165,13 +170,10 @@ class ConfigEditActivity : ThemedActivity() { } } - override fun onBackPressed() { - if (dirty) UnsavedChangesDialogFragment().apply { key() } - .show(supportFragmentManager, null) else super.onBackPressed() - } - override fun onSupportNavigateUp(): Boolean { - if (!super.onSupportNavigateUp()) finish() + // Route the action-bar up button through the back dispatcher so it honors the + // unsaved-changes guard instead of finishing directly (avoids data loss). + onBackPressedDispatcher.onBackPressed() return true } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt index 6424502ba..2a2babbd7 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt @@ -12,6 +12,7 @@ import android.view.View import android.widget.LinearLayout import android.widget.ScrollView import android.widget.Toast +import androidx.activity.addCallback import androidx.activity.result.component1 import androidx.activity.result.component2 import androidx.activity.result.contract.ActivityResultContracts @@ -92,6 +93,10 @@ abstract class ProfileSettingsActivity( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + onBackPressedDispatcher.addCallback(this) { + if (DataStore.dirty) UnsavedChangesDialogFragment().apply { key() } + .show(supportFragmentManager, null) else finish() + } setSupportActionBar(findViewById(R.id.toolbar)) supportActionBar?.apply { setTitle(R.string.profile_config) @@ -174,13 +179,10 @@ abstract class ProfileSettingsActivity( override fun onOptionsItemSelected(item: MenuItem) = child.onOptionsItemSelected(item) - override fun onBackPressed() { - if (DataStore.dirty) UnsavedChangesDialogFragment().apply { key() } - .show(supportFragmentManager, null) else super.onBackPressed() - } - override fun onSupportNavigateUp(): Boolean { - if (!super.onSupportNavigateUp()) finish() + // Route the action-bar up button through the same back dispatcher so it honors + // the unsaved-changes guard instead of finishing directly (avoids data loss). + onBackPressedDispatcher.onBackPressed() return true } diff --git a/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt b/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt index e8e0183f4..723aaba46 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt @@ -21,6 +21,7 @@ import io.nekohasekai.sagernet.bg.BaseService import io.nekohasekai.sagernet.ktx.getColorAttr import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import java.util.* class ServiceButton @JvmOverloads constructor( @@ -64,7 +65,7 @@ class ServiceButton @JvmOverloads constructor( private val iconConnecting by lazy { AnimatedState(R.drawable.ic_service_connecting) { hideProgress() - delayedAnimation = (context as LifecycleOwner).lifecycleScope.launchWhenStarted { + delayedAnimation = (context as LifecycleOwner).lifecycleScope.launch { delay(context.resources.getInteger(android.R.integer.config_mediumAnimTime) + 1000L) isIndeterminate = true show() diff --git a/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt b/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt index 7cddcfc46..a6cdde97a 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt @@ -11,7 +11,7 @@ import android.widget.TextView import androidx.appcompat.widget.TooltipCompat import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.whenStarted +import androidx.lifecycle.withStarted import com.google.android.material.bottomappbar.BottomAppBar import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.bg.BaseService @@ -125,7 +125,7 @@ class StatsBar @JvmOverloads constructor( val activity = context as MainActivity fun postWhenStarted(what: () -> Unit) = activity.lifecycleScope.launch(Dispatchers.Main) { delay(100L) - activity.whenStarted { what() } + activity.withStarted { what() } } if ((state == BaseService.State.Connected).also { hideOnScroll = it }) { postWhenStarted { From 5494970c47c08c823845288ebac71adb908c474a Mon Sep 17 00:00:00 2001 From: hawkff <109485367+hawkff@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:18:00 -0400 Subject: [PATCH 2/2] review: keep STARTED gate on delayed connecting animation Plain lifecycleScope.launch (replacing launchWhenStarted) could run the delayed progress show() while the activity is stopped. Gate the UI mutation with withStarted to preserve the lifecycle-aware behavior (matches StatsBar). --- .../java/io/nekohasekai/sagernet/widget/ServiceButton.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt b/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt index 723aaba46..6a578a823 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt @@ -12,6 +12,7 @@ import androidx.appcompat.widget.TooltipCompat import androidx.dynamicanimation.animation.DynamicAnimation import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.withStarted import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.google.android.material.floatingactionbutton.FloatingActionButton @@ -67,8 +68,12 @@ class ServiceButton @JvmOverloads constructor( hideProgress() delayedAnimation = (context as LifecycleOwner).lifecycleScope.launch { delay(context.resources.getInteger(android.R.integer.config_mediumAnimTime) + 1000L) - isIndeterminate = true - show() + // Gate the UI mutation on STARTED so a delayed progress reveal doesn't run + // while the activity is stopped (the old launchWhenStarted suspended here). + (context as LifecycleOwner).withStarted { + isIndeterminate = true + show() + } } } }