From 7ff5d11bb34eaa1673fa7606a8ef1c4cb9867045 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:07:43 +0000 Subject: [PATCH 1/2] Initial plan From 2aa5d9a95b0d9b0a1de2a9a7556ca9442a537dc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:14:59 +0000 Subject: [PATCH 2/2] Fix ANR risk: move ContentResolver openOutputStream/openInputStream to IO dispatcher Agent-Logs-Url: https://github.com/shadowsocks/shadowsocks-android/sessions/19a7ede5-fd03-4e27-807d-b78fc55334fe Co-authored-by: madeye <627917+madeye@users.noreply.github.com> --- .../github/shadowsocks/ProfilesFragment.kt | 30 +++++++++++----- .../shadowsocks/tv/MainPreferenceFragment.kt | 34 +++++++++++++------ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt b/mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt index aa830498e0..606c1801a9 100644 --- a/mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/ProfilesFragment.kt @@ -42,7 +42,11 @@ import androidx.appcompat.widget.TooltipCompat import androidx.core.os.bundleOf import androidx.core.view.ViewCompat import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import com.github.shadowsocks.aidl.TrafficStats import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.database.Profile @@ -457,22 +461,30 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, Sea private fun importOrReplaceProfiles(dataUris: List, replace: Boolean = false) { if (dataUris.isEmpty()) return val activity = activity as MainActivity - try { - ProfileManager.createProfilesFromJson(dataUris.asSequence().map { - activity.contentResolver.openInputStream(it) - }.filterNotNull(), replace) - } catch (e: Exception) { - activity.snackbar(e.readableMessage).show() + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + ProfileManager.createProfilesFromJson(dataUris.asSequence().map { + activity.contentResolver.openInputStream(it) + }.filterNotNull(), replace) + } + } catch (e: Exception) { + activity.snackbar(e.readableMessage).show() + } } } private val importProfiles = registerForActivityResult(OpenJson) { importOrReplaceProfiles(it) } private val replaceProfiles = registerForActivityResult(OpenJson) { importOrReplaceProfiles(it, true) } private val exportProfiles = registerForActivityResult(SaveJson) { data -> - if (data != null) ProfileManager.serializeToJson()?.let { profiles -> + if (data != null) lifecycleScope.launch { val activity = activity as MainActivity try { - activity.contentResolver.openOutputStream(data)!!.bufferedWriter().use { - it.write(profiles.toString(2)) + withContext(Dispatchers.IO) { + ProfileManager.serializeToJson()?.let { profiles -> + activity.contentResolver.openOutputStream(data)!!.bufferedWriter().use { + it.write(profiles.toString(2)) + } + } } } catch (e: Exception) { Timber.w(e) diff --git a/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt b/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt index 198245f728..d18a2c9abc 100644 --- a/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt +++ b/tv/src/main/java/com/github/shadowsocks/tv/MainPreferenceFragment.kt @@ -30,7 +30,11 @@ import androidx.activity.result.ActivityResultLauncher import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels import androidx.leanback.preference.LeanbackPreferenceFragmentCompat +import androidx.lifecycle.lifecycleScope import androidx.preference.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import com.github.shadowsocks.BootReceiver import com.github.shadowsocks.Core import com.github.shadowsocks.aidl.IShadowsocksService @@ -228,22 +232,30 @@ class MainPreferenceFragment : LeanbackPreferenceFragmentCompat(), ShadowsocksCo private val replaceProfiles = registerForActivityResult(OpenJson) { dataUris -> if (dataUris.isEmpty()) return@registerForActivityResult val context = requireContext() - try { - ProfileManager.createProfilesFromJson(dataUris.asSequence().map { - context.contentResolver.openInputStream(it) - }.filterNotNull(), true) - } catch (e: Exception) { - Timber.w(e) - Toast.makeText(context, e.readableMessage, Toast.LENGTH_SHORT).show() + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + ProfileManager.createProfilesFromJson(dataUris.asSequence().map { + context.contentResolver.openInputStream(it) + }.filterNotNull(), true) + } + } catch (e: Exception) { + Timber.w(e) + Toast.makeText(context, e.readableMessage, Toast.LENGTH_SHORT).show() + } + populateProfiles() } - populateProfiles() } private val exportProfiles = registerForActivityResult(SaveJson) { data -> - if (data != null) ProfileManager.serializeToJson()?.let { profiles -> + if (data != null) lifecycleScope.launch { val context = requireContext() try { - context.contentResolver.openOutputStream(data)!!.bufferedWriter().use { - it.write(profiles.toString(2)) + withContext(Dispatchers.IO) { + ProfileManager.serializeToJson()?.let { profiles -> + context.contentResolver.openOutputStream(data)!!.bufferedWriter().use { + it.write(profiles.toString(2)) + } + } } } catch (e: Exception) { Timber.w(e)