Skip to content

Commit

Permalink
refactor: move most of the Preferences activity into a fragment
Browse files Browse the repository at this point in the history
The activity wasn't fully removed because currently SearchConfiguration demands an activity that implements SearchPreferenceResultListener.

That probably can be changed in the future in the upstream library
  • Loading branch information
BrayanDSO committed Nov 23, 2024
1 parent 0e1c197 commit 259df21
Show file tree
Hide file tree
Showing 15 changed files with 119 additions and 110 deletions.
2 changes: 1 addition & 1 deletion AnkiDroid/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@
android:configChanges="orientation|screenSize"
android:exported="false" />
<activity
android:name="com.ichi2.anki.preferences.Preferences"
android:name="com.ichi2.anki.preferences.PreferencesActivity"
android:exported="false"
android:configChanges="screenSize"
>
Expand Down
3 changes: 1 addition & 2 deletions AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import androidx.core.content.edit
import com.ichi2.anki.AnkiDroidFolder.AppPrivateFolder
import com.ichi2.anki.exception.StorageAccessException
import com.ichi2.anki.exception.UnknownDatabaseVersionException
import com.ichi2.anki.preferences.Preferences
import com.ichi2.anki.preferences.sharedPrefs
import com.ichi2.libanki.Collection
import com.ichi2.libanki.DB
Expand All @@ -47,7 +46,7 @@ object CollectionHelper {
* This directory contains all AnkiDroid data and media for a given collection
* Except the Android preferences, cached files and [MetaDB]
*
* This can be changed by the [Preferences] screen
* This can be changed by the Preferences screen
* to allow a user to access a second collection via the same AnkiDroid app instance.
*
* The path also defines the collection that the AnkiDroid API accesses
Expand Down
3 changes: 2 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ import com.ichi2.anki.pages.AnkiPackageImporterFragment
import com.ichi2.anki.pages.CongratsPage
import com.ichi2.anki.pages.CongratsPage.Companion.onDeckCompleted
import com.ichi2.anki.preferences.AdvancedSettingsFragment
import com.ichi2.anki.preferences.PreferencesActivity
import com.ichi2.anki.preferences.sharedPrefs
import com.ichi2.anki.receiver.SdCardReceiver
import com.ichi2.anki.servicelayer.ScopedStorageService
Expand Down Expand Up @@ -788,7 +789,7 @@ open class DeckPicker :
convertDpToPixel(32F, this@DeckPicker).toInt()
)
positiveButton(R.string.open_settings) {
val settingsIntent = AdvancedSettingsFragment.getSubscreenIntent(this@DeckPicker)
val settingsIntent = PreferencesActivity.getIntent(this@DeckPicker, AdvancedSettingsFragment::class)
requestPathUpdateLauncher.launch(settingsIntent)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import androidx.drawerlayout.widget.DrawerLayout
import com.google.android.material.color.MaterialColors
import com.google.android.material.navigation.NavigationView
import com.ichi2.anki.dialogs.help.HelpDialog
import com.ichi2.anki.preferences.Preferences
import com.ichi2.anki.preferences.PreferencesActivity
import com.ichi2.anki.preferences.sharedPrefs
import com.ichi2.anki.workarounds.FullDraggableContainerFix
import com.ichi2.compat.CompatHelper
Expand Down Expand Up @@ -357,10 +357,9 @@ abstract class NavigationDrawerActivity :

/**
* Opens AnkiDroid's Settings Screen.
* @see Preferences
*/
protected fun openSettings() {
val intent = Intent(this, Preferences::class.java)
val intent = PreferencesActivity.getIntent(this)
preferencesLauncher.launch(intent)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package com.ichi2.anki.preferences

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.appcompat.app.AlertDialog
Expand Down Expand Up @@ -142,10 +141,4 @@ class AdvancedSettingsFragment : SettingsFragment() {
}
}
}

