feat: Allow the user to trigger update checks (#458)
Add an additional preference entry that triggers an update when tapped. It also displays the earliest time of the next automatic update check as the preference summary. Move the code that performs the update check (and the logic for whether to perform the check) out of `MainActivity` and in to `UpdateCheck` so it's available from `PreferencesFragment`.
This commit is contained in:
parent
13cfa1a15d
commit
8293e90b73
|
@ -17,18 +17,21 @@
|
|||
|
||||
package app.pachli.updatecheck
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import app.pachli.BuildConfig
|
||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class UpdateCheck @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
sharedPreferencesRepository: SharedPreferencesRepository,
|
||||
private val fdroidService: FdroidService,
|
||||
) : UpdateCheckBase(sharedPreferencesRepository) {
|
||||
) : UpdateCheckBase(context, sharedPreferencesRepository) {
|
||||
override val updateIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse("market://details?id=${BuildConfig.APPLICATION_ID}")
|
||||
}
|
||||
|
|
|
@ -17,15 +17,18 @@
|
|||
|
||||
package app.pachli.updatecheck
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class UpdateCheck @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
sharedPreferencesRepository: SharedPreferencesRepository,
|
||||
private val gitHubService: GitHubService,
|
||||
) : UpdateCheckBase(sharedPreferencesRepository) {
|
||||
) : UpdateCheckBase(context, sharedPreferencesRepository) {
|
||||
private val versionCodeExtractor = """(\d+)\.apk""".toRegex()
|
||||
|
||||
override val updateIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
|
|
|
@ -17,18 +17,21 @@
|
|||
|
||||
package app.pachli.updatecheck
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import app.pachli.BuildConfig
|
||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||
import com.google.android.play.core.appupdate.AppUpdateManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
class UpdateCheck @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
sharedPreferencesRepository: SharedPreferencesRepository,
|
||||
private val appUpdateManager: AppUpdateManager,
|
||||
) : UpdateCheckBase(sharedPreferencesRepository) {
|
||||
) : UpdateCheckBase(context, sharedPreferencesRepository) {
|
||||
override val updateIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(
|
||||
"https://play.google.com/store/apps/details?id=${BuildConfig.APPLICATION_ID}",
|
||||
|
|
|
@ -106,7 +106,6 @@ import app.pachli.interfaces.FabFragment
|
|||
import app.pachli.interfaces.ReselectableFragment
|
||||
import app.pachli.pager.MainPagerAdapter
|
||||
import app.pachli.updatecheck.UpdateCheck
|
||||
import app.pachli.updatecheck.UpdateNotificationFrequency
|
||||
import app.pachli.usecase.DeveloperToolsUseCase
|
||||
import app.pachli.usecase.LogoutUsecase
|
||||
import app.pachli.util.deleteStaleCachedMedia
|
||||
|
@ -428,7 +427,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
showJankyAnimationWarning()
|
||||
}
|
||||
|
||||
checkForUpdate()
|
||||
lifecycleScope.launch { updateCheck.checkForUpdate() }
|
||||
}
|
||||
|
||||
/** Warn the user about possibly-broken animations. */
|
||||
|
@ -441,60 +440,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
sharedPreferencesRepository.edit { putBoolean(PrefKeys.SHOW_JANKY_ANIMATION_WARNING, false) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for available updates, and prompt user to update.
|
||||
*
|
||||
* Show a dialog prompting the user to update if a newer version of the app is available.
|
||||
* The user can start an update, ignore this version, or dismiss all future update
|
||||
* notifications.
|
||||
*/
|
||||
private fun checkForUpdate() = lifecycleScope.launch {
|
||||
val frequency = UpdateNotificationFrequency.from(sharedPreferencesRepository.getString(PrefKeys.UPDATE_NOTIFICATION_FREQUENCY, null))
|
||||
if (frequency == UpdateNotificationFrequency.NEVER) return@launch
|
||||
|
||||
val latestVersionCode = updateCheck.getLatestVersionCode()
|
||||
|
||||
if (latestVersionCode <= BuildConfig.VERSION_CODE) return@launch
|
||||
|
||||
if (frequency == UpdateNotificationFrequency.ONCE_PER_VERSION) {
|
||||
val ignoredVersion = sharedPreferencesRepository.getInt(PrefKeys.UPDATE_NOTIFICATION_VERSIONCODE, -1)
|
||||
if (latestVersionCode == ignoredVersion) {
|
||||
Timber.d("Ignoring update to %d", latestVersionCode)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("New version is: %d", latestVersionCode)
|
||||
when (showUpdateDialog()) {
|
||||
AlertDialog.BUTTON_POSITIVE -> {
|
||||
startActivity(updateCheck.updateIntent)
|
||||
}
|
||||
AlertDialog.BUTTON_NEUTRAL -> {
|
||||
with(sharedPreferencesRepository.edit()) {
|
||||
putInt(PrefKeys.UPDATE_NOTIFICATION_VERSIONCODE, latestVersionCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
AlertDialog.BUTTON_NEGATIVE -> {
|
||||
with(sharedPreferencesRepository.edit()) {
|
||||
putString(
|
||||
PrefKeys.UPDATE_NOTIFICATION_FREQUENCY,
|
||||
UpdateNotificationFrequency.NEVER.name,
|
||||
)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun showUpdateDialog() = AlertDialog.Builder(this)
|
||||
.setTitle(R.string.update_dialog_title)
|
||||
.setMessage(R.string.update_dialog_message)
|
||||
.setCancelable(true)
|
||||
.setIcon(DR.mipmap.ic_launcher)
|
||||
.create()
|
||||
.await(R.string.update_dialog_positive, R.string.update_dialog_negative, R.string.update_dialog_neutral)
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
// For some reason the navigation drawer is opened when the activity is recreated
|
||||
|
|
|
@ -17,6 +17,13 @@
|
|||
package app.pachli.components.preference
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import app.pachli.R
|
||||
|
@ -26,6 +33,7 @@ import app.pachli.core.network.model.Notification
|
|||
import app.pachli.core.preferences.AppTheme
|
||||
import app.pachli.core.preferences.AppTheme.Companion.APP_THEME_DEFAULT
|
||||
import app.pachli.core.preferences.PrefKeys
|
||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||
import app.pachli.settings.emojiPreference
|
||||
import app.pachli.settings.listPreference
|
||||
import app.pachli.settings.makePreferenceScreen
|
||||
|
@ -33,6 +41,8 @@ import app.pachli.settings.preference
|
|||
import app.pachli.settings.preferenceCategory
|
||||
import app.pachli.settings.sliderPreference
|
||||
import app.pachli.settings.switchPreference
|
||||
import app.pachli.updatecheck.UpdateCheck
|
||||
import app.pachli.updatecheck.UpdateCheckResult.AT_LATEST
|
||||
import app.pachli.updatecheck.UpdateNotificationFrequency
|
||||
import app.pachli.util.LocaleManager
|
||||
import app.pachli.util.deserialize
|
||||
|
@ -45,6 +55,7 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
@ -55,8 +66,43 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
|||
@Inject
|
||||
lateinit var localeManager: LocaleManager
|
||||
|
||||
@Inject
|
||||
lateinit var updateCheck: UpdateCheck
|
||||
|
||||
@Inject
|
||||
lateinit var sharedPreferencesRepository: SharedPreferencesRepository
|
||||
|
||||
private val iconSize by unsafeLazy { resources.getDimensionPixelSize(DR.dimen.preference_icon_size) }
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
// Show the "Check for update now" summary. This must also change
|
||||
// depending on the update notification frequency. You can't link two
|
||||
// preferences like that as a dependency, so listen for changes to
|
||||
// the relevant keys and update the summary when they change.
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
sharedPreferencesRepository.changes.collect { prefKey ->
|
||||
when (prefKey) {
|
||||
PrefKeys.UPDATE_NOTIFICATION_FREQUENCY,
|
||||
PrefKeys.UPDATE_NOTIFICATION_LAST_NOTIFICATION_MS,
|
||||
-> {
|
||||
findPreference<Preference>(PrefKeys.UPDATE_NOTIFICATION_LAST_NOTIFICATION_MS)?.let {
|
||||
it.summary = updateCheck.provideSummary(it)
|
||||
}
|
||||
}
|
||||
else -> { /* do nothing */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.onCreateView(inflater, container, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
makePreferenceScreen {
|
||||
preferenceCategory(R.string.pref_title_appearance_settings) {
|
||||
|
@ -288,6 +334,8 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
|
||||
preferenceCategory(R.string.pref_title_update_settings) {
|
||||
it.icon = makeIcon(GoogleMaterial.Icon.gmd_upgrade)
|
||||
|
||||
listPreference {
|
||||
setDefaultValue(UpdateNotificationFrequency.ALWAYS.name)
|
||||
setEntries(R.array.pref_update_notification_frequency_names)
|
||||
|
@ -296,7 +344,26 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
|||
setSummaryProvider { entry }
|
||||
setTitle(R.string.pref_title_update_notification_frequency)
|
||||
isSingleLineTitle = false
|
||||
icon = makeIcon(GoogleMaterial.Icon.gmd_upgrade)
|
||||
icon = makeIcon(GoogleMaterial.Icon.gmd_calendar_today)
|
||||
}
|
||||
|
||||
preference {
|
||||
title = getString(R.string.pref_title_update_check_now)
|
||||
key = PrefKeys.UPDATE_NOTIFICATION_LAST_NOTIFICATION_MS
|
||||
setOnPreferenceClickListener {
|
||||
lifecycleScope.launch {
|
||||
if (updateCheck.checkForUpdate(true) == AT_LATEST) {
|
||||
Toast.makeText(
|
||||
this@PreferencesFragment.requireContext(),
|
||||
getString(R.string.pref_update_check_no_updates),
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
summary = updateCheck.provideSummary(this)
|
||||
icon = makeIcon(GoogleMaterial.Icon.gmd_refresh)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,13 +17,30 @@
|
|||
|
||||
package app.pachli.updatecheck
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.Preference
|
||||
import app.pachli.BuildConfig
|
||||
import app.pachli.R
|
||||
import app.pachli.core.common.util.AbsoluteTimeFormatter
|
||||
import app.pachli.core.designsystem.R as DR
|
||||
import app.pachli.core.preferences.PrefKeys
|
||||
import app.pachli.core.preferences.SharedPreferencesRepository
|
||||
import app.pachli.core.ui.await
|
||||
import app.pachli.updatecheck.UpdateCheckResult.AT_LATEST
|
||||
import app.pachli.updatecheck.UpdateCheckResult.DIALOG_SHOWN
|
||||
import app.pachli.updatecheck.UpdateCheckResult.IGNORED
|
||||
import app.pachli.updatecheck.UpdateCheckResult.SKIPPED_BECAUSE_NEVER
|
||||
import app.pachli.updatecheck.UpdateCheckResult.SKIPPED_BECAUSE_TOO_SOON
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.toJavaDuration
|
||||
import timber.log.Timber
|
||||
|
||||
enum class UpdateNotificationFrequency {
|
||||
/** Never prompt the user to update */
|
||||
|
@ -50,31 +67,31 @@ enum class UpdateNotificationFrequency {
|
|||
}
|
||||
}
|
||||
|
||||
enum class UpdateCheckResult {
|
||||
/** Skipped update check because user configured frequency is "never" */
|
||||
SKIPPED_BECAUSE_NEVER,
|
||||
|
||||
/** Skipped update check because it's too soon relative to the last check */
|
||||
SKIPPED_BECAUSE_TOO_SOON,
|
||||
|
||||
/** Performed update check, user is at latest available version */
|
||||
AT_LATEST,
|
||||
|
||||
/** Performed update check, user is ignoring the remote version */
|
||||
IGNORED,
|
||||
|
||||
/** Performed update check, update dialog was shown to the user */
|
||||
DIALOG_SHOWN,
|
||||
}
|
||||
|
||||
@Singleton
|
||||
abstract class UpdateCheckBase(private val sharedPreferencesRepository: SharedPreferencesRepository) {
|
||||
abstract class UpdateCheckBase(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val sharedPreferencesRepository: SharedPreferencesRepository,
|
||||
) : Preference.SummaryProvider<Preference> {
|
||||
/** An intent that can be used to start the update process (e.g., open a store listing) */
|
||||
abstract val updateIntent: Intent
|
||||
|
||||
/**
|
||||
* @return The newest available versionCode (which may be the current version code if there is
|
||||
* no newer version, or if [MINIMUM_DURATION_BETWEEN_CHECKS] has not elapsed since the last
|
||||
* check.
|
||||
*/
|
||||
suspend fun getLatestVersionCode(): Int {
|
||||
val now = System.currentTimeMillis()
|
||||
val lastCheck = sharedPreferencesRepository.getLong(PrefKeys.UPDATE_NOTIFICATION_LAST_NOTIFICATION_MS, 0)
|
||||
|
||||
if (now - lastCheck < MINIMUM_DURATION_BETWEEN_CHECKS.inWholeMilliseconds) {
|
||||
return BuildConfig.VERSION_CODE
|
||||
}
|
||||
|
||||
sharedPreferencesRepository.edit {
|
||||
putLong(PrefKeys.UPDATE_NOTIFICATION_LAST_NOTIFICATION_MS, now)
|
||||
}
|
||||
|
||||
return remoteFetchLatestVersionCode() ?: BuildConfig.VERSION_CODE
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the version code of the latest available version of Pachli from whatever
|
||||
* remote service the running version was downloaded from.
|
||||
|
@ -83,8 +100,116 @@ abstract class UpdateCheckBase(private val sharedPreferencesRepository: SharedPr
|
|||
*/
|
||||
abstract suspend fun remoteFetchLatestVersionCode(): Int?
|
||||
|
||||
/**
|
||||
* Check for available updates, and prompt user to update.
|
||||
*
|
||||
* Show a dialog prompting the user to update if a newer version of the app is available.
|
||||
* The user can start an update, ignore this version, or dismiss all future update
|
||||
* notifications.
|
||||
*
|
||||
* @param force If true then the user's preferences for update checking frequency are
|
||||
* ignored and the update check is always performed.
|
||||
*
|
||||
* @return The result of performing the update check
|
||||
*/
|
||||
suspend fun checkForUpdate(force: Boolean = false): UpdateCheckResult {
|
||||
val frequency = UpdateNotificationFrequency.from(
|
||||
sharedPreferencesRepository.getString(PrefKeys.UPDATE_NOTIFICATION_FREQUENCY, null),
|
||||
)
|
||||
|
||||
if (!force && frequency == UpdateNotificationFrequency.NEVER) return SKIPPED_BECAUSE_NEVER
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
if (!force) {
|
||||
val lastCheck = sharedPreferencesRepository.getLong(
|
||||
PrefKeys.UPDATE_NOTIFICATION_LAST_NOTIFICATION_MS,
|
||||
0,
|
||||
)
|
||||
|
||||
if (now - lastCheck < MINIMUM_DURATION_BETWEEN_CHECKS.inWholeMilliseconds) {
|
||||
return SKIPPED_BECAUSE_TOO_SOON
|
||||
}
|
||||
}
|
||||
|
||||
sharedPreferencesRepository.edit {
|
||||
putLong(PrefKeys.UPDATE_NOTIFICATION_LAST_NOTIFICATION_MS, now)
|
||||
}
|
||||
|
||||
val latestVersionCode = remoteFetchLatestVersionCode() ?: BuildConfig.VERSION_CODE
|
||||
|
||||
if (latestVersionCode <= BuildConfig.VERSION_CODE) return AT_LATEST
|
||||
|
||||
if (frequency == UpdateNotificationFrequency.ONCE_PER_VERSION) {
|
||||
val ignoredVersion =
|
||||
sharedPreferencesRepository.getInt(PrefKeys.UPDATE_NOTIFICATION_VERSIONCODE, -1)
|
||||
if (latestVersionCode == ignoredVersion && !force) {
|
||||
Timber.d("Ignoring update to %d", latestVersionCode)
|
||||
return IGNORED
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("New version is: %d", latestVersionCode)
|
||||
when (showUpdateDialog(context)) {
|
||||
AlertDialog.BUTTON_POSITIVE -> {
|
||||
context.startActivity(updateIntent)
|
||||
}
|
||||
|
||||
AlertDialog.BUTTON_NEUTRAL -> {
|
||||
with(sharedPreferencesRepository.edit()) {
|
||||
putInt(PrefKeys.UPDATE_NOTIFICATION_VERSIONCODE, latestVersionCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog.BUTTON_NEGATIVE -> {
|
||||
with(sharedPreferencesRepository.edit()) {
|
||||
putString(
|
||||
PrefKeys.UPDATE_NOTIFICATION_FREQUENCY,
|
||||
app.pachli.updatecheck.UpdateNotificationFrequency.NEVER.name,
|
||||
)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DIALOG_SHOWN
|
||||
}
|
||||
|
||||
private suspend fun showUpdateDialog(context: Context) = AlertDialog.Builder(context)
|
||||
.setTitle(R.string.update_dialog_title)
|
||||
.setMessage(R.string.update_dialog_message)
|
||||
.setCancelable(true)
|
||||
.setIcon(DR.mipmap.ic_launcher)
|
||||
.create()
|
||||
.await(
|
||||
R.string.update_dialog_positive,
|
||||
R.string.update_dialog_negative,
|
||||
R.string.update_dialog_neutral,
|
||||
)
|
||||
|
||||
override fun provideSummary(preference: Preference): CharSequence? {
|
||||
val frequency = UpdateNotificationFrequency.from(
|
||||
sharedPreferencesRepository.getString(PrefKeys.UPDATE_NOTIFICATION_FREQUENCY, null),
|
||||
)
|
||||
|
||||
if (frequency == UpdateNotificationFrequency.NEVER) return null
|
||||
|
||||
val now = Instant.now()
|
||||
val lastCheck = sharedPreferencesRepository.getLong(
|
||||
PrefKeys.UPDATE_NOTIFICATION_LAST_NOTIFICATION_MS,
|
||||
now.toEpochMilli(),
|
||||
)
|
||||
|
||||
val nextCheck = Instant.ofEpochMilli(lastCheck).plus(MINIMUM_DURATION_BETWEEN_CHECKS.toJavaDuration())
|
||||
|
||||
val dateString = AbsoluteTimeFormatter().format(Date.from(nextCheck))
|
||||
|
||||
return context.getString(R.string.pref_update_next_scheduled_check, dateString)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** How much time should elapse between version checks */
|
||||
private val MINIMUM_DURATION_BETWEEN_CHECKS = 24.hours
|
||||
val MINIMUM_DURATION_BETWEEN_CHECKS = 24.hours
|
||||
}
|
||||
}
|
||||
|
|
|
@ -716,4 +716,7 @@
|
|||
<string name="server_repository_error_validate_node_info">validating nodeinfo %1$s failed: %2$s</string>
|
||||
<string name="server_repository_error_get_instance_info">fetching /api/v1/instance failed: %1$s</string>
|
||||
<string name="server_repository_error_capabilities">parsing server capabilities failed: %1$s</string>
|
||||
<string name="pref_title_update_check_now">Check for update now</string>
|
||||
<string name="pref_update_check_no_updates">There are no updates available</string>
|
||||
<string name="pref_update_next_scheduled_check">Next scheduled check: %1$s</string>
|
||||
</resources>
|
||||
|
|
|
@ -36,7 +36,7 @@ import timber.log.Timber
|
|||
*/
|
||||
@Singleton
|
||||
class SharedPreferencesRepository @Inject constructor(
|
||||
val sharedPreferences: SharedPreferences,
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
@ApplicationScope private val externalScope: CoroutineScope,
|
||||
) : SharedPreferences by sharedPreferences {
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue