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
  • Loading branch information
BrayanDSO committed Nov 27, 2024
1 parent cf6faf7 commit c723f4b
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 111 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)
}
}
}
171 changes: 103 additions & 68 deletions AnkiDroid/src/main/java/com/ichi2/anki/preferences/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,80 +19,73 @@
****************************************************************************************/
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.activity.OnBackPressedCallback
import androidx.annotation.XmlRes
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
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.isWindowCompact
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(),
class PreferencesFragment :
Fragment(R.layout.preferences),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
SearchPreferenceResultListener {

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

enableToolbar().setDisplayHomeAsUpEnabled(true)

// Load initial fragment if activity is being first created
if (savedInstanceState == null) {
loadInitialFragment()
}
supportFragmentManager.addOnBackStackChangedListener {
// Expand bar in new fragments if scrolled to top
val fragment = supportFragmentManager.findFragmentById(R.id.settings_container)
as? PreferenceFragmentCompat ?: return@addOnBackStackChangedListener
fragment.listView.post {
val viewHolder = fragment.listView?.findViewHolderForAdapterPosition(0)
val isAtTop = viewHolder != null && viewHolder.itemView.top >= 0
findViewById<AppBarLayout>(R.id.appbar).setExpanded(isAtTop, false)
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (childFragmentManager.backStackEntryCount > 0) {
childFragmentManager.popBackStack()
} else {
requireActivity().finish()
}

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.isWindowCompact()) HeaderFragment() else GeneralSettingsFragment()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<MaterialToolbar>(R.id.toolbar)
.setNavigationOnClickListener { onBackPressedCallback.handleOnBackPressed() }

requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackPressedCallback)

// Load initial subscreen if activity is being first created
if (savedInstanceState == null) {
loadInitialSubscreen()
} else {
try {
getInstanceFromClassName<Fragment>(fragmentClassName)
} catch (e: Exception) {
throw RuntimeException("Failed to load $fragmentClassName", e)
childFragmentManager.findFragmentById(R.id.settings_container)?.let {
setFragmentTitleOnToolbar(it)
}
}
supportFragmentManager.commit {
// In tablets, show the headers fragment at the lateral navigation container
if (!resources.isWindowCompact()) {
replace(R.id.lateral_nav_container, HeaderFragment())

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

setFragmentTitleOnToolbar(fragment)

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

Expand All @@ -101,49 +94,91 @@ class Preferences :
pref: Preference
): Boolean {
// avoid reopening the same fragment if already active
val currentFragment = supportFragmentManager.findFragmentById(R.id.settings_container)
val currentFragment = childFragmentManager.findFragmentById(R.id.settings_container)
?: return true
if (pref.fragment == currentFragment::class.jvmName) return true

val fragment = supportFragmentManager.fragmentFactory.instantiate(
classLoader,
val fragment = childFragmentManager.fragmentFactory.instantiate(
requireActivity().classLoader,
pref.fragment ?: return true
)
fragment.arguments = pref.extras
supportFragmentManager.commit {
childFragmentManager.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) {
if (resources.isWindowCompact()) {
onBackPressedDispatcher.onBackPressed()
} else {
finish()
}
}
return true
}

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

override fun onSearchResultClicked(result: SearchPreferenceResult) {
val fragment = getFragmentFromXmlRes(result.resourceFile) ?: return

supportFragmentManager.popBackStack() // clear the search fragment from the backstack
supportFragmentManager.commit {
parentFragmentManager.popBackStack() // clear the search fragment from the backstack
childFragmentManager.commit {
replace(R.id.settings_container, fragment, fragment.javaClass.name)
addToBackStack(fragment.javaClass.name)
}

Timber.i("Highlighting key '%s' on %s", result.key, fragment)
result.highlight(fragment as PreferenceFragmentCompat)
}

private fun setFragmentTitleOnToolbar(fragment: Fragment) {
val title = if (fragment is TitleProvider) fragment.title else getString(R.string.settings)

view?.findViewById<CollapsingToolbarLayout>(R.id.collapsingToolbarLayout)?.title = title
view?.findViewById<MaterialToolbar>(R.id.toolbar)?.title = title
}

/**
* 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.isWindowCompact()) HeaderFragment() else GeneralSettingsFragment()
} else {
try {
getInstanceFromClassName<Fragment>(fragmentClassName)
} catch (e: Exception) {
throw RuntimeException("Failed to load $fragmentClassName", e)
}
}
childFragmentManager.commit {
// In big screens, show the headers fragment at the lateral navigation container
if (!resources.isWindowCompact()) {
replace(R.id.lateral_nav_container, HeaderFragment())
}
replace(R.id.settings_container, initialFragment, initialFragment::class.java.name)
}
}
}

/**
* 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 = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG)
if (fragment is SearchPreferenceResultListener) {
fragment.onSearchResultClicked(result)
}
}

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
4 changes: 2 additions & 2 deletions 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 All @@ -45,7 +45,7 @@
app:layout_collapseMode="pin"
app:navigationContentDescription="@string/abc_action_bar_up_description"
app:navigationIcon="?attr/homeAsUpIndicator"
tools:title="@string/settings"/>
app:title="@string/settings"/>

</com.google.android.material.appbar.CollapsingToolbarLayout>

Expand Down
Loading

0 comments on commit c723f4b

Please sign in to comment.