Refactor app settings.

This commit is contained in:
Alex Hart
2021-05-12 13:02:44 -03:00
committed by Greyson Parrelli
parent a94d77d81e
commit f2d5ea0391
179 changed files with 5244 additions and 3894 deletions

View File

@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.components.mention.MentionDeleter;
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
import org.thoughtcrime.securesms.components.mention.MentionValidatorWatcher;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -201,7 +202,7 @@ public class ComposeText extends EmojiEditText {
}
public void setTransport(TransportOption transport) {
final boolean useSystemEmoji = TextSecurePreferences.isSystemEmojiPreferred(getContext());
final boolean useSystemEmoji = SignalStore.settings().isPreferSystemEmoji();
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
int inputType = getInputType();
@@ -225,7 +226,7 @@ public class ComposeText extends EmojiEditText {
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
InputConnection inputConnection = super.onCreateInputConnection(editorInfo);
if(TextSecurePreferences.isEnterSendsEnabled(getContext())) {
if(SignalStore.settings().isEnterKeySends()) {
editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
}

View File

@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.conversation.ConversationStickerSuggestionAdapter;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.mms.GlideApp;
@@ -124,7 +125,7 @@ public class InputPanel extends LinearLayout
this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction());
if (TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
if (SignalStore.settings().isPreferSystemEmoji()) {
mediaKeyboard.setVisibility(View.GONE);
emojiVisible = false;
} else {

View File

@@ -13,6 +13,7 @@ import androidx.appcompat.widget.AppCompatEditText;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -34,7 +35,7 @@ public class EmojiEditText extends AppCompatEditText {
boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
a.recycle();
if (forceCustom || !TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
if (forceCustom || !SignalStore.settings().isPreferSystemEmoji()) {
if (!isInEditMode()) {
setFilters(appendEmojiFilter(this.getFilters()));
}

View File

@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -215,7 +216,7 @@ public class EmojiTextView extends AppCompatTextView {
}
private boolean useSystemEmoji() {
return !forceCustom && TextSecurePreferences.isSystemEmojiPreferred(getContext());
return !forceCustom && SignalStore.settings().isPreferSystemEmoji();
}
@Override

View File

@@ -0,0 +1,71 @@
package org.thoughtcrime.securesms.components.settings
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.navigation.fragment.NavHostFragment
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
open class DSLSettingsActivity : PassphraseRequiredActivity() {
private val dynamicTheme = DynamicNoActionBarTheme()
protected lateinit var navController: NavController
private set
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
setContentView(R.layout.dsl_settings_activity)
if (savedInstanceState == null) {
val navGraphId = intent.getIntExtra(ARG_NAV_GRAPH, -1)
if (navGraphId == -1) {
throw IllegalStateException("No navgraph id was passed to activity")
}
val fragment: NavHostFragment = NavHostFragment.create(navGraphId)
supportFragmentManager.beginTransaction()
.replace(R.id.nav_host_fragment, fragment)
.commitNow()
navController = fragment.navController
} else {
val fragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = fragment.navController
}
dynamicTheme.onCreate(this)
onBackPressedDispatcher.addCallback(this, OnBackPressed())
}
override fun onResume() {
super.onResume()
dynamicTheme.onResume(this)
}
override fun onNavigateUp(): Boolean {
return if (!Navigation.findNavController(this, R.id.nav_host_fragment).popBackStack()) {
onWillFinish()
finish()
true
} else {
false
}
}
protected open fun onWillFinish() {}
companion object {
const val ARG_NAV_GRAPH = "nav_graph"
}
private inner class OnBackPressed : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onNavigateUp()
}
}
}

View File

@@ -0,0 +1,181 @@
package org.thoughtcrime.securesms.components.settings
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.CallSuper
import androidx.core.content.ContextCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.switchmaterial.SwitchMaterial
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.MappingAdapter
import org.thoughtcrime.securesms.util.MappingViewHolder
import org.thoughtcrime.securesms.util.ViewUtil
class DSLSettingsAdapter : MappingAdapter() {
init {
registerFactory(ClickPreference::class.java, LayoutFactory(::ClickPreferenceViewHolder, R.layout.dsl_preference_item))
registerFactory(TextPreference::class.java, LayoutFactory(::TextPreferenceViewHolder, R.layout.dsl_preference_item))
registerFactory(RadioListPreference::class.java, LayoutFactory(::RadioListPreferenceViewHolder, R.layout.dsl_preference_item))
registerFactory(MultiSelectListPreference::class.java, LayoutFactory(::MultiSelectListPreferenceViewHolder, R.layout.dsl_preference_item))
registerFactory(ExternalLinkPreference::class.java, LayoutFactory(::ExternalLinkPreferenceViewHolder, R.layout.dsl_preference_item))
registerFactory(DividerPreference::class.java, LayoutFactory(::DividerPreferenceViewHolder, R.layout.dsl_divider_item))
registerFactory(SectionHeaderPreference::class.java, LayoutFactory(::SectionHeaderPreferenceViewHolder, R.layout.dsl_section_header))
registerFactory(SwitchPreference::class.java, LayoutFactory(::SwitchPreferenceViewHolder, R.layout.dsl_switch_preference_item))
}
}
abstract class PreferenceViewHolder<T : PreferenceModel<T>>(itemView: View) : MappingViewHolder<T>(itemView) {
protected val iconView: ImageView = itemView.findViewById(R.id.icon)
protected val titleView: TextView = itemView.findViewById(R.id.title)
protected val summaryView: TextView = itemView.findViewById(R.id.summary)
@CallSuper
override fun bind(model: T) {
listOf(itemView, titleView, summaryView).forEach {
it.isEnabled = model.isEnabled
}
if (model.iconId != -1) {
iconView.setImageResource(model.iconId)
iconView.visibility = View.VISIBLE
} else {
iconView.setImageDrawable(null)
iconView.visibility = View.GONE
}
val title = model.title?.resolve(context)
if (title != null) {
titleView.text = model.title?.resolve(context)
titleView.visibility = View.VISIBLE
} else {
titleView.visibility = View.GONE
}
val summary = model.summary?.resolve(context)
if (summary != null) {
summaryView.text = summary
summaryView.visibility = View.VISIBLE
val spans = (summaryView.text as? Spanned)?.getSpans(0, summaryView.text.length, ClickableSpan::class.java)
if (spans?.isEmpty() == false) {
summaryView.movementMethod = LinkMovementMethod.getInstance()
} else {
summaryView.movementMethod = null
}
} else {
summaryView.visibility = View.GONE
summaryView.movementMethod = null
}
}
}
class TextPreferenceViewHolder(itemView: View) : PreferenceViewHolder<TextPreference>(itemView)
class ClickPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ClickPreference>(itemView) {
override fun bind(model: ClickPreference) {
super.bind(model)
itemView.setOnClickListener { model.onClick() }
}
}
class RadioListPreferenceViewHolder(itemView: View) : PreferenceViewHolder<RadioListPreference>(itemView) {
override fun bind(model: RadioListPreference) {
super.bind(model)
summaryView.visibility = View.VISIBLE
summaryView.text = model.listItems[model.selected]
itemView.setOnClickListener {
MaterialAlertDialogBuilder(context)
.setTitle(model.title.resolve(context))
.setSingleChoiceItems(model.listItems, model.selected) { dialog, which ->
model.onSelected(which)
dialog.dismiss()
}
.show()
}
}
}
class MultiSelectListPreferenceViewHolder(itemView: View) : PreferenceViewHolder<MultiSelectListPreference>(itemView) {
override fun bind(model: MultiSelectListPreference) {
super.bind(model)
summaryView.visibility = View.VISIBLE
val summaryText = model.selected
.mapIndexed { index, isChecked -> if (isChecked) model.listItems[index] else null }
.filterNotNull()
.joinToString(", ")
if (summaryText.isEmpty()) {
summaryView.setText(R.string.preferences__none)
} else {
summaryView.text = summaryText
}
val selected = model.selected.copyOf()
itemView.setOnClickListener {
MaterialAlertDialogBuilder(context)
.setTitle(model.title.resolve(context))
.setMultiChoiceItems(model.listItems, selected) { _, _, _ ->
// Intentionally empty
}
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { d, _ ->
model.onSelected(selected)
d.dismiss()
}
.show()
}
}
}
class SwitchPreferenceViewHolder(itemView: View) : PreferenceViewHolder<SwitchPreference>(itemView) {
private val switchWidget: SwitchMaterial = itemView.findViewById(R.id.switch_widget)
override fun bind(model: SwitchPreference) {
super.bind(model)
switchWidget.isEnabled = model.isEnabled
switchWidget.isChecked = model.isChecked
itemView.setOnClickListener {
model.onClick()
}
}
}
class ExternalLinkPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ExternalLinkPreference>(itemView) {
override fun bind(model: ExternalLinkPreference) {
super.bind(model)
val externalLinkIcon = requireNotNull(ContextCompat.getDrawable(context, R.drawable.ic_open_20))
externalLinkIcon.setBounds(0, 0, ViewUtil.dpToPx(20), ViewUtil.dpToPx(20))
if (itemView.layoutDirection == View.LAYOUT_DIRECTION_LTR) {
titleView.setCompoundDrawables(null, null, externalLinkIcon, null)
} else {
titleView.setCompoundDrawables(externalLinkIcon, null, null, null)
}
itemView.setOnClickListener { CommunicationActions.openBrowserLink(itemView.context, itemView.context.getString(model.linkId)) }
}
}
class DividerPreferenceViewHolder(itemView: View) : MappingViewHolder<DividerPreference>(itemView) {
override fun bind(model: DividerPreference) = Unit
}
class SectionHeaderPreferenceViewHolder(itemView: View) : MappingViewHolder<SectionHeaderPreference>(itemView) {
private val sectionHeader: TextView = itemView.findViewById(R.id.section_header)
override fun bind(model: SectionHeaderPreference) {
sectionHeader.text = model.title.resolve(context)
}
}

View File

@@ -0,0 +1,94 @@
package org.thoughtcrime.securesms.components.settings
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.EdgeEffect
import androidx.annotation.MenuRes
import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R
abstract class DSLSettingsFragment(
@StringRes private val titleId: Int,
@MenuRes private val menuId: Int = -1
) : Fragment(R.layout.dsl_settings_fragment) {
private lateinit var recyclerView: RecyclerView
private lateinit var toolbarShadowHelper: ToolbarShadowHelper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
val toolbarShadow: View = view.findViewById(R.id.toolbar_shadow)
toolbar.setTitle(titleId)
toolbar.setNavigationOnClickListener {
requireActivity().onBackPressed()
}
if (menuId != -1) {
toolbar.inflateMenu(menuId)
toolbar.setOnMenuItemClickListener { onOptionsItemSelected(it) }
}
recyclerView = view.findViewById(R.id.recycler)
recyclerView.edgeEffectFactory = EdgeEffectFactory()
toolbarShadowHelper = ToolbarShadowHelper(toolbarShadow)
val adapter = DSLSettingsAdapter()
recyclerView.adapter = adapter
recyclerView.addOnScrollListener(toolbarShadowHelper)
bindAdapter(adapter)
}
override fun onResume() {
super.onResume()
toolbarShadowHelper.onScrolled(recyclerView, 0, 0)
}
abstract fun bindAdapter(adapter: DSLSettingsAdapter)
private class EdgeEffectFactory : RecyclerView.EdgeEffectFactory() {
override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
return super.createEdgeEffect(view, direction).apply {
if (Build.VERSION.SDK_INT > 21) {
color =
requireNotNull(ContextCompat.getColor(view.context, R.color.settings_ripple_color))
}
}
}
}
class ToolbarShadowHelper(private val toolbarShadow: View) : RecyclerView.OnScrollListener() {
private var lastAnimationState = ToolbarAnimationState.NONE
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val newAnimationState =
if (recyclerView.canScrollVertically(-1)) ToolbarAnimationState.SHOW else ToolbarAnimationState.HIDE
if (newAnimationState == lastAnimationState) {
return
}
when (newAnimationState) {
ToolbarAnimationState.NONE -> throw AssertionError()
ToolbarAnimationState.HIDE -> toolbarShadow.animate().alpha(0f)
ToolbarAnimationState.SHOW -> toolbarShadow.animate().alpha(1f)
}
lastAnimationState = newAnimationState
}
}
private enum class ToolbarAnimationState {
NONE,
HIDE,
SHOW
}
}

View File

@@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.components.settings
import android.content.Context
import androidx.annotation.ColorInt
import androidx.annotation.StringRes
import org.thoughtcrime.securesms.util.SpanUtil
sealed class DSLSettingsText {
private data class FromResource(
@StringRes private val stringId: Int,
@ColorInt private val textColor: Int?
) : DSLSettingsText() {
override fun resolve(context: Context): CharSequence {
val text = context.getString(stringId)
return if (textColor == null) {
text
} else {
SpanUtil.color(textColor, text)
}
}
}
private data class FromCharSequence(private val charSequence: CharSequence) : DSLSettingsText() {
override fun resolve(context: Context): CharSequence = charSequence
}
abstract fun resolve(context: Context): CharSequence
companion object {
fun from(@StringRes stringId: Int, @ColorInt textColor: Int? = null): DSLSettingsText =
FromResource(stringId, textColor)
fun from(charSequence: CharSequence): DSLSettingsText = FromCharSequence(charSequence)
}
}

View File

@@ -0,0 +1,121 @@
package org.thoughtcrime.securesms.components.settings.app
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.navigation.NavDirections
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
import org.thoughtcrime.securesms.help.HelpFragment
import org.thoughtcrime.securesms.keyvalue.SettingsValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.CachedInflater
import org.thoughtcrime.securesms.util.DynamicTheme
private const val START_LOCATION = "app.settings.start.location"
private const val NOTIFICATION_CATEGORY = "android.intent.category.NOTIFICATION_PREFERENCES"
private const val STATE_WAS_CONFIGURATION_UPDATED = "app.settings.state.configuration.updated"
class AppSettingsActivity : DSLSettingsActivity() {
private var wasConfigurationUpdated = false
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
if (intent?.hasExtra(ARG_NAV_GRAPH) != true) {
intent?.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
}
super.onCreate(savedInstanceState, ready)
val startingAction: NavDirections? = if (intent?.categories?.contains(NOTIFICATION_CATEGORY) == true) {
AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
} else {
when (StartLocation.fromCode(intent?.getIntExtra(START_LOCATION, StartLocation.HOME.code))) {
StartLocation.HOME -> null
StartLocation.BACKUPS -> AppSettingsFragmentDirections.actionDirectToBackupsPreferenceFragment()
StartLocation.HELP -> AppSettingsFragmentDirections.actionDirectToHelpFragment()
.setStartCategoryIndex(intent.getIntExtra(HelpFragment.START_CATEGORY_INDEX, 0))
StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment()
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
}
}
if (startingAction == null && savedInstanceState != null) {
wasConfigurationUpdated = savedInstanceState.getBoolean(STATE_WAS_CONFIGURATION_UPDATED)
}
startingAction?.let {
navController.navigate(it)
}
SignalStore.settings().onConfigurationSettingChanged.observe(this) { key ->
if (key == SettingsValues.THEME) {
DynamicTheme.setDefaultDayNightMode(this)
recreate()
} else if (key == SettingsValues.LANGUAGE) {
CachedInflater.from(this).clear()
wasConfigurationUpdated = true
recreate()
val intent = Intent(this, KeyCachingService::class.java)
intent.action = KeyCachingService.LOCALE_CHANGE_EVENT
startService(intent)
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(STATE_WAS_CONFIGURATION_UPDATED, wasConfigurationUpdated)
}
override fun onWillFinish() {
if (wasConfigurationUpdated) {
setResult(MainActivity.RESULT_CONFIG_CHANGED)
} else {
setResult(RESULT_OK)
}
}
companion object {
@JvmStatic
fun home(context: Context): Intent = getIntentForStartLocation(context, StartLocation.HOME)
@JvmStatic
fun backups(context: Context): Intent = getIntentForStartLocation(context, StartLocation.BACKUPS)
@JvmStatic
fun help(context: Context, startCategoryIndex: Int = 0): Intent {
return getIntentForStartLocation(context, StartLocation.HOME)
.putExtra(HelpFragment.START_CATEGORY_INDEX, startCategoryIndex)
}
@JvmStatic
fun proxy(context: Context): Intent = getIntentForStartLocation(context, StartLocation.HELP)
@JvmStatic
fun notifications(context: Context): Intent = getIntentForStartLocation(context, StartLocation.NOTIFICATIONS)
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
return Intent(context, AppSettingsActivity::class.java)
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
.putExtra(START_LOCATION, startLocation.code)
}
}
private enum class StartLocation(val code: Int) {
HOME(0),
BACKUPS(1),
HELP(2),
PROXY(3),
NOTIFICATIONS(4);
companion object {
fun fromCode(code: Int?): StartLocation {
return values().find { code == it.code } ?: HOME
}
}
}
}

View File

@@ -0,0 +1,198 @@
package org.thoughtcrime.securesms.components.settings.app
import android.view.View
import android.widget.TextView
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView
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.PreferenceModel
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.MappingAdapter
import org.thoughtcrime.securesms.util.MappingViewHolder
class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__menu_settings) {
override fun bindAdapter(adapter: DSLSettingsAdapter) {
adapter.registerFactory(BioPreference::class.java, MappingAdapter.LayoutFactory(::BioPreferenceViewHolder, R.layout.bio_preference_item))
adapter.registerFactory(PaymentsPreference::class.java, MappingAdapter.LayoutFactory(::PaymentsPreferenceViewHolder, R.layout.dsl_payments_preference))
val viewModel = ViewModelProviders.of(this)[AppSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
}
private fun getConfiguration(state: AppSettingsState): DSLConfiguration {
return configure {
customPref(
BioPreference(state.self) {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_manageProfileActivity)
}
)
clickPref(
title = DSLSettingsText.from(R.string.AccountSettingsFragment__account),
iconId = R.drawable.ic_profile_circle_24,
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_accountSettingsFragment)
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__linked_devices),
iconId = R.drawable.ic_linked_devices_24,
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_deviceActivity)
}
)
if (SignalStore.paymentsValues().paymentsAvailability.showPaymentsMenu()) {
customPref(
PaymentsPreference(
unreadCount = state.unreadPaymentsCount
) {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_paymentsActivity)
}
)
}
dividerPref()
clickPref(
title = DSLSettingsText.from(R.string.preferences__appearance),
iconId = R.drawable.ic_appearance_24,
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_appearanceSettingsFragment)
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences_chats__chats),
iconId = R.drawable.ic_message_tinted_bitmap_24,
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment)
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__notifications),
iconId = R.drawable.ic_bell_24,
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_notificationsSettingsFragment)
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__privacy),
iconId = R.drawable.ic_lock_24,
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_privacySettingsFragment)
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__data_and_storage),
iconId = R.drawable.ic_archive_24dp,
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_dataAndStorageSettingsFragment)
}
)
dividerPref()
clickPref(
title = DSLSettingsText.from(R.string.preferences__help),
iconId = R.drawable.ic_help_24,
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_helpSettingsFragment)
}
)
clickPref(
title = DSLSettingsText.from(R.string.AppSettingsFragment__invite_your_friends),
iconId = R.drawable.ic_invite_24,
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_inviteActivity)
}
)
externalLinkPref(
title = DSLSettingsText.from(R.string.preferences__donate_to_signal),
iconId = R.drawable.ic_heart_24,
linkId = R.string.donate_url
)
if (FeatureFlags.internalUser()) {
dividerPref()
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_preferences),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_internalSettingsFragment)
}
)
}
}
}
private class BioPreference(val recipient: Recipient, val onClick: () -> Unit) : PreferenceModel<BioPreference>() {
override fun areContentsTheSame(newItem: BioPreference): Boolean {
return super.areContentsTheSame(newItem) && recipient.hasSameContent(newItem.recipient)
}
override fun areItemsTheSame(newItem: BioPreference): Boolean {
return recipient == newItem.recipient
}
}
private class BioPreferenceViewHolder(itemView: View) : PreferenceViewHolder<BioPreference>(itemView) {
private val avatarView: AvatarImageView = itemView.findViewById(R.id.icon)
override fun bind(model: BioPreference) {
super.bind(model)
itemView.setOnClickListener { model.onClick() }
titleView.text = model.recipient.getDisplayName(itemView.context)
summaryView.text = model.recipient.requireE164()
avatarView.setAvatar(GlideApp.with(itemView), model.recipient, false, true)
titleView.visibility = View.VISIBLE
summaryView.visibility = View.VISIBLE
avatarView.visibility = View.VISIBLE
}
}
private class PaymentsPreference(val unreadCount: Int, val onClick: () -> Unit) : PreferenceModel<PaymentsPreference>() {
override fun areContentsTheSame(newItem: PaymentsPreference): Boolean {
return super.areContentsTheSame(newItem) && unreadCount == newItem.unreadCount
}
}
private class PaymentsPreferenceViewHolder(itemView: View) : MappingViewHolder<PaymentsPreference>(itemView) {
private val unreadCountView: TextView = itemView.findViewById(R.id.unread_indicator)
override fun bind(model: PaymentsPreference) {
unreadCountView.text = model.unreadCount.toString()
unreadCountView.visibility = if (model.unreadCount > 0) View.VISIBLE else View.GONE
itemView.setOnClickListener {
model.onClick()
}
}
}
}

