Skip to content

Commit 93e8181

Browse files
committed
Do not allow selecting less than 2 assistants for selector
- Organize and refactor constants
1 parent e9b9b7d commit 93e8181

File tree

9 files changed

+222
-16
lines changed

9 files changed

+222
-16
lines changed

app/src/main/java/com/wstxda/switchai/fragment/SettingsFragment.kt

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import androidx.preference.PreferenceCategory
1414
import androidx.preference.PreferenceFragmentCompat
1515
import com.wstxda.switchai.R
1616
import com.wstxda.switchai.activity.LibraryActivity
17+
import com.wstxda.switchai.fragment.preferences.MultiSelectListPreference
1718
import com.wstxda.switchai.fragment.preferences.DigitalAssistantPreference
1819
import com.wstxda.switchai.ui.TileManager
20+
import com.wstxda.switchai.ui.component.AssistantManagerDialog
1921
import com.wstxda.switchai.ui.component.DigitalAssistantSetupDialog
2022
import com.wstxda.switchai.utils.Constants
2123
import com.wstxda.switchai.viewmodel.SettingsViewModel
@@ -46,7 +48,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
4648

4749
private fun observeViewModel() {
4850
viewModel.isAssistSetupDone.observe(this) { isDone ->
49-
findPreference<Preference>(Constants.DIGITAL_ASSISTANT_SETUP_PREF_KEY)?.isVisible = !isDone
51+
findPreference<Preference>(Constants.DIGITAL_ASSISTANT_SETUP_PREF_KEY)?.isVisible =
52+
!isDone
5053
}
5154
}
5255

@@ -77,7 +80,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
7780
private fun setupInitialVisibility() {
7881
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
7982
findPreference<Preference>(Constants.DIGITAL_ASSISTANT_TILE_PREF_KEY)?.isVisible = false
80-
findPreference<PreferenceCategory>(Constants.SETTINGS_CATEGORY_SHORTCUTS)?.isVisible = false
83+
findPreference<PreferenceCategory>(Constants.SETTINGS_CATEGORY_SHORTCUTS)?.isVisible =
84+
false
8185
}
8286
}
8387

@@ -102,6 +106,20 @@ class SettingsFragment : PreferenceFragmentCompat() {
102106
}
103107
}
104108

