1

I want to use a preference summary to show the preference's current value, so I want to update the summary whenever the preference is changed. The preference in question is a storage location, chosen interactively by the user via an intent, using Android's Storage Access Framework. I've been beating my head over this for hours, trying all sorts of things found in SO threads, but I just can't figure out what combination of setSummary,findPreference, onSharedPreferenceChanged, onSharedPreferenceChangeListener, invoked in which class, I need.

My code currently looks something like this:

const val REQUEST_TARGET_FOLDER = 4

class SettingsActivity : AppCompatActivity() {

    private lateinit var prefs: SharedPreferences
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.settings_activity)
        if (savedInstanceState == null) {
            supportFragmentManager
                .beginTransaction()
                .replace(R.id.settings, SettingsFragment())
                .commit()
        }

    }

    class SettingsFragment : PreferenceFragmentCompat() {
        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
            setPreferencesFromResource(R.xml.root_preferences, rootKey)
            // from: https://stackoverflow.com/questions/63575398/how-to-correctly-receive-and-store-a-local-directory-path-in-android-preferences
            val targetDirPreference: Preference? = findPreference("export_dir")
            targetDirPreference?.setOnPreferenceClickListener {
                val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
                activity?.startActivityForResult(intent, REQUEST_TARGET_FOLDER)
                true
            }
        }

        override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
            super.onActivityResult(requestCode, resultCode, intent)
            // from: https://stackoverflow.com/questions/34331956/trying-to-takepersistableuripermission-fails-for-custom-documentsprovider-via
            if (requestCode == REQUEST_TARGET_FOLDER && resultCode == RESULT_OK && intent != null) {
                val treeUri = intent.data
                if (treeUri != null) {
                    // do stuff
                }
                with(prefs.edit()) {
                    putString("export_dir", intent.data.toString())
                    apply()
                }
            }
        }
    }

This is the preference involved:

   <Preference
        android:key="export_dir"
        android:title="Export to directory:" />

Can someone please help me figure out what to do to set / update the preference's summary when the user selects a directory? (The directory selection part itself currently works.)

atrocia6
  • 319
  • 1
  • 8

1 Answers1

2

Since you are manually changing the setting outside of the Preference itself, you cannot do this with a SummaryProvider. Instead, you must manually change the summary both (1) when the summary first appears and (2) when you manually change the preference value and commit it. (You could use an OnSharedPreferenceChangeListener to do the second step automatically, but that's more complicated.)

So, create a function that updates its summary and call it in both places: in onCreatePreferences and in onActivityResult where you are setting the value.

By the way you can use preferences.edit { ... } extension function instead of with(preferences.edit) { ... ; apply() } for simpler code.

class SettingsFragment : PreferenceFragmentCompat() {

    private val TARGET_DIR_KEY = "export_dir"
    private val prefs by lazy { preferenceManager.sharedPreferences }
    private val targetDirPreference: Preference by lazy {
        findPreference<Preference>(TARGET_DIR_KEY) ?: error("Missing target directory preference!")
    }

    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.root_preferences, rootKey)

        targetDirPreference.setOnPreferenceClickListener {
            val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
            startActivityForResult(intent, REQUEST_TARGET_FOLDER)
            true
        }

        updateTargetDirPreferenceSummary()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
        super.onActivityResult(requestCode, resultCode, intent)
        // from: https://stackoverflow.com/questions/34331956/trying-to-takepersistableuripermission-fails-for-custom-documentsprovider-via
        if (requestCode == REQUEST_TARGET_FOLDER && resultCode == RESULT_OK && intent != null) {
            val treeUri = intent.data
            if (treeUri != null) {
                // do stuff
            }
            prefs.edit {
                putString(TARGET_DIR_KEY, intent.data.toString())
            }
            updateTargetDirPreferenceSummary()
        }
    }

    private fun updateTargetDirPreferenceSummary() {
        targetDirPreference.summary = prefs.getString("feedback", "")
    }
}

OR, if you want to solve this in a way that provides cleaner code in your Fragment, you can create a subclass of Preference that helps manage the changing of the setting value and internally uses a SummaryProvider mechanism to automatically update itself.