View File

@@ -0,0 +1,5 @@
package org.thoughtcrime.securesms.components.settings.app
import org.thoughtcrime.securesms.recipients.Recipient
data class AppSettingsState(val self: Recipient, val unreadPaymentsCount: Int)

View File

@@ -0,0 +1,19 @@
package org.thoughtcrime.securesms.components.settings.app
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
class AppSettingsViewModel : ViewModel() {
val unreadPaymentsLiveData = UnreadPaymentsLiveData()
val selfLiveData: LiveData<Recipient> = Recipient.self().live().liveData
val state: LiveData<AppSettingsState> = LiveDataUtil.combineLatest(unreadPaymentsLiveData, selfLiveData) { payments, self ->
val unreadPaymentsCount = payments.transform { it.unreadCount }.or(0)
AppSettingsState(self, unreadPaymentsCount)
}
}

View File

@@ -0,0 +1,180 @@
package org.thoughtcrime.securesms.components.settings.app.account
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.Typeface
import android.text.InputType
import android.util.DisplayMetrics
import android.view.ViewGroup
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.autofill.HintConstants
import androidx.core.app.DialogCompat
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
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.contactshare.SimpleTextWatcher
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.PinHashing
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity
import org.thoughtcrime.securesms.lock.v2.KbsConstants
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.ThemeUtil
class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFragment__account) {
lateinit var viewModel: AccountSettingsViewModel
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN && resultCode == CreateKbsPinActivity.RESULT_OK) {
Snackbar.make(requireView(), R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).setTextColor(Color.WHITE).show()
}
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
viewModel = ViewModelProviders.of(this)[AccountSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
}
private fun getConfiguration(state: AccountSettingsState): DSLConfiguration {
return configure {
sectionHeaderPref(R.string.preferences_app_protection__signal_pin)
clickPref(
title = DSLSettingsText.from(if (state.hasPin) R.string.preferences_app_protection__change_your_pin else R.string.preferences_app_protection__create_a_pin),
onClick = {
if (state.hasPin) {
startActivityForResult(CreateKbsPinActivity.getIntentForPinChangeFromSettings(requireContext()), CreateKbsPinActivity.REQUEST_NEW_PIN)
} else {
startActivityForResult(CreateKbsPinActivity.getIntentForPinCreate(requireContext()), CreateKbsPinActivity.REQUEST_NEW_PIN)
}
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences_app_protection__pin_reminders),
summary = DSLSettingsText.from(R.string.AccountSettingsFragment__youll_be_asked_less_frequently),
isChecked = state.pinRemindersEnabled,
onClick = {
setPinRemindersEnabled(!state.pinRemindersEnabled)
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences_app_protection__registration_lock),
summary = DSLSettingsText.from(R.string.AccountSettingsFragment__require_your_signal_pin),
isChecked = state.registrationLockEnabled,
onClick = {
setRegistrationLockEnabled(!state.registrationLockEnabled)
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__advanced_pin_settings),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_accountSettingsFragment_to_advancedPinSettingsActivity)
}
)
dividerPref()
sectionHeaderPref(R.string.AccountSettingsFragment__account)
clickPref(
title = DSLSettingsText.from(R.string.preferences_chats__transfer_account),
summary = DSLSettingsText.from(R.string.preferences_chats__transfer_account_to_a_new_android_device),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_accountSettingsFragment_to_oldDeviceTransferActivity)
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__delete_account, ContextCompat.getColor(requireContext(), R.color.signal_alert_primary)),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_accountSettingsFragment_to_deleteAccountFragment)
}
)
}
}
private fun setRegistrationLockEnabled(enabled: Boolean) {
if (enabled) {
RegistrationLockV2Dialog.showEnableDialog(requireContext()) { viewModel.refreshState() }
} else {
RegistrationLockV2Dialog.showDisableDialog(requireContext()) { viewModel.refreshState() }
}
}
private fun setPinRemindersEnabled(enabled: Boolean) {
if (!enabled) {
val context: Context = requireContext()
val metrics: DisplayMetrics = resources.displayMetrics
val dialog: AlertDialog = MaterialAlertDialogBuilder(context, if (ThemeUtil.isDarkTheme(context)) R.style.Theme_Signal_AlertDialog_Dark_Cornered_ColoredAccent else R.style.Theme_Signal_AlertDialog_Light_Cornered_ColoredAccent)
.setView(R.layout.pin_disable_reminders_dialog)
.create()
dialog.show()
dialog.window!!.setLayout((metrics.widthPixels * .80).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT)
val pinEditText = DialogCompat.requireViewById(dialog, R.id.reminder_disable_pin) as EditText
val statusText = DialogCompat.requireViewById(dialog, R.id.reminder_disable_status) as TextView
val cancelButton = DialogCompat.requireViewById(dialog, R.id.reminder_disable_cancel)
val turnOffButton = DialogCompat.requireViewById(dialog, R.id.reminder_disable_turn_off)
pinEditText.post {
if (pinEditText.requestFocus()) {
ServiceUtil.getInputMethodManager(pinEditText.context).showSoftInput(pinEditText, 0)
}
}
ViewCompat.setAutofillHints(pinEditText, HintConstants.AUTOFILL_HINT_PASSWORD)
when (SignalStore.pinValues().keyboardType) {
PinKeyboardType.NUMERIC -> pinEditText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
PinKeyboardType.ALPHA_NUMERIC -> pinEditText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
pinEditText.addTextChangedListener(object : SimpleTextWatcher() {
override fun onTextChanged(text: String) {
turnOffButton.isEnabled = text.length >= KbsConstants.MINIMUM_PIN_LENGTH
}
})
pinEditText.typeface = Typeface.DEFAULT
turnOffButton.setOnClickListener {
val pin = pinEditText.text.toString()
val correct = PinHashing.verifyLocalPinHash(SignalStore.kbsValues().localPinHash!!, pin)
if (correct) {
SignalStore.pinValues().setPinRemindersEnabled(false)
viewModel.refreshState()
dialog.dismiss()
} else {
statusText.setText(R.string.preferences_app_protection__incorrect_pin_try_again)
}
}
cancelButton.setOnClickListener { dialog.dismiss() }
} else {
SignalStore.pinValues().setPinRemindersEnabled(true)
viewModel.refreshState()
}
}
}

