Skip to content

Commit 12a0b37

Browse files
stariflyhawkff
authored andcommitted
fix: reopen profile picker on reselect
1 parent f935a15 commit 12a0b37

5 files changed

Lines changed: 170 additions & 16 deletions

File tree

app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ class GroupSettingsActivity(
5151

5252
DataStore.frontProxy = frontProxy
5353
DataStore.landingProxy = landingProxy
54-
DataStore.frontProxyTmp = if (frontProxy >= 0) 3 else 0
55-
DataStore.landingProxyTmp = if (landingProxy >= 0) 3 else 0
54+
DataStore.frontProxyTmp =
55+
if (frontProxy >= 0) OutboundPreference.VALUE_SELECT_PROFILE.toInt() else 0
56+
DataStore.landingProxyTmp =
57+
if (landingProxy >= 0) OutboundPreference.VALUE_SELECT_PROFILE.toInt() else 0
5658

5759
val subscription = subscription ?: SubscriptionBean().applyDefaultValues()
5860
DataStore.subscriptionLink = subscription.link
@@ -72,8 +74,18 @@ class GroupSettingsActivity(
7274
order = DataStore.groupOrder
7375
isSelector = DataStore.groupIsSelector
7476

75-
frontProxy = if (DataStore.frontProxyTmp == 3) DataStore.frontProxy else -1
76-
landingProxy = if (DataStore.landingProxyTmp == 3) DataStore.landingProxy else -1
77+
frontProxy =
78+
if (DataStore.frontProxyTmp == OutboundPreference.VALUE_SELECT_PROFILE.toInt()) {
79+
DataStore.frontProxy
80+
} else {
81+
-1
82+
}
83+
landingProxy =
84+
if (DataStore.landingProxyTmp == OutboundPreference.VALUE_SELECT_PROFILE.toInt()) {
85+
DataStore.landingProxy
86+
} else {
87+
-1
88+
}
7789

7890
val isSubscription = type == GroupType.SUBSCRIPTION
7991
if (isSubscription) {
@@ -108,9 +120,15 @@ class GroupSettingsActivity(
108120
setEntries(R.array.front_proxy_entry)
109121
setEntryValues(R.array.front_proxy_value)
110122
setOnPreferenceChangeListener { _, newValue ->
111-
if (newValue.toString() == "3") {
123+
if (newValue.toString() == OutboundPreference.VALUE_SELECT_PROFILE) {
112124
selectProfileForAddFront.launch(
113-
Intent(this@GroupSettingsActivity, ProfileSelectActivity::class.java)
125+
Intent(
126+
this@GroupSettingsActivity, ProfileSelectActivity::class.java
127+
).apply {
128+
ProfileManager.getProfile(DataStore.frontProxy)?.let {
129+
putExtra(ProfileSelectActivity.EXTRA_SELECTED, it)
130+
}
131+
}
114132
)
115133
false
116134
} else {
@@ -123,9 +141,15 @@ class GroupSettingsActivity(
123141
setEntries(R.array.front_proxy_entry)
124142
setEntryValues(R.array.front_proxy_value)
125143
setOnPreferenceChangeListener { _, newValue ->
126-
if (newValue.toString() == "3") {
144+
if (newValue.toString() == OutboundPreference.VALUE_SELECT_PROFILE) {
127145
selectProfileForAddLanding.launch(
128-
Intent(this@GroupSettingsActivity, ProfileSelectActivity::class.java)
146+
Intent(
147+
this@GroupSettingsActivity, ProfileSelectActivity::class.java
148+
).apply {
149+
ProfileManager.getProfile(DataStore.landingProxy)?.let {
150+
putExtra(ProfileSelectActivity.EXTRA_SELECTED, it)
151+
}
152+
}
129153
)
130154
false
131155
} else {
@@ -401,7 +425,7 @@ class GroupSettingsActivity(
401425
) ?: return@runOnDefaultDispatcher
402426
DataStore.frontProxy = profile.id
403427
onMainDispatcher {
404-
frontProxyPreference.value = "3"
428+
frontProxyPreference.value = OutboundPreference.VALUE_SELECT_PROFILE
405429
}
406430
}
407431
}
@@ -415,7 +439,7 @@ class GroupSettingsActivity(
415439
) ?: return@runOnDefaultDispatcher
416440
DataStore.landingProxy = profile.id
417441
onMainDispatcher {
418-
landingProxyPreference.value = "3"
442+
landingProxyPreference.value = OutboundPreference.VALUE_SELECT_PROFILE
419443
}
420444
}
421445
}

app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class RouteSettingsActivity(
7272
0L -> 0
7373
-1L -> 1
7474
-2L -> 2
75-
else -> 3
75+
else -> OutboundPreference.VALUE_SELECT_PROFILE.toInt()
7676
}
7777
DataStore.routePackages = packages.joinToString("\n")
7878
}
@@ -135,7 +135,7 @@ class RouteSettingsActivity(
135135
) ?: return@runOnDefaultDispatcher
136136
DataStore.routeOutboundRule = profile.id
137137
onMainDispatcher {
138-
outbound.value = "3"
138+
outbound.value = OutboundPreference.VALUE_SELECT_PROFILE
139139
}
140140
}
141141
}
@@ -154,11 +154,15 @@ class RouteSettingsActivity(
154154
apps = findPreference(Key.ROUTE_PACKAGES)!!
155155

156156
outbound.setOnPreferenceChangeListener { _, newValue ->
157-
if (newValue.toString() == "3") {
157+
if (newValue.toString() == OutboundPreference.VALUE_SELECT_PROFILE) {
158158
selectProfileForAdd.launch(
159159
Intent(
160160
this@RouteSettingsActivity, ProfileSelectActivity::class.java
161-
)
161+
).apply {
162+
ProfileManager.getProfile(DataStore.routeOutboundRule)?.let {
163+
putExtra(ProfileSelectActivity.EXTRA_SELECTED, it)
164+
}
165+
}
162166
)
163167
false
164168
} else {

app/src/main/java/io/nekohasekai/sagernet/widget/OutboundPreference.kt

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package io.nekohasekai.sagernet.widget
22

33
import android.content.Context
44
import android.util.AttributeSet
5+
import android.view.View
6+
import android.widget.AdapterView
7+
import android.widget.Spinner
8+
import androidx.preference.PreferenceViewHolder
59
import io.nekohasekai.sagernet.R
610
import io.nekohasekai.sagernet.database.DataStore
711
import io.nekohasekai.sagernet.database.ProfileManager
@@ -12,13 +16,64 @@ class OutboundPreference
1216
context: Context, attrs: AttributeSet? = null, defStyle: Int = R.attr.dropdownPreferenceStyle
1317
) : SimpleMenuPreference(context, attrs, defStyle, 0) {
1418

19+
companion object {
20+
const val VALUE_SELECT_PROFILE = "3"
21+
}
22+
1523
init {
1624
setEntries(R.array.outbound_entry)
1725
setEntryValues(R.array.outbound_value)
26+
layoutResource = R.layout.preference_dropdown_reselectable
27+
}
28+
29+
override fun setValue(value: String?) {
30+
val oldValue = this.value
31+
super.setValue(value)
32+
if (oldValue == value) {
33+
notifyChanged()
34+
}
35+
}
36+
37+
private var dropdownOpened = false
38+
39+
override fun onClick() {
40+
dropdownOpened = true
41+
super.onClick()
42+
}
43+
44+
override fun onBindViewHolder(holder: PreferenceViewHolder) {
45+
dropdownOpened = false
46+
super.onBindViewHolder(holder)
47+
48+
val spinner = holder.itemView.findViewById<Spinner>(R.id.spinner)
49+
(spinner as? ReselectableSpinner)?.onPopupClosed = { dropdownOpened = false }
50+
var selectionReady = false
51+
holder.itemView.post { selectionReady = true }
52+
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
53+
override fun onItemSelected(
54+
parent: AdapterView<*>?,
55+
view: View?,
56+
position: Int,
57+
id: Long,
58+
) {
59+
if (!selectionReady || position < 0) return
60+
val newValue = entryValues?.getOrNull(position)?.toString() ?: return
61+
val reselectedProfile =
62+
dropdownOpened && newValue == value && newValue == VALUE_SELECT_PROFILE
63+
if ((newValue != value || reselectedProfile) && callChangeListener(newValue)) {
64+
value = newValue
65+
}
66+
dropdownOpened = false
67+
}
68+
69+
override fun onNothingSelected(parent: AdapterView<*>?) {
70+
dropdownOpened = false
71+
}
72+
}
1873
}
1974

2075
override fun getSummary(): CharSequence? {
21-
if (value == "3") {
76+
if (value == VALUE_SELECT_PROFILE) {
2277
val routeOutbound = DataStore.profileCacheStore.getLong(key + "Long") ?: 0
2378
if (routeOutbound > 0) {
2479
ProfileManager.getProfile(routeOutbound)?.displayName()?.let {
@@ -29,4 +84,4 @@ class OutboundPreference
2984
return super.getSummary()
3085
}
3186

32-
}
87+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package io.nekohasekai.sagernet.widget
2+
3+
import android.content.Context
4+
import android.util.AttributeSet
5+
import androidx.appcompat.widget.AppCompatSpinner
6+
7+
class ReselectableSpinner @JvmOverloads constructor(
8+
context: Context, attrs: AttributeSet? = null
9+
) : AppCompatSpinner(context, attrs) {
10+
11+
var onPopupClosed: (() -> Unit)? = null
12+
13+
/**
14+
* Android's [Spinner] has no official "popup dismissed" callback, so we use
15+
* the window regaining focus as a proxy: when the dropdown popup closes,
16+
* focus returns to this spinner's window. This is intentionally a best-effort
17+
* signal, not a precise one — [onPopupClosed] may also fire for unrelated
18+
* focus changes (app foregrounding, keyboard/IME show-hide, system dialogs,
19+
* permission prompts, orientation changes, etc.). Callers must therefore
20+
* treat it only as a hint and stay correct if it fires spuriously (the
21+
* OutboundPreference reselect logic does, by re-checking value/position).
22+
*/
23+
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
24+
super.onWindowFocusChanged(hasWindowFocus)
25+
if (hasWindowFocus) onPopupClosed?.invoke()
26+
}
27+
28+
override fun setSelection(position: Int) {
29+
val reselected = position == selectedItemPosition
30+
super.setSelection(position)
31+
if (reselected) notifyReselected(position)
32+
}
33+
34+
override fun setSelection(position: Int, animate: Boolean) {
35+
val reselected = position == selectedItemPosition
36+
super.setSelection(position, animate)
37+
if (reselected) notifyReselected(position)
38+
}
39+
40+
private fun notifyReselected(position: Int) {
41+
if (position < 0) return
42+
onItemSelectedListener?.onItemSelected(
43+
this, selectedView, position, adapter?.getItemId(position) ?: 0L
44+
)
45+
}
46+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Mirror of androidx's preference_dropdown_material.xml, with the anchor spinner
4+
swapped for ReselectableSpinner so re-selecting the current item still fires a
5+
selection callback. The (invisible) spinner is only the popup anchor; the
6+
visible row comes from the standard @layout/preference_material include, so the
7+
appearance is identical to a normal dropdown preference.
8+
-->
9+
<FrameLayout
10+
xmlns:android="http://schemas.android.com/apk/res/android"
11+
android:layout_width="match_parent"
12+
android:layout_height="wrap_content">
13+
14+
<io.nekohasekai.sagernet.widget.ReselectableSpinner
15+
android:id="@+id/spinner"
16+
android:layout_width="0dp"
17+
android:layout_height="wrap_content"
18+
android:layout_marginStart="@dimen/preference_dropdown_padding_start"
19+
android:layout_marginLeft="@dimen/preference_dropdown_padding_start"
20+
android:spinnerMode="dropdown"
21+
android:visibility="invisible" />
22+
23+
<include layout="@layout/preference_material" />
24+
25+
</FrameLayout>

0 commit comments

Comments
 (0)