diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt index bea5a85f46..53620719a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt @@ -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(itemView) { + override fun bind(model: LongClickPreference) { + super.bind(model) + itemView.setOnLongClickListener() { + model.onLongClick() + true + } + } +} + class RadioListPreferenceViewHolder(itemView: View) : PreferenceViewHolder(itemView) { override fun bind(model: RadioListPreference) { super.bind(model) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt index 9d0248678e..eae04245e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt @@ -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) } ) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt index 1ba0d3be80..3c97435aab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt index 85cf224dae..2b35df8a39 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt @@ -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( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt new file mode 100644 index 0000000000..f2fb01acaf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt @@ -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 create(modelClass: Class): T { + return Objects.requireNonNull(modelClass.cast(InternalViewModel(recipientId))) + } + } + + data class InternalState( + val recipient: Recipient + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/InternalPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/InternalPreference.kt index 9396595969..355c26eaf5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/InternalPreference.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/InternalPreference.kt @@ -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() { - 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(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() } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt index 04cd62df32..475e8ff381 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt @@ -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() +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() + class ExternalLinkPreference( override val title: DSLSettingsText, override val icon: DSLSettingsIcon?, diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java index 69ad958d12..dde99495b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java @@ -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); } /** diff --git a/app/src/main/res/layout/conversation_settings_internal_preference.xml b/app/src/main/res/layout/conversation_settings_internal_preference.xml index 7677384acc..083316fe2e 100644 --- a/app/src/main/res/layout/conversation_settings_internal_preference.xml +++ b/app/src/main/res/layout/conversation_settings_internal_preference.xml @@ -1,35 +1,17 @@ - - - + android:layout_marginTop="4dp" + android:text="@string/preferences__internal_details" /> - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/navigation/conversation_settings.xml b/app/src/main/res/navigation/conversation_settings.xml index 6e91c3f93f..0bbb2c3bd5 100644 --- a/app/src/main/res/navigation/conversation_settings.xml +++ b/app/src/main/res/navigation/conversation_settings.xml @@ -75,6 +75,20 @@ app:nullable="true" /> + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e2264a39f..acccc5e711 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2517,6 +2517,7 @@ Current version: %1$d at density %2$s Delete all dynamic shortcuts Click to delete all dynamic shortcuts + Internal Details Disable Profile Sharing Delete Session Sender Key @@ -3687,6 +3688,7 @@ Search Disappearing messages Sounds & notifications + Internal details Contact details View safety number Block