View File

@@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.components.settings.app.account
data class AccountSettingsState(
val hasPin: Boolean,
val pinRemindersEnabled: Boolean,
val registrationLockEnabled: Boolean
)

View File

@@ -0,0 +1,24 @@
package org.thoughtcrime.securesms.components.settings.app.account
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.livedata.Store
class AccountSettingsViewModel : ViewModel() {
private val store: Store<AccountSettingsState> = Store(getCurrentState())
val state: LiveData<AccountSettingsState> = store.stateLiveData
fun refreshState() {
store.update { getCurrentState() }
}
private fun getCurrentState(): AccountSettingsState {
return AccountSettingsState(
hasPin = SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().hasOptedOut(),
pinRemindersEnabled = SignalStore.pinValues().arePinRemindersEnabled(),
registrationLockEnabled = SignalStore.kbsValues().isV2RegistrationLockEnabled
)
}
}

View File

@@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.components.settings.app.appearance
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
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
class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__appearance) {
private lateinit var viewModel: AppearanceSettingsViewModel
private val themeLabels by lazy { resources.getStringArray(R.array.pref_theme_entries) }
private val themeValues by lazy { resources.getStringArray(R.array.pref_theme_values) }
private val messageFontSizeLabels by lazy { resources.getStringArray(R.array.pref_message_font_size_entries) }
private val messageFontSizeValues by lazy { resources.getStringArray(R.array.pref_message_font_size_values) }
private val languageLabels by lazy { resources.getStringArray(R.array.language_entries) }
private val languageValues by lazy { resources.getStringArray(R.array.language_values) }
override fun bindAdapter(adapter: DSLSettingsAdapter) {
viewModel = ViewModelProviders.of(this)[AppearanceSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
}
private fun getConfiguration(state: AppearanceSettingsState): DSLConfiguration {
return configure {
radioListPref(
title = DSLSettingsText.from(R.string.preferences__theme),
listItems = themeLabels,
selected = themeValues.indexOf(state.theme),
onSelected = {
viewModel.setTheme(themeValues[it])
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__chat_wallpaper),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appearanceSettings_to_wallpaperActivity)
}
)
radioListPref(
title = DSLSettingsText.from(R.string.preferences_chats__message_text_size),
listItems = messageFontSizeLabels,
selected = messageFontSizeValues.indexOf(state.messageFontSize.toString()),
onSelected = {
viewModel.setMessageFontSize(messageFontSizeValues[it].toInt())
}
)
radioListPref(
title = DSLSettingsText.from(R.string.preferences__language),
listItems = languageLabels,
selected = languageValues.indexOf(state.language),
onSelected = {
viewModel.setLanguage(languageValues[it])
}
)
}
}
}

View File

@@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.components.settings.app.appearance
data class AppearanceSettingsState(
val theme: String,
val messageFontSize: Int,
val language: String
)

View File

@@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.components.settings.app.appearance
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.livedata.Store
class AppearanceSettingsViewModel : ViewModel() {
private val store: Store<AppearanceSettingsState>
init {
val initialState = AppearanceSettingsState(
SignalStore.settings().theme,
SignalStore.settings().messageFontSize,
SignalStore.settings().language
)
store = Store(initialState)
}
val state: LiveData<AppearanceSettingsState> = store.stateLiveData
fun setTheme(theme: String) {
store.update { it.copy(theme = theme) }
SignalStore.settings().theme = theme
}
fun setLanguage(language: String) {
store.update { it.copy(language = language) }
SignalStore.settings().language = language
}
fun setMessageFontSize(size: Int) {
store.update { it.copy(messageFontSize = size) }
SignalStore.settings().messageFontSize = size
}
}

View File

@@ -0,0 +1,87 @@
package org.thoughtcrime.securesms.components.settings.app.chats
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
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
class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__chats) {
private lateinit var viewModel: ChatsSettingsViewModel
override fun bindAdapter(adapter: DSLSettingsAdapter) {
val repository = ChatsSettingsRepository()
val factory = ChatsSettingsViewModel.Factory(repository)
viewModel = ViewModelProviders.of(this, factory)[ChatsSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())
}
}
private fun getConfiguration(state: ChatsSettingsState): DSLConfiguration {
return configure {
clickPref(
title = DSLSettingsText.from(R.string.preferences__sms_mms),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_chatsSettingsFragment_to_smsSettingsFragment)
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences__generate_link_previews),
summary = DSLSettingsText.from(R.string.preferences__retrieve_link_previews_from_websites_for_messages),
isChecked = state.generateLinkPreviews,
onClick = {
viewModel.setGenerateLinkPreviewsEnabled(!state.generateLinkPreviews)
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences__pref_use_address_book_photos),
summary = DSLSettingsText.from(R.string.preferences__display_contact_photos_from_your_address_book_if_available),
isChecked = state.useAddressBook,
onClick = {
viewModel.setUseAddressBook(!state.useAddressBook)
}
)
dividerPref()
sectionHeaderPref(R.string.ChatsSettingsFragment__keyboard)
switchPref(
title = DSLSettingsText.from(R.string.preferences_advanced__use_system_emoji),
isChecked = state.useSystemEmoji,
onClick = {
viewModel.setUseSystemEmoji(!state.useSystemEmoji)
}
)
switchPref(
title = DSLSettingsText.from(R.string.ChatsSettingsFragment__enter_key_sends),
isChecked = state.enterKeySends,
onClick = {
viewModel.setEnterKeySends(!state.enterKeySends)
}
)
dividerPref()
sectionHeaderPref(R.string.preferences_chats__backups)
clickPref(
title = DSLSettingsText.from(R.string.preferences_chats__chat_backups),
summary = DSLSettingsText.from(if (state.chatBackupsEnabled) R.string.arrays__enabled else R.string.arrays__disabled),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_chatsSettingsFragment_to_backupsPreferenceFragment)
}
)
}
}
}

View File

@@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.components.settings.app.chats
import android.content.Context
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.megaphone.Megaphones
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.TextSecurePreferences
class ChatsSettingsRepository {
private val context: Context = ApplicationDependencies.getApplication()
fun syncLinkPreviewsState() {
SignalExecutors.BOUNDED.execute {
val isLinkPreviewsEnabled = SignalStore.settings().isLinkPreviewsEnabled
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
ApplicationDependencies.getJobManager().add(
MultiDeviceConfigurationUpdateJob(
TextSecurePreferences.isReadReceiptsEnabled(context),
TextSecurePreferences.isTypingIndicatorsEnabled(context),
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
isLinkPreviewsEnabled
)
)
if (isLinkPreviewsEnabled) {
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.LINK_PREVIEWS)
}
}
}
}

View File

@@ -0,0 +1,9 @@
package org.thoughtcrime.securesms.components.settings.app.chats
data class ChatsSettingsState(
val generateLinkPreviews: Boolean,
val useAddressBook: Boolean,
val useSystemEmoji: Boolean,
val enterKeySends: Boolean,
val chatBackupsEnabled: Boolean
)

View File

@@ -0,0 +1,56 @@
package org.thoughtcrime.securesms.components.settings.app.chats
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.ThrottledDebouncer
import org.thoughtcrime.securesms.util.livedata.Store
class ChatsSettingsViewModel(private val repository: ChatsSettingsRepository) : ViewModel() {
private val refreshDebouncer = ThrottledDebouncer(500L)
private val store: Store<ChatsSettingsState> = Store(
ChatsSettingsState(
generateLinkPreviews = SignalStore.settings().isLinkPreviewsEnabled,
useAddressBook = SignalStore.settings().isPreferSystemContactPhotos,
useSystemEmoji = SignalStore.settings().isPreferSystemEmoji,
enterKeySends = SignalStore.settings().isEnterKeySends,
chatBackupsEnabled = SignalStore.settings().isBackupEnabled
)
)
val state: LiveData<ChatsSettingsState> = store.stateLiveData
fun setGenerateLinkPreviewsEnabled(enabled: Boolean) {
store.update { it.copy(generateLinkPreviews = enabled) }
SignalStore.settings().isLinkPreviewsEnabled = enabled
repository.syncLinkPreviewsState()
}
fun setUseAddressBook(enabled: Boolean) {
store.update { it.copy(useAddressBook = enabled) }
SignalStore.settings().isPreferSystemContactPhotos = enabled
refreshDebouncer.publish { ConversationUtil.refreshRecipientShortcuts() }
StorageSyncHelper.scheduleSyncForDataChange()
}
fun setUseSystemEmoji(enabled: Boolean) {
store.update { it.copy(useSystemEmoji = enabled) }
SignalStore.settings().isPreferSystemEmoji = enabled
}
fun setEnterKeySends(enabled: Boolean) {
store.update { it.copy(enterKeySends = enabled) }
SignalStore.settings().isEnterKeySends = enabled
}
class Factory(private val repository: ChatsSettingsRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(ChatsSettingsViewModel(repository)))
}
}
}

View File

@@ -0,0 +1,90 @@
package org.thoughtcrime.securesms.components.settings.app.chats.sms
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
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.util.SmsUtil
private const val SMS_REQUEST_CODE: Short = 1234
class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
private lateinit var viewModel: SmsSettingsViewModel
override fun onResume() {
super.onResume()
viewModel.checkSmsEnabled()
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
viewModel = ViewModelProviders.of(this)[SmsSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())
}
}
private fun getConfiguration(state: SmsSettingsState): DSLConfiguration {
return configure {
clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__use_as_default_sms_app),
summary = DSLSettingsText.from(if (state.useAsDefaultSmsApp) R.string.arrays__enabled else R.string.arrays__disabled),
onClick = {
if (state.useAsDefaultSmsApp) {
startDefaultAppSelectionIntent()
} else {
startActivityForResult(SmsUtil.getSmsRoleIntent(requireContext()), SMS_REQUEST_CODE.toInt())
}
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences__sms_delivery_reports),
summary = DSLSettingsText.from(R.string.preferences__request_a_delivery_report_for_each_sms_message_you_send),
isChecked = state.smsDeliveryReportsEnabled,
onClick = {
viewModel.setSmsDeliveryReportsEnabled(!state.smsDeliveryReportsEnabled)
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences__support_wifi_calling),
summary = DSLSettingsText.from(R.string.preferences__enable_if_your_device_supports_sms_mms_delivery_over_wifi),
isChecked = state.wifiCallingCompatibilityEnabled,
onClick = {
viewModel.setWifiCallingCompatibilityEnabled(!state.wifiCallingCompatibilityEnabled)
}
)
if (Build.VERSION.SDK_INT < 21) {
clickPref(
title = DSLSettingsText.from(R.string.preferences__advanced_mms_access_point_names),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_smsSettingsFragment_to_mmsPreferencesFragment)
}
)
}
}
}
// Linter isn't smart enough to figure out the else only happens if API >= 24
@SuppressLint("InlinedApi")
private fun startDefaultAppSelectionIntent() {
startActivity(
when {
Build.VERSION.SDK_INT < 23 -> Intent(Settings.ACTION_WIRELESS_SETTINGS)
Build.VERSION.SDK_INT < 24 -> Intent(Settings.ACTION_SETTINGS)
else -> Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
}
)
}
}

View File

@@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.components.settings.app.chats.sms
data class SmsSettingsState(
val useAsDefaultSmsApp: Boolean,
val smsDeliveryReportsEnabled: Boolean,
val wifiCallingCompatibilityEnabled: Boolean
)

View File

@@ -0,0 +1,35 @@
package org.thoughtcrime.securesms.components.settings.app.chats.sms
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.livedata.Store
class SmsSettingsViewModel : ViewModel() {
private val store = Store(
SmsSettingsState(
useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication()),
smsDeliveryReportsEnabled = SignalStore.settings().isSmsDeliveryReportsEnabled,
wifiCallingCompatibilityEnabled = SignalStore.settings().isWifiCallingCompatibilityModeEnabled
)
)
val state: LiveData<SmsSettingsState> = store.stateLiveData
fun setSmsDeliveryReportsEnabled(enabled: Boolean) {
store.update { it.copy(smsDeliveryReportsEnabled = enabled) }
SignalStore.settings().isSmsDeliveryReportsEnabled = enabled
}
fun setWifiCallingCompatibilityEnabled(enabled: Boolean) {
store.update { it.copy(wifiCallingCompatibilityEnabled = enabled) }
SignalStore.settings().isWifiCallingCompatibilityModeEnabled = enabled
}
fun checkSmsEnabled() {
store.update { it.copy(useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication())) }
}
}

View File

