Replace with new custom notifications page.

This commit is contained in:
Alex Hart
2021-08-04 13:21:26 -03:00
parent 3585667fb9
commit fe9b8a9f47
10 changed files with 371 additions and 768 deletions

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components.settings.conversation.sounds
import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.thoughtcrime.securesms.MuteDialog
import org.thoughtcrime.securesms.R
@@ -13,7 +14,6 @@ import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment
class SoundsAndNotificationsSettingsFragment : DSLSettingsFragment(
titleId = R.string.ConversationSettingsFragment__sounds_and_notifications
@@ -110,7 +110,8 @@ class SoundsAndNotificationsSettingsFragment : DSLSettingsFragment(
icon = DSLSettingsIcon.from(R.drawable.ic_speaker_24),
summary = DSLSettingsText.from(customSoundSummary),
onClick = {
CustomNotificationsDialogFragment.create(state.recipientId).show(parentFragmentManager, null)
val action = SoundsAndNotificationsSettingsFragmentDirections.actionSoundsAndNotificationsSettingsFragmentToCustomNotificationsSettingsFragment(state.recipientId)
Navigation.findNavController(requireView()).navigate(action)
}
)
}

View File

@@ -0,0 +1,168 @@
package org.thoughtcrime.securesms.components.settings.conversation.sounds.custom
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.provider.Settings
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.viewModels
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.util.RingtoneUtil
class CustomNotificationsSettingsFragment : DSLSettingsFragment(R.string.CustomNotificationsDialogFragment__custom_notifications) {
private val vibrateLabels: Array<String> by lazy {
resources.getStringArray(R.array.recipient_vibrate_entries)
}
private val viewModel: CustomNotificationsSettingsViewModel by viewModels(factoryProducer = this::createFactory)
private lateinit var callSoundResultLauncher: ActivityResultLauncher<Intent>
private lateinit var messageSoundResultLauncher: ActivityResultLauncher<Intent>
private fun createFactory(): CustomNotificationsSettingsViewModel.Factory {
val recipientId = CustomNotificationsSettingsFragmentArgs.fromBundle(requireArguments()).recipientId
val repository = CustomNotificationsSettingsRepository(requireContext())
return CustomNotificationsSettingsViewModel.Factory(recipientId, repository)
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
messageSoundResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleResult(result, viewModel::setMessageSound)
}
callSoundResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleResult(result, viewModel::setCallSound)
}
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
}
private fun handleResult(result: ActivityResult, resultHandler: (Uri?) -> Unit) {
val resultCode = result.resultCode
val data = result.data
if (resultCode == Activity.RESULT_OK && data != null) {
val uri: Uri? = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
resultHandler(uri)
}
}
private fun getConfiguration(state: CustomNotificationsSettingsState): DSLConfiguration {
return configure {
val controlsEnabled = state.hasCustomNotifications && state.isInitialLoadComplete
sectionHeaderPref(R.string.CustomNotificationsDialogFragment__messages)
if (NotificationChannels.supported()) {
switchPref(
title = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__use_custom_notifications),
isEnabled = state.isInitialLoadComplete,
isChecked = state.hasCustomNotifications,
onClick = { viewModel.setHasCustomNotifications(!state.hasCustomNotifications) }
)
}
clickPref(
title = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__notification_sound),
summary = DSLSettingsText.from(getRingtoneSummary(requireContext(), state.messageSound, Settings.System.DEFAULT_NOTIFICATION_URI)),
isEnabled = controlsEnabled,
onClick = { requestSound(state.messageSound, false) }
)
if (NotificationChannels.supported()) {
switchPref(
title = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__vibrate),
isEnabled = controlsEnabled,
isChecked = state.messageVibrateEnabled,
onClick = { viewModel.setMessageVibrate(RecipientDatabase.VibrateState.fromBoolean(!state.messageVibrateEnabled)) }
)
} else {
radioListPref(
title = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__vibrate),
isEnabled = controlsEnabled,
listItems = vibrateLabels,
selected = state.messageVibrateState.id,
onSelected = {
viewModel.setMessageVibrate(RecipientDatabase.VibrateState.fromId(it))
}
)
}
if (state.showCallingOptions) {
dividerPref()
sectionHeaderPref(R.string.CustomNotificationsDialogFragment__call_settings)
clickPref(
title = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__ringtone),
summary = DSLSettingsText.from(getRingtoneSummary(requireContext(), state.callSound, Settings.System.DEFAULT_RINGTONE_URI)),
isEnabled = controlsEnabled,
onClick = { requestSound(state.callSound, true) }
)
radioListPref(
title = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__vibrate),
isEnabled = controlsEnabled,
listItems = vibrateLabels,
selected = state.callVibrateState.id,
onSelected = {
viewModel.setCallVibrate(RecipientDatabase.VibrateState.fromId(it))
}
)
}
}
}
private fun getRingtoneSummary(context: Context, ringtone: Uri?, defaultNotificationUri: Uri?): String {
if (ringtone == null || ringtone == defaultNotificationUri) {
return context.getString(R.string.CustomNotificationsDialogFragment__default)
} else if (ringtone.toString().isEmpty()) {
return context.getString(R.string.preferences__silent)
} else {
val tone = RingtoneUtil.getRingtone(requireContext(), ringtone)
if (tone != null) {
return tone.getTitle(context)
}
}
return context.getString(R.string.CustomNotificationsDialogFragment__default)
}
private fun requestSound(current: Uri?, forCalls: Boolean) {
val existing: Uri? = when {
current == null -> getDefaultSound(forCalls)
current.toString().isEmpty() -> null
else -> current
}
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true)
putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, if (forCalls) RingtoneManager.TYPE_RINGTONE else RingtoneManager.TYPE_NOTIFICATION)
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, existing)
}
if (forCalls) {
callSoundResultLauncher.launch(intent)
} else {
messageSoundResultLauncher.launch(intent)
}
}
private fun getDefaultSound(forCalls: Boolean) = if (forCalls) Settings.System.DEFAULT_RINGTONE_URI else Settings.System.DEFAULT_NOTIFICATION_URI
}