companion object {
fun getSubscreenIntent(context: Context): Intent {
return getSubscreenIntent(context, AdvancedSettingsFragment::class)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
*/
package com.ichi2.anki.preferences

import android.content.Context
import android.content.Intent
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
Expand Down Expand Up @@ -59,10 +57,4 @@ class CustomButtonsSettingsFragment : SettingsFragment() {
fun allKeys(): HashSet<String> {
return allPreferences().mapTo(hashSetOf()) { it.key }
}

companion object {
fun getSubscreenIntent(context: Context): Intent {
return getSubscreenIntent(context, CustomButtonsSettingsFragment::class)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@ class DevOptionsFragment : SettingsFragment() {
* Ensure that the preference is searchable or not
* based on the same condition at [HeaderFragment.configureSearchBar]
*/
enableDevOptionsPref.setOnPreferenceChangeListener { _, _ ->
showDisableDevOptionsDialog()
false
}
if (BuildConfig.DEBUG) {
enableDevOptionsPref.isVisible = false
enableDevOptionsPref.isVisible = true
} else {
enableDevOptionsPref.setOnPreferenceChangeListener { _, _ ->
showDisableDevOptionsDialog()
Expand Down Expand Up @@ -165,7 +169,7 @@ class DevOptionsFragment : SettingsFragment() {
* or if the user has enabled it with the secret on [com.ichi2.anki.preferences.AboutFragment]
*/
fun isEnabled(context: Context): Boolean {
return BuildConfig.DEBUG || context.sharedPrefs()
return context.sharedPrefs()
.getBoolean(context.getString(R.string.dev_options_enabled_by_user_key), false)
}
}
Expand Down
156 changes: 93 additions & 63 deletions AnkiDroid/src/main/java/com/ichi2/anki/preferences/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,121 +19,141 @@
****************************************************************************************/
package com.ichi2.anki.preferences

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.annotation.XmlRes
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentOnAttachListener
import androidx.fragment.app.commit
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.bytehamster.lib.preferencesearch.SearchConfiguration
import com.bytehamster.lib.preferencesearch.SearchPreferenceResult
import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.ichi2.anki.AnkiActivity
import com.google.android.material.appbar.MaterialToolbar
import com.ichi2.anki.R
import com.ichi2.anki.SingleFragmentActivity
import com.ichi2.anki.utils.isSw600dp
import com.ichi2.themes.setTransparentStatusBar
import com.ichi2.utils.getInstanceFromClassName
import timber.log.Timber
import kotlin.reflect.KClass
import kotlin.reflect.jvm.jvmName

class Preferences :
AnkiActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
SearchPreferenceResultListener {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.preferences)
setTransparentStatusBar()

enableToolbar().setDisplayHomeAsUpEnabled(true)
class PreferencesFragment :
Fragment(R.layout.preferences),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<MaterialToolbar>(R.id.toolbar).apply {
setNavigationOnClickListener {
if (resources.isSw600dp()) {
requireActivity().finish()
} else {
requireActivity().onBackPressedDispatcher.onBackPressed()
}
}
}

// Load initial fragment if activity is being first created
// Load initial subscreen if activity is being first created
if (savedInstanceState == null) {
loadInitialFragment()
loadInitialSubscreen()
} else {
parentFragmentManager.findFragmentById(R.id.settings_container)?.let {
setFragmentTitleOnToolbar(it)
}
}
supportFragmentManager.addOnBackStackChangedListener {

parentFragmentManager.addOnBackStackChangedListener {
val fragment = parentFragmentManager.findFragmentById(R.id.settings_container)
?: return@addOnBackStackChangedListener

setFragmentTitleOnToolbar(fragment)

// Expand bar in new fragments if scrolled to top
val fragment = supportFragmentManager.findFragmentById(R.id.settings_container)
as? PreferenceFragmentCompat ?: return@addOnBackStackChangedListener
fragment.listView.post {
(fragment as? PreferenceFragmentCompat)?.listView?.post {
val viewHolder = fragment.listView?.findViewHolderForAdapterPosition(0)
val isAtTop = viewHolder != null && viewHolder.itemView.top >= 0
findViewById<AppBarLayout>(R.id.appbar).setExpanded(isAtTop, false)
view.findViewById<AppBarLayout>(R.id.appbar).setExpanded(isAtTop, false)
}

val title = if (fragment is TitleProvider) fragment.title else ""
findViewById<CollapsingToolbarLayout>(R.id.collapsingToolbarLayout)?.title = title
supportActionBar?.title = title
}
}

/**
* Starts the first fragment for the [Preferences] activity,
* which by default is [HeaderFragment].
* The initial fragment may be overridden by putting the java class name
* of the fragment on an intent extra with the key [INITIAL_FRAGMENT_EXTRA]
*/
private fun loadInitialFragment() {
val fragmentClassName = intent?.getStringExtra(INITIAL_FRAGMENT_EXTRA)
val initialFragment = if (fragmentClassName == null) {
if (resources.isSw600dp()) GeneralSettingsFragment() else HeaderFragment()
} else {
try {
getInstanceFromClassName<Fragment>(fragmentClassName)
} catch (e: Exception) {
throw RuntimeException("Failed to load $fragmentClassName", e)
}
}
supportFragmentManager.commit {
// In tablets, show the headers fragment at the lateral navigation container
if (resources.isSw600dp()) {
replace(R.id.lateral_nav_container, HeaderFragment())
replace(R.id.settings_container, initialFragment, initialFragment::class.java.name)
} else {
replace(R.id.settings_container, initialFragment, initialFragment::class.java.name)
}
private fun setFragmentTitleOnToolbar(fragment: Fragment) {
val title = when (fragment) {
is TitleProvider -> fragment.title
else -> getString(R.string.settings)
}
view?.findViewById<CollapsingToolbarLayout>(R.id.collapsingToolbarLayout)?.title = title
}

override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
// avoid reopening the same fragment if already active
val currentFragment = supportFragmentManager.findFragmentById(R.id.settings_container)
val currentFragment = parentFragmentManager.findFragmentById(R.id.settings_container)
?: return true
if (pref.fragment == currentFragment::class.jvmName) return true

val fragment = supportFragmentManager.fragmentFactory.instantiate(
classLoader,
val fragment = parentFragmentManager.fragmentFactory.instantiate(
requireContext().classLoader,
pref.fragment ?: return true
)
fragment.arguments = pref.extras
supportFragmentManager.commit {
parentFragmentManager.commit {
replace(R.id.settings_container, fragment, fragment::class.jvmName)
addToBackStack(null)
}
return true
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
/**
* Starts the first settings fragment, which by default is [HeaderFragment].
* The initial fragment may be overridden by putting the java class name
* of the fragment on an intent extra with the key [INITIAL_FRAGMENT_EXTRA]
*/
private fun loadInitialSubscreen() {
val fragmentClassName = arguments?.getString(INITIAL_FRAGMENT_EXTRA)
val initialFragment = if (fragmentClassName == null) {
if (resources.isSw600dp()) GeneralSettingsFragment() else HeaderFragment()
} else {
try {
getInstanceFromClassName<Fragment>(fragmentClassName)
} catch (e: Exception) {
throw RuntimeException("Failed to load $fragmentClassName", e)
}
}
parentFragmentManager.addFragmentOnAttachListener(object : FragmentOnAttachListener {
override fun onAttachFragment(fragmentManager: FragmentManager, fragment: Fragment) {
setFragmentTitleOnToolbar(fragment)
fragmentManager.removeFragmentOnAttachListener(this)
}
})
parentFragmentManager.commit {
// In big screens, show the headers fragment at the lateral navigation container
if (resources.isSw600dp()) {
finish()
replace(R.id.lateral_nav_container, HeaderFragment())
replace(R.id.settings_container, initialFragment, initialFragment::class.java.name)
} else {
onBackPressedDispatcher.onBackPressed()
replace(R.id.settings_container, initialFragment, initialFragment::class.java.name)
}
}
return true
}
}

// ----------------------------------------------------------------------------
// Class methods
// ----------------------------------------------------------------------------

/**
* Host activity for [PreferencesFragment].
*
* Only necessary because [SearchConfiguration] demands an activity that implements
* [SearchPreferenceResultListener].
*/
class PreferencesActivity : SingleFragmentActivity(), SearchPreferenceResultListener {
override fun onSearchResultClicked(result: SearchPreferenceResult) {
val fragment = getFragmentFromXmlRes(result.resourceFile) ?: return

Expand All @@ -146,6 +166,16 @@ class Preferences :
Timber.i("Highlighting key '%s' on %s", result.key, fragment)
result.highlight(fragment as PreferenceFragmentCompat)
}

companion object {
fun getIntent(context: Context, initialFragment: KClass<out SettingsFragment>? = null): Intent {
val arguments = bundleOf(INITIAL_FRAGMENT_EXTRA to initialFragment?.jvmName)
return Intent(context, PreferencesActivity::class.java).apply {
putExtra(FRAGMENT_NAME_EXTRA, PreferencesFragment::class.jvmName)
putExtra(FRAGMENT_ARGS_EXTRA, arguments)
}
}
}
}

interface TitleProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
*/
package com.ichi2.anki.preferences

import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import androidx.annotation.VisibleForTesting
Expand All @@ -31,8 +29,6 @@ import com.ichi2.anki.analytics.UsageAnalytics
import com.ichi2.preferences.DialogFragmentProvider
import timber.log.Timber
import java.lang.NumberFormatException
import kotlin.reflect.KClass
import kotlin.reflect.jvm.jvmName

abstract class SettingsFragment :
PreferenceFragmentCompat(),
Expand Down Expand Up @@ -132,12 +128,6 @@ abstract class SettingsFragment :
}

companion object {
@JvmStatic // Using protected members which are not @JvmStatic in the superclass companion is unsupported yet
protected fun getSubscreenIntent(context: Context, fragmentClass: KClass<out SettingsFragment>): Intent {
return Intent(context, Preferences::class.java)
.putExtra(INITIAL_FRAGMENT_EXTRA, fragmentClass.jvmName)
}

/**
* Converts a preference value to a numeric number that
* can be reported to analytics, since analytics events only accept
Expand Down
2 changes: 1 addition & 1 deletion AnkiDroid/src/main/res/layout/preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".preferences.Preferences">
tools:context=".preferences.PreferencesFragment">

<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
Expand Down
Loading

0 comments on commit 259df21

Please sign in to comment.