@@ -0,0 +1,115 @@
package org.thoughtcrime.securesms.components.settings.app.data
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import androidx.preference.PreferenceManager
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.util.Util
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
import kotlin.math.abs
class DataAndStorageSettingsFragment : DSLSettingsFragment(R.string.preferences__data_and_storage) {
private val autoDownloadValues by lazy { resources.getStringArray(R.array.pref_media_download_entries) }
private val autoDownloadLabels by lazy { resources.getStringArray(R.array.pref_media_download_values) }
private val callBandwidthLabels by lazy { resources.getStringArray(R.array.pref_data_and_storage_call_bandwidth_values) }
private lateinit var viewModel: DataAndStorageSettingsViewModel
override fun onResume() {
super.onResume()
viewModel.refresh()
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val repository = DataAndStorageSettingsRepository()
val factory = DataAndStorageSettingsViewModel.Factory(preferences, repository)
viewModel = ViewModelProviders.of(this, factory)[DataAndStorageSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())
}
}
fun getConfiguration(state: DataAndStorageSettingsState): DSLConfiguration {
return configure {
clickPref(
title = DSLSettingsText.from(R.string.preferences_data_and_storage__manage_storage),
summary = DSLSettingsText.from(Util.getPrettyFileSize(state.totalStorageUse)),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_dataAndStorageSettingsFragment_to_storagePreferenceFragment)
}
)
dividerPref()
sectionHeaderPref(R.string.preferences_chats__media_auto_download)
multiSelectPref(
title = DSLSettingsText.from(R.string.preferences_chats__when_using_mobile_data),
listItems = autoDownloadLabels,
selected = autoDownloadValues.map { state.mobileAutoDownloadValues.contains(it) }.toBooleanArray(),
onSelected = {
val resultSet = it.mapIndexed { index, selected -> if (selected) autoDownloadValues[index] else null }.filterNotNull().toSet()
viewModel.setMobileAutoDownloadValues(resultSet)
}
)
multiSelectPref(
title = DSLSettingsText.from(R.string.preferences_chats__when_using_wifi),
listItems = autoDownloadLabels,
selected = autoDownloadValues.map { state.wifiAutoDownloadValues.contains(it) }.toBooleanArray(),
onSelected = {
val resultSet = it.mapIndexed { index, selected -> if (selected) autoDownloadValues[index] else null }.filterNotNull().toSet()
viewModel.setWifiAutoDownloadValues(resultSet)
}
)
multiSelectPref(
title = DSLSettingsText.from(R.string.preferences_chats__when_roaming),
listItems = autoDownloadLabels,
selected = autoDownloadValues.map { state.roamingAutoDownloadValues.contains(it) }.toBooleanArray(),
onSelected = {
val resultSet = it.mapIndexed { index, selected -> if (selected) autoDownloadValues[index] else null }.filterNotNull().toSet()
viewModel.setRoamingAutoDownloadValues(resultSet)
}
)
dividerPref()
sectionHeaderPref(R.string.DataAndStorageSettingsFragment__calls)
radioListPref(
title = DSLSettingsText.from(R.string.preferences_data_and_storage__use_less_data_for_calls),
listItems = callBandwidthLabels,
selected = abs(state.callBandwidthMode.code - 2),
onSelected = {
viewModel.setCallBandwidthMode(CallBandwidthMode.fromCode(abs(it - 2)))
}
)
textPref(
summary = DSLSettingsText.from(R.string.preference_data_and_storage__using_less_data_may_improve_calls_on_bad_networks)
)
dividerPref()
sectionHeaderPref(R.string.preferences_proxy)
clickPref(
title = DSLSettingsText.from(R.string.preferences_use_proxy),
summary = DSLSettingsText.from(if (state.isProxyEnabled) R.string.preferences_on else R.string.preferences_off),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_dataAndStorageSettingsFragment_to_editProxyFragment)
}
)
}
}
}

View File

@@ -0,0 +1,19 @@
package org.thoughtcrime.securesms.components.settings.app.data
import android.content.Context
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
class DataAndStorageSettingsRepository {
private val context: Context = ApplicationDependencies.getApplication()
fun getTotalStorageUse(consumer: (Long) -> Unit) {
SignalExecutors.BOUNDED.execute {
val breakdown = DatabaseFactory.getMediaDatabase(context).storageBreakdown
consumer(listOf(breakdown.audioSize, breakdown.documentSize, breakdown.photoSize, breakdown.videoSize).sum())
}
}
}

View File

@@ -0,0 +1,12 @@
package org.thoughtcrime.securesms.components.settings.app.data
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
data class DataAndStorageSettingsState(
val totalStorageUse: Long,
val mobileAutoDownloadValues: Set<String>,
val wifiAutoDownloadValues: Set<String>,
val roamingAutoDownloadValues: Set<String>,
val callBandwidthMode: CallBandwidthMode,
val isProxyEnabled: Boolean
)

View File

@@ -0,0 +1,77 @@
package org.thoughtcrime.securesms.components.settings.app.data
import android.content.SharedPreferences
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.livedata.Store
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
class DataAndStorageSettingsViewModel(
private val sharedPreferences: SharedPreferences,
private val repository: DataAndStorageSettingsRepository
) : ViewModel() {
private val store = Store(getState())
val state: LiveData<DataAndStorageSettingsState> = store.stateLiveData
fun refresh() {
repository.getTotalStorageUse { totalStorageUse ->
store.update { getState().copy(totalStorageUse = totalStorageUse) }
}
}
fun setMobileAutoDownloadValues(resultSet: Set<String>) {
sharedPreferences.edit().putStringSet(TextSecurePreferences.MEDIA_DOWNLOAD_MOBILE_PREF, resultSet).apply()
getStateAndCopyStorageUsage()
}
fun setWifiAutoDownloadValues(resultSet: Set<String>) {
sharedPreferences.edit().putStringSet(TextSecurePreferences.MEDIA_DOWNLOAD_WIFI_PREF, resultSet).apply()
getStateAndCopyStorageUsage()
}
fun setRoamingAutoDownloadValues(resultSet: Set<String>) {
sharedPreferences.edit().putStringSet(TextSecurePreferences.MEDIA_DOWNLOAD_ROAMING_PREF, resultSet).apply()
getStateAndCopyStorageUsage()
}
fun setCallBandwidthMode(callBandwidthMode: CallBandwidthMode) {
SignalStore.settings().callBandwidthMode = callBandwidthMode
ApplicationDependencies.getSignalCallManager().bandwidthModeUpdate()
getStateAndCopyStorageUsage()
}
private fun getStateAndCopyStorageUsage() {
store.update { getState().copy(totalStorageUse = it.totalStorageUse) }
}
private fun getState() = DataAndStorageSettingsState(
totalStorageUse = 0,
mobileAutoDownloadValues = TextSecurePreferences.getMobileMediaDownloadAllowed(
ApplicationDependencies.getApplication()
),
wifiAutoDownloadValues = TextSecurePreferences.getWifiMediaDownloadAllowed(
ApplicationDependencies.getApplication()
),
roamingAutoDownloadValues = TextSecurePreferences.getRoamingMediaDownloadAllowed(
ApplicationDependencies.getApplication()
),
callBandwidthMode = SignalStore.settings().callBandwidthMode,
isProxyEnabled = SignalStore.proxy().isProxyEnabled
)
class Factory(
private val sharedPreferences: SharedPreferences,
private val repository: DataAndStorageSettingsRepository
) :
ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(DataAndStorageSettingsViewModel(sharedPreferences, repository)))
}
}
}

View File

@@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.components.settings.app.help
import android.view.MenuItem
import androidx.navigation.Navigation
import org.thoughtcrime.securesms.BuildConfig
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
class HelpSettingsFragment : DSLSettingsFragment(R.string.preferences__help, R.menu.help_settings) {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.action_submit_debug_log) {
Navigation.findNavController(requireView()).navigate(R.id.action_helpSettingsFragment_to_submitDebugLogActivity)
true
} else {
false
}
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
adapter.submitList(getConfiguration().toMappingModelList())
}
fun getConfiguration(): DSLConfiguration {
return configure {
externalLinkPref(
title = DSLSettingsText.from(R.string.HelpSettingsFragment__support_center),
linkId = R.string.support_center_url
)
clickPref(
title = DSLSettingsText.from(R.string.HelpSettingsFragment__contact_us),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_helpSettingsFragment_to_helpFragment)
}
)
dividerPref()
textPref(
title = DSLSettingsText.from(R.string.HelpSettingsFragment__version),
summary = DSLSettingsText.from(BuildConfig.VERSION_NAME)
)
externalLinkPref(
title = DSLSettingsText.from(R.string.HelpSettingsFragment__terms_amp_privacy_policy),
linkId = R.string.terms_and_privacy_policy_url
)
textPref(
summary = DSLSettingsText.from(
StringBuilder().apply {
append(getString(R.string.HelpFragment__copyright_signal_messenger))
append("\n")
append(getString(R.string.HelpFragment__licenced_under_the_gplv3))
}
)
)
}
}
}

View File

