mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
fix(client/android): clear all favorites when other settings are reset (#6707)
This required some refactoring since we want to move away from `LiveData` and towards `StateFlow` Closes #6293
This commit is contained in:
@@ -10,6 +10,8 @@ import dev.firezone.android.BuildConfig
|
||||
import dev.firezone.android.core.data.model.Config
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import java.security.MessageDigest
|
||||
@@ -49,6 +51,10 @@ fun ResourceState.toggle(): ResourceState {
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper class used because `MutableStateFlow` will not
|
||||
// notify subscribers if you submit the same object that's already in it.
|
||||
class Favorites(val inner: HashSet<String>)
|
||||
|
||||
internal class Repository
|
||||
@Inject
|
||||
constructor(
|
||||
@@ -56,6 +62,12 @@ internal class Repository
|
||||
private val coroutineDispatcher: CoroutineDispatcher,
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
) {
|
||||
// We are the only thing that can modify favorites so we shouldn't need to reload it after
|
||||
// this initial load
|
||||
private val _favorites =
|
||||
MutableStateFlow(Favorites(HashSet(sharedPreferences.getStringSet(FAVORITE_RESOURCES_KEY, null).orEmpty())))
|
||||
val favorites = _favorites.asStateFlow()
|
||||
|
||||
fun getConfigSync(): Config {
|
||||
return Config(
|
||||
sharedPreferences.getString(AUTH_BASE_URL_KEY, null)
|
||||
@@ -98,9 +110,25 @@ internal class Repository
|
||||
|
||||
fun getDeviceIdSync(): String? = sharedPreferences.getString(DEVICE_ID_KEY, null)
|
||||
|
||||
fun getFavoritesSync(): HashSet<String> = HashSet(sharedPreferences.getStringSet(FAVORITE_RESOURCES_KEY, null).orEmpty())
|
||||
private fun saveFavoritesSync() {
|
||||
sharedPreferences.edit().putStringSet(FAVORITE_RESOURCES_KEY, favorites.value.inner).apply()
|
||||
_favorites.value = Favorites(favorites.value.inner)
|
||||
}
|
||||
|
||||
fun saveFavoritesSync(value: HashSet<String>) = sharedPreferences.edit().putStringSet(FAVORITE_RESOURCES_KEY, value).apply()
|
||||
fun addFavoriteResource(id: String) {
|
||||
favorites.value.inner.add(id)
|
||||
saveFavoritesSync()
|
||||
}
|
||||
|
||||
fun removeFavoriteResource(id: String) {
|
||||
favorites.value.inner.remove(id)
|
||||
saveFavoritesSync()
|
||||
}
|
||||
|
||||
fun resetFavorites() {
|
||||
favorites.value.inner.clear()
|
||||
saveFavoritesSync()
|
||||
}
|
||||
|
||||
fun getToken(): Flow<String?> =
|
||||
flow {
|
||||
|
||||
@@ -12,6 +12,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dev.firezone.android.core.data.Repository
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@@ -21,6 +22,7 @@ class DataModule {
|
||||
@ApplicationContext context: Context,
|
||||
): Bundle = (context.getSystemService(Context.RESTRICTIONS_SERVICE) as android.content.RestrictionsManager).applicationRestrictions
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
internal fun provideRepository(
|
||||
@ApplicationContext context: Context,
|
||||
|
||||
@@ -172,7 +172,7 @@ class ResourceDetailsBottomSheet(
|
||||
private fun refreshButtons() {
|
||||
val addToFavoritesBtn: MaterialButton = view.findViewById(R.id.addToFavoritesBtn)
|
||||
val removeFromFavoritesBtn: MaterialButton = view.findViewById(R.id.removeFromFavoritesBtn)
|
||||
val isFavorite = viewModel.favoriteResourcesLiveData.value!!.contains(resource.id)
|
||||
val isFavorite = viewModel.isFavorite(resource.id)
|
||||
addToFavoritesBtn.visibility = if (isFavorite) View.GONE else View.VISIBLE
|
||||
removeFromFavoritesBtn.visibility = if (isFavorite) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
@@ -20,6 +20,7 @@ import dev.firezone.android.core.data.toggle
|
||||
import dev.firezone.android.databinding.ActivitySessionBinding
|
||||
import dev.firezone.android.features.settings.ui.SettingsActivity
|
||||
import dev.firezone.android.tunnel.TunnelService
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SessionActivity : AppCompatActivity() {
|
||||
@@ -129,6 +130,7 @@ class SessionActivity : AppCompatActivity() {
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||
},
|
||||
)
|
||||
viewModel.tabSelected(binding.tabLayout.selectedTabPosition)
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
@@ -143,24 +145,20 @@ class SessionActivity : AppCompatActivity() {
|
||||
refreshList()
|
||||
}
|
||||
|
||||
viewModel.favoriteResourcesLiveData.observe(this) {
|
||||
refreshList()
|
||||
// This coroutine could still resume while the Activity is not shown, but this is probably
|
||||
// fine since the Flow will only emit if the user interacts with the UI anyway.
|
||||
lifecycleScope.launch {
|
||||
viewModel.favorites.collect {
|
||||
refreshList()
|
||||
}
|
||||
}
|
||||
viewModel.tabSelected(binding.tabLayout.selectedTabPosition)
|
||||
viewModel.favoriteResourcesLiveData.value = viewModel.repo.getFavoritesSync()
|
||||
}
|
||||
|
||||
private fun refreshList(afterLoad: () -> Unit = {}) {
|
||||
if (viewModel.forceAllResourcesTab()) {
|
||||
binding.tabLayout.selectTab(binding.tabLayout.getTabAt(SessionViewModel.RESOURCES_TAB_ALL), true)
|
||||
}
|
||||
binding.tabLayout.visibility =
|
||||
if (viewModel.showFavoritesTab()) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
viewModel.forceTab()?.let { tab -> binding.tabLayout.selectTab(binding.tabLayout.getTabAt(tab), true) }
|
||||
|
||||
binding.tabLayout.visibility = viewModel.tabLayoutVisibility()
|
||||
resourcesAdapter.submitList(viewModel.resourcesList(internetState())) {
|
||||
afterLoad()
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
/* Licensed under Apache 2.0 (C) 2024 Firezone, Inc. */
|
||||
package dev.firezone.android.features.session.ui
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.firezone.android.core.data.Favorites
|
||||
import dev.firezone.android.core.data.Repository
|
||||
import dev.firezone.android.core.data.ResourceState
|
||||
import dev.firezone.android.tunnel.TunnelService.Companion.State
|
||||
import dev.firezone.android.tunnel.model.Resource
|
||||
import dev.firezone.android.tunnel.model.isInternetResource
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class SessionViewModel
|
||||
@Inject
|
||||
constructor() : ViewModel() {
|
||||
// Must be `internal` because Dagger does not support injection into `private` fields
|
||||
@Inject
|
||||
internal lateinit var repo: Repository
|
||||
private val _favoriteResourcesLiveData = MutableLiveData<HashSet<String>>(HashSet())
|
||||
private val _serviceStatusLiveData = MutableLiveData<State>()
|
||||
private val _resourcesLiveData = MutableLiveData<List<Resource>>(emptyList())
|
||||
private var showOnlyFavorites: Boolean = false
|
||||
private var selectedTab = RESOURCES_TAB_FAVORITES
|
||||
|
||||
val favoriteResourcesLiveData: MutableLiveData<HashSet<String>>
|
||||
get() = _favoriteResourcesLiveData
|
||||
val serviceStatusLiveData: MutableLiveData<State>
|
||||
get() = _serviceStatusLiveData
|
||||
val resourcesLiveData: MutableLiveData<List<Resource>>
|
||||
get() = _resourcesLiveData
|
||||
|
||||
private val favoriteResources: HashSet<String>
|
||||
get() = favoriteResourcesLiveData.value!!
|
||||
val favorites: StateFlow<Favorites>
|
||||
get() = repo.favorites
|
||||
|
||||
// Actor name
|
||||
fun clearActorName() = repo.clearActorName()
|
||||
@@ -38,22 +38,11 @@ internal class SessionViewModel
|
||||
fun getActorName() = repo.getActorNameSync()
|
||||
|
||||
fun addFavoriteResource(id: String) {
|
||||
val value = favoriteResources
|
||||
value.add(id)
|
||||
repo.saveFavoritesSync(value)
|
||||
// Update LiveData
|
||||
_favoriteResourcesLiveData.value = value
|
||||
repo.addFavoriteResource(id)
|
||||
}
|
||||
|
||||
fun removeFavoriteResource(id: String) {
|
||||
val value = favoriteResources
|
||||
value.remove(id)
|
||||
repo.saveFavoritesSync(value)
|
||||
if (forceAllResourcesTab()) {
|
||||
showOnlyFavorites = false
|
||||
}
|
||||
// Update LiveData
|
||||
_favoriteResourcesLiveData.value = value
|
||||
repo.removeFavoriteResource(id)
|
||||
}
|
||||
|
||||
fun clearToken() = repo.clearToken()
|
||||
@@ -69,40 +58,37 @@ internal class SessionViewModel
|
||||
}
|
||||
}
|
||||
|
||||
return if (favoriteResources.isEmpty()) {
|
||||
return if (repo.favorites.value.inner.isEmpty()) {
|
||||
resources
|
||||
} else if (showOnlyFavorites) {
|
||||
resources.filter { favoriteResources.contains(it.id) }
|
||||
} else if (selectedTab == RESOURCES_TAB_FAVORITES) {
|
||||
resources.filter { repo.favorites.value.inner.contains(it.id) }
|
||||
} else {
|
||||
resources
|
||||
}
|
||||
}
|
||||
|
||||
fun forceAllResourcesTab(): Boolean {
|
||||
return favoriteResources.isEmpty()
|
||||
}
|
||||
|
||||
fun showFavoritesTab(): Boolean {
|
||||
return favoriteResources.isNotEmpty()
|
||||
}
|
||||
fun forceTab(): Int? =
|
||||
if (repo.favorites.value.inner.isEmpty()) {
|
||||
RESOURCES_TAB_ALL
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun tabSelected(position: Int) {
|
||||
showOnlyFavorites =
|
||||
when (position) {
|
||||
RESOURCES_TAB_FAVORITES -> {
|
||||
true
|
||||
}
|
||||
|
||||
RESOURCES_TAB_ALL -> {
|
||||
false
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Invalid tab position: $position")
|
||||
}
|
||||
selectedTab = position
|
||||
}
|
||||
|
||||
fun isFavorite(id: String) = repo.favorites.value.inner.contains(id)
|
||||
|
||||
fun tabLayoutVisibility(): Int =
|
||||
if (repo.favorites.value.inner.isNotEmpty()) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val RESOURCES_TAB_FAVORITES = 0
|
||||
const val RESOURCES_TAB_ALL = 1
|
||||
private const val RESOURCES_TAB_FAVORITES = 0
|
||||
private const val RESOURCES_TAB_ALL = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ internal class SettingsViewModel
|
||||
|
||||
fun resetSettingsToDefaults() {
|
||||
config = repo.getDefaultConfigSync()
|
||||
repo.resetFavorites()
|
||||
onFieldUpdated()
|
||||
actionMutableLiveData.postValue(
|
||||
ViewAction.FillSettings(config),
|
||||
|
||||
@@ -9,6 +9,15 @@ export default function Android() {
|
||||
href="https://play.google.com/store/apps/details?id=dev.firezone.android"
|
||||
title="Android"
|
||||
>
|
||||
{/*
|
||||
<Entry version="1.3.3" date={new Date("todo")}>
|
||||
<ul className="list-disc space-y-2 pl-4 mb-4">
|
||||
<ChangeItem pull="6707">
|
||||
Resetting the settings now resets the list of favorited Resources, too.
|
||||
</ChangeItem>
|
||||
</ul>
|
||||
</Entry>
|
||||
*/}
|
||||
<Entry version="1.3.2" date={new Date("2024-09-05")}>
|
||||
<ul className="list-disc space-y-2 pl-4 mb-4">
|
||||
<ChangeItem pull="6605">
|
||||
|
||||
Reference in New Issue
Block a user