Skip to content

Commit

Permalink
UpdateSettingsFragment: Migrate to Jetpack Compose (#586)
Browse files Browse the repository at this point in the history
* Add `UpdateSettingsScreen` composable in `:features:settings` module (for #369)
* Use `ViewModel` for data storage, handled by
  the `flow-preferences` library (https://github.com/tfcporciuncula/flow-preferences)
* Add drawables and update pref strings from app module
* Update fragment to use screen composable
* Add `coreLibraryDesugaring`
  • Loading branch information
EdricChan03 committed Nov 24, 2024
1 parent 43026b6 commit 67db42c
Show file tree
Hide file tree
Showing 11 changed files with 393 additions and 144 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ dependencies {
// Other dependencies
implementation(libs.materialAbout)
implementation(libs.appUpdater)
implementation(libs.flowPreferences)
implementation(libs.migration)

// Dagger dependencies
Expand Down
Original file line number Diff line number Diff line change
@@ -1,150 +1,68 @@
package com.edricchan.studybuddy.ui.modules.settings.fragment

import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.text.format.DateUtils
import androidx.preference.Preference
import com.edricchan.studybuddy.R
import com.edricchan.studybuddy.constants.Constants
import com.edricchan.studybuddy.core.settings.updates.UpdateInfoPrefConstants
import com.edricchan.studybuddy.exts.android.getSerializableCompat
import com.edricchan.studybuddy.ui.modules.updates.UpdatesActivity
import com.edricchan.studybuddy.ui.preference.MaterialPreferenceFragment
import java.time.Instant

class UpdateSettingsFragment : MaterialPreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener {

private lateinit var updateInfoPreferences: SharedPreferences
private var lastUpdatedInstant: Instant? = null
private var lastCheckedForUpdatesInstant: Instant? = null

private fun setLastCheckedForUpdates(lastCheckedForUpdatesMs: Long) {
lastCheckedForUpdatesInstant =
lastCheckedForUpdatesMs.takeIf { it <= DEFAULT_INSTANT }
?.let { Instant.ofEpochMilli(it) }
}

private fun setLastUpdated(lastUpdatedMs: Long) {
lastUpdatedInstant =
lastUpdatedMs.takeIf { it <= DEFAULT_INSTANT }
?.let { Instant.ofEpochMilli(it) }
}

override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
if (key == UpdateInfoPrefConstants.PREF_LAST_CHECKED_FOR_UPDATES_DATE || key == UpdateInfoPrefConstants.PREF_LAST_UPDATED_DATE) {
when (key) {
UpdateInfoPrefConstants.PREF_LAST_CHECKED_FOR_UPDATES_DATE -> {
setLastCheckedForUpdates(
updateInfoPreferences.getLong(
UpdateInfoPrefConstants.PREF_LAST_UPDATED_DATE,
DEFAULT_INSTANT
)
)
}

UpdateInfoPrefConstants.PREF_LAST_UPDATED_DATE -> {
setLastUpdated(
updateInfoPreferences.getLong(
UpdateInfoPrefConstants.PREF_LAST_CHECKED_FOR_UPDATES_DATE,
DEFAULT_INSTANT
)
)
}
}
updateUpdatesPreferenceSummary()
}
}

override fun onSaveInstanceState(outState: Bundle) {
outState.putSerializable(
LAST_CHECK_FOR_UPDATES_DATE_TAG,
lastCheckedForUpdatesInstant
)
outState.putSerializable(LAST_UPDATED_DATE_TAG, lastUpdatedInstant)
super.onSaveInstanceState(outState)
}

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

updateInfoPreferences = requireContext().getSharedPreferences(
UpdateInfoPrefConstants.FILE_UPDATE_INFO,
Context.MODE_PRIVATE
).apply {
registerOnSharedPreferenceChangeListener(this@UpdateSettingsFragment)
}

savedInstanceState?.run {
lastCheckedForUpdatesInstant = getSerializableCompat(
LAST_CHECK_FOR_UPDATES_DATE_TAG
)
lastUpdatedInstant = getSerializableCompat(
LAST_UPDATED_DATE_TAG
)
} ?: run {
setLastCheckedForUpdates(
updateInfoPreferences
.getLong(
UpdateInfoPrefConstants.PREF_LAST_CHECKED_FOR_UPDATES_DATE,
DEFAULT_INSTANT
)
)
setLastUpdated(
updateInfoPreferences
.getLong(
UpdateInfoPrefConstants.PREF_LAST_UPDATED_DATE,
DEFAULT_INSTANT
)
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.fragment.app.viewModels
import androidx.fragment.compose.content
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.edricchan.studybuddy.core.compat.navigation.navigateToUpdates
import com.edricchan.studybuddy.features.settings.updates.model.CheckFrequencyCompat
import com.edricchan.studybuddy.features.settings.updates.ui.UpdateSettingsScreen
import com.edricchan.studybuddy.ui.common.fragment.BaseFragment
import com.edricchan.studybuddy.ui.modules.settings.fragment.vm.UpdateSettingsViewModel
import com.edricchan.studybuddy.ui.theming.compose.StudyBuddyTheme
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class UpdateSettingsFragment : BaseFragment() {

private val viewModel by viewModels<UpdateSettingsViewModel>()

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = content {
val nestedScrollInterop = rememberNestedScrollInteropConnection()

val checkFrequency by viewModel.prefCheckFrequency.asFlow()
.collectAsStateWithLifecycle(initialValue = CheckFrequencyCompat.SixHours)

val canDownloadMetered by viewModel.prefCanDownloadMetered.asFlow()
.collectAsStateWithLifecycle(initialValue = false)

val onlyDownloadCharging by viewModel.prefOnlyDownloadCharging.asFlow()
.collectAsStateWithLifecycle(initialValue = false)

val lastChecked by viewModel.lastChecked.asFlow()
.collectAsStateWithLifecycle(initialValue = null)

val lastUpdated by viewModel.lastUpdated.asFlow()
.collectAsStateWithLifecycle(initialValue = null)

StudyBuddyTheme {
UpdateSettingsScreen(
modifier = Modifier
.nestedScroll(nestedScrollInterop)
.windowInsetsPadding(WindowInsets.navigationBars),
onUpdatesClick = navController::navigateToUpdates,
lastChecked = lastChecked,
lastUpdated = lastUpdated,
checkFrequency = checkFrequency,
onCheckFrequencyChange = viewModel.prefCheckFrequency::set,
canDownloadMetered = canDownloadMetered,
onCanDownloadMeteredChange = viewModel.prefCanDownloadMetered::set,
onlyDownloadCharging = onlyDownloadCharging,
onOnlyDownloadCharging = viewModel.prefOnlyDownloadCharging::set,
)
}

findPreference<Preference>(Constants.prefUpdates)?.intent =
Intent(context, UpdatesActivity::class.java)

updateUpdatesPreferenceSummary()
}

override fun onPause() {
super.onPause()
updateInfoPreferences.unregisterOnSharedPreferenceChangeListener(this)
}

override fun onResume() {
super.onResume()
updateInfoPreferences.registerOnSharedPreferenceChangeListener(this)
}

private fun updateUpdatesPreferenceSummary() {
findPreference<Preference>(Constants.prefUpdates)?.apply {
val lastCheckedForUpdates =
lastCheckedForUpdatesInstant?.let { getRelativeDateTimeString(it) }
?: getString(R.string.pref_updates_summary_never)
val lastUpdated = lastUpdatedInstant?.let { getRelativeDateTimeString(it) }
?: getString(R.string.pref_updates_summary_never)
summary = getString(R.string.pref_updates_summary, lastCheckedForUpdates, lastUpdated)
}
}

private fun getRelativeDateTimeString(instant: Instant): CharSequence =
DateUtils.getRelativeDateTimeString(
context,
instant.toEpochMilli(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
0
)

companion object {
// Indicates when the app was last updated
private const val LAST_UPDATED_DATE_TAG = "lastUpdatedDate"

// Indicates when the app last checked for updates
private const val LAST_CHECK_FOR_UPDATES_DATE_TAG = "lastCheckForUpdatesDate"

private const val DEFAULT_INSTANT = -1L
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.edricchan.studybuddy.ui.modules.settings.fragment.vm

import android.content.Context
import androidx.lifecycle.ViewModel
import com.edricchan.studybuddy.core.settings.updates.UpdateInfoPrefConstants
import com.edricchan.studybuddy.core.settings.updates.keyPrefCanDownloadMetered
import com.edricchan.studybuddy.core.settings.updates.keyPrefOnlyDownloadCharging
import com.edricchan.studybuddy.core.settings.updates.keyPrefUpdatesFrequency
import com.edricchan.studybuddy.exts.androidx.preference.defaultSharedPreferences
import com.edricchan.studybuddy.features.settings.updates.model.CheckFrequencyCompat
import com.edricchan.studybuddy.features.settings.updates.model.hourValue
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.fredporciuncula.flow.preferences.Preference
import com.fredporciuncula.flow.preferences.map
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import java.time.Instant
import javax.inject.Inject

@HiltViewModel
class UpdateSettingsViewModel @Inject constructor(
@ApplicationContext context: Context
) : ViewModel() {
private val updateInfoPreferences = FlowSharedPreferences(
context.getSharedPreferences(
UpdateInfoPrefConstants.FILE_UPDATE_INFO,
Context.MODE_PRIVATE
)
)

private val appPreferences = FlowSharedPreferences(
context.defaultSharedPreferences
)

val prefCheckFrequency: Preference<CheckFrequencyCompat> = appPreferences
// ListPreference uses a string to persist its data, see
// https://stackoverflow.com/q/11346916
.getString(
keyPrefUpdatesFrequency,
defaultValue = CheckFrequencyCompat.SixHours.hourValue.toString()
).map(
mapper = {
CheckFrequencyCompat.fromHoursOrNull(it.toInt()) ?: CheckFrequencyCompat.SixHours
},
reverse = { it.hourValue.toString() }
)

val prefCanDownloadMetered: Preference<Boolean> =
appPreferences.getBoolean(keyPrefCanDownloadMetered)

val prefOnlyDownloadCharging: Preference<Boolean> =
appPreferences.getBoolean(keyPrefOnlyDownloadCharging)

val lastChecked: Preference<Instant?> = updateInfoPreferences.getLong(
UpdateInfoPrefConstants.PREF_LAST_CHECKED_FOR_UPDATES_DATE,
Long.MIN_VALUE
).map(
mapper = { valLong ->
valLong.takeIf { it >= Instant.EPOCH.toEpochMilli() }?.let { Instant.ofEpochMilli(it) }
},
reverse = { it?.toEpochMilli() ?: Long.MIN_VALUE }
)

val lastUpdated: Preference<Instant?> = updateInfoPreferences.getLong(
UpdateInfoPrefConstants.PREF_LAST_UPDATED_DATE,
Long.MIN_VALUE
).map(
mapper = { valLong ->
valLong.takeIf { it >= Instant.EPOCH.toEpochMilli() }?.let { Instant.ofEpochMilli(it) }
},
reverse = { it?.toEpochMilli() ?: Long.MIN_VALUE }
)
}
18 changes: 17 additions & 1 deletion features/settings/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
com.edricchan.studybuddy.library.android
com.edricchan.studybuddy.library.`android-compose`
}

android {
Expand All @@ -18,17 +18,33 @@ android {
)
}
}

compileOptions.isCoreLibraryDesugaringEnabled = true
}

dependencies {
implementation(projects.core.resources.temporal)
implementation(projects.core.settings.updates)

api(projects.ui.preference)

implementation(projects.exts.common)
implementation(projects.ui.theming.compose)
implementation(projects.ui.theming.views)
implementation(projects.ui.preference.compose)

api(libs.androidx.preference.ktx)

// Compose
implementation(libs.bundles.androidx.compose)
debugImplementation(libs.bundles.androidx.compose.tooling)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.espresso.core)

// Compose rule support
androidTestImplementation(libs.androidx.compose.ui.test.junit4)

coreLibraryDesugaring(libs.android.desugar)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.edricchan.studybuddy.features.settings.updates.model

import androidx.annotation.StringRes
import com.edricchan.studybuddy.features.settings.R

private val hourToEnumMap = mapOf(
CheckFrequencyCompat.Manual to 0,
CheckFrequencyCompat.ThreeHours to 3,
CheckFrequencyCompat.SixHours to 6,
CheckFrequencyCompat.TwelveHours to 12,
CheckFrequencyCompat.Daily to 24,
CheckFrequencyCompat.Weekly to 24 * 7
)

enum class CheckFrequencyCompat(@StringRes val stringResource: Int) {
Manual(R.string.pref_check_for_update_freq_manual),
ThreeHours(R.string.pref_check_for_update_freq_three_hour),
SixHours(R.string.pref_check_for_update_freq_six_hour),
TwelveHours(R.string.pref_check_for_update_freq_twelve_hour),
Daily(R.string.pref_check_for_update_freq_daily),
Weekly(R.string.pref_check_for_update_freq_weekly);

companion object {
fun fromHoursOrNull(hours: Int): CheckFrequencyCompat? =
hourToEnumMap.entries
.find { entry -> entry.value == hours }
?.key
}
}

val CheckFrequencyCompat.hourValue: Int
get() = hourToEnumMap[this] ?: 0
Loading

0 comments on commit 67db42c

Please sign in to comment.