@@ -0,0 +1,281 @@
package org.thoughtcrime.securesms.components.settings.app.internal
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.widget.Toast
import androidx.lifecycle.ViewModelProviders
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.concurrent.SignalExecutors
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.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob
import org.thoughtcrime.securesms.jobs.StorageForcePushJob
import org.thoughtcrime.securesms.payments.DataExportUtil
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.concurrent.SimpleTask
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) {
private lateinit var viewModel: InternalSettingsViewModel
override fun bindAdapter(adapter: DSLSettingsAdapter) {
val repository = InternalSettingsRepository(requireContext())
val factory = InternalSettingsViewModel.Factory(repository)
viewModel = ViewModelProviders.of(this, factory)[InternalSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())
}
}
private fun getConfiguration(state: InternalSettingsState): DSLConfiguration {
return configure {
sectionHeaderPref(R.string.preferences__internal_payments)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_payment_copy_data),
summary = DSLSettingsText.from(R.string.preferences__internal_payment_copy_data_description),
onClick = {
copyPaymentsDataToClipboard()
}
)
dividerPref()
sectionHeaderPref(R.string.preferences__internal_account)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_refresh_attributes),
summary = DSLSettingsText.from(R.string.preferences__internal_refresh_attributes_description),
onClick = {
refreshAttributes()
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_rotate_profile_key),
summary = DSLSettingsText.from(R.string.preferences__internal_rotate_profile_key_description),
onClick = {
rotateProfileKey()
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_refresh_remote_values),
summary = DSLSettingsText.from(R.string.preferences__internal_refresh_remote_values_description),
onClick = {
refreshRemoteValues()
}
)
dividerPref()
sectionHeaderPref(R.string.preferences__internal_display)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_user_details),
summary = DSLSettingsText.from(R.string.preferences__internal_user_details_description),
isChecked = state.seeMoreUserDetails,
onClick = {
viewModel.setSeeMoreUserDetails(!state.seeMoreUserDetails)
}
)
dividerPref()
sectionHeaderPref(R.string.preferences__internal_storage_service)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_force_storage_service_sync),
summary = DSLSettingsText.from(R.string.preferences__internal_force_storage_service_sync_description),
onClick = {
forceStorageServiceSync()
}
)
dividerPref()
sectionHeaderPref(R.string.preferences__internal_preferences_groups_v2)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_do_not_create_gv2),
summary = DSLSettingsText.from(R.string.preferences__internal_do_not_create_gv2_description),
isChecked = state.gv2doNotCreateGv2Groups,
onClick = {
viewModel.setGv2DoNotCreateGv2Groups(!state.gv2doNotCreateGv2Groups)
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_force_gv2_invites),
summary = DSLSettingsText.from(R.string.preferences__internal_force_gv2_invites_description),
isChecked = state.gv2forceInvites,
onClick = {
viewModel.setGv2ForceInvites(!state.gv2forceInvites)
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_ignore_gv2_server_changes),
summary = DSLSettingsText.from(R.string.preferences__internal_ignore_gv2_server_changes_description),
isChecked = state.gv2ignoreServerChanges,
onClick = {
viewModel.setGv2IgnoreServerChanges(!state.gv2ignoreServerChanges)
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_ignore_gv2_p2p_changes),
summary = DSLSettingsText.from(R.string.preferences__internal_ignore_gv2_server_changes_description),
isChecked = state.gv2ignoreP2PChanges,
onClick = {
viewModel.setGv2IgnoreP2PChanges(!state.gv2ignoreP2PChanges)
}
)
dividerPref()
sectionHeaderPref(R.string.preferences__internal_preferences_groups_v1_migration)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_do_not_initiate_automigrate),
summary = DSLSettingsText.from(R.string.preferences__internal_do_not_initiate_automigrate_description),
isChecked = state.disableAutoMigrationInitiation,
onClick = {
viewModel.setDisableAutoMigrationInitiation(!state.disableAutoMigrationInitiation)
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_do_not_notify_automigrate),
summary = DSLSettingsText.from(R.string.preferences__internal_do_not_notify_automigrate_description),
isChecked = state.disableAutoMigrationNotification,
onClick = {
viewModel.setDisableAutoMigrationNotification(!state.disableAutoMigrationNotification)
}
)
dividerPref()
sectionHeaderPref(R.string.preferences__internal_network)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_force_censorship),
summary = DSLSettingsText.from(R.string.preferences__internal_force_censorship_description),
isChecked = state.forceCensorship,
onClick = {
viewModel.setDisableAutoMigrationNotification(!state.forceCensorship)
}
)
dividerPref()
sectionHeaderPref(R.string.preferences__internal_conversations_and_shortcuts)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_delete_all_dynamic_shortcuts),
summary = DSLSettingsText.from(R.string.preferences__internal_click_to_delete_all_dynamic_shortcuts),
onClick = {
deleteAllDynamicShortcuts()
}
)
dividerPref()
sectionHeaderPref(R.string.preferences__internal_emoji)
val emojiSummary = if (state.emojiVersion == null) {
getString(R.string.preferences__internal_use_built_in_emoji_set)
} else {
getString(
R.string.preferences__internal_current_version_d_at_density_s,
state.emojiVersion.version,
state.emojiVersion.density
)
}
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_use_built_in_emoji_set),
summary = DSLSettingsText.from(emojiSummary),
isChecked = state.useBuiltInEmojiSet,
onClick = {
viewModel.setDisableAutoMigrationNotification(!state.useBuiltInEmojiSet)
}
)
}
}
private fun copyPaymentsDataToClipboard() {
MaterialAlertDialogBuilder(requireContext())
.setMessage(
"""
Local payments history will be copied to the clipboard.
It may therefore compromise privacy.
However, no private keys will be copied.
""".trimIndent()
)
.setPositiveButton(
"Copy"
) { _: DialogInterface?, _: Int ->
SimpleTask.run<Any?>(
SignalExecutors.UNBOUNDED,
{
val context: Context = ApplicationDependencies.getApplication()
val clipboard =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val tsv = DataExportUtil.createTsv()
val clip = ClipData.newPlainText(context.getString(R.string.app_name), tsv)
clipboard.setPrimaryClip(clip)
null
},
{
Toast.makeText(
context,
"Payments have been copied",
Toast.LENGTH_SHORT
).show()
}
)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun refreshAttributes() {
ApplicationDependencies.getJobManager()
.startChain(RefreshAttributesJob())
.then(RefreshOwnProfileJob())
.enqueue()
Toast.makeText(context, "Scheduled attribute refresh", Toast.LENGTH_SHORT).show()
}
private fun rotateProfileKey() {
ApplicationDependencies.getJobManager().add(RotateProfileKeyJob())
Toast.makeText(context, "Scheduled profile key rotation", Toast.LENGTH_SHORT).show()
}
private fun refreshRemoteValues() {
ApplicationDependencies.getJobManager().add(RemoteConfigRefreshJob())
Toast.makeText(context, "Scheduled remote config refresh", Toast.LENGTH_SHORT).show()
}
private fun forceStorageServiceSync() {
ApplicationDependencies.getJobManager().add(StorageForcePushJob())
Toast.makeText(context, "Scheduled storage force push", Toast.LENGTH_SHORT).show()
}
private fun deleteAllDynamicShortcuts() {
ConversationUtil.clearAllShortcuts(requireContext())
Toast.makeText(context, "Deleted all dynamic shortcuts.", Toast.LENGTH_SHORT).show()
}
}

View File

@@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.components.settings.app.internal
import android.content.Context
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.emoji.EmojiFiles
class InternalSettingsRepository(context: Context) {
private val context = context.applicationContext
fun getEmojiVersionInfo(consumer: (EmojiFiles.Version?) -> Unit) {
SignalExecutors.BOUNDED.execute {
consumer(EmojiFiles.Version.readVersion(context))
}
}
}

View File

@@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.components.settings.app.internal
import org.thoughtcrime.securesms.emoji.EmojiFiles
data class InternalSettingsState(
val seeMoreUserDetails: Boolean,
val gv2doNotCreateGv2Groups: Boolean,
val gv2forceInvites: Boolean,
val gv2ignoreServerChanges: Boolean,
val gv2ignoreP2PChanges: Boolean,
val disableAutoMigrationInitiation: Boolean,
val disableAutoMigrationNotification: Boolean,
val forceCensorship: Boolean,
val useBuiltInEmojiSet: Boolean,
val emojiVersion: EmojiFiles.Version?
)

View File

@@ -0,0 +1,90 @@
package org.thoughtcrime.securesms.components.settings.app.internal
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.keyvalue.InternalValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.livedata.Store
class InternalSettingsViewModel(private val repository: InternalSettingsRepository) : ViewModel() {
private val preferenceDataStore = SignalStore.getPreferenceDataStore()
private val store = Store(getState())
init {
repository.getEmojiVersionInfo { version ->
store.update { it.copy(emojiVersion = version) }
}
}
val state: LiveData<InternalSettingsState> = store.stateLiveData
fun setSeeMoreUserDetails(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.RECIPIENT_DETAILS, enabled)
refresh()
}
fun setGv2DoNotCreateGv2Groups(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.GV2_DO_NOT_CREATE_GV2, enabled)
refresh()
}
fun setGv2ForceInvites(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.GV2_FORCE_INVITES, enabled)
refresh()
}
fun setGv2IgnoreServerChanges(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.GV2_IGNORE_SERVER_CHANGES, enabled)
refresh()
}
fun setGv2IgnoreP2PChanges(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.GV2_IGNORE_P2P_CHANGES, enabled)
refresh()
}
fun setDisableAutoMigrationInitiation(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.GV2_DISABLE_AUTOMIGRATE_INITIATION, enabled)
refresh()
}
fun setDisableAutoMigrationNotification(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.GV2_DISABLE_AUTOMIGRATE_NOTIFICATION, enabled)
refresh()
}
fun setForceCensorship(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.FORCE_CENSORSHIP, enabled)
refresh()
}
fun setUseBuiltInEmoji(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.FORCE_BUILT_IN_EMOJI, enabled)
refresh()
}
private fun refresh() {
store.update { getState().copy(emojiVersion = it.emojiVersion) }
}
private fun getState() = InternalSettingsState(
seeMoreUserDetails = SignalStore.internalValues().recipientDetails(),
gv2doNotCreateGv2Groups = SignalStore.internalValues().gv2DoNotCreateGv2Groups(),
gv2forceInvites = SignalStore.internalValues().gv2ForceInvites(),
gv2ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges(),
gv2ignoreP2PChanges = SignalStore.internalValues().gv2IgnoreP2PChanges(),
disableAutoMigrationInitiation = SignalStore.internalValues().disableGv1AutoMigrateInitiation(),
disableAutoMigrationNotification = SignalStore.internalValues().disableGv1AutoMigrateNotification(),
forceCensorship = SignalStore.internalValues().forcedCensorship(),
useBuiltInEmojiSet = SignalStore.internalValues().forceBuiltInEmoji(),
emojiVersion = null
)
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(InternalSettingsViewModel(repository)))
}
}
}

View File

@@ -0,0 +1,337 @@
package org.thoughtcrime.securesms.components.settings.app.notifications
import android.app.Activity
import android.content.Intent
import android.graphics.ColorFilter
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.media.RingtoneManager
import android.net.Uri
import android.provider.Settings
import android.text.TextUtils
import android.view.View
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProviders
import androidx.preference.PreferenceManager
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.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.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.util.MappingAdapter
import org.thoughtcrime.securesms.util.RingtoneUtil
import org.thoughtcrime.securesms.util.ViewUtil
private const val MESSAGE_SOUND_SELECT: Int = 1
private const val CALL_RINGTONE_SELECT: Int = 2
class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__notifications) {
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) }
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 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 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) }
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) }
private lateinit var viewModel: NotificationsSettingsViewModel
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == MESSAGE_SOUND_SELECT && resultCode == Activity.RESULT_OK && data != null) {
val uri = data.getParcelableExtra<Uri>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
viewModel.setMessageNotificationsSound(uri)
} else if (requestCode == CALL_RINGTONE_SELECT && resultCode == Activity.RESULT_OK && data != null) {
val uri = data.getParcelableExtra<Uri>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
viewModel.setCallRingtone(uri)
}
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
adapter.registerFactory(
LedColorPreference::class.java,
MappingAdapter.LayoutFactory(::LedColorPreferenceViewHolder, R.layout.dsl_preference_item)
)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val factory = NotificationsSettingsViewModel.Factory(sharedPreferences)
viewModel = ViewModelProviders.of(this, factory)[NotificationsSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())
}
}
private fun getConfiguration(state: NotificationsSettingsState): DSLConfiguration {
return configure {
sectionHeaderPref(R.string.NotificationsSettingsFragment__messages)
switchPref(
title = DSLSettingsText.from(R.string.preferences__notifications),
isChecked = state.messageNotificationsState.notificationsEnabled,
onClick = {
viewModel.setMessageNotificationsEnabled(!state.messageNotificationsState.notificationsEnabled)
}
)
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 (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),
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__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)
} else {
val tone = RingtoneUtil.getRingtone(requireContext(), uri)
if (tone != null) {
tone.getTitle(requireContext())
} else {
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)
startActivityForResult(intent, MESSAGE_SOUND_SELECT)
}
@RequiresApi(26)
private fun launchNotificationPriorityIntent() {
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
intent.putExtra(
Settings.EXTRA_CHANNEL_ID,
NotificationChannels.getMessagesChannel(requireContext())
)
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)
startActivityForResult(intent, CALL_RINGTONE_SELECT)
}
private class LedColorPreference(
val colorValues: Array<String>,
val radioListPreference: RadioListPreference
) : PreferenceModel<LedColorPreference>(
title = radioListPreference.title,
iconId = radioListPreference.iconId,
summary = radioListPreference.summary
) {
override fun areContentsTheSame(newItem: LedColorPreference): Boolean {
return super.areContentsTheSame(newItem) && radioListPreference.areContentsTheSame(newItem.radioListPreference)
}
}
private class LedColorPreferenceViewHolder(itemView: View) :
PreferenceViewHolder<LedColorPreference>(itemView) {
val radioListPreferenceViewHolder = RadioListPreferenceViewHolder(itemView)
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 (titleView.layoutDirection == View.LAYOUT_DIRECTION_LTR) {
titleView.setCompoundDrawables(null, null, circleDrawable, null)
} else {
titleView.setCompoundDrawables(circleDrawable, null, null, null)
}
}
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)
}
return PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
}
}
}

View File

@@ -0,0 +1,27 @@
package org.thoughtcrime.securesms.components.settings.app.notifications
import android.net.Uri
data class NotificationsSettingsState(
val messageNotificationsState: MessageNotificationsState,
val callNotificationsState: CallNotificationsState,
val notifyWhenContactJoinsSignal: Boolean
)
data class MessageNotificationsState(
val notificationsEnabled: Boolean,
val sound: Uri,
val vibrateEnabled: Boolean,
val ledColor: String,
val ledBlink: String,
val inChatSoundsEnabled: Boolean,
val repeatAlerts: Int,
val messagePrivacy: String,
val priority: Int
)
data class CallNotificationsState(
val notificationsEnabled: Boolean,
val ringtone: Uri,
val vibrateEnabled: Boolean
)

View File

@@ -0,0 +1,121 @@
package org.thoughtcrime.securesms.components.settings.app.notifications
import android.content.SharedPreferences
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
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() {
init {
if (NotificationChannels.supported()) {
SignalStore.settings().messageNotificationSound = NotificationChannels.getMessageRingtone(ApplicationDependencies.getApplication())
SignalStore.settings().isMessageVibrateEnabled = NotificationChannels.getMessageVibrate(ApplicationDependencies.getApplication())
}
}
private val store = Store(getState())
val state: LiveData<NotificationsSettingsState> = store.stateLiveData
fun setMessageNotificationsEnabled(enabled: Boolean) {
SignalStore.settings().isMessageNotificationsEnabled = enabled
store.update { getState() }
}
fun setMessageNotificationsSound(sound: Uri?) {
SignalStore.settings().messageNotificationSound = sound ?: Uri.EMPTY
NotificationChannels.updateMessageRingtone(ApplicationDependencies.getApplication(), sound)
store.update { getState() }
}
fun setMessageNotificationVibration(enabled: Boolean) {
SignalStore.settings().isMessageVibrateEnabled = enabled
NotificationChannels.updateMessageVibrate(ApplicationDependencies.getApplication(), enabled)
store.update { getState() }
}
fun setMessageNotificationLedColor(color: String) {
SignalStore.settings().messageLedColor = color
NotificationChannels.updateMessagesLedColor(ApplicationDependencies.getApplication(), color)
store.update { getState() }
}
fun setMessageNotificationLedBlink(blink: String) {
SignalStore.settings().messageLedBlinkPattern = blink
store.update { getState() }
}
fun setMessageNotificationInChatSoundsEnabled(enabled: Boolean) {
SignalStore.settings().isMessageNotificationsInChatSoundsEnabled = enabled
store.update { getState() }
}
fun setMessageRepeatAlerts(repeats: Int) {
SignalStore.settings().messageNotificationsRepeatAlerts = repeats
store.update { getState() }
}
fun setMessageNotificationPrivacy(preference: String) {
SignalStore.settings().messageNotificationsPrivacy = NotificationPrivacyPreference(preference)
store.update { getState() }
}
fun setMessageNotificationPriority(priority: Int) {
sharedPreferences.edit().putInt(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF, priority).apply()
store.update { getState() }
}
fun setCallNotificationsEnabled(enabled: Boolean) {
SignalStore.settings().isCallNotificationsEnabled = enabled
store.update { getState() }
}
fun setCallRingtone(ringtone: Uri?) {
SignalStore.settings().callRingtone = ringtone ?: Uri.EMPTY
store.update { getState() }
}
fun setCallVibrateEnabled(enabled: Boolean) {
SignalStore.settings().isCallVibrateEnabled = enabled
store.update { getState() }
}
fun setNotifyWhenContactJoinsSignal(enabled: Boolean) {
SignalStore.settings().isNotifyWhenContactJoinsSignal = enabled
store.update { getState() }
}
private fun getState(): NotificationsSettingsState = NotificationsSettingsState(
messageNotificationsState = MessageNotificationsState(
notificationsEnabled = SignalStore.settings().isMessageNotificationsEnabled,
sound = SignalStore.settings().messageNotificationSound,
vibrateEnabled = SignalStore.settings().isMessageVibrateEnabled,
ledColor = SignalStore.settings().messageLedColor,
ledBlink = SignalStore.settings().messageLedBlinkPattern,
inChatSoundsEnabled = SignalStore.settings().isMessageNotificationsInChatSoundsEnabled,
repeatAlerts = SignalStore.settings().messageNotificationsRepeatAlerts,
messagePrivacy = SignalStore.settings().messageNotificationsPrivacy.toString(),
priority = TextSecurePreferences.getNotificationPriority(ApplicationDependencies.getApplication())
),
callNotificationsState = CallNotificationsState(
notificationsEnabled = SignalStore.settings().isCallNotificationsEnabled,
ringtone = SignalStore.settings().callRingtone,
vibrateEnabled = SignalStore.settings().isCallVibrateEnabled
),
notifyWhenContactJoinsSignal = SignalStore.settings().isNotifyWhenContactJoinsSignal
)
class Factory(private val sharedPreferences: SharedPreferences) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(NotificationsSettingsViewModel(sharedPreferences)))
}
}
}

