Add an internal recipient details screen.

This commit is contained in:
Greyson Parrelli
2021-09-11 16:58:35 -04:00
parent e2e0caa94a
commit 903c5c6db6
11 changed files with 261 additions and 102 deletions

View File

@@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.util.visible
class DSLSettingsAdapter : MappingAdapter() {
init {
registerFactory(ClickPreference::class.java, LayoutFactory(::ClickPreferenceViewHolder, R.layout.dsl_preference_item))
registerFactory(LongClickPreference::class.java, LayoutFactory(::LongClickPreferenceViewHolder, 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))
@@ -82,6 +83,16 @@ class ClickPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ClickPref
}
}
class LongClickPreferenceViewHolder(itemView: View) : PreferenceViewHolder<LongClickPreference>(itemView) {
override fun bind(model: LongClickPreference) {
super.bind(model)
itemView.setOnLongClickListener() {
model.onLongClick()
true
}
}
}
class RadioListPreferenceViewHolder(itemView: View) : PreferenceViewHolder<RadioListPreference>(itemView) {
override fun bind(model: RadioListPreference) {
super.bind(model)

View File

@@ -302,11 +302,9 @@ class ConversationSettingsFragment : DSLSettingsFragment(
customPref(
InternalPreference.Model(
recipient = state.recipient,
onDisableProfileSharingClick = {
viewModel.disableProfileSharing()
},
onDeleteSessionClick = {
viewModel.deleteSession()
onInternalDetailsClicked = {
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToInternalDetailsSettingsFragment(state.recipient.id)
navController.navigate(action)
}
)
)

View File

@@ -26,7 +26,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.libsignal.util.guava.Preconditions
import java.io.IOException
import java.util.concurrent.TimeUnit
@@ -204,29 +203,6 @@ class ConversationSettingsRepository(
}
}
fun disableProfileSharingForInternalUser(recipientId: RecipientId) {
Preconditions.checkArgument(FeatureFlags.internalUser(), "Internal users only!")
SignalExecutors.BOUNDED.execute {
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipientId, false)
}
}
fun deleteSessionForInternalUser(recipientId: RecipientId) {
Preconditions.checkArgument(FeatureFlags.internalUser(), "Internal users only!")
SignalExecutors.BOUNDED.execute {
val recipient = Recipient.resolved(recipientId)
if (recipient.hasUuid()) {
DatabaseFactory.getSessionDatabase(context).deleteAllFor(recipient.requireUuid().toString())
}
if (recipient.hasE164()) {
DatabaseFactory.getSessionDatabase(context).deleteAllFor(recipient.requireE164())
}
}
}
@WorkerThread
fun isMessageRequestAccepted(recipient: Recipient): Boolean {
return RecipientUtil.isMessageRequestAccepted(context, recipient)

View File

@@ -114,10 +114,6 @@ sealed class ConversationSettingsViewModel(
}
}
open fun disableProfileSharing(): Unit = error("This ViewModel does not support this interaction")
open fun deleteSession(): Unit = error("This ViewModel does not support this interaction")
open fun initiateGroupUpgrade(): Unit = error("This ViewModel does not support this interaction")
private class RecipientSettingsViewModel(
@@ -237,14 +233,6 @@ sealed class ConversationSettingsViewModel(
override fun unblock() {
repository.unblock(recipientId)
}
override fun disableProfileSharing() {
repository.disableProfileSharingForInternalUser(recipientId)
}
override fun deleteSession() {
repository.deleteSessionForInternalUser(recipientId)
}
}
private class GroupSettingsViewModel(

View File

@@ -0,0 +1,192 @@
package org.thoughtcrime.securesms.components.settings.conversation
import android.graphics.Color
import android.text.TextUtils
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
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.database.DatabaseFactory
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.Hex
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.livedata.Store
import java.util.Objects
import java.util.UUID
/**
* Shows internal details about a recipient that you can view from the conversation settings.
*/
class InternalConversationSettingsFragment : DSLSettingsFragment(
titleId = R.string.ConversationSettingsFragment__internal_details
) {
private val viewModel: InternalViewModel by viewModels(
factoryProducer = {
val recipientId = InternalConversationSettingsFragmentArgs.fromBundle(requireArguments()).recipientId
MyViewModelFactory(recipientId)
}
)
override fun bindAdapter(adapter: DSLSettingsAdapter) {
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
}
private fun getConfiguration(state: InternalState): DSLConfiguration {
val recipient = state.recipient
return configure {
sectionHeaderPref(DSLSettingsText.from("Data"))
textPref(
title = DSLSettingsText.from("RecipientId"),
summary = DSLSettingsText.from(recipient.id.serialize())
)
val uuid = recipient.uuid.transform(UUID::toString).or("null")
longClickPref(
title = DSLSettingsText.from("UUID"),
summary = DSLSettingsText.from(uuid),
onLongClick = { copyToClipboard(uuid) }
)
textPref(
title = DSLSettingsText.from("Profile Name"),
summary = DSLSettingsText.from("[${recipient.profileName.givenName}] [${state.recipient.profileName.familyName}]")
)
val profileKeyBase64 = recipient.profileKey?.let(Base64::encodeBytes) ?: "None"
longClickPref(
title = DSLSettingsText.from("Profile Key (Base64)"),
summary = DSLSettingsText.from(profileKeyBase64),
onLongClick = { copyToClipboard(profileKeyBase64) }
)
val profileKeyHex = recipient.profileKey?.let(Hex::toStringCondensed) ?: ""
longClickPref(
title = DSLSettingsText.from("Profile Key (Hex)"),
summary = DSLSettingsText.from(profileKeyHex),
onLongClick = { copyToClipboard(profileKeyHex) }
)
textPref(
title = DSLSettingsText.from("Sealed Sender Mode"),
summary = DSLSettingsText.from(recipient.unidentifiedAccessMode.toString())
)
textPref(
title = DSLSettingsText.from("Profile Sharing (AKA \"Whitelisted\")"),
summary = DSLSettingsText.from(recipient.isProfileSharing.toString())
)
textPref(
title = DSLSettingsText.from("Capabilities"),
summary = DSLSettingsText.from(buildCapabilitySpan(recipient))
)
sectionHeaderPref(DSLSettingsText.from("Actions"))
clickPref(
title = DSLSettingsText.from("Disable Profile Sharing"),
summary = DSLSettingsText.from("Clears profile sharing/whitelisted status, which should cause the Message Request UI to show."),
onClick = {
MaterialAlertDialogBuilder(requireContext())
.setTitle("Are you sure?")
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { _, _ -> DatabaseFactory.getRecipientDatabase(requireContext()).setProfileSharing(recipient.id, false) }
.show()
}
)
clickPref(
title = DSLSettingsText.from("Delete Session"),
summary = DSLSettingsText.from("Deletes the session, essentially guaranteeing an encryption error if they send you a message."),
onClick = {
MaterialAlertDialogBuilder(requireContext())
.setTitle("Are you sure?")
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { _, _ ->
if (recipient.hasUuid()) {
DatabaseFactory.getSessionDatabase(context).deleteAllFor(recipient.requireUuid().toString())
}
if (recipient.hasE164()) {
DatabaseFactory.getSessionDatabase(context).deleteAllFor(recipient.requireE164())
}
}
.show()
}
)
}
}
private fun copyToClipboard(text: String) {
Util.copyToClipboard(requireContext(), text)
Toast.makeText(requireContext(), "Copied to clipboard", Toast.LENGTH_SHORT).show()
}
private fun buildCapabilitySpan(recipient: Recipient): CharSequence {
return TextUtils.concat(
colorize("GV2", recipient.groupsV2Capability),
", ",
colorize("GV1Migration", recipient.groupsV1MigrationCapability),
", ",
colorize("AnnouncementGroup", recipient.announcementGroupCapability),
", ",
colorize("SenderKey", recipient.senderKeyCapability),
", ",
colorize("ChangeNumber", recipient.changeNumberCapability),
)
}
private fun colorize(name: String, support: Recipient.Capability): CharSequence {
return when (support) {
Recipient.Capability.SUPPORTED -> SpanUtil.color(Color.rgb(0, 150, 0), name)
Recipient.Capability.NOT_SUPPORTED -> SpanUtil.color(Color.RED, name)
Recipient.Capability.UNKNOWN -> SpanUtil.italic(name)
}
}
class InternalViewModel(
val recipientId: RecipientId
) : ViewModel(), RecipientForeverObserver {
private val store = Store(InternalState(Recipient.resolved(recipientId)))
val state = store.stateLiveData
val liveRecipient = Recipient.live(recipientId)
init {
liveRecipient.observeForever(this)
}
override fun onRecipientChanged(recipient: Recipient) {
store.update { InternalState(recipient) }
}
override fun onCleared() {
liveRecipient.removeForeverObserver(this)
}
}
class MyViewModelFactory(val recipientId: RecipientId) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return Objects.requireNonNull(modelClass.cast(InternalViewModel(recipientId)))
}
}
data class InternalState(
val recipient: Recipient
)
}

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.components.settings.conversation.preferences
import android.view.View
import android.widget.TextView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.recipients.Recipient
@@ -19,37 +18,9 @@ object InternalPreference {
class Model(
private val recipient: Recipient,
val onDisableProfileSharingClick: () -> Unit,
val onDeleteSessionClick: () -> Unit
val onInternalDetailsClicked: () -> Unit,
) : PreferenceModel<Model>() {
val body: String get() {
return String.format(
"""
-- Profile Name --
[${recipient.profileName.givenName}] [${recipient.profileName.familyName}]
-- Profile Sharing --
${recipient.isProfileSharing}
-- Profile Key (Base64) --
${recipient.profileKey?.let(Base64::encodeBytes) ?: "None"}
-- Profile Key (Hex) --
${recipient.profileKey?.let(Hex::toStringCondensed) ?: "None"}
-- Sealed Sender Mode --
${recipient.unidentifiedAccessMode}
-- UUID --
${recipient.uuid.transform { obj: UUID -> obj.toString() }.or("None")}
-- RecipientId --
${recipient.id.serialize()}
""".trimIndent(),
)
}
override fun areItemsTheSame(newItem: Model): Boolean {
return recipient == newItem.recipient
}
@@ -57,14 +28,10 @@ object InternalPreference {
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
private val body: TextView = itemView.findViewById(R.id.internal_preference_body)
private val disableProfileSharing: View = itemView.findViewById(R.id.internal_disable_profile_sharing)
private val deleteSession: View = itemView.findViewById(R.id.internal_delete_session)
private val internalDetails: View = itemView.findViewById(R.id.internal_details)
override fun bind(model: Model) {
body.text = model.body
disableProfileSharing.setOnClickListener { model.onDisableProfileSharingClick() }
deleteSession.setOnClickListener { model.onDeleteSessionClick() }
internalDetails.setOnClickListener { model.onInternalDetailsClicked() }
}
}
}

View File

@@ -86,6 +86,17 @@ class DSLConfiguration {
children.add(preference)
}
fun longClickPref(
title: DSLSettingsText,
summary: DSLSettingsText? = null,
icon: DSLSettingsIcon? = null,
isEnabled: Boolean = true,
onLongClick: () -> Unit
) {
val preference = LongClickPreference(title, summary, icon, isEnabled, onLongClick)
children.add(preference)
}
fun externalLinkPref(
title: DSLSettingsText,
icon: DSLSettingsIcon? = null,
@@ -216,6 +227,14 @@ class ClickPreference(
val onClick: () -> Unit
) : PreferenceModel<ClickPreference>()
class LongClickPreference(
override val title: DSLSettingsText,
override val summary: DSLSettingsText? = null,
override val icon: DSLSettingsIcon? = null,
override val isEnabled: Boolean = true,
val onLongClick: () -> Unit
) : PreferenceModel<LongClickPreference>()
class ExternalLinkPreference(
override val title: DSLSettingsText,
override val icon: DSLSettingsIcon?,

View File

@@ -74,10 +74,10 @@ public final class InternalValues extends SignalStoreValues {
}
/**
* Show detailed recipient info in the {@link org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientFragment}.
* Show detailed recipient info in the {@link org.thoughtcrime.securesms.components.settings.conversation.InternalConversationSettingsFragment}.
*/
public synchronized boolean recipientDetails() {
return FeatureFlags.internalUser() && getBoolean(RECIPIENT_DETAILS, false);
return FeatureFlags.internalUser() && getBoolean(RECIPIENT_DETAILS, true);
}
/**

View File

@@ -1,35 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/internal_preference_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/signal_text_secondary"
android:textIsSelectable="true"
tools:text="@tools:sample/lorem" />
<com.google.android.material.button.MaterialButton
android:id="@+id/internal_disable_profile_sharing"
android:id="@+id/internal_details"
style="@style/Signal.Widget.Button.Small.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/preferences__internal_disable_profile_sharing" />
android:layout_marginTop="4dp"
android:text="@string/preferences__internal_details" />
<com.google.android.material.button.MaterialButton
android:id="@+id/internal_delete_session"
style="@style/Signal.Widget.Button.Small.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/preferences__internal_delete_session" />
</LinearLayout>
</FrameLayout>

View File

@@ -75,6 +75,20 @@
app:nullable="true" />
</action>
<action
android:id="@+id/action_conversationSettingsFragment_to_internalDetailsSettingsFragment"
app:destination="@id/internalDetailsSettingsFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit">
<argument
android:name="recipient_id"
app:argType="org.thoughtcrime.securesms.recipients.RecipientId" />
</action>
</fragment>
<fragment
@@ -95,6 +109,16 @@
</fragment>
<fragment
android:id="@+id/internalDetailsSettingsFragment"
android:name="org.thoughtcrime.securesms.components.settings.conversation.InternalConversationSettingsFragment">
<argument
android:name="recipient_id"
app:argType="org.thoughtcrime.securesms.recipients.RecipientId" />
</fragment>
<fragment
android:id="@+id/permissionsSettingsFragment"
android:name="org.thoughtcrime.securesms.components.settings.conversation.permissions.PermissionsSettingsFragment">

View File

@@ -2517,6 +2517,7 @@
<string name="preferences__internal_current_version_d_at_density_s" translatable="false">Current version: %1$d at density %2$s</string>
<string name="preferences__internal_delete_all_dynamic_shortcuts" translatable="false">Delete all dynamic shortcuts</string>
<string name="preferences__internal_click_to_delete_all_dynamic_shortcuts" translatable="false">Click to delete all dynamic shortcuts</string>
<string name="preferences__internal_details" translatable="false">Internal Details</string>
<string name="preferences__internal_disable_profile_sharing" translatable="false">Disable Profile Sharing</string>
<string name="preferences__internal_delete_session" translatable="false">Delete Session</string>
<string name="preferences__internal_sender_key" translatable="false">Sender Key</string>
@@ -3687,6 +3688,7 @@
<string name="ConversationSettingsFragment__search">Search</string>
<string name="ConversationSettingsFragment__disappearing_messages">Disappearing messages</string>
<string name="ConversationSettingsFragment__sounds_and_notifications">Sounds &amp; notifications</string>
<string name="ConversationSettingsFragment__internal_details" translatable="false">Internal details</string>
<string name="ConversationSettingsFragment__contact_details">Contact details</string>
<string name="ConversationSettingsFragment__view_safety_number">View safety number</string>
<string name="ConversationSettingsFragment__block">Block</string>