View File

@@ -0,0 +1,94 @@
package org.thoughtcrime.securesms.components.settings.conversation.sounds.custom
import android.content.Context
import android.net.Uri
import androidx.annotation.WorkerThread
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor
class CustomNotificationsSettingsRepository(context: Context) {
private val context = context.applicationContext
private val executor = SerialExecutor(SignalExecutors.BOUNDED)
fun initialize(recipientId: RecipientId, onInitializationComplete: () -> Unit) {
executor.execute {
val recipient = Recipient.resolved(recipientId)
val database = DatabaseFactory.getRecipientDatabase(context)
if (NotificationChannels.supported() && recipient.notificationChannel != null) {
database.setMessageRingtone(recipient.id, NotificationChannels.getMessageRingtone(context, recipient))
database.setMessageVibrate(recipient.id, RecipientDatabase.VibrateState.fromBoolean(NotificationChannels.getMessageVibrate(context, recipient)))
NotificationChannels.ensureCustomChannelConsistency(context)
}
onInitializationComplete()
}
}
fun setHasCustomNotifications(recipientId: RecipientId, hasCustomNotifications: Boolean) {
executor.execute {
if (hasCustomNotifications) {
createCustomNotificationChannel(recipientId)
} else {
deleteCustomNotificationChannel(recipientId)
}
}
}
fun setMessageVibrate(recipientId: RecipientId, vibrateState: RecipientDatabase.VibrateState) {
executor.execute {
val recipient: Recipient = Recipient.resolved(recipientId)
DatabaseFactory.getRecipientDatabase(context).setMessageVibrate(recipient.id, vibrateState)
NotificationChannels.updateMessageVibrate(context, recipient, vibrateState)
}
}
fun setCallingVibrate(recipientId: RecipientId, vibrateState: RecipientDatabase.VibrateState) {
executor.execute {
DatabaseFactory.getRecipientDatabase(context).setCallVibrate(recipientId, vibrateState)
}
}
fun setMessageSound(recipientId: RecipientId, sound: Uri?) {
executor.execute {
val recipient: Recipient = Recipient.resolved(recipientId)
val defaultValue = SignalStore.settings().messageNotificationSound
val newValue: Uri? = if (defaultValue == sound) null else sound ?: Uri.EMPTY
DatabaseFactory.getRecipientDatabase(context).setMessageRingtone(recipient.id, newValue)
NotificationChannels.updateMessageRingtone(context, recipient, newValue)
}
}
fun setCallSound(recipientId: RecipientId, sound: Uri?) {
executor.execute {
val defaultValue = SignalStore.settings().callRingtone
val newValue: Uri? = if (defaultValue == sound) null else sound ?: Uri.EMPTY
DatabaseFactory.getRecipientDatabase(context).setCallRingtone(recipientId, newValue)
}
}
@WorkerThread
private fun createCustomNotificationChannel(recipientId: RecipientId) {
val recipient: Recipient = Recipient.resolved(recipientId)
val channelId = NotificationChannels.createChannelFor(context, recipient)
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.id, channelId)
}
@WorkerThread
private fun deleteCustomNotificationChannel(recipientId: RecipientId) {
val recipient: Recipient = Recipient.resolved(recipientId)
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.id, null)
NotificationChannels.deleteChannelFor(context, recipient)
}
}