View File

@@ -0,0 +1,391 @@
package org.thoughtcrime.securesms.components.settings.app.privacy
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.TextAppearanceSpan
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import mobi.upod.timedurationpicker.TimeDurationPicker
import mobi.upod.timedurationpicker.TimeDurationPickerDialog
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.PassphraseChangeActivity
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.crypto.MasterSecretUtil
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberListingMode
import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import java.lang.Integer.max
import java.util.ArrayList
import java.util.LinkedHashMap
import java.util.Locale
import java.util.concurrent.TimeUnit
private val TAG = Log.tag(PrivacySettingsFragment::class.java)
class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privacy) {
private lateinit var viewModel: PrivacySettingsViewModel
private val incognitoSummary: CharSequence by lazy {
SpannableStringBuilder(getString(R.string.preferences__this_setting_is_not_a_guarantee))
.append(" ")
.append(
SpanUtil.learnMore(requireContext(), ContextCompat.getColor(requireContext(), R.color.signal_text_primary)) {
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.preferences__incognito_keyboard_learn_more))
}
)
}
override fun onResume() {
super.onResume()
viewModel.refreshBlockedCount()
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val repository = PrivacySettingsRepository()
val factory = PrivacySettingsViewModel.Factory(sharedPreferences, repository)
viewModel = ViewModelProviders.of(this, factory)[PrivacySettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
}
private fun getConfiguration(state: PrivacySettingsState): DSLConfiguration {
return configure {
clickPref(
title = DSLSettingsText.from(R.string.PrivacySettingsFragment__blocked),
summary = DSLSettingsText.from(getString(R.string.PrivacySettingsFragment__d_contacts, state.blockedCount)),
onClick = {
Navigation.findNavController(requireView())
.navigate(R.id.action_privacySettingsFragment_to_blockedUsersActivity)
}
)
dividerPref()
if (FeatureFlags.phoneNumberPrivacy()) {
sectionHeaderPref(R.string.preferences_app_protection__who_can)
clickPref(
title = DSLSettingsText.from(R.string.preferences_app_protection__see_my_phone_number),
summary = DSLSettingsText.from(getWhoCanSeeMyPhoneNumberSummary(state.seeMyPhoneNumber)),
onClick = {
onSeeMyPhoneNumberClicked(state.seeMyPhoneNumber)
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences_app_protection__find_me_by_phone_number),
summary = DSLSettingsText.from(getWhoCanFindMeByPhoneNumberSummary(state.findMeByPhoneNumber)),
onClick = {
onFindMyPhoneNumberClicked(state.findMeByPhoneNumber)
}
)
dividerPref()
}
sectionHeaderPref(R.string.PrivacySettingsFragment__messaging)
switchPref(
title = DSLSettingsText.from(R.string.preferences__read_receipts),
summary = DSLSettingsText.from(R.string.preferences__if_read_receipts_are_disabled_you_wont_be_able_to_see_read_receipts),
isChecked = state.readReceipts,
onClick = {
viewModel.setReadReceiptsEnabled(!state.readReceipts)
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences__typing_indicators),
summary = DSLSettingsText.from(R.string.preferences__if_typing_indicators_are_disabled_you_wont_be_able_to_see_typing_indicators),
isChecked = state.typingIndicators,
onClick = {
viewModel.setTypingIndicatorsEnabled(!state.typingIndicators)
}
)
dividerPref()
sectionHeaderPref(R.string.PrivacySettingsFragment__app_security)
if (state.isObsoletePasswordEnabled) {
switchPref(
title = DSLSettingsText.from(R.string.preferences__enable_passphrase),
summary = DSLSettingsText.from(R.string.preferences__lock_signal_and_message_notifications_with_a_passphrase),
isChecked = true,
onClick = {
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(R.string.ApplicationPreferencesActivity_disable_passphrase)
setMessage(R.string.ApplicationPreferencesActivity_this_will_permanently_unlock_signal_and_message_notifications)
setIcon(R.drawable.ic_warning)
setPositiveButton(R.string.ApplicationPreferencesActivity_disable) { dialog, which ->
MasterSecretUtil.changeMasterSecretPassphrase(
activity,
KeyCachingService.getMasterSecret(context),
MasterSecretUtil.UNENCRYPTED_PASSPHRASE
)
TextSecurePreferences.setPasswordDisabled(activity, true)
val intent = Intent(activity, KeyCachingService::class.java)
intent.action = KeyCachingService.DISABLE_ACTION
requireActivity().startService(intent)
viewModel.refresh()
}
setNegativeButton(android.R.string.cancel, null)
show()
}
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__change_passphrase),
summary = DSLSettingsText.from(R.string.preferences__change_your_passphrase),
onClick = {
if (MasterSecretUtil.isPassphraseInitialized(activity)) {
startActivity(Intent(activity, PassphraseChangeActivity::class.java))
} else {
Toast.makeText(
activity,
R.string.ApplicationPreferenceActivity_you_havent_set_a_passphrase_yet,
Toast.LENGTH_LONG
).show()
}
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences__inactivity_timeout_passphrase),
summary = DSLSettingsText.from(R.string.preferences__auto_lock_signal_after_a_specified_time_interval_of_inactivity),
isChecked = state.isObsoletePasswordTimeoutEnabled,
onClick = {
viewModel.setObsoletePasswordTimeoutEnabled(!state.isObsoletePasswordEnabled)
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__inactivity_timeout_interval),
onClick = {
TimeDurationPickerDialog(
context,
{ _: TimeDurationPicker?, duration: Long ->
val timeoutMinutes = max(TimeUnit.MILLISECONDS.toMinutes(duration).toInt(), 1)
viewModel.setObsoletePasswordTimeout(timeoutMinutes)
},
0, TimeDurationPicker.HH_MM
).show()
}
)
} else {
val isKeyguardSecure = ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure
switchPref(
title = DSLSettingsText.from(R.string.preferences_app_protection__screen_lock),
summary = DSLSettingsText.from(R.string.preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint),
isChecked = state.screenLock && isKeyguardSecure,
isEnabled = isKeyguardSecure,
onClick = {
viewModel.setScreenLockEnabled(!state.screenLock)
val intent = Intent(requireContext(), KeyCachingService::class.java)
intent.action = KeyCachingService.LOCK_TOGGLED_EVENT
requireContext().startService(intent)
ConversationUtil.refreshRecipientShortcuts()
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences_app_protection__screen_lock_inactivity_timeout),
summary = DSLSettingsText.from(getScreenLockInactivityTimeoutSummary(state.screenLockActivityTimeout)),
isEnabled = isKeyguardSecure,
onClick = {
TimeDurationPickerDialog(
context,
{ _: TimeDurationPicker?, duration: Long ->
val timeoutSeconds = TimeUnit.MILLISECONDS.toSeconds(duration)
viewModel.setScreenLockTimeout(timeoutSeconds)
},
0, TimeDurationPicker.HH_MM
).show()
}
)
}
switchPref(
title = DSLSettingsText.from(R.string.preferences__screen_security),
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__block_screenshots_in_the_recents_list_and_inside_the_app),
isChecked = state.screenSecurity,
onClick = {
viewModel.setScreenSecurityEnabled(!state.screenSecurity)
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences__incognito_keyboard),
summary = DSLSettingsText.from(R.string.preferences__request_keyboard_to_disable),
isChecked = state.incognitoKeyboard,
onClick = {
viewModel.setIncognitoKeyboard(!state.incognitoKeyboard)
}
)
textPref(
summary = DSLSettingsText.from(incognitoSummary),
)
dividerPref()
clickPref(
title = DSLSettingsText.from(R.string.preferences__advanced),
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__signal_message_and_calls),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_privacySettingsFragment_to_advancedPrivacySettingsFragment)
}
)
}
}
private fun getScreenLockInactivityTimeoutSummary(timeoutSeconds: Long): String {
val hours = TimeUnit.SECONDS.toHours(timeoutSeconds)
val minutes =
TimeUnit.SECONDS.toMinutes(timeoutSeconds) - TimeUnit.SECONDS.toHours(timeoutSeconds) * 60
return if (timeoutSeconds <= 0) {
getString(R.string.AppProtectionPreferenceFragment_none)
} else {
String.format(Locale.getDefault(), "%02d:%02d:00", hours, minutes)
}
}
@StringRes
private fun getWhoCanSeeMyPhoneNumberSummary(phoneNumberSharingMode: PhoneNumberPrivacyValues.PhoneNumberSharingMode): Int {
return when (phoneNumberSharingMode) {
PhoneNumberPrivacyValues.PhoneNumberSharingMode.EVERYONE -> R.string.PhoneNumberPrivacy_everyone
PhoneNumberPrivacyValues.PhoneNumberSharingMode.CONTACTS -> R.string.PhoneNumberPrivacy_my_contacts
PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY -> R.string.PhoneNumberPrivacy_nobody
}
}
@StringRes
private fun getWhoCanFindMeByPhoneNumberSummary(phoneNumberListingMode: PhoneNumberListingMode): Int {
return when (phoneNumberListingMode) {
PhoneNumberListingMode.LISTED -> R.string.PhoneNumberPrivacy_everyone
PhoneNumberListingMode.UNLISTED -> R.string.PhoneNumberPrivacy_nobody
}
}
private fun onSeeMyPhoneNumberClicked(phoneNumberSharingMode: PhoneNumberPrivacyValues.PhoneNumberSharingMode) {
val value = arrayOf(phoneNumberSharingMode)
val items = items(requireContext())
val modes: List<PhoneNumberPrivacyValues.PhoneNumberSharingMode> = ArrayList(items.keys)
val modeStrings = items.values.toTypedArray()
val selectedMode = modes.indexOf(value[0])
MaterialAlertDialogBuilder(requireActivity()).apply {
setTitle(R.string.preferences_app_protection__see_my_phone_number)
setCancelable(true)
setSingleChoiceItems(
modeStrings,
selectedMode
) { _: DialogInterface?, which: Int -> value[0] = modes[which] }
setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
val newSharingMode = value[0]
Log.i(
TAG,
String.format(
"PhoneNumberSharingMode changed to %s. Scheduling storage value sync",
newSharingMode
)
)
viewModel.setPhoneNumberSharingMode(value[0])
}
setNegativeButton(android.R.string.cancel, null)
show()
}
}
private fun items(context: Context): Map<PhoneNumberPrivacyValues.PhoneNumberSharingMode, CharSequence> {
val map: MutableMap<PhoneNumberPrivacyValues.PhoneNumberSharingMode, CharSequence> = LinkedHashMap()
map[PhoneNumberPrivacyValues.PhoneNumberSharingMode.EVERYONE] = titleAndDescription(
context,
context.getString(R.string.PhoneNumberPrivacy_everyone),
context.getString(R.string.PhoneNumberPrivacy_everyone_see_description)
)
map[PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY] =
context.getString(R.string.PhoneNumberPrivacy_nobody)
return map
}
private fun titleAndDescription(
context: Context,
header: String,
description: String
): CharSequence {
return SpannableStringBuilder().apply {
append("\n")
append(header)
append("\n")
setSpan(
TextAppearanceSpan(context, android.R.style.TextAppearance_Small),
length,
length,
Spanned.SPAN_INCLUSIVE_INCLUSIVE
)
append(description)
append("\n")
}
}
fun onFindMyPhoneNumberClicked(phoneNumberListingMode: PhoneNumberListingMode) {
val context = requireContext()
val value = arrayOf(phoneNumberListingMode)
MaterialAlertDialogBuilder(requireActivity()).apply {
setTitle(R.string.preferences_app_protection__find_me_by_phone_number)
setCancelable(true)
setSingleChoiceItems(
arrayOf(
titleAndDescription(
context,
context.getString(R.string.PhoneNumberPrivacy_everyone),
context.getString(R.string.PhoneNumberPrivacy_everyone_find_description)
),
context.getString(R.string.PhoneNumberPrivacy_nobody)
),
value[0].ordinal
) { _: DialogInterface?, which: Int -> value[0] = PhoneNumberListingMode.values()[which] }
setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
Log.i(
TAG,
String.format(
"PhoneNumberListingMode changed to %s. Scheduling storage value sync",
value[0]
)
)
viewModel.setPhoneNumberListingMode(value[0])
}
setNegativeButton(android.R.string.cancel, null)
show()
}
}
}

