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 6aba247ee..5c827eda0 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt @@ -51,8 +51,10 @@ class GroupSettingsActivity( DataStore.frontProxy = frontProxy DataStore.landingProxy = landingProxy - DataStore.frontProxyTmp = if (frontProxy >= 0) 3 else 0 - DataStore.landingProxyTmp = if (landingProxy >= 0) 3 else 0 + DataStore.frontProxyTmp = + if (frontProxy >= 0) OutboundPreference.VALUE_SELECT_PROFILE.toInt() else 0 + DataStore.landingProxyTmp = + if (landingProxy >= 0) OutboundPreference.VALUE_SELECT_PROFILE.toInt() else 0 val subscription = subscription ?: SubscriptionBean().applyDefaultValues() DataStore.subscriptionLink = subscription.link @@ -72,8 +74,18 @@ class GroupSettingsActivity( order = DataStore.groupOrder isSelector = DataStore.groupIsSelector - frontProxy = if (DataStore.frontProxyTmp == 3) DataStore.frontProxy else -1 - landingProxy = if (DataStore.landingProxyTmp == 3) DataStore.landingProxy else -1 + frontProxy = + if (DataStore.frontProxyTmp == OutboundPreference.VALUE_SELECT_PROFILE.toInt()) { + DataStore.frontProxy + } else { + -1 + } + landingProxy = + if (DataStore.landingProxyTmp == OutboundPreference.VALUE_SELECT_PROFILE.toInt()) { + DataStore.landingProxy + } else { + -1 + } val isSubscription = type == GroupType.SUBSCRIPTION if (isSubscription) { @@ -108,9 +120,15 @@ class GroupSettingsActivity( setEntries(R.array.front_proxy_entry) setEntryValues(R.array.front_proxy_value) setOnPreferenceChangeListener { _, newValue -> - if (newValue.toString() == "3") { + if (newValue.toString() == OutboundPreference.VALUE_SELECT_PROFILE) { selectProfileForAddFront.launch( - Intent(this@GroupSettingsActivity, ProfileSelectActivity::class.java) + Intent( + this@GroupSettingsActivity, ProfileSelectActivity::class.java + ).apply { + ProfileManager.getProfile(DataStore.frontProxy)?.let { + putExtra(ProfileSelectActivity.EXTRA_SELECTED, it) + } + } ) false } else { @@ -123,9 +141,15 @@ class GroupSettingsActivity( setEntries(R.array.front_proxy_entry) setEntryValues(R.array.front_proxy_value) setOnPreferenceChangeListener { _, newValue -> - if (newValue.toString() == "3") { + if (newValue.toString() == OutboundPreference.VALUE_SELECT_PROFILE) { selectProfileForAddLanding.launch( - Intent(this@GroupSettingsActivity, ProfileSelectActivity::class.java) + Intent( + this@GroupSettingsActivity, ProfileSelectActivity::class.java + ).apply { + ProfileManager.getProfile(DataStore.landingProxy)?.let { + putExtra(ProfileSelectActivity.EXTRA_SELECTED, it) + } + } ) false } else { @@ -401,7 +425,7 @@ class GroupSettingsActivity( ) ?: return@runOnDefaultDispatcher DataStore.frontProxy = profile.id onMainDispatcher { - frontProxyPreference.value = "3" + frontProxyPreference.value = OutboundPreference.VALUE_SELECT_PROFILE } } } @@ -415,7 +439,7 @@ class GroupSettingsActivity( ) ?: return@runOnDefaultDispatcher DataStore.landingProxy = profile.id onMainDispatcher { - landingProxyPreference.value = "3" + landingProxyPreference.value = OutboundPreference.VALUE_SELECT_PROFILE } } } 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 fd68ab909..94e0b8f7d 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt @@ -72,7 +72,7 @@ class RouteSettingsActivity( 0L -> 0 -1L -> 1 -2L -> 2 - else -> 3 + else -> OutboundPreference.VALUE_SELECT_PROFILE.toInt() } DataStore.routePackages = packages.joinToString("\n") } @@ -135,7 +135,7 @@ class RouteSettingsActivity( ) ?: return@runOnDefaultDispatcher DataStore.routeOutboundRule = profile.id onMainDispatcher { - outbound.value = "3" + outbound.value = OutboundPreference.VALUE_SELECT_PROFILE } } } @@ -154,11 +154,15 @@ class RouteSettingsActivity( apps = findPreference(Key.ROUTE_PACKAGES)!! outbound.setOnPreferenceChangeListener { _, newValue -> - if (newValue.toString() == "3") { + if (newValue.toString() == OutboundPreference.VALUE_SELECT_PROFILE) { selectProfileForAdd.launch( Intent( this@RouteSettingsActivity, ProfileSelectActivity::class.java - ) + ).apply { + ProfileManager.getProfile(DataStore.routeOutboundRule)?.let { + putExtra(ProfileSelectActivity.EXTRA_SELECTED, it) + } + } ) false } else { diff --git a/app/src/main/java/io/nekohasekai/sagernet/widget/OutboundPreference.kt b/app/src/main/java/io/nekohasekai/sagernet/widget/OutboundPreference.kt index 67f6ab25a..f7f4fc6bc 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/widget/OutboundPreference.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/widget/OutboundPreference.kt @@ -2,6 +2,10 @@ package io.nekohasekai.sagernet.widget import android.content.Context import android.util.AttributeSet +import android.view.View +import android.widget.AdapterView +import android.widget.Spinner +import androidx.preference.PreferenceViewHolder import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.database.ProfileManager @@ -12,13 +16,64 @@ class OutboundPreference context: Context, attrs: AttributeSet? = null, defStyle: Int = R.attr.dropdownPreferenceStyle ) : SimpleMenuPreference(context, attrs, defStyle, 0) { + companion object { + const val VALUE_SELECT_PROFILE = "3" + } + init { setEntries(R.array.outbound_entry) setEntryValues(R.array.outbound_value) + layoutResource = R.layout.preference_dropdown_reselectable + } + + override fun setValue(value: String?) { + val oldValue = this.value + super.setValue(value) + if (oldValue == value) { + notifyChanged() + } + } + + private var dropdownOpened = false + + override fun onClick() { + dropdownOpened = true + super.onClick() + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + dropdownOpened = false + super.onBindViewHolder(holder) + + val spinner = holder.itemView.findViewById(R.id.spinner) + (spinner as? ReselectableSpinner)?.onPopupClosed = { dropdownOpened = false } + var selectionReady = false + holder.itemView.post { selectionReady = true } + spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long, + ) { + if (!selectionReady || position < 0) return + val newValue = entryValues?.getOrNull(position)?.toString() ?: return + val reselectedProfile = + dropdownOpened && newValue == value && newValue == VALUE_SELECT_PROFILE + if ((newValue != value || reselectedProfile) && callChangeListener(newValue)) { + value = newValue + } + dropdownOpened = false + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + dropdownOpened = false + } + } } override fun getSummary(): CharSequence? { - if (value == "3") { + if (value == VALUE_SELECT_PROFILE) { val routeOutbound = DataStore.profileCacheStore.getLong(key + "Long") ?: 0 if (routeOutbound > 0) { ProfileManager.getProfile(routeOutbound)?.displayName()?.let { @@ -29,4 +84,4 @@ class OutboundPreference return super.getSummary() } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/widget/ReselectableSpinner.kt b/app/src/main/java/io/nekohasekai/sagernet/widget/ReselectableSpinner.kt new file mode 100644 index 000000000..9211d7b61 --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/widget/ReselectableSpinner.kt @@ -0,0 +1,46 @@ +package io.nekohasekai.sagernet.widget + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatSpinner + +class ReselectableSpinner @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : AppCompatSpinner(context, attrs) { + + var onPopupClosed: (() -> Unit)? = null + + /** + * Android's [Spinner] has no official "popup dismissed" callback, so we use + * the window regaining focus as a proxy: when the dropdown popup closes, + * focus returns to this spinner's window. This is intentionally a best-effort + * signal, not a precise one — [onPopupClosed] may also fire for unrelated + * focus changes (app foregrounding, keyboard/IME show-hide, system dialogs, + * permission prompts, orientation changes, etc.). Callers must therefore + * treat it only as a hint and stay correct if it fires spuriously (the + * OutboundPreference reselect logic does, by re-checking value/position). + */ + override fun onWindowFocusChanged(hasWindowFocus: Boolean) { + super.onWindowFocusChanged(hasWindowFocus) + if (hasWindowFocus) onPopupClosed?.invoke() + } + + override fun setSelection(position: Int) { + val reselected = position == selectedItemPosition + super.setSelection(position) + if (reselected) notifyReselected(position) + } + + override fun setSelection(position: Int, animate: Boolean) { + val reselected = position == selectedItemPosition + super.setSelection(position, animate) + if (reselected) notifyReselected(position) + } + + private fun notifyReselected(position: Int) { + if (position < 0) return + onItemSelectedListener?.onItemSelected( + this, selectedView, position, adapter?.getItemId(position) ?: 0L + ) + } +} diff --git a/app/src/main/res/drawable/ic_baseline_folder_open_24.xml b/app/src/main/res/drawable/ic_baseline_folder_open_24.xml new file mode 100644 index 000000000..941b84df6 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_folder_open_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/preference_dropdown_reselectable.xml b/app/src/main/res/layout/preference_dropdown_reselectable.xml new file mode 100644 index 000000000..b050d1d38 --- /dev/null +++ b/app/src/main/res/layout/preference_dropdown_reselectable.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/xml/webdav_preferences.xml b/app/src/main/res/xml/webdav_preferences.xml index cfd8639a5..283bf41c5 100644 --- a/app/src/main/res/xml/webdav_preferences.xml +++ b/app/src/main/res/xml/webdav_preferences.xml @@ -4,24 +4,28 @@