class ManualStringPreference @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
): Preference(context, attrs) {

    init {
        setSummaryProvider { getPersistedString("") }
    }

    var value: String = ""
        set(inValue) {
            if (inValue != field) {
                field = inValue
                persistString(inValue)
                notifyChanged()
            }
        }

    override fun onSetInitialValue(defaultValue: Any?) {
        value = getPersistedString(defaultValue as? String ?: "")
    }

}

You need to set this as your preference type in your XML.

Then your Fragment looks like this. Notice that you change the SharedPreferences value through the Preference subclass you created.

class SettingsFragment : PreferenceFragmentCompat() {

    private val prefs by lazy { preferenceManager.sharedPreferences }
    private val targetDirPreference: ManualStringPreference by lazy {
        findPreference<ManualStringPreference>("export_dir") ?: error("Missing target directory preference!")
    }

    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.root_preferences, rootKey)

        targetDirPreference.setOnPreferenceClickListener {
            val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
            startActivityForResult(intent, REQUEST_TARGET_FOLDER)
            true
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
        super.onActivityResult(requestCode, resultCode, intent)
        // from: https://stackoverflow.com/questions/34331956/trying-to-takepersistableuripermission-fails-for-custom-documentsprovider-via
        if (requestCode == REQUEST_TARGET_FOLDER && resultCode == RESULT_OK && intent != null) {
            val treeUri = intent.data
            if (treeUri != null) {
                // do stuff
            }
            targetDirPreference.value = intent.data.toString()
        }
    }

}
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Thank you, but neither of your suggestions worked when I tried them. The `prefs.edit` syntax generates these errors: `Too many arguments for public abstract fun edit(): SharedPreferences.Editor! defined in android.content.SharedPreferences` and `Unresolved reference: putString` – atrocia6 Jan 21 '22 at 17:40
  • The `updateTargetDirPreferenceSummary` function generates these errors: for `findPreference`, `Not enough information to infer type variable T`, and for `summary`, `Variable expected`. I must be doing something wrong, but I've been hitting these sorts of errors for hours now - am I just putting some class or function in the wrong place? – atrocia6 Jan 21 '22 at 17:44
  • That was just a suggestion. You don’t have to change that piece of your code. It requires importing the extension function and you need the -ktx version of the Jetpack code library in your dependencies to be able to use it. – Tenfour04 Jan 21 '22 at 17:44
  • It should be `findPreference(`. Sorry, didn’t test that part and assumed the compiler could infer it. – Tenfour04 Jan 21 '22 at 17:46
  • Ah, okay. I have the `-ktx` stuff; I had just forgotten the import. Sorry for the noise. I added the type declaration and that fixed the other function. – atrocia6 Jan 21 '22 at 17:49
  • Thanks much, but I'm still missing a major piece here. When I define `onActivityResult` inside `SettingsFragment`, as in the code samples above, it doesn't get called at all - the intent fires, the user chooses a directory, but `onActivityResult` doesn't get called. When I define it inside `SettingsActivity`, then it gets called and runs successfully - but then it can't call `updateTargetPreferenceSummary`, defined inside `SettingsFragment`. – atrocia6 Jan 23 '22 at 03:00
  • I tried calling it as `SettingsFragment().updateTargetDirPreferenceSummary()` - that works, but then throws `java.lang.RuntimeException: Failure delivering result ResultInfo ... Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.SharedPreferences androidx.preference.PreferenceManager.getSharedPreferences()' on a null object reference` – atrocia6 Jan 23 '22 at 03:01
  • Instantiating some new copy of SettingsFragment is not the way to go. Lots of the Fragment properties will be invalid if it hasn't been passed through Fragment manager, because none of its lifecycle methods will have been called. It also doesn't make sense because that other fragment instance would not be the one that's being shown on screen. I think (not 100% sure) `Fragment.onActivityResult` is only called after a call to `Fragment.startActivityForResult` from the same Fragment. You're calling `Activity.startActivityForResult`, so your Fragment doesn't get informed of the result. – Tenfour04 Jan 23 '22 at 03:06
  • Okay, so if I can't instantiate a new copy of `SettingsFragment`, and I can't put `onActivityResult` inside `SettingsFragment`, and I can't call `updateTargetDirPreferenceSummary` outside `SettingsFragment`, then how am I supposed to make this work? – atrocia6 Jan 23 '22 at 04:50
  • Read my comment again and see my updated code. – Tenfour04 Jan 23 '22 at 05:25