Skip to content

Commit bb41f23

Browse files
Feat: Add file picker UI
1 parent 5d54567 commit bb41f23

File tree

3 files changed

+133
-2
lines changed

3 files changed

+133
-2
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright (c) 2024 David Allison <davidallisongithub@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.preferences
18+
19+
import android.content.Context
20+
import android.os.Build
21+
import android.os.Looper
22+
import android.util.AttributeSet
23+
import androidx.fragment.app.DialogFragment
24+
import androidx.preference.EditTextPreference
25+
import androidx.preference.R
26+
import com.ichi2.anki.CollectionHelper
27+
import com.ichi2.utils.Permissions
28+
import java.io.File
29+
30+
class ExternalDirectorySelectionPreference(
31+
context: Context,
32+
attrs: AttributeSet?,
33+
) : EditTextPreference(
34+
context,
35+
attrs,
36+
styleAttr(context),
37+
android.R.attr.dialogPreferenceStyle,
38+
),
39+
ListPreferenceTrait {
40+
private var forceCustomSelector = false
41+
42+
private val initialLayoutResId = dialogLayoutResource
43+
44+
override fun makeDialogFragment(): DialogFragment? {
45+
val useTextBox = (forceCustomSelector || isEditText(context))
46+
forceCustomSelector = false
47+
if (useTextBox) {
48+
dialogLayoutResource = R.layout.preference_dialog_edittext
49+
return null
50+
} else {
51+
dialogLayoutResource = initialLayoutResId
52+
return FullWidthListPreferenceDialogFragment()
53+
}
54+
}
55+
56+
// only used if a user has no file access
57+
override var listEntries: List<ListPreferenceTrait.Entry> =
58+
CollectionHelper
59+
.getAppSpecificExternalDirectories(context)
60+
.filterNotNull()
61+
.let { dirs ->
62+
// This should ALWAYS start with the current AnkiDroid directory
63+
listOf(File(CollectionHelper.getDefaultAnkiDroidDirectory(context).toString())) +
64+
dirs.flatMap { findAnkiDroidSubDirectories(it) }
65+
}.distinct()
66+
.map {
67+
val label = it.absolutePath.replace("/Android/", "\n/Android/")
68+
ListPreferenceTrait.Entry(label, it.absolutePath)
69+
}.toMutableList()
70+
.apply { add(ListPreferenceTrait.Entry("Select custom path...", "CUSTOM_PATH_SENTINEL")) }
71+
72+
override var listValue: String = getPersistedString(listEntries.firstOrNull()?.value ?: "")
73+
74+
override fun callChangeListener(newValue: Any?): Boolean {
75+
if (newValue == "CUSTOM_PATH_SENTINEL") {
76+
forceCustomSelector = true
77+
android.os.Handler(Looper.getMainLooper()).post {
78+
this.onClick()
79+
}
80+
return false
81+
}
82+
return super.callChangeListener(newValue)
83+
}
84+
85+
// In future, we may want to disable this is there is only one selection
86+
override fun isEnabled() = listEntries.isNotEmpty()
87+
88+
companion object {
89+
fun findAnkiDroidSubDirectories(f: File): List<File> {
90+
// either returns '/AnkiDroid1...AnkiDroid100' from storage migration
91+
// OR returns '/AnkiDroid' if no directories are available
92+
return (f.listFiles()?.toList() ?: listOf<File>())
93+
.filter { it.isDirectory }
94+
.filter { it.name.startsWith("AnkiDroid") }
95+
.ifEmpty { listOf(File(f, "AnkiDroid")) }
96+
}
97+
98+
/**
99+
* Determines the UI mode for the preference.
100+
*
101+
* @return true if the user has full storage access (MANAGE_EXTERNAL_STORAGE),
102+
* @return false if the user is restricted, forcing the Volume Picker UI.
103+
*/
104+
private fun isEditText(context: Context): Boolean {
105+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
106+
return true
107+
}
108+
return Permissions.canManageExternalStorage(context)
109+
}
110+
111+
fun styleAttr(context: Context): Int =
112+
if (isEditText(context)) {
113+
R.attr.editTextPreferenceStyle
114+
} else {
115+
ListPreferenceTrait.STYLE_ATTR
116+
}
117+
}
118+
}
119+
120+
/**
121+
* A custom DialogFragment that expands to fill the screen width.
122+
*/
123+
class FullWidthListPreferenceDialogFragment : ListPreferenceDialogFragment() {
124+
override fun onStart() {
125+
super.onStart()
126+
dialog?.window?.setLayout(
127+
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
128+
android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
129+
)
130+
}
131+
}

AnkiDroid/src/main/java/com/ichi2/preferences/ListPreferenceTrait.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ interface ListPreferenceTrait : DialogFragmentProvider {
8484
* Adapted from: [ListPreferenceDialogFragmentCompat]
8585
* @see ListPreferenceDialogFragmentCompat
8686
*/
87-
class ListPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
87+
open class ListPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
8888
// synthetic access
8989
private var clickedDialogEntryIndex = 0
9090
private lateinit var entries: Array<CharSequence>

AnkiDroid/src/main/res/xml/preferences_advanced.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
xmlns:app1="http://schemas.android.com/apk/res-auto"
2626
android:title="@string/pref_cat_advanced"
2727
android:key="@string/pref_advanced_screen_key">
28-
<EditTextPreference
28+
<com.ichi2.preferences.ExternalDirectorySelectionPreference
2929
android:defaultValue="/sdcard/AnkiDroid"
3030
android:key="@string/pref_ankidroid_directory_key"
3131
android:title="@string/col_path"

0 commit comments

Comments
 (0)