View File

@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.components.settings.app.privacy
import android.content.Context
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.TextSecurePreferences
class PrivacySettingsRepository {
private val context: Context = ApplicationDependencies.getApplication()
fun getBlockedCount(consumer: (Int) -> Unit) {
SignalExecutors.BOUNDED.execute {
val recipientDatabase = DatabaseFactory.getRecipientDatabase(context)
consumer(recipientDatabase.blocked.count)
}
}
fun syncReadReceiptState() {
SignalExecutors.BOUNDED.execute {
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
ApplicationDependencies.getJobManager().add(
MultiDeviceConfigurationUpdateJob(
TextSecurePreferences.isReadReceiptsEnabled(context),
TextSecurePreferences.isTypingIndicatorsEnabled(context),
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
SignalStore.settings().isLinkPreviewsEnabled
)
)
}
}
fun syncTypingIndicatorsState() {
val enabled = TextSecurePreferences.isTypingIndicatorsEnabled(context)
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
ApplicationDependencies.getJobManager().add(
MultiDeviceConfigurationUpdateJob(
TextSecurePreferences.isReadReceiptsEnabled(context),
enabled,
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
SignalStore.settings().isLinkPreviewsEnabled
)
)
if (!enabled) {
ApplicationDependencies.getTypingStatusRepository().clear()
}
}
}

View File

@@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.components.settings.app.privacy
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
data class PrivacySettingsState(
val blockedCount: Int,
val seeMyPhoneNumber: PhoneNumberPrivacyValues.PhoneNumberSharingMode,
val findMeByPhoneNumber: PhoneNumberPrivacyValues.PhoneNumberListingMode,
val readReceipts: Boolean,
val typingIndicators: Boolean,
val screenLock: Boolean,
val screenLockActivityTimeout: Long,
val screenSecurity: Boolean,
val incognitoKeyboard: Boolean,
val isObsoletePasswordEnabled: Boolean,
val isObsoletePasswordTimeoutEnabled: Boolean,
val obsoletePasswordTimeout: Int
)

View File

@@ -0,0 +1,118 @@
package org.thoughtcrime.securesms.components.settings.app.privacy
import android.content.SharedPreferences
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.livedata.Store
class PrivacySettingsViewModel(
private val sharedPreferences: SharedPreferences,
private val repository: PrivacySettingsRepository
) : ViewModel() {
private val store = Store(getState())
val state: LiveData<PrivacySettingsState> = store.stateLiveData
fun refreshBlockedCount() {
repository.getBlockedCount { count ->
store.update { it.copy(blockedCount = count) }
}
}
fun setReadReceiptsEnabled(enabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.READ_RECEIPTS_PREF, enabled).apply()
repository.syncReadReceiptState()
refresh()
}
fun setTypingIndicatorsEnabled(enabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.TYPING_INDICATORS, enabled).apply()
repository.syncTypingIndicatorsState()
refresh()
}
fun setScreenLockEnabled(enabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.SCREEN_LOCK, enabled).apply()
refresh()
}
fun setScreenLockTimeout(seconds: Long) {
TextSecurePreferences.setScreenLockTimeout(ApplicationDependencies.getApplication(), seconds)
refresh()
}
fun setScreenSecurityEnabled(enabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.SCREEN_SECURITY_PREF, enabled).apply()
refresh()
}
fun setPhoneNumberSharingMode(phoneNumberSharingMode: PhoneNumberPrivacyValues.PhoneNumberSharingMode) {
SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = phoneNumberSharingMode
StorageSyncHelper.scheduleSyncForDataChange()
refresh()
}
fun setPhoneNumberListingMode(phoneNumberListingMode: PhoneNumberPrivacyValues.PhoneNumberListingMode) {
SignalStore.phoneNumberPrivacy().phoneNumberListingMode = phoneNumberListingMode
StorageSyncHelper.scheduleSyncForDataChange()
ApplicationDependencies.getJobManager().add(RefreshAttributesJob())
refresh()
}
fun setIncognitoKeyboard(enabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.INCOGNITO_KEYBORAD_PREF, enabled).apply()
refresh()
}
fun setObsoletePasswordTimeoutEnabled(enabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.PASSPHRASE_TIMEOUT_PREF, enabled).apply()
refresh()
}
fun setObsoletePasswordTimeout(minutes: Int) {
TextSecurePreferences.setPassphraseTimeoutInterval(ApplicationDependencies.getApplication(), minutes)
refresh()
}
fun refresh() {
store.update(this::updateState)
}
private fun getState(): PrivacySettingsState {
return PrivacySettingsState(
blockedCount = 0,
readReceipts = TextSecurePreferences.isReadReceiptsEnabled(ApplicationDependencies.getApplication()),
typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(ApplicationDependencies.getApplication()),
screenLock = TextSecurePreferences.isScreenLockEnabled(ApplicationDependencies.getApplication()),
screenLockActivityTimeout = TextSecurePreferences.getScreenLockTimeout(ApplicationDependencies.getApplication()),
screenSecurity = TextSecurePreferences.isScreenSecurityEnabled(ApplicationDependencies.getApplication()),
incognitoKeyboard = TextSecurePreferences.isIncognitoKeyboardEnabled(ApplicationDependencies.getApplication()),
seeMyPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberSharingMode,
findMeByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberListingMode,
isObsoletePasswordEnabled = !TextSecurePreferences.isPasswordDisabled(ApplicationDependencies.getApplication()),
isObsoletePasswordTimeoutEnabled = TextSecurePreferences.isPassphraseTimeoutEnabled(ApplicationDependencies.getApplication()),
obsoletePasswordTimeout = TextSecurePreferences.getPassphraseTimeoutInterval(ApplicationDependencies.getApplication())
)
}
private fun updateState(state: PrivacySettingsState): PrivacySettingsState {
return getState().copy(blockedCount = state.blockedCount)
}
class Factory(
private val sharedPreferences: SharedPreferences,
private val repository: PrivacySettingsRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(PrivacySettingsViewModel(sharedPreferences, repository)))
}
}
}

View File

@@ -0,0 +1,161 @@
package org.thoughtcrime.securesms.components.settings.app.privacy.advanced
import android.app.ProgressDialog
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.text.SpannableStringBuilder
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProviders
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.ViewUtil
class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__advanced) {
lateinit var viewModel: AdvancedPrivacySettingsViewModel
private val sealedSenderSummary: CharSequence by lazy {
SpanUtil.learnMore(
requireContext(),
ContextCompat.getColor(requireContext(), R.color.signal_text_primary)
) {
CommunicationActions.openBrowserLink(
requireContext(),
getString(R.string.AdvancedPrivacySettingsFragment__sealed_sender_link)
)
}
}
var progressDialog: ProgressDialog? = null
val statusIcon: CharSequence by lazy {
val unidentifiedDeliveryIcon = requireNotNull(
ContextCompat.getDrawable(
requireContext(),
R.drawable.ic_unidentified_delivery
)
)
unidentifiedDeliveryIcon.setBounds(0, 0, ViewUtil.dpToPx(20), ViewUtil.dpToPx(20))
val iconTint = ContextCompat.getColor(requireContext(), R.color.signal_text_primary_dialog)
unidentifiedDeliveryIcon.colorFilter = PorterDuffColorFilter(iconTint, PorterDuff.Mode.SRC_IN)
SpanUtil.buildImageSpan(unidentifiedDeliveryIcon)
}
override fun onResume() {
super.onResume()
viewModel.refresh()
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
val repository = AdvancedPrivacySettingsRepository(requireContext())
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val factory = AdvancedPrivacySettingsViewModel.Factory(preferences, repository)
viewModel = ViewModelProviders.of(this, factory)[AdvancedPrivacySettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) {
if (it.showProgressSpinner) {
if (progressDialog?.isShowing == false) {
progressDialog = ProgressDialog.show(requireContext(), null, null, true)
}
} else {
progressDialog?.hide()
}
adapter.submitList(getConfiguration(it).toMappingModelList())
}
viewModel.events.observe(viewLifecycleOwner) {
if (it == AdvancedPrivacySettingsViewModel.Event.DISABLE_PUSH_FAILED) {
Toast.makeText(
requireContext(),
R.string.ApplicationPreferencesActivity_error_connecting_to_server,
Toast.LENGTH_LONG
).show()
}
}
}
private fun getConfiguration(state: AdvancedPrivacySettingsState): DSLConfiguration {
return configure {
switchPref(
title = DSLSettingsText.from(R.string.preferences__signal_messages_and_calls),
summary = DSLSettingsText.from(getPushToggleSummary(state.isPushEnabled)),
isChecked = state.isPushEnabled
) {
if (state.isPushEnabled) {
MaterialAlertDialogBuilder(requireContext()).apply {
setIcon(R.drawable.ic_info_outline)
setTitle(R.string.ApplicationPreferencesActivity_disable_signal_messages_and_calls)
setMessage(R.string.ApplicationPreferencesActivity_disable_signal_messages_and_calls_by_unregistering)
setNegativeButton(android.R.string.cancel, null)
setPositiveButton(
android.R.string.ok
) { _, _ -> viewModel.disablePushMessages() }
show()
}
} else {
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()))
}
}
switchPref(
title = DSLSettingsText.from(R.string.preferences_advanced__always_relay_calls),
summary = DSLSettingsText.from(R.string.preferences_advanced__relay_all_calls_through_the_signal_server_to_avoid_revealing_your_ip_address),
isChecked = state.alwaysRelayCalls
) {
viewModel.setAlwaysRelayCalls(!state.alwaysRelayCalls)
}
dividerPref()
sectionHeaderPref(R.string.preferences_communication__category_sealed_sender)
switchPref(
title = DSLSettingsText.from(
SpannableStringBuilder(getString(R.string.AdvancedPrivacySettingsFragment__show_status_icon))
.append(" ")
.append(statusIcon)
),
summary = DSLSettingsText.from(R.string.AdvancedPrivacySettingsFragment__show_an_icon),
isChecked = state.showSealedSenderStatusIcon
) {
viewModel.setShowStatusIconForSealedSender(!state.showSealedSenderStatusIcon)
}
switchPref(
title = DSLSettingsText.from(R.string.preferences_communication__sealed_sender_allow_from_anyone),
summary = DSLSettingsText.from(R.string.preferences_communication__sealed_sender_allow_from_anyone_description),
isChecked = state.allowSealedSenderFromAnyone
) {
viewModel.setAllowSealedSenderFromAnyone(!state.allowSealedSenderFromAnyone)
}
textPref(
summary = DSLSettingsText.from(sealedSenderSummary)
)
}
}
private fun getPushToggleSummary(isPushEnabled: Boolean): String {
return if (isPushEnabled) {
PhoneNumberFormatter.prettyPrint(TextSecurePreferences.getLocalNumber(requireContext()))
} else {
getString(R.string.preferences__free_private_messages_and_calls)
}
}
}

View File

@@ -0,0 +1,63 @@
package org.thoughtcrime.securesms.components.settings.app.privacy.advanced
import android.content.Context
import com.google.firebase.iid.FirebaseInstanceId
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException
import java.io.IOException
private val TAG = Log.tag(AdvancedPrivacySettingsRepository::class.java)
class AdvancedPrivacySettingsRepository(private val context: Context) {
fun disablePushMessages(consumer: (DisablePushMessagesResult) -> Unit) {
SignalExecutors.BOUNDED.execute {
val result = try {
val accountManager = ApplicationDependencies.getSignalServiceAccountManager()
try {
accountManager.setGcmId(Optional.absent())
} catch (e: AuthorizationFailedException) {
Log.w(TAG, e)
}
if (!TextSecurePreferences.isFcmDisabled(context)) {
FirebaseInstanceId.getInstance().deleteInstanceId()
}
DisablePushMessagesResult.SUCCESS
} catch (ioe: IOException) {
Log.w(TAG, ioe)
DisablePushMessagesResult.NETWORK_ERROR
}
consumer(result)
}
}
fun syncShowSealedSenderIconState() {
SignalExecutors.BOUNDED.execute {
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
ApplicationDependencies.getJobManager().add(
MultiDeviceConfigurationUpdateJob(
TextSecurePreferences.isReadReceiptsEnabled(context),
TextSecurePreferences.isTypingIndicatorsEnabled(context),
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
SignalStore.settings().isLinkPreviewsEnabled
)
)
}
}
enum class DisablePushMessagesResult {
SUCCESS,
NETWORK_ERROR
}
}

View File

@@ -0,0 +1,9 @@
package org.thoughtcrime.securesms.components.settings.app.privacy.advanced
data class AdvancedPrivacySettingsState(
val isPushEnabled: Boolean,
val alwaysRelayCalls: Boolean,
val showSealedSenderStatusIcon: Boolean,
val allowSealedSenderFromAnyone: Boolean,
val showProgressSpinner: Boolean
)

View File

