Migrate DataAndStorageFragment to compose.

This commit is contained in:
Alex Hart
2025-08-22 16:18:54 -03:00
committed by Michelle Tang
parent 94ed0650dc
commit 45c64f825d
5 changed files with 419 additions and 111 deletions

View File

@@ -1,134 +1,269 @@
package org.thoughtcrime.securesms.components.settings.app.data
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import org.signal.core.ui.compose.Dividers
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Rows
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.Texts
import org.signal.core.util.bytes
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.webrtc.CallDataMode
import kotlin.math.abs
class DataAndStorageSettingsFragment : DSLSettingsFragment(R.string.preferences__data_and_storage) {
class DataAndStorageSettingsFragment : ComposeFragment() {
private val autoDownloadValues by lazy { resources.getStringArray(R.array.pref_media_download_entries) }
private val autoDownloadLabels by lazy { resources.getStringArray(R.array.pref_media_download_values) }
private val sentMediaQualityLabels by lazy { SentMediaQuality.getLabels(requireContext()) }
private val callDataModeLabels by lazy { resources.getStringArray(R.array.pref_data_and_storage_call_data_mode_values) }
private lateinit var viewModel: DataAndStorageSettingsViewModel
private val viewModel: DataAndStorageSettingsViewModel by viewModels(
factoryProducer = {
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val repository = DataAndStorageSettingsRepository()
DataAndStorageSettingsViewModel.Factory(preferences, repository)
}
)
override fun onResume() {
super.onResume()
viewModel.refresh()
}
override fun bindAdapter(adapter: MappingAdapter) {
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val repository = DataAndStorageSettingsRepository()
val factory = DataAndStorageSettingsViewModel.Factory(preferences, repository)
viewModel = ViewModelProvider(this, factory)[DataAndStorageSettingsViewModel::class.java]
@Composable
override fun FragmentContent() {
val state by viewModel.state.collectAsStateWithLifecycle()
val callbacks = remember { Callbacks() }
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())
}
DataAndStorageSettingsScreen(
state = state,
callbacks = callbacks
)
}
fun getConfiguration(state: DataAndStorageSettingsState): DSLConfiguration {
return configure {
clickPref(
title = DSLSettingsText.from(R.string.preferences_data_and_storage__manage_storage),
summary = DSLSettingsText.from(state.totalStorageUse.bytes.toUnitString()),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_dataAndStorageSettingsFragment_to_storagePreferenceFragment)
}
)
private inner class Callbacks : DataAndStorageSettingsCallbacks {
override fun onNavigationClick() {
requireActivity().onBackPressedDispatcher.onBackPressed()
}
dividerPref()
override fun onManageStorageClick() {
findNavController().safeNavigate(R.id.action_dataAndStorageSettingsFragment_to_storagePreferenceFragment)
}
sectionHeaderPref(R.string.preferences_chats__media_auto_download)
override fun onSentMediaQualitySelected(code: String) {
viewModel.setSentMediaQuality(SentMediaQuality.fromCode(code.toInt()))
}
multiSelectPref(
title = DSLSettingsText.from(R.string.preferences_chats__when_using_mobile_data),
listItems = autoDownloadLabels,
selected = autoDownloadValues.map { state.mobileAutoDownloadValues.contains(it) }.toBooleanArray(),
onSelected = {
val resultSet = it.mapIndexed { index, selected -> if (selected) autoDownloadValues[index] else null }.filterNotNull().toSet()
viewModel.setMobileAutoDownloadValues(resultSet)
}
)
override fun onCallDataModeSelected(code: String) {
viewModel.setCallDataMode(CallDataMode.fromCode(abs(code.toInt() - 2)))
}
multiSelectPref(
title = DSLSettingsText.from(R.string.preferences_chats__when_using_wifi),
listItems = autoDownloadLabels,
selected = autoDownloadValues.map { state.wifiAutoDownloadValues.contains(it) }.toBooleanArray(),
onSelected = {
val resultSet = it.mapIndexed { index, selected -> if (selected) autoDownloadValues[index] else null }.filterNotNull().toSet()
viewModel.setWifiAutoDownloadValues(resultSet)
}
)
override fun onUseProxyClick() {
findNavController().safeNavigate(R.id.action_dataAndStorageSettingsFragment_to_editProxyFragment)
}
multiSelectPref(
title = DSLSettingsText.from(R.string.preferences_chats__when_roaming),
listItems = autoDownloadLabels,
selected = autoDownloadValues.map { state.roamingAutoDownloadValues.contains(it) }.toBooleanArray(),
onSelected = {
val resultSet = it.mapIndexed { index, selected -> if (selected) autoDownloadValues[index] else null }.filterNotNull().toSet()
viewModel.setRoamingAutoDownloadValues(resultSet)
}
)
override fun onMobileDataAutoDownloadSelectionChanged(selection: Array<String>) {
viewModel.setMobileAutoDownloadValues(selection.toSet())
}
dividerPref()
override fun onWifiDataAutoDownloadSelectionChanged(selection: Array<String>) {
viewModel.setWifiAutoDownloadValues(selection.toSet())
}
sectionHeaderPref(R.string.DataAndStorageSettingsFragment__media_quality)
radioListPref(
title = DSLSettingsText.from(R.string.DataAndStorageSettingsFragment__sent_media_quality),
listItems = sentMediaQualityLabels,
selected = SentMediaQuality.entries.indexOf(state.sentMediaQuality),
onSelected = { viewModel.setSentMediaQuality(SentMediaQuality.entries[it]) }
)
textPref(
summary = DSLSettingsText.from(R.string.DataAndStorageSettingsFragment__sending_high_quality_media_will_use_more_data)
)
dividerPref()
sectionHeaderPref(R.string.DataAndStorageSettingsFragment__calls)
radioListPref(
title = DSLSettingsText.from(R.string.preferences_data_and_storage__use_less_data_for_calls),
listItems = callDataModeLabels,
selected = abs(state.callDataMode.code - 2),
onSelected = {
viewModel.setCallDataMode(CallDataMode.fromCode(abs(it - 2)))
}
)
textPref(
summary = DSLSettingsText.from(R.string.preference_data_and_storage__using_less_data_may_improve_calls_on_bad_networks)
)
dividerPref()
sectionHeaderPref(R.string.preferences_proxy)
clickPref(
title = DSLSettingsText.from(R.string.preferences_use_proxy),
summary = DSLSettingsText.from(if (state.isProxyEnabled) R.string.preferences_on else R.string.preferences_off),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_dataAndStorageSettingsFragment_to_editProxyFragment)
}
)
override fun onRoamingDataAutoDownloadSelectionChanged(selection: Array<String>) {
viewModel.setRoamingAutoDownloadValues(selection.toSet())
}
}
}
private interface DataAndStorageSettingsCallbacks {
fun onNavigationClick() = Unit
fun onManageStorageClick() = Unit
fun onSentMediaQualitySelected(code: String) = Unit
fun onCallDataModeSelected(code: String) = Unit
fun onUseProxyClick() = Unit
fun onMobileDataAutoDownloadSelectionChanged(selection: Array<String>) = Unit
fun onWifiDataAutoDownloadSelectionChanged(selection: Array<String>) = Unit
fun onRoamingDataAutoDownloadSelectionChanged(selection: Array<String>) = Unit
object Empty : DataAndStorageSettingsCallbacks
}
@Composable
private fun DataAndStorageSettingsScreen(
state: DataAndStorageSettingsState,
callbacks: DataAndStorageSettingsCallbacks
) {
Scaffolds.Settings(
title = stringResource(R.string.preferences__data_and_storage),
onNavigationClick = callbacks::onNavigationClick,
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24)
) { paddingValues ->
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.then(rememberStatusBarColorNestedScrollModifier())
) {
item {
Rows.TextRow(
text = stringResource(R.string.preferences_data_and_storage__manage_storage),
label = state.totalStorageUse.bytes.toUnitString(),
onClick = callbacks::onManageStorageClick
)
}
item {
Dividers.Default()
}
item {
Texts.SectionHeader(stringResource(R.string.preferences_chats__media_auto_download))
}
item {
Rows.MultiSelectRow(
text = stringResource(R.string.preferences_chats__when_using_mobile_data),
labels = stringArrayResource(R.array.pref_media_download_entries),
values = stringArrayResource(R.array.pref_media_download_values),
selection = state.mobileAutoDownloadValues.toTypedArray(),
onSelectionChanged = callbacks::onMobileDataAutoDownloadSelectionChanged
)
}
item {
Rows.MultiSelectRow(
text = stringResource(R.string.preferences_chats__when_using_wifi),
labels = stringArrayResource(R.array.pref_media_download_entries),
values = stringArrayResource(R.array.pref_media_download_values),
selection = state.wifiAutoDownloadValues.toTypedArray(),
onSelectionChanged = callbacks::onWifiDataAutoDownloadSelectionChanged
)
}
item {
Rows.MultiSelectRow(
text = stringResource(R.string.preferences_chats__when_roaming),
labels = stringArrayResource(R.array.pref_media_download_entries),
values = stringArrayResource(R.array.pref_media_download_values),
selection = state.roamingAutoDownloadValues.toTypedArray(),
onSelectionChanged = callbacks::onRoamingDataAutoDownloadSelectionChanged
)
}
item {
Dividers.Default()
}
item {
Texts.SectionHeader(stringResource(R.string.DataAndStorageSettingsFragment__media_quality))
}
item {
val context = LocalContext.current
val labels = remember { SentMediaQuality.getLabels(context) }
Rows.RadioListRow(
text = stringResource(R.string.DataAndStorageSettingsFragment__sent_media_quality),
labels = labels,
values = SentMediaQuality.entries.map { it.code.toString() }.toTypedArray(),
selectedValue = state.sentMediaQuality.code.toString(),
onSelected = callbacks::onSentMediaQualitySelected
)
}
item {
Rows.TextRow(
text = {
Text(
text = stringResource(R.string.DataAndStorageSettingsFragment__sending_high_quality_media_will_use_more_data),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
)
}
item {
Dividers.Default()
}
item {
Texts.SectionHeader(stringResource(R.string.DataAndStorageSettingsFragment__calls))
}
item {
Rows.RadioListRow(
text = stringResource(R.string.preferences_data_and_storage__use_less_data_for_calls),
labels = stringArrayResource(R.array.pref_data_and_storage_call_data_mode_values),
values = CallDataMode.entries.map { it.code.toString() }.toTypedArray(),
selectedValue = abs(state.callDataMode.code - 2).toString(),
onSelected = callbacks::onCallDataModeSelected
)
}
item {
Rows.TextRow(
text = {
Text(
text = stringResource(R.string.preference_data_and_storage__using_less_data_may_improve_calls_on_bad_networks),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
)
}
item {
Dividers.Default()
}
item {
Texts.SectionHeader(stringResource(R.string.preferences_proxy))
}
item {
Rows.TextRow(
text = stringResource(R.string.preferences_use_proxy),
label = stringResource(if (state.isProxyEnabled) R.string.preferences_on else R.string.preferences_off),
onClick = callbacks::onUseProxyClick
)
}
}
}
}
@SignalPreview
@Composable
private fun DataAndStorageSettingsScreenPreview() {
Previews.Preview {
DataAndStorageSettingsScreen(
state = DataAndStorageSettingsState(
totalStorageUse = 100_000,
mobileAutoDownloadValues = setOf(),
wifiAutoDownloadValues = setOf(),
roamingAutoDownloadValues = setOf(),
callDataMode = CallDataMode.HIGH_ALWAYS,
isProxyEnabled = false,
sentMediaQuality = SentMediaQuality.STANDARD
),
callbacks = DataAndStorageSettingsCallbacks.Empty
)
}
}

View File

@@ -1,14 +1,15 @@
package org.thoughtcrime.securesms.components.settings.app.data
import android.content.SharedPreferences
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.livedata.Store
import org.thoughtcrime.securesms.webrtc.CallDataMode
class DataAndStorageSettingsViewModel(
@@ -16,9 +17,9 @@ class DataAndStorageSettingsViewModel(
private val repository: DataAndStorageSettingsRepository
) : ViewModel() {
private val store = Store(getState())
private val store = MutableStateFlow(getState())
val state: LiveData<DataAndStorageSettingsState> = store.stateLiveData
val state: StateFlow<DataAndStorageSettingsState> = store
fun refresh() {
repository.getTotalStorageUse { totalStorageUse ->