109+
override fun onDisplayPreferenceDialog(preference: Preference) {
110+
val dialogFragment = parentFragmentManager.findFragmentByTag(Constants.PREFERENCE_DIALOG)
111+
112+
if (dialogFragment != null) return
113+
114+
if (preference is MultiSelectListPreference) {
115+
val dialog = AssistantManagerDialog.newInstance(preference.key)
116+
@Suppress("DEPRECATION") dialog.setTargetFragment(this, 0)
117+
dialog.show(parentFragmentManager, Constants.PREFERENCE_DIALOG)
118+
} else {
119+
super.onDisplayPreferenceDialog(preference)
120+
}
121+
}
122+
105123
private fun setupLibraryPreference() {
106124
findPreference<Preference>(Constants.LIBRARY_PREF_KEY)?.setOnPreferenceClickListener {
107125
val intent = Intent(requireContext(), LibraryActivity::class.java)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.wstxda.switchai.fragment.preferences
2+
3+
import android.content.Context
4+
import android.util.AttributeSet
5+
import androidx.core.content.withStyledAttributes
6+
import androidx.preference.MultiSelectListPreference
7+
import com.wstxda.switchai.R
8+
9+
class MultiSelectListPreference @JvmOverloads constructor(
10+
context: Context,
11+
attrs: AttributeSet? = null,
12+
) : MultiSelectListPreference(context, attrs) {
13+
14+
var minSelection = 1
15+
var maxSelection = Int.MAX_VALUE
16+
17+
init {
18+
context.withStyledAttributes(attrs, R.styleable.CustomMultiSelectListPreference) {
19+
minSelection =
20+
getInt(R.styleable.CustomMultiSelectListPreference_minSelection, 1).coerceAtLeast(1)
21+
maxSelection = getInt(
22+
R.styleable.CustomMultiSelectListPreference_maxSelection, Int.MAX_VALUE
23+
).coerceAtLeast(minSelection)
24+
}
25+
}
26+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.wstxda.switchai.ui.component
2+
3+
import android.os.Bundle
4+
import android.widget.Toast
5+
import androidx.appcompat.app.AlertDialog
6+
import androidx.preference.PreferenceDialogFragmentCompat
7+
import com.wstxda.switchai.R
8+
import com.wstxda.switchai.fragment.preferences.MultiSelectListPreference
9+
import com.wstxda.switchai.utils.Constants
10+
11+
class AssistantManagerDialog : PreferenceDialogFragmentCompat() {
12+
13+
private lateinit var pref: MultiSelectListPreference
14+
private val currentSelections = HashSet<String>()
15+
private var toastShownForInvalidSelection = false
16+
17+
companion object {
18+
fun newInstance(key: String) = AssistantManagerDialog().apply {
19+
arguments = Bundle().apply { putString(ARG_KEY, key) }
20+
}
21+
}
22+
23+
override fun onCreate(savedInstanceState: Bundle?) {
24+
super.onCreate(savedInstanceState)
25+
pref = preference as MultiSelectListPreference
26+
27+
currentSelections.clear()
28+
val restored = savedInstanceState?.getStringArray(Constants.ASSISTANT_MANAGER_DIALOG)
29+
if (restored != null) {
30+
currentSelections.addAll(restored)
31+
} else {
32+
currentSelections.addAll(pref.values)
33+
}
34+
}
35+
36+
override fun onSaveInstanceState(outState: Bundle) {
37+
super.onSaveInstanceState(outState)
38+
outState.putStringArray(
39+
Constants.ASSISTANT_MANAGER_DIALOG, currentSelections.toTypedArray()
40+
)
41+
}
42+
43+
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
44+
super.onPrepareDialogBuilder(builder)
45+
46+
val entries = pref.entries
47+
val entryValues = pref.entryValues
48+
val checkedItems = BooleanArray(entryValues.size) { index ->
49+
currentSelections.contains(entryValues[index].toString())
50+
}
51+
52+
builder.setMultiChoiceItems(entries, checkedItems) { _, which, isChecked ->
53+
val value = entryValues[which].toString()
54+
if (isChecked) currentSelections.add(value) else currentSelections.remove(value)
55+
updatePositiveButtonState()
56+
}
57+
}
58+
59+
override fun onStart() {
60+
super.onStart()
61+
updatePositiveButtonState()
62+
}
63+
64+
private fun updatePositiveButtonState() {
65+
val dialog = dialog as? AlertDialog ?: return
66+
val okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
67+
68+
val selectionCount = currentSelections.size
69+
val valid = selectionCount in pref.minSelection..pref.maxSelection
70+
okButton.isEnabled = valid
71+
72+
if (!valid) {
73+
if (!toastShownForInvalidSelection) {
74+
toastShownForInvalidSelection = true
75+
76+
val messageRes = when {
77+
selectionCount < pref.minSelection -> R.string.error_min_selection
78+
selectionCount > pref.maxSelection -> R.string.error_max_selection
79+
else -> 0
80+
}
81+
82+
if (messageRes != 0) {
83+
Toast.makeText(
84+
requireContext(), getString(
85+
messageRes,
86+
if (selectionCount < pref.minSelection) pref.minSelection else pref.maxSelection
87+
), Toast.LENGTH_SHORT
88+
).show()
89+
}
90+
}
91+
} else {
92+
toastShownForInvalidSelection = false
93+
}
94+
}
95+
96+
override fun onDialogClosed(positiveResult: Boolean) {
97+
if (positiveResult) {
98+
if (pref.callChangeListener(currentSelections)) {
99+
pref.values = currentSelections
100+
}
101+
}
102+
}
103+
}
Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,51 @@
11
package com.wstxda.switchai.utils
22

33
object Constants {
4+
5+
// Preferences keys
6+
47
const val DIGITAL_ASSISTANT_SETUP_PREF_KEY = "digital_assistant_setup"
58
const val DIGITAL_ASSISTANT_SELECT_PREF_KEY = "digital_assistant_select"
69
const val DIGITAL_ASSISTANT_TILE_PREF_KEY = "digital_assistant_tile"
710
const val ASSISTANT_SELECTOR_DIALOG_PREF_KEY = "assistant_selector_dialog"
8-
const val ASSISTANT_VISIBILITY_DIALOG_PREF_KEY = "assistant_selector_visibility"
11+
const val ASSISTANT_MANAGER_DIALOG_PREF_KEY = "assistant_selector_manager"
912
const val ASSISTANT_ROOT_PREF_KEY = "assistant_root"
1013
const val LIBRARY_PREF_KEY = "library"
1114
const val THEME_PREF_KEY = "select_theme"
1215

16+
// Theme values
17+
1318
const val THEME_SYSTEM = "system"
1419
const val THEME_LIGHT = "light"
1520
const val THEME_DARK = "dark"
1621

22+
// Used to hide category view with digital assistant tile preference
23+
24+
const val SETTINGS_CATEGORY_SHORTCUTS = "shortcuts"
25+
26+
// Dialog fragments
27+
28+
const val ASSISTANT_MANAGER_DIALOG = "AssistantManagerDialog"
1729
const val DIGITAL_ASSISTANT_DIALOG = "DigitalAssistantSetupDialog"
1830
const val DIGITAL_ASSISTANT_SELECTOR_DIALOG = "AssistantSelectorBottomSheet"
31+
const val PREFERENCE_DIALOG = "PreferenceDialog"
32+
33+
// Constants for preferences
1934

2035
const val IS_ASSIST_SETUP_DONE = "is_assist_setup_done"
36+
const val PREFS_NAME = "assistant_selector_prefs"
2137

22-
// Used to hide category view with digital assistant tile preference
23-
const val SETTINGS_CATEGORY_SHORTCUTS = "shortcuts"
38+
// AssistantSelector recyclerView
2439

2540
const val VIEW_TYPE_CATEGORY_HEADER = 0
2641
const val VIEW_TYPE_ASSISTANT_ITEM = 1
27-
28-
const val PREFS_NAME = "assistant_selector_prefs"
42+
// Maximum number of recently used assistants
2943
const val CAT_MAX_RECENTLY_USED = 3
44+
// Category for AssistantSelectorBottomSheet
3045
const val CAT_PINNED_ASSISTANTS_KEY = "pinned_assistants"
3146
const val CAT_RECENTLY_USED_ASSISTANTS_KEY = "recently_used_assistants"
3247

48+
// GitHub API releases URL
49+
3350
const val GITHUB_RELEASE_URL = "https://api.github.com/repos/WSTxda/SwitchAI/releases/latest"
3451
}

app/src/main/java/com/wstxda/switchai/viewmodel/AssistantSelectorViewModel.kt

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ class AssistantSelectorViewModel(application: Application) : AndroidViewModel(ap
4444
defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this)
4545

4646
val loadedPinnedKeys =
47-
assistantStatePreferences.getStringSet(CAT_PINNED_ASSISTANTS_KEY, emptySet()) ?: emptySet()
47+
assistantStatePreferences.getStringSet(CAT_PINNED_ASSISTANTS_KEY, emptySet())
48+
?: emptySet()
4849
pinnedAssistantKeys.clear()
4950
pinnedAssistantKeys.addAll(loadedPinnedKeys)
5051

@@ -125,7 +126,7 @@ class AssistantSelectorViewModel(application: Application) : AndroidViewModel(ap
125126
val defaultVisibleAssistants =
126127
resources.getStringArray(R.array.assistant_visibility_values).toSet()
127128
val visibleAssistantKeys = preferenceHelper.getStringSet(
128-
Constants.ASSISTANT_VISIBILITY_DIALOG_PREF_KEY, defaultVisibleAssistants
129+
Constants.ASSISTANT_MANAGER_DIALOG_PREF_KEY, defaultVisibleAssistants
129130
)
130131

131132
val allVisibleAssistantDetails =
@@ -154,20 +155,38 @@ class AssistantSelectorViewModel(application: Application) : AndroidViewModel(ap
154155
pinnedItems.add(AssistantSelectorRecyclerView.AssistantSelector(item.copy(isPinned = true)))
155156
}
156157
if (pinnedItems.isNotEmpty()) {
157-
finalRecyclerViewItems.add(AssistantSelectorRecyclerView.CategoryHeader(context.getString(R.string.assistant_category_pin)))
158+
finalRecyclerViewItems.add(
159+
AssistantSelectorRecyclerView.CategoryHeader(
160+
context.getString(
161+
R.string.assistant_category_pin
162+
)
163+
)
164+
)
158165
finalRecyclerViewItems.addAll(pinnedItems)
159166
}
160167

161168
val recentItems = mutableListOf<AssistantSelectorRecyclerView.AssistantSelector>()
162169
recentlyUsedAssistants.forEach { (key, timestamp) ->
163170
if (visibleAssistantKeys.contains(key) && !pinnedAssistantKeys.contains(key)) {
164171
allVisibleAssistantDetails.find { it.key == key }?.let { item ->
165-
recentItems.add(AssistantSelectorRecyclerView.AssistantSelector(item.copy(lastUsedTimestamp = timestamp)))
172+
recentItems.add(
173+
AssistantSelectorRecyclerView.AssistantSelector(
174+
item.copy(
175+
lastUsedTimestamp = timestamp
176+
)
177+
)
178+
)
166179
}
167180
}
168181
}
169182
if (recentItems.isNotEmpty()) {
170-
finalRecyclerViewItems.add(AssistantSelectorRecyclerView.CategoryHeader(context.getString(R.string.assistant_category_recent)))
183+
finalRecyclerViewItems.add(
184+
AssistantSelectorRecyclerView.CategoryHeader(
185+
context.getString(
186+
R.string.assistant_category_recent
187+
)
188+
)
189+
)
171190
finalRecyclerViewItems.addAll(recentItems)
172191
}
173192

@@ -181,15 +200,21 @@ class AssistantSelectorViewModel(application: Application) : AndroidViewModel(ap
181200
}
182201

183202
if (otherItems.isNotEmpty()) {
184-
finalRecyclerViewItems.add(AssistantSelectorRecyclerView.CategoryHeader(context.getString(R.string.assistant_category_all)))
203+
finalRecyclerViewItems.add(
204+
AssistantSelectorRecyclerView.CategoryHeader(
205+
context.getString(
206+
R.string.assistant_category_all
207+
)
208+
)
209+
)
185210
finalRecyclerViewItems.addAll(otherItems)
186211
}
187212

188213
_assistantItems.value = finalRecyclerViewItems
189214
}
190215

191216
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
192-
if (key == Constants.ASSISTANT_VISIBILITY_DIALOG_PREF_KEY) {
217+
if (key == Constants.ASSISTANT_MANAGER_DIALOG_PREF_KEY) {
193218
loadAssistants()
194219
}
195220
}

app/src/main/res/values-pt-rBR/strings.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,9 @@
103103
<!--root permission-->
104104

105105
<string name="root_access_error">Conceda permissão root primeiro</string>
106+
107+
<!--multi select list preference selection-->
108+
109+
<string name="error_min_selection">Selecione pelo menos %1$d opções</string>
110+
<string name="error_max_selection">Selecione no máximo %1$d opções</string>
106111
</resources>

app/src/main/res/values/attrs.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<resources>
2+
<declare-styleable name="CustomMultiSelectListPreference">
3+
<attr name="minSelection" format="integer" />
4+
<attr name="maxSelection" format="integer" />
5+
</declare-styleable>
6+
</resources>

app/src/main/res/values/strings.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,9 @@
107107
<!--root permission-->
108108

109109
<string name="root_access_error">Grant root permission first</string>
110+
111+
<!--multi select list preference selection-->
112+
113+
<string name="error_min_selection">Select at least %1$d options</string>
114+
<string name="error_max_selection">Select a maximum of %1$d options</string>
110115
</resources>

app/src/main/res/xml/main_preferences.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,16 @@
3333
android:title="@string/pref_assistant_selector"
3434
app:layout="@layout/preference_material_top" />
3535

36-
<MultiSelectListPreference
36+
<com.wstxda.switchai.fragment.preferences.MultiSelectListPreference
3737
android:dependency="assistant_selector_dialog"
3838
android:entries="@array/assistant_entries"
3939
android:entryValues="@array/assistant_values"
40-
android:key="assistant_selector_visibility"
40+
android:key="assistant_selector_manager"
4141
android:layout="@layout/preference_material_bottom"
4242
android:title="@string/pref_manage_assistant"
4343
app:defaultValue="@array/assistant_visibility_values"
4444
app:dialogIcon="@drawable/ic_assistant"
45+
app:minSelection="2"
4546
app:summary="@string/pref_manage_assistant_summary" />
4647

4748
<PreferenceCategory

0 commit comments

Comments
 (0)