@@ -0,0 +1,95 @@
package org.thoughtcrime.securesms.components.settings.app.privacy.advanced
import android.content.SharedPreferences
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.SingleLiveEvent
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.livedata.Store
class AdvancedPrivacySettingsViewModel(
private val sharedPreferences: SharedPreferences,
private val repository: AdvancedPrivacySettingsRepository
) : ViewModel() {
private val store = Store(getState())
private val singleEvents = SingleLiveEvent<Event>()
val state: LiveData<AdvancedPrivacySettingsState> = store.stateLiveData
val events: LiveData<Event> = singleEvents
fun disablePushMessages() {
store.update { getState().copy(showProgressSpinner = true) }
repository.disablePushMessages {
when (it) {
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.SUCCESS -> {
TextSecurePreferences.setPushRegistered(ApplicationDependencies.getApplication(), false)
SignalStore.registrationValues().clearRegistrationComplete()
}
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.NETWORK_ERROR -> {
singleEvents.postValue(Event.DISABLE_PUSH_FAILED)
}
}
store.update { getState().copy(showProgressSpinner = false) }
}
}
fun setAlwaysRelayCalls(enabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.ALWAYS_RELAY_CALLS_PREF, enabled).apply()
refresh()
}
fun setShowStatusIconForSealedSender(enabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.SHOW_UNIDENTIFIED_DELIVERY_INDICATORS, enabled).apply()
repository.syncShowSealedSenderIconState()
refresh()
}
fun setAllowSealedSenderFromAnyone(enabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.UNIVERSAL_UNIDENTIFIED_ACCESS, enabled).apply()
ApplicationDependencies.getJobManager().add(RefreshAttributesJob())
refresh()
}
fun refresh() {
store.update { getState().copy(showProgressSpinner = it.showProgressSpinner) }
}
private fun getState() = AdvancedPrivacySettingsState(
isPushEnabled = TextSecurePreferences.isPushRegistered(ApplicationDependencies.getApplication()),
alwaysRelayCalls = TextSecurePreferences.isTurnOnly(ApplicationDependencies.getApplication()),
showSealedSenderStatusIcon = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(
ApplicationDependencies.getApplication()
),
allowSealedSenderFromAnyone = TextSecurePreferences.isUniversalUnidentifiedAccess(
ApplicationDependencies.getApplication()
),
false
)
enum class Event {
DISABLE_PUSH_FAILED
}
class Factory(
private val sharedPreferences: SharedPreferences,
private val repository: AdvancedPrivacySettingsRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(
modelClass.cast(
AdvancedPrivacySettingsViewModel(
sharedPreferences,
repository
)
)
)
}
}
}

View File

@@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.components.settings.app.wrapped
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import org.thoughtcrime.securesms.R
/**
* Wraps a fragment to give it a Settings style toolbar. This class should be used sparingly, and
* is really only here as stop-gap as we migrate more settings screens to the new UI
*/
abstract class SettingsWrapperFragment : Fragment(R.layout.settings_wrapper_fragment) {
protected lateinit var toolbar: Toolbar
private set
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
toolbar = view.findViewById(R.id.toolbar)
toolbar.setNavigationOnClickListener {
onBackPressed()
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, OnBackPressed())
childFragmentManager
.beginTransaction()
.replace(R.id.wrapped_fragment, getFragment())
.commit()
}
abstract fun getFragment(): Fragment
fun setTitle(@StringRes titleId: Int) {
toolbar.setTitle(titleId)
}
private fun onBackPressed() {
if (!childFragmentManager.popBackStackImmediate()) {
requireActivity().onNavigateUp()
}
}
private inner class OnBackPressed : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onBackPressed()
}
}
}

View File

@@ -0,0 +1,12 @@
package org.thoughtcrime.securesms.components.settings.app.wrapped
import androidx.fragment.app.Fragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.preferences.AdvancedPinPreferenceFragment
class WrappedAdvancedPinPreferenceFragment : SettingsWrapperFragment() {
override fun getFragment(): Fragment {
toolbar.setTitle(R.string.preferences__advanced_pin_settings)
return AdvancedPinPreferenceFragment()
}
}

View File

@@ -0,0 +1,12 @@
package org.thoughtcrime.securesms.components.settings.app.wrapped
import androidx.fragment.app.Fragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment
class WrappedBackupsPreferenceFragment : SettingsWrapperFragment() {
override fun getFragment(): Fragment {
toolbar.setTitle(R.string.BackupsPreferenceFragment__chat_backups)
return BackupsPreferenceFragment()
}
}

View File

@@ -0,0 +1,12 @@
package org.thoughtcrime.securesms.components.settings.app.wrapped
import androidx.fragment.app.Fragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.delete.DeleteAccountFragment
class WrappedDeleteAccountFragment : SettingsWrapperFragment() {
override fun getFragment(): Fragment {
toolbar.setTitle(R.string.preferences__delete_account)
return DeleteAccountFragment()
}
}

View File

@@ -0,0 +1,12 @@
package org.thoughtcrime.securesms.components.settings.app.wrapped
import androidx.fragment.app.Fragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.preferences.EditProxyFragment
class WrappedEditProxyFragment : SettingsWrapperFragment() {
override fun getFragment(): Fragment {
toolbar.setTitle(R.string.preferences_use_proxy)
return EditProxyFragment()
}
}

View File

@@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.components.settings.app.wrapped
import androidx.fragment.app.Fragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.help.HelpFragment
class WrappedHelpFragment : SettingsWrapperFragment() {
override fun getFragment(): Fragment {
toolbar.title = getString(R.string.preferences__help)
val fragment = HelpFragment()
fragment.arguments = arguments
return fragment
}
}

View File

@@ -0,0 +1,12 @@
package org.thoughtcrime.securesms.components.settings.app.wrapped
import androidx.fragment.app.Fragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.preferences.MmsPreferencesFragment
class WrappedMmsPreferencesFragment : SettingsWrapperFragment() {
override fun getFragment(): Fragment {
toolbar.setTitle(R.string.preferences__advanced_mms_access_point_names)
return MmsPreferencesFragment()
}
}

View File

@@ -0,0 +1,10 @@
package org.thoughtcrime.securesms.components.settings.app.wrapped
import androidx.fragment.app.Fragment
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment
class WrappedStoragePreferenceFragment : SettingsWrapperFragment() {
override fun getFragment(): Fragment {
return StoragePreferenceFragment()
}
}

View File

@@ -0,0 +1,192 @@
package org.thoughtcrime.securesms.components.settings
import androidx.annotation.CallSuper
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.thoughtcrime.securesms.util.MappingModel
import org.thoughtcrime.securesms.util.MappingModelList
private const val UNSET = -1
fun configure(init: DSLConfiguration.() -> Unit): DSLConfiguration {
val configuration = DSLConfiguration()
configuration.init()
return configuration
}
class DSLConfiguration {
private val children = arrayListOf<PreferenceModel<*>>()
fun customPref(customPreference: PreferenceModel<*>) {
children.add(customPreference)
}
fun radioListPref(
title: DSLSettingsText,
@DrawableRes iconId: Int = UNSET,
isEnabled: Boolean = true,
listItems: Array<String>,
selected: Int,
onSelected: (Int) -> Unit
) {
val preference = RadioListPreference(title, iconId, isEnabled, listItems, selected, onSelected)
children.add(preference)
}
fun multiSelectPref(
title: DSLSettingsText,
isEnabled: Boolean = true,
listItems: Array<String>,
selected: BooleanArray,
onSelected: (BooleanArray) -> Unit
) {
val preference = MultiSelectListPreference(title, isEnabled, listItems, selected, onSelected)
children.add(preference)
}
fun switchPref(
title: DSLSettingsText,
summary: DSLSettingsText? = null,
@DrawableRes iconId: Int = UNSET,
isEnabled: Boolean = true,
isChecked: Boolean,
onClick: () -> Unit
) {
val preference = SwitchPreference(title, summary, iconId, isEnabled, isChecked, onClick)
children.add(preference)
}
fun clickPref(
title: DSLSettingsText,
summary: DSLSettingsText? = null,
@DrawableRes iconId: Int = UNSET,
isEnabled: Boolean = true,
onClick: () -> Unit
) {
val preference = ClickPreference(title, summary, iconId, isEnabled, onClick)
children.add(preference)
}
fun externalLinkPref(
title: DSLSettingsText,
@DrawableRes iconId: Int = UNSET,
@StringRes linkId: Int
) {
val preference = ExternalLinkPreference(title, iconId, linkId)
children.add(preference)
}
fun dividerPref() {
val preference = DividerPreference()
children.add(preference)
}
fun sectionHeaderPref(title: DSLSettingsText) {
val preference = SectionHeaderPreference(title)
children.add(preference)
}
fun sectionHeaderPref(title: Int) {
val preference = SectionHeaderPreference(DSLSettingsText.from(title))
children.add(preference)
}
fun textPref(
title: DSLSettingsText? = null,
summary: DSLSettingsText? = null
) {
val preference = TextPreference(title, summary)
children.add(preference)
}
fun toMappingModelList(): MappingModelList = MappingModelList().apply { addAll(children) }
}
abstract class PreferenceModel<T : PreferenceModel<T>>(
open val title: DSLSettingsText? = null,
open val summary: DSLSettingsText? = null,
@DrawableRes open val iconId: Int = UNSET,
open val isEnabled: Boolean = true
) : MappingModel<T> {
override fun areItemsTheSame(newItem: T): Boolean {
return when {
title != null -> title == newItem.title
summary != null -> summary == newItem.summary
else -> throw AssertionError("Could not determine equality.")
}
}
@CallSuper
override fun areContentsTheSame(newItem: T): Boolean {
return areItemsTheSame(newItem) &&
newItem.summary == summary &&
newItem.iconId == iconId &&
newItem.isEnabled == isEnabled
}
}
class TextPreference(
title: DSLSettingsText?,
summary: DSLSettingsText?
) : PreferenceModel<TextPreference>(title = title, summary = summary)
class DividerPreference : PreferenceModel<DividerPreference>() {
override fun areItemsTheSame(newItem: DividerPreference) = false
}
class RadioListPreference(
override val title: DSLSettingsText,
@DrawableRes override val iconId: Int = UNSET,
override val isEnabled: Boolean,
val listItems: Array<String>,
val selected: Int,
val onSelected: (Int) -> Unit
) : PreferenceModel<RadioListPreference>(title = title, iconId = iconId, isEnabled = isEnabled) {
override fun areContentsTheSame(newItem: RadioListPreference): Boolean {
return super.areContentsTheSame(newItem) && listItems.contentEquals(newItem.listItems) && selected == newItem.selected
}
}
class MultiSelectListPreference(
override val title: DSLSettingsText,
override val isEnabled: Boolean,
val listItems: Array<String>,
val selected: BooleanArray,
val onSelected: (BooleanArray) -> Unit
) : PreferenceModel<MultiSelectListPreference>(title = title, isEnabled = isEnabled) {
override fun areContentsTheSame(newItem: MultiSelectListPreference): Boolean {
return super.areContentsTheSame(newItem) &&
listItems.contentEquals(newItem.listItems) &&
selected.contentEquals(newItem.selected)
}
}
class SwitchPreference(
override val title: DSLSettingsText,
override val summary: DSLSettingsText? = null,
@DrawableRes override val iconId: Int = UNSET,
isEnabled: Boolean,
val isChecked: Boolean,
val onClick: () -> Unit
) : PreferenceModel<SwitchPreference>(title = title, summary = summary, iconId = iconId, isEnabled = isEnabled) {
override fun areContentsTheSame(newItem: SwitchPreference): Boolean {
return super.areContentsTheSame(newItem) && isChecked == newItem.isChecked
}
}
class ClickPreference(
override val title: DSLSettingsText,
override val summary: DSLSettingsText?,
@DrawableRes override val iconId: Int,
isEnabled: Boolean,
val onClick: () -> Unit
) : PreferenceModel<ClickPreference>(title = title, summary = summary, iconId = iconId, isEnabled = isEnabled)
class ExternalLinkPreference(
override val title: DSLSettingsText,
@DrawableRes override val iconId: Int,
@StringRes val linkId: Int
) : PreferenceModel<ExternalLinkPreference>(title = title, iconId = iconId)
class SectionHeaderPreference(override val title: DSLSettingsText) : PreferenceModel<SectionHeaderPreference>(title = title)

View File

@@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.DateUtils;
@@ -67,7 +68,7 @@ class VoiceNoteMediaDescriptionCompatFactory {
extras.putString(EXTRA_COLOR, threadRecipient.getColor().serialize());
extras.putLong(EXTRA_MESSAGE_ID, messageRecord.getId());
NotificationPrivacyPreference preference = TextSecurePreferences.getNotificationPrivacy(context);
NotificationPrivacyPreference preference = SignalStore.settings().getMessageNotificationsPrivacy();
String title;
if (preference.isDisplayContact() && threadRecipient.isGroup()) {

View File

@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -130,7 +131,7 @@ class VoiceNoteNotificationManager {
@Override
public @Nullable Bitmap getCurrentLargeIcon(Player player, PlayerNotificationManager.BitmapCallback callback) {
if (!hasMetadata() || !TextSecurePreferences.getNotificationPrivacy(context).isDisplayContact()) {
if (!hasMetadata() || !SignalStore.settings().getMessageNotificationsPrivacy().isDisplayContact()) {
cachedBitmap = null;
cachedRecipientId = null;
return null;