mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Convert NotificationsSettingsFragment to compose.
This commit is contained in:
committed by
Greyson Parrelli
parent
7af811eb3f
commit
2eb4f650d8
@@ -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<Unit, Unit>() {
|
||||
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
|
||||
}
|
||||
@@ -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<Unit, Uri?>() {
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -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 <I, O> registerForActivityResult(
|
||||
contract: ActivityResultContract<I, O>,
|
||||
callback: ActivityResultCallback<O>
|
||||
): ActivityResultLauncher<I>
|
||||
|
||||
class ForActivity(val activity: FragmentActivity) : ActivityResultRegisterer {
|
||||
override fun <I, O> registerForActivityResult(
|
||||
contract: ActivityResultContract<I, O>,
|
||||
callback: ActivityResultCallback<O>
|
||||
): ActivityResultLauncher<I> {
|
||||
return activity.registerForActivityResult(contract, callback)
|
||||
}
|
||||
}
|
||||
|
||||
class ForFragment(val fragment: Fragment) : ActivityResultRegisterer {
|
||||
override fun <I, O> registerForActivityResult(
|
||||
contract: ActivityResultContract<I, O>,
|
||||
callback: ActivityResultCallback<O>
|
||||
): ActivityResultLauncher<I> {
|
||||
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<Unit> = activityResultRegisterer.registerForActivityResult(
|
||||
NotificationSoundSelectionContract(NotificationSoundSelectionContract.Target.MESSAGE),
|
||||
viewModel::setMessageNotificationsSound
|
||||
)
|
||||
|
||||
Banner.register(adapter)
|
||||
private val callsSoundSelectionLauncher: ActivityResultLauncher<Unit> = activityResultRegisterer.registerForActivityResult(
|
||||
NotificationSoundSelectionContract(NotificationSoundSelectionContract.Target.CALL),
|
||||
viewModel::setCallRingtone
|
||||
)
|
||||
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val factory = NotificationsSettingsViewModel.Factory(sharedPreferences)
|
||||
private val notificationPrioritySelectionLauncher: ActivityResultLauncher<Unit> = 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<String>,
|
||||
val radioListPreference: RadioListPreference
|
||||
) : PreferenceModel<LedColorPreference>(
|
||||
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<LedColorPreference>(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()
|
||||
)
|
||||
|
||||
@@ -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<NotificationsSettingsState> = store.stateLiveData
|
||||
val state: StateFlow<NotificationsSettingsState> = store
|
||||
|
||||
init {
|
||||
if (NotificationChannels.supported()) {
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user