View File

@@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.components.settings.conversation.sounds.custom
import android.net.Uri
import org.thoughtcrime.securesms.database.RecipientDatabase
data class CustomNotificationsSettingsState(
val isInitialLoadComplete: Boolean = false,
val hasCustomNotifications: Boolean = false,
val messageVibrateState: RecipientDatabase.VibrateState = RecipientDatabase.VibrateState.DEFAULT,
val messageVibrateEnabled: Boolean = false,
val messageSound: Uri? = null,
val callVibrateState: RecipientDatabase.VibrateState = RecipientDatabase.VibrateState.DEFAULT,
val callSound: Uri? = null,
val showCallingOptions: Boolean = false,
)

View File

@@ -0,0 +1,73 @@
package org.thoughtcrime.securesms.components.settings.conversation.sounds.custom
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.livedata.Store
class CustomNotificationsSettingsViewModel(
private val recipientId: RecipientId,
private val repository: CustomNotificationsSettingsRepository
) : ViewModel() {
private val store = Store(CustomNotificationsSettingsState())
val state: LiveData<CustomNotificationsSettingsState> = store.stateLiveData
init {
repository.initialize(recipientId) {
store.update { it.copy(isInitialLoadComplete = true) }
}
store.update(Recipient.live(recipientId).liveData) { recipient, state ->
state.copy(
hasCustomNotifications = NotificationChannels.supported() && recipient.notificationChannel != null,
messageSound = recipient.messageRingtone,
messageVibrateState = recipient.messageVibrate,
messageVibrateEnabled = when (recipient.messageVibrate) {
RecipientDatabase.VibrateState.DEFAULT -> SignalStore.settings().isMessageVibrateEnabled
RecipientDatabase.VibrateState.ENABLED -> true
RecipientDatabase.VibrateState.DISABLED -> false
},
showCallingOptions = !recipient.isGroup && recipient.isRegistered,
callSound = recipient.callRingtone,
callVibrateState = recipient.callVibrate
)
}
}
fun setHasCustomNotifications(hasCustomNotifications: Boolean) {
repository.setHasCustomNotifications(recipientId, hasCustomNotifications)
}
fun setMessageVibrate(messageVibrateState: RecipientDatabase.VibrateState) {
repository.setMessageVibrate(recipientId, messageVibrateState)
}
fun setMessageSound(uri: Uri?) {
repository.setMessageSound(recipientId, uri)
}
fun setCallVibrate(callVibrateState: RecipientDatabase.VibrateState) {
repository.setCallingVibrate(recipientId, callVibrateState)
}
fun setCallSound(uri: Uri?) {
repository.setCallSound(recipientId, uri)
}
class Factory(
private val recipientId: RecipientId,
private val repository: CustomNotificationsSettingsRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(CustomNotificationsSettingsViewModel(recipientId, repository)))
}
}
}