From 2eb4f650d81ec8c772131414e48dcc7abd2a095c Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 10 Sep 2025 15:12:54 -0300 Subject: [PATCH] Convert NotificationsSettingsFragment to compose. --- .../NotificationPrioritySelectionContract.kt | 32 + .../NotificationSoundSelectionContract.kt | 78 ++ .../NotificationsSettingsFragment.kt | 893 +++++++++++------- .../NotificationsSettingsViewModel.kt | 9 +- .../components/settings/models/Banner.kt | 68 ++ app/src/main/res/layout/dsl_banner.xml | 1 + .../org/signal/core/ui/compose/Dialogs.kt | 14 +- .../java/org/signal/core/ui/compose/Rows.kt | 60 +- 8 files changed, 778 insertions(+), 377 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationPrioritySelectionContract.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationSoundSelectionContract.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationPrioritySelectionContract.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationPrioritySelectionContract.kt new file mode 100644 index 0000000000..3513e1e4c8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationPrioritySelectionContract.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.notifications + +import android.content.Context +import android.content.Intent +import android.provider.Settings +import androidx.activity.result.contract.ActivityResultContract +import org.thoughtcrime.securesms.notifications.NotificationChannels + +/** + * Activity result contract for launching the system notification channel settings screen + * for the messages notification channel. + * + * This contract allows users to configure notification priority, sound, vibration, and other + * channel-specific settings through the system's native notification settings UI. + */ +class NotificationPrioritySelectionContract : ActivityResultContract() { + override fun createIntent(context: Context, input: Unit): Intent { + return Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + .putExtra( + Settings.EXTRA_CHANNEL_ID, + NotificationChannels.getInstance().messagesChannel + ) + .putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + + override fun parseResult(resultCode: Int, intent: Intent?) = Unit +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationSoundSelectionContract.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationSoundSelectionContract.kt new file mode 100644 index 0000000000..8ed7195574 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationSoundSelectionContract.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.notifications + +import android.content.Context +import android.content.Intent +import android.media.RingtoneManager +import android.net.Uri +import android.provider.Settings +import androidx.activity.result.contract.ActivityResultContract +import org.signal.core.util.getParcelableExtraCompat +import org.thoughtcrime.securesms.keyvalue.SignalStore + +/** + * Activity result contract for launching the system ringtone picker to select notification sounds. + * + * Supports selecting sounds for both message notifications and call ringtones through the + * Android system's ringtone picker interface. + * + * @param target Specifies whether to configure sounds for messages or calls + */ +class NotificationSoundSelectionContract( + private val target: Target +) : ActivityResultContract() { + + /** + * Defines the type of notification sound to configure. + */ + enum class Target { + /** Message notification sounds */ + MESSAGE, + + /** Call ringtones */ + CALL + } + + override fun createIntent(context: Context, input: Unit): Intent { + return when (target) { + Target.MESSAGE -> createIntentForMessageSoundSelection() + Target.CALL -> createIntentForCallSoundSelection() + } + } + + private fun createIntentForMessageSoundSelection(): Intent { + val current = SignalStore.settings.messageNotificationSound + + return Intent(RingtoneManager.ACTION_RINGTONE_PICKER) + .putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) + .putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true) + .putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION) + .putExtra( + RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, + Settings.System.DEFAULT_NOTIFICATION_URI + ) + .putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current) + } + + private fun createIntentForCallSoundSelection(): Intent { + val current = SignalStore.settings.callRingtone + + return Intent(RingtoneManager.ACTION_RINGTONE_PICKER) + .putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) + .putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true) + .putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE) + .putExtra( + RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, + Settings.System.DEFAULT_RINGTONE_URI + ) + .putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current) + } + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? { + return intent?.getParcelableExtraCompat(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsFragment.kt index c7017ca091..a56d5e5748 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsFragment.kt @@ -1,420 +1,615 @@ package org.thoughtcrime.securesms.components.settings.app.notifications -import android.app.Activity import android.content.ActivityNotFoundException -import android.content.Intent -import android.graphics.ColorFilter -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter import android.media.Ringtone -import android.media.RingtoneManager import android.net.Uri import android.os.Build -import android.provider.Settings -import android.text.TextUtils +import android.os.Bundle import android.view.View import android.widget.Toast +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContract import androidx.annotation.RequiresApi -import androidx.core.content.ContextCompat -import androidx.lifecycle.ViewModelProvider +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager -import org.signal.core.util.getParcelableExtraCompat +import kotlinx.coroutines.launch +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.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment -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.PreferenceModel -import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder -import org.thoughtcrime.securesms.components.settings.RadioListPreference -import org.thoughtcrime.securesms.components.settings.RadioListPreferenceViewHolder -import org.thoughtcrime.securesms.components.settings.configure +import org.thoughtcrime.securesms.components.settings.app.routes.AppSettingsRoute +import org.thoughtcrime.securesms.components.settings.app.routes.AppSettingsRouter import org.thoughtcrime.securesms.components.settings.models.Banner -import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.TurnOnNotificationsBottomSheet import org.thoughtcrime.securesms.util.BottomSheetUtil import org.thoughtcrime.securesms.util.RingtoneUtil -import org.thoughtcrime.securesms.util.ViewUtil -import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory -import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.navigation.safeNavigate +import org.thoughtcrime.securesms.util.viewModel -private const val MESSAGE_SOUND_SELECT: Int = 1 -private const val CALL_RINGTONE_SELECT: Int = 2 -private val TAG = Log.tag(NotificationsSettingsFragment::class.java) +class NotificationsSettingsFragment : ComposeFragment() { -class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__notifications) { + private val viewModel: NotificationsSettingsViewModel by viewModel { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) - private val repeatAlertsValues by lazy { resources.getStringArray(R.array.pref_repeat_alerts_values) } - private val repeatAlertsLabels by lazy { resources.getStringArray(R.array.pref_repeat_alerts_entries) } + NotificationsSettingsViewModel.Factory(sharedPreferences).create(NotificationsSettingsViewModel::class.java) + } - private val notificationPrivacyValues by lazy { resources.getStringArray(R.array.pref_notification_privacy_values) } - private val notificationPrivacyLabels by lazy { resources.getStringArray(R.array.pref_notification_privacy_entries) } + private val appSettingsRouter: AppSettingsRouter by viewModel { + AppSettingsRouter() + } - private val notificationPriorityValues by lazy { resources.getStringArray(R.array.pref_notification_priority_values) } - private val notificationPriorityLabels by lazy { resources.getStringArray(R.array.pref_notification_priority_entries) } + private lateinit var callbacks: DefaultNotificationsSettingsCallbacks - private val ledColorValues by lazy { resources.getStringArray(R.array.pref_led_color_values) } - private val ledColorLabels by lazy { resources.getStringArray(R.array.pref_led_color_entries) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - private val ledBlinkValues by lazy { resources.getStringArray(R.array.pref_led_blink_pattern_values) } - private val ledBlinkLabels by lazy { resources.getStringArray(R.array.pref_led_blink_pattern_entries) } + callbacks = DefaultNotificationsSettingsCallbacks(requireActivity(), viewModel, appSettingsRouter, DefaultNotificationsSettingsCallbacks.ActivityResultRegisterer.ForFragment(this)) - private lateinit var viewModel: NotificationsSettingsViewModel + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + appSettingsRouter.currentRoute.collect { + when (it) { + AppSettingsRoute.NotificationsRoute.NotificationProfiles -> { + findNavController().safeNavigate(R.id.action_notificationsSettingsFragment_to_notificationProfilesFragment) + } + + else -> error("Unexpected route: ${it.javaClass.name}") + } + } + } + } + } override fun onResume() { super.onResume() viewModel.refresh() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == MESSAGE_SOUND_SELECT && resultCode == Activity.RESULT_OK && data != null) { - val uri: Uri? = data.getParcelableExtraCompat(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java) - viewModel.setMessageNotificationsSound(uri) - } else if (requestCode == CALL_RINGTONE_SELECT && resultCode == Activity.RESULT_OK && data != null) { - val uri: Uri? = data.getParcelableExtraCompat(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java) - viewModel.setCallRingtone(uri) + @Composable + override fun FragmentContent() { + val state by viewModel.state.collectAsStateWithLifecycle() + + NotificationsSettingsScreen(state = state, callbacks = callbacks) + } +} + +/** + * Default callbacks that package up launcher handling and other logic that was in the original fragment. + * This must be called during the creation cycle of the component it is attached to. + */ +open class DefaultNotificationsSettingsCallbacks( + val activity: FragmentActivity, + val viewModel: NotificationsSettingsViewModel, + val appSettingsRouter: AppSettingsRouter, + activityResultRegisterer: ActivityResultRegisterer = ActivityResultRegisterer.ForActivity(activity) +) : NotificationsSettingsCallbacks { + + companion object { + private val TAG = Log.tag(DefaultNotificationsSettingsCallbacks::class) + } + + interface ActivityResultRegisterer { + fun registerForActivityResult( + contract: ActivityResultContract, + callback: ActivityResultCallback + ): ActivityResultLauncher + + class ForActivity(val activity: FragmentActivity) : ActivityResultRegisterer { + override fun registerForActivityResult( + contract: ActivityResultContract, + callback: ActivityResultCallback + ): ActivityResultLauncher { + return activity.registerForActivityResult(contract, callback) + } + } + + class ForFragment(val fragment: Fragment) : ActivityResultRegisterer { + override fun registerForActivityResult( + contract: ActivityResultContract, + callback: ActivityResultCallback + ): ActivityResultLauncher { + return fragment.registerForActivityResult(contract, callback) + } } } - override fun bindAdapter(adapter: MappingAdapter) { - adapter.registerFactory( - LedColorPreference::class.java, - LayoutFactory(::LedColorPreferenceViewHolder, R.layout.dsl_preference_item) - ) + private val messageSoundSelectionLauncher: ActivityResultLauncher = activityResultRegisterer.registerForActivityResult( + NotificationSoundSelectionContract(NotificationSoundSelectionContract.Target.MESSAGE), + viewModel::setMessageNotificationsSound + ) - Banner.register(adapter) + private val callsSoundSelectionLauncher: ActivityResultLauncher = activityResultRegisterer.registerForActivityResult( + NotificationSoundSelectionContract(NotificationSoundSelectionContract.Target.CALL), + viewModel::setCallRingtone + ) - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) - val factory = NotificationsSettingsViewModel.Factory(sharedPreferences) + private val notificationPrioritySelectionLauncher: ActivityResultLauncher = activityResultRegisterer.registerForActivityResult( + contract = NotificationPrioritySelectionContract(), + callback = {} + ) - viewModel = ViewModelProvider(this, factory)[NotificationsSettingsViewModel::class.java] + override fun onTurnOnNotificationsActionClick() { + TurnOnNotificationsBottomSheet.turnOnSystemNotificationsFragment(activity).show(activity.supportFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + } - viewModel.state.observe(viewLifecycleOwner) { - adapter.submitList(getConfiguration(it).toMappingModelList()) + override fun onNavigationClick() { + activity.onBackPressedDispatcher.onBackPressed() + } + + override fun setMessageNotificationsEnabled(enabled: Boolean) { + viewModel.setMessageNotificationsEnabled(enabled) + } + + override fun onCustomizeClick() { + activity.let { + NotificationChannels.getInstance().openChannelSettings(it, NotificationChannels.getInstance().messagesChannel, null) } } - private fun getConfiguration(state: NotificationsSettingsState): DSLConfiguration { - return configure { - if (!state.messageNotificationsState.canEnableNotifications) { - customPref( - Banner.Model( - textId = R.string.NotificationSettingsFragment__to_enable_notifications, - actionId = R.string.NotificationSettingsFragment__turn_on, - onClick = { - TurnOnNotificationsBottomSheet.turnOnSystemNotificationsFragment(requireContext()).show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) - } - ) - ) - } - - sectionHeaderPref(R.string.NotificationsSettingsFragment__messages) - - switchPref( - title = DSLSettingsText.from(R.string.preferences__notifications), - isEnabled = state.messageNotificationsState.canEnableNotifications, - isChecked = state.messageNotificationsState.notificationsEnabled, - onClick = { - viewModel.setMessageNotificationsEnabled(!state.messageNotificationsState.notificationsEnabled) - } - ) - - if (Build.VERSION.SDK_INT >= 30) { - clickPref( - title = DSLSettingsText.from(R.string.preferences__customize), - summary = DSLSettingsText.from(R.string.preferences__change_sound_and_vibration), - isEnabled = state.messageNotificationsState.notificationsEnabled, - onClick = { - NotificationChannels.getInstance().openChannelSettings(requireActivity(), NotificationChannels.getInstance().messagesChannel, null) - } - ) - } else { - clickPref( - title = DSLSettingsText.from(R.string.preferences__sound), - summary = DSLSettingsText.from(getRingtoneSummary(state.messageNotificationsState.sound)), - isEnabled = state.messageNotificationsState.notificationsEnabled, - onClick = { - launchMessageSoundSelectionIntent() - } - ) - - switchPref( - title = DSLSettingsText.from(R.string.preferences__vibrate), - isChecked = state.messageNotificationsState.vibrateEnabled, - isEnabled = state.messageNotificationsState.notificationsEnabled, - onClick = { - viewModel.setMessageNotificationVibration(!state.messageNotificationsState.vibrateEnabled) - } - ) - - customPref( - LedColorPreference( - colorValues = ledColorValues, - radioListPreference = RadioListPreference( - title = DSLSettingsText.from(R.string.preferences__led_color), - listItems = ledColorLabels, - selected = ledColorValues.indexOf(state.messageNotificationsState.ledColor), - isEnabled = state.messageNotificationsState.notificationsEnabled, - onSelected = { - viewModel.setMessageNotificationLedColor(ledColorValues[it]) - } - ) - ) - ) - - if (!NotificationChannels.supported()) { - radioListPref( - title = DSLSettingsText.from(R.string.preferences__pref_led_blink_title), - listItems = ledBlinkLabels, - selected = ledBlinkValues.indexOf(state.messageNotificationsState.ledBlink), - isEnabled = state.messageNotificationsState.notificationsEnabled, - onSelected = { - viewModel.setMessageNotificationLedBlink(ledBlinkValues[it]) - } - ) - } - } - - switchPref( - title = DSLSettingsText.from(R.string.preferences_notifications__in_chat_sounds), - isChecked = state.messageNotificationsState.inChatSoundsEnabled, - isEnabled = state.messageNotificationsState.notificationsEnabled, - onClick = { - viewModel.setMessageNotificationInChatSoundsEnabled(!state.messageNotificationsState.inChatSoundsEnabled) - } - ) - - radioListPref( - title = DSLSettingsText.from(R.string.preferences__repeat_alerts), - listItems = repeatAlertsLabels, - selected = repeatAlertsValues.indexOf(state.messageNotificationsState.repeatAlerts.toString()), - isEnabled = state.messageNotificationsState.notificationsEnabled, - onSelected = { - viewModel.setMessageRepeatAlerts(repeatAlertsValues[it].toInt()) - } - ) - - radioListPref( - title = DSLSettingsText.from(R.string.preferences_notifications__show), - listItems = notificationPrivacyLabels, - selected = notificationPrivacyValues.indexOf(state.messageNotificationsState.messagePrivacy), - isEnabled = state.messageNotificationsState.notificationsEnabled, - onSelected = { - viewModel.setMessageNotificationPrivacy(notificationPrivacyValues[it]) - } - ) - - if (Build.VERSION.SDK_INT >= 23 && state.messageNotificationsState.troubleshootNotifications) { - clickPref( - title = DSLSettingsText.from(R.string.preferences_notifications__troubleshoot), - isEnabled = true, - onClick = { - PromptBatterySaverDialogFragment.show(childFragmentManager) - } - ) - } - - if (Build.VERSION.SDK_INT < 30) { - if (NotificationChannels.supported()) { - clickPref( - title = DSLSettingsText.from(R.string.preferences_notifications__priority), - isEnabled = state.messageNotificationsState.notificationsEnabled, - onClick = { - launchNotificationPriorityIntent() - } - ) - } else { - radioListPref( - title = DSLSettingsText.from(R.string.preferences_notifications__priority), - listItems = notificationPriorityLabels, - selected = notificationPriorityValues.indexOf(state.messageNotificationsState.priority.toString()), - isEnabled = state.messageNotificationsState.notificationsEnabled, - onSelected = { - viewModel.setMessageNotificationPriority(notificationPriorityValues[it].toInt()) - } - ) - } - } - - dividerPref() - - sectionHeaderPref(R.string.NotificationsSettingsFragment__calls) - - switchPref( - title = DSLSettingsText.from(R.string.preferences__notifications), - isEnabled = state.callNotificationsState.canEnableNotifications, - isChecked = state.callNotificationsState.notificationsEnabled, - onClick = { - viewModel.setCallNotificationsEnabled(!state.callNotificationsState.notificationsEnabled) - } - ) - - clickPref( - title = DSLSettingsText.from(R.string.preferences_notifications__ringtone), - summary = DSLSettingsText.from(getRingtoneSummary(state.callNotificationsState.ringtone)), - isEnabled = state.callNotificationsState.notificationsEnabled, - onClick = { - launchCallRingtoneSelectionIntent() - } - ) - - switchPref( - title = DSLSettingsText.from(R.string.preferences__vibrate), - isChecked = state.callNotificationsState.vibrateEnabled, - isEnabled = state.callNotificationsState.notificationsEnabled, - onClick = { - viewModel.setCallVibrateEnabled(!state.callNotificationsState.vibrateEnabled) - } - ) - - dividerPref() - - sectionHeaderPref(R.string.NotificationsSettingsFragment__notification_profiles) - - clickPref( - title = DSLSettingsText.from(R.string.NotificationsSettingsFragment__profiles), - summary = DSLSettingsText.from(R.string.NotificationsSettingsFragment__create_a_profile_to_receive_notifications_only_from_people_and_groups_you_choose), - onClick = { - findNavController().safeNavigate(R.id.action_notificationsSettingsFragment_to_notificationProfilesFragment) - } - ) - - dividerPref() - - sectionHeaderPref(R.string.NotificationsSettingsFragment__notify_when) - - switchPref( - title = DSLSettingsText.from(R.string.NotificationsSettingsFragment__contact_joins_signal), - isChecked = state.notifyWhenContactJoinsSignal, - onClick = { - viewModel.setNotifyWhenContactJoinsSignal(!state.notifyWhenContactJoinsSignal) - } - ) - } - } - - private fun getRingtoneSummary(uri: Uri): String { - return if (TextUtils.isEmpty(uri.toString())) { - getString(R.string.preferences__silent) + override fun getRingtoneSummary(uri: Uri): String { + return if (uri.toString().isBlank()) { + activity.getString(R.string.preferences__silent) } else { - val tone: Ringtone? = RingtoneUtil.getRingtone(requireContext(), uri) + val tone: Ringtone? = RingtoneUtil.getRingtone(activity, uri) if (tone != null) { try { - tone.getTitle(requireContext()) ?: getString(R.string.NotificationsSettingsFragment__unknown_ringtone) + tone.getTitle(activity) ?: activity.getString(R.string.NotificationsSettingsFragment__unknown_ringtone) } catch (e: SecurityException) { Log.w(TAG, "Unable to get title for ringtone", e) - return getString(R.string.NotificationsSettingsFragment__unknown_ringtone) + return activity.getString(R.string.NotificationsSettingsFragment__unknown_ringtone) } } else { - getString(R.string.preferences__default) + activity.getString(R.string.preferences__default) } } } - private fun launchMessageSoundSelectionIntent() { - val current = SignalStore.settings.messageNotificationSound - - val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER) - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true) - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION) - intent.putExtra( - RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, - Settings.System.DEFAULT_NOTIFICATION_URI - ) - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current) - - openRingtonePicker(intent, MESSAGE_SOUND_SELECT) - } - - @RequiresApi(26) - private fun launchNotificationPriorityIntent() { - val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) - intent.putExtra( - Settings.EXTRA_CHANNEL_ID, - NotificationChannels.getInstance().messagesChannel - ) - intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) - startActivity(intent) - } - - private fun launchCallRingtoneSelectionIntent() { - val current = SignalStore.settings.callRingtone - - val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER) - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true) - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE) - intent.putExtra( - RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, - Settings.System.DEFAULT_RINGTONE_URI - ) - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current) - - openRingtonePicker(intent, CALL_RINGTONE_SELECT) - } - - @Suppress("DEPRECATION") - private fun openRingtonePicker(intent: Intent, requestCode: Int) { + override fun launchMessageSoundSelectionIntent() { try { - startActivityForResult(intent, requestCode) + messageSoundSelectionLauncher.launch(Unit) } catch (e: ActivityNotFoundException) { - Toast.makeText(requireContext(), R.string.NotificationSettingsFragment__failed_to_open_picker, Toast.LENGTH_LONG).show() + Toast.makeText(activity, R.string.NotificationSettingsFragment__failed_to_open_picker, Toast.LENGTH_LONG).show() } } - private class LedColorPreference( - val colorValues: Array, - val radioListPreference: RadioListPreference - ) : PreferenceModel( - title = radioListPreference.title, - icon = radioListPreference.icon, - summary = radioListPreference.summary + override fun launchCallsSoundSelectionIntent() { + try { + callsSoundSelectionLauncher.launch(Unit) + } catch (e: ActivityNotFoundException) { + Toast.makeText(activity, R.string.NotificationSettingsFragment__failed_to_open_picker, Toast.LENGTH_LONG).show() + } + } + + override fun setMessageNotificationVibration(enabled: Boolean) { + viewModel.setMessageNotificationsEnabled(enabled) + } + + override fun setMessasgeNotificationLedColor(selection: String) { + viewModel.setMessageNotificationLedColor(selection) + } + + override fun setMessasgeNotificationLedBlink(selection: String) { + viewModel.setMessageNotificationLedBlink(selection) + } + + override fun setMessageNotificationInChatSoundsEnabled(enabled: Boolean) { + viewModel.setMessageNotificationInChatSoundsEnabled(enabled) + } + + override fun setMessageRepeatAlerts(selection: String) { + viewModel.setMessageRepeatAlerts(selection.toInt()) + } + + override fun setMessageNotificationPrivacy(selection: String) { + viewModel.setMessageNotificationPrivacy(selection) + } + + @RequiresApi(23) + override fun onTroubleshootNotificationsClick() { + PromptBatterySaverDialogFragment.show(activity.supportFragmentManager) + } + + override fun launchNotificationPriorityIntent() { + notificationPrioritySelectionLauncher.launch(Unit) + } + + override fun setMessageNotificationPriority(selection: String) { + viewModel.setMessageNotificationPriority(selection.toInt()) + } + + override fun setCallNotificationsEnabled(enabled: Boolean) { + viewModel.setCallNotificationsEnabled(enabled) + } + + override fun setCallVibrateEnabled(enabled: Boolean) { + viewModel.setCallVibrateEnabled(enabled) + } + + override fun onNavigationProfilesClick() { + appSettingsRouter.navigateTo(AppSettingsRoute.NotificationsRoute.NotificationProfiles) + } + + override fun setNotifyWhenContactJoinsSignal(enabled: Boolean) { + viewModel.setNotifyWhenContactJoinsSignal(enabled) + } +} + +interface NotificationsSettingsCallbacks { + fun onTurnOnNotificationsActionClick() = Unit + fun onNavigationClick() = Unit + fun setMessageNotificationsEnabled(enabled: Boolean) = Unit + fun onCustomizeClick() = Unit + fun getRingtoneSummary(uri: Uri): String = "Test Sound" + fun launchMessageSoundSelectionIntent(): Unit = Unit + fun launchCallsSoundSelectionIntent(): Unit = Unit + fun setMessageNotificationVibration(enabled: Boolean) = Unit + fun setMessasgeNotificationLedColor(selection: String) = Unit + fun setMessasgeNotificationLedBlink(selection: String) = Unit + fun setMessageNotificationInChatSoundsEnabled(enabled: Boolean) = Unit + fun setMessageRepeatAlerts(selection: String) = Unit + fun setMessageNotificationPrivacy(selection: String) = Unit + fun onTroubleshootNotificationsClick() = Unit + fun launchNotificationPriorityIntent() = Unit + fun setMessageNotificationPriority(selection: String) = Unit + fun setCallNotificationsEnabled(enabled: Boolean) = Unit + fun setCallVibrateEnabled(enabled: Boolean) = Unit + fun onNavigationProfilesClick() = Unit + fun setNotifyWhenContactJoinsSignal(enabled: Boolean) = Unit + + object Empty : NotificationsSettingsCallbacks +} + +@Composable +fun NotificationsSettingsScreen( + state: NotificationsSettingsState, + callbacks: NotificationsSettingsCallbacks, + deviceState: DeviceState = remember { DeviceState() } +) { + Scaffolds.Settings( + title = stringResource(R.string.preferences__notifications), + onNavigationClick = callbacks::onNavigationClick, + navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24) ) { - override fun areContentsTheSame(newItem: LedColorPreference): Boolean { - return super.areContentsTheSame(newItem) && radioListPreference.areContentsTheSame(newItem.radioListPreference) - } - } + LazyColumn( + modifier = Modifier.padding(it) + ) { + if (!state.messageNotificationsState.canEnableNotifications) { + item { + Banner( + text = stringResource(R.string.NotificationSettingsFragment__to_enable_notifications), + action = stringResource(R.string.NotificationSettingsFragment__turn_on), + onActionClick = callbacks::onTurnOnNotificationsActionClick + ) + } + } - private class LedColorPreferenceViewHolder(itemView: View) : - PreferenceViewHolder(itemView) { + item { + Texts.SectionHeader(stringResource(R.string.NotificationsSettingsFragment__messages)) + } - val radioListPreferenceViewHolder = RadioListPreferenceViewHolder(itemView) + item { + Rows.ToggleRow( + text = stringResource(R.string.preferences__notifications), + enabled = state.messageNotificationsState.canEnableNotifications, + checked = state.messageNotificationsState.notificationsEnabled, + onCheckChanged = callbacks::setMessageNotificationsEnabled + ) + } - override fun bind(model: LedColorPreference) { - super.bind(model) - radioListPreferenceViewHolder.bind(model.radioListPreference) - - summaryView.visibility = View.GONE - - val circleDrawable = requireNotNull(ContextCompat.getDrawable(context, R.drawable.circle_tintable)) - circleDrawable.setBounds(0, 0, ViewUtil.dpToPx(20), ViewUtil.dpToPx(20)) - circleDrawable.colorFilter = model.colorValues[model.radioListPreference.selected].toColorFilter() - - if (ViewUtil.isLtr(itemView)) { - titleView.setCompoundDrawables(null, null, circleDrawable, null) + if (deviceState.apiLevel >= 30) { + item { + Rows.TextRow( + text = stringResource(R.string.preferences__customize), + label = stringResource(R.string.preferences__change_sound_and_vibration), + enabled = state.messageNotificationsState.notificationsEnabled, + onClick = callbacks::onCustomizeClick + ) + } } else { - titleView.setCompoundDrawables(circleDrawable, null, null, null) - } - } + item { + Rows.TextRow( + text = stringResource(R.string.preferences__sound), + label = remember(state.messageNotificationsState.sound) { + callbacks.getRingtoneSummary(state.messageNotificationsState.sound) + }, + enabled = state.messageNotificationsState.notificationsEnabled, + onClick = callbacks::launchMessageSoundSelectionIntent + ) + } - private fun String.toColorFilter(): ColorFilter { - val color = when (this) { - "green" -> ContextCompat.getColor(context, R.color.green_500) - "red" -> ContextCompat.getColor(context, R.color.red_500) - "blue" -> ContextCompat.getColor(context, R.color.blue_500) - "yellow" -> ContextCompat.getColor(context, R.color.yellow_500) - "cyan" -> ContextCompat.getColor(context, R.color.cyan_500) - "magenta" -> ContextCompat.getColor(context, R.color.pink_500) - "white" -> ContextCompat.getColor(context, R.color.white) - else -> ContextCompat.getColor(context, R.color.transparent) + item { + Rows.ToggleRow( + text = stringResource(R.string.preferences__vibrate), + checked = state.messageNotificationsState.vibrateEnabled, + enabled = state.messageNotificationsState.notificationsEnabled, + onCheckChanged = callbacks::setMessageNotificationsEnabled + ) + } + + item { + Rows.RadioListRow( + text = { + Box( + modifier = Modifier + .clip(CircleShape) + .size(24.dp) + .background(color = getLedColor(state.messageNotificationsState.ledColor)) + ) + + Spacer(modifier = Modifier.size(10.dp)) + + Text(text = stringResource(R.string.preferences__led_color)) + }, + dialogTitle = stringResource(R.string.preferences__led_color), + labels = stringArrayResource(R.array.pref_led_color_entries), + values = stringArrayResource(R.array.pref_led_color_values), + selectedValue = state.messageNotificationsState.ledColor, + enabled = state.messageNotificationsState.notificationsEnabled, + onSelected = callbacks::setMessasgeNotificationLedColor + ) + } + + if (!deviceState.supportsNotificationChannels) { + item { + Rows.RadioListRow( + text = stringResource(R.string.preferences__pref_led_blink_title), + labels = stringArrayResource(R.array.pref_led_blink_pattern_entries), + values = stringArrayResource(R.array.pref_led_blink_pattern_values), + selectedValue = state.messageNotificationsState.ledBlink, + enabled = state.messageNotificationsState.notificationsEnabled, + onSelected = callbacks::setMessasgeNotificationLedBlink + ) + } + } } - return PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) + item { + Rows.ToggleRow( + text = stringResource(R.string.preferences_notifications__in_chat_sounds), + checked = state.messageNotificationsState.inChatSoundsEnabled, + enabled = state.messageNotificationsState.notificationsEnabled, + onCheckChanged = callbacks::setMessageNotificationInChatSoundsEnabled + ) + } + + item { + Rows.RadioListRow( + text = stringResource(R.string.preferences__repeat_alerts), + labels = stringArrayResource(R.array.pref_repeat_alerts_entries), + values = stringArrayResource(R.array.pref_repeat_alerts_values), + selectedValue = state.messageNotificationsState.repeatAlerts.toString(), + enabled = state.messageNotificationsState.notificationsEnabled, + onSelected = callbacks::setMessageRepeatAlerts + ) + } + + item { + Rows.RadioListRow( + text = stringResource(R.string.preferences_notifications__show), + labels = stringArrayResource(R.array.pref_notification_privacy_entries), + values = stringArrayResource(R.array.pref_notification_privacy_values), + selectedValue = state.messageNotificationsState.messagePrivacy, + enabled = state.messageNotificationsState.notificationsEnabled, + onSelected = callbacks::setMessageNotificationPrivacy + ) + } + + if (deviceState.apiLevel >= 23 && state.messageNotificationsState.troubleshootNotifications) { + item { + Rows.TextRow( + text = stringResource(R.string.preferences_notifications__troubleshoot), + onClick = callbacks::onTroubleshootNotificationsClick + ) + } + } + + if (deviceState.apiLevel < 30) { + if (deviceState.supportsNotificationChannels) { + item { + Rows.TextRow( + text = stringResource(R.string.preferences_notifications__priority), + enabled = state.messageNotificationsState.notificationsEnabled, + onClick = callbacks::launchNotificationPriorityIntent + ) + } + } else { + item { + Rows.RadioListRow( + text = stringResource(R.string.preferences_notifications__priority), + labels = stringArrayResource(R.array.pref_notification_priority_entries), + values = stringArrayResource(R.array.pref_notification_priority_values), + selectedValue = state.messageNotificationsState.priority.toString(), + enabled = state.messageNotificationsState.notificationsEnabled, + onSelected = callbacks::setMessageNotificationPriority + ) + } + } + } + + item { + Dividers.Default() + } + + item { + Texts.SectionHeader(stringResource(R.string.NotificationsSettingsFragment__calls)) + } + + item { + Rows.ToggleRow( + text = stringResource(R.string.preferences__notifications), + enabled = state.callNotificationsState.canEnableNotifications, + checked = state.callNotificationsState.notificationsEnabled, + onCheckChanged = callbacks::setCallNotificationsEnabled + ) + } + + item { + val ringtoneSummary = remember(state.callNotificationsState.ringtone) { + callbacks.getRingtoneSummary(state.callNotificationsState.ringtone) + } + + Rows.TextRow( + text = stringResource(R.string.preferences_notifications__ringtone), + label = ringtoneSummary, + enabled = state.callNotificationsState.notificationsEnabled, + onClick = callbacks::launchCallsSoundSelectionIntent + ) + } + + item { + Rows.ToggleRow( + text = stringResource(R.string.preferences__vibrate), + checked = state.callNotificationsState.vibrateEnabled, + enabled = state.callNotificationsState.notificationsEnabled, + onCheckChanged = callbacks::setCallVibrateEnabled + ) + } + + item { + Dividers.Default() + } + + item { + Texts.SectionHeader(stringResource(R.string.NotificationsSettingsFragment__notification_profiles)) + } + + item { + Rows.TextRow( + text = stringResource(R.string.NotificationsSettingsFragment__profiles), + label = stringResource(R.string.NotificationsSettingsFragment__create_a_profile_to_receive_notifications_only_from_people_and_groups_you_choose), + onClick = callbacks::onNavigationProfilesClick + ) + } + + item { + Dividers.Default() + } + + item { + Texts.SectionHeader(stringResource(R.string.NotificationsSettingsFragment__notify_when)) + } + + item { + Rows.ToggleRow( + text = stringResource(R.string.NotificationsSettingsFragment__contact_joins_signal), + checked = state.notifyWhenContactJoinsSignal, + onCheckChanged = callbacks::setNotifyWhenContactJoinsSignal + ) + } } } } + +@Composable +private fun getLedColor(ledColorString: String): Color { + return when (ledColorString) { + "green" -> colorResource(R.color.green_500) + "red" -> colorResource(R.color.red_500) + "blue" -> colorResource(R.color.blue_500) + "yellow" -> colorResource(R.color.yellow_500) + "cyan" -> colorResource(R.color.cyan_500) + "magenta" -> colorResource(R.color.pink_500) + "white" -> colorResource(R.color.white) + else -> colorResource(R.color.transparent) + } +} + +@SignalPreview +@Composable +private fun NotificationsSettingsScreenPreview() { + Previews.Preview { + NotificationsSettingsScreen( + deviceState = rememberTestDeviceState(), + state = rememberTestState(), + callbacks = NotificationsSettingsCallbacks.Empty + ) + } +} + +@SignalPreview +@Composable +private fun NotificationsSettingsScreenAPI21Preview() { + Previews.Preview { + NotificationsSettingsScreen( + deviceState = rememberTestDeviceState(apiLevel = 21, supportsNotificationChannels = false), + state = rememberTestState(), + callbacks = NotificationsSettingsCallbacks.Empty + ) + } +} + +@Composable +private fun rememberTestDeviceState( + apiLevel: Int = 35, + supportsNotificationChannels: Boolean = true +): DeviceState = remember { + DeviceState( + apiLevel = apiLevel, + supportsNotificationChannels = supportsNotificationChannels + ) +} + +@Composable +private fun rememberTestState(): NotificationsSettingsState = remember { + NotificationsSettingsState( + messageNotificationsState = MessageNotificationsState( + notificationsEnabled = true, + canEnableNotifications = true, + sound = Uri.EMPTY, + vibrateEnabled = true, + ledColor = "blue", + ledBlink = "", + inChatSoundsEnabled = true, + repeatAlerts = 1, + messagePrivacy = "", + priority = 1, + troubleshootNotifications = true + ), + callNotificationsState = CallNotificationsState( + notificationsEnabled = true, + canEnableNotifications = true, + ringtone = Uri.EMPTY, + vibrateEnabled = true + ), + notifyWhenContactJoinsSignal = true + ) +} + +data class DeviceState( + val apiLevel: Int = Build.VERSION.SDK_INT, + val supportsNotificationChannels: Boolean = NotificationChannels.supported() +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsViewModel.kt index 4447dcdeee..4574db37fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsViewModel.kt @@ -3,9 +3,11 @@ package org.thoughtcrime.securesms.components.settings.app.notifications import android.content.SharedPreferences import android.net.Uri import android.os.Build -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.notifications.DeviceSpecificNotificationConfig @@ -13,13 +15,12 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.SlowNotificationHeuristics import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference import org.thoughtcrime.securesms.util.TextSecurePreferences -import org.thoughtcrime.securesms.util.livedata.Store class NotificationsSettingsViewModel(private val sharedPreferences: SharedPreferences) : ViewModel() { - private val store = Store(getState()) + private val store = MutableStateFlow(getState()) - val state: LiveData = store.stateLiveData + val state: StateFlow = store init { if (NotificationChannels.supported()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/models/Banner.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/models/Banner.kt index 4fa3527e49..947f3d260b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/models/Banner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/models/Banner.kt @@ -6,6 +6,24 @@ package org.thoughtcrime.securesms.components.settings.models import androidx.annotation.StringRes +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.dp +import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.SignalPreview +import org.signal.core.ui.compose.horizontalGutters +import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.databinding.DslBannerBinding import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder @@ -42,3 +60,53 @@ object Banner { } } } + +/** + * Replicates the Banner DSL preference for use in compose components. + */ +@Composable +fun Banner( + text: String, + action: String, + onActionClick: () -> Unit +) { + OutlinedCard( + shape = RoundedCornerShape(18.dp), + border = BorderStroke(width = 1.dp, color = colorResource(R.color.signal_colorOutline_38)), + modifier = Modifier + .horizontalGutters() + .fillMaxWidth() + ) { + Column { + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .padding(horizontal = 16.57.dp) + .padding(top = 16.dp, bottom = 10.dp) + ) + + TextButton( + onClick = onActionClick, + modifier = Modifier + .align(Alignment.End) + .padding(horizontal = 8.dp) + ) { + Text(text = action) + } + } + } +} + +@SignalPreview +@Composable +private fun BannerPreview() { + Previews.Preview { + Banner( + text = "Banner text will go here and probably be about something important", + action = "Action", + onActionClick = {} + ) + } +} diff --git a/app/src/main/res/layout/dsl_banner.xml b/app/src/main/res/layout/dsl_banner.xml index 66bfe29f73..d884a505ba 100644 --- a/app/src/main/res/layout/dsl_banner.xml +++ b/app/src/main/res/layout/dsl_banner.xml @@ -10,6 +10,7 @@ android:layout_marginHorizontal="16dp" android:layout_marginStart="24dp" android:layout_marginEnd="24dp" + app:cardBackgroundColor="@color/signal_colorBackground" app:cardCornerRadius="18dp" app:cardElevation="0dp" app:strokeColor="@color/signal_colorOutline_38" diff --git a/core-ui/src/main/java/org/signal/core/ui/compose/Dialogs.kt b/core-ui/src/main/java/org/signal/core/ui/compose/Dialogs.kt index 4c3ec2bd23..3625c0ae48 100644 --- a/core-ui/src/main/java/org/signal/core/ui/compose/Dialogs.kt +++ b/core-ui/src/main/java/org/signal/core/ui/compose/Dialogs.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -44,6 +45,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -372,7 +375,7 @@ object Dialogs { ) { Surface( modifier = Modifier - .padding(vertical = 100.dp) + .heightIn(min = 0.dp, max = getScreenHeight() - 200.dp) .background( color = SignalTheme.colors.colorSurface2, shape = AlertDialogDefaults.shape @@ -456,7 +459,7 @@ object Dialogs { ) { Surface( modifier = Modifier - .padding(vertical = 100.dp) + .heightIn(min = 0.dp, max = getScreenHeight() - 200.dp) .background( color = SignalTheme.colors.colorSurface2, shape = AlertDialogDefaults.shape @@ -587,6 +590,13 @@ object Dialogs { } } } + + @Composable + private fun getScreenHeight(): Dp { + return with(LocalDensity.current) { + LocalWindowInfo.current.containerSize.height.toDp() + } + } } @SignalPreview diff --git a/core-ui/src/main/java/org/signal/core/ui/compose/Rows.kt b/core-ui/src/main/java/org/signal/core/ui/compose/Rows.kt index cf7fc5b1a1..1e4dbfc2b4 100644 --- a/core-ui/src/main/java/org/signal/core/ui/compose/Rows.kt +++ b/core-ui/src/main/java/org/signal/core/ui/compose/Rows.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import org.signal.core.ui.R import org.signal.core.ui.compose.Rows.TextAndLabel +import org.signal.core.ui.compose.Rows.TextRow object Rows { @@ -150,20 +151,47 @@ object Rows { labels: Array, values: Array, selectedValue: String, - onSelected: (String) -> Unit + onSelected: (String) -> Unit, + enabled: Boolean = true + ) { + RadioListRow( + text = { selectedIndex -> + val selectedLabel = if (selectedIndex in labels.indices) { + labels[selectedIndex] + } else { + null + } + + TextAndLabel( + text = text, + label = selectedLabel + ) + }, + dialogTitle = text, + labels = labels, + values = values, + selectedValue = selectedValue, + onSelected = onSelected, + enabled = enabled + ) + } + + @Composable + fun RadioListRow( + text: @Composable RowScope.(Int) -> Unit, + dialogTitle: String, + labels: Array, + values: Array, + selectedValue: String, + onSelected: (String) -> Unit, + enabled: Boolean = true ) { val selectedIndex = values.indexOf(selectedValue) - val selectedLabel = if (selectedIndex in labels.indices) { - labels[selectedIndex] - } else { - null - } - var displayDialog by remember { mutableStateOf(false) } TextRow( - text = text, - label = selectedLabel, + text = { text(selectedIndex) }, + enabled = enabled, onClick = { displayDialog = true } @@ -175,7 +203,7 @@ object Rows { labels = labels, values = values, selectedIndex = selectedIndex, - title = text, + title = dialogTitle, onSelected = { onSelected(values[it]) } @@ -183,18 +211,6 @@ object Rows { } } - /* - multiSelectPref( - text = stringResource(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) - } - ) - */ - @Composable fun MultiSelectRow( text: String,