feat: clear all data dialog with local and network only options

This commit is contained in:
jubb 2021-06-22 17:01:27 +10:00
parent 05b0e5f308
commit 1df6fa46a4
6 changed files with 107 additions and 104 deletions

View File

@ -31,10 +31,8 @@ import org.session.libsession.utilities.ProfilePictureUtilities
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.ProfileKeyUtil
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.dialogs.ChangeUiModeDialog
import org.thoughtcrime.securesms.loki.dialogs.ClearAllDataDialog
import org.thoughtcrime.securesms.loki.dialogs.SeedDialog
@ -92,7 +90,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
helpTranslateButton.setOnClickListener { helpTranslate() }
seedButton.setOnClickListener { showSeed() }
clearAllDataButton.setOnClickListener { clearAllData() }
clearAllDataAndNetworkButton.setOnClickListener { clearAllDataIncludingNetwork() }
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
@ -303,11 +300,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
}
private fun clearAllData() {
ClearAllDataDialog(deleteNetworkMessages = false).show(supportFragmentManager, "Clear All Data Dialog")
}
private fun clearAllDataIncludingNetwork() {
ClearAllDataDialog(deleteNetworkMessages = true).show(supportFragmentManager, "Clear All Data Dialog")
ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog")
}
// endregion

View File

@ -23,18 +23,44 @@ import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
import java.util.concurrent.Executors
class ClearAllDataDialog(val deleteNetworkMessages: Boolean) : DialogFragment() {
class ClearAllDataDialog : DialogFragment() {
enum class Steps {
INFO_PROMPT,
NETWORK_PROMPT,
DELETING
}
var clearJob: Job? = null
set(value) {
field = value
}
var step = Steps.INFO_PROMPT
set(value) {
field = value
updateUI()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(requireContext())
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_clear_all_data, null)
contentView.cancelButton.setOnClickListener { dismiss() }
contentView.clearAllDataButton.setOnClickListener { clearAllData() }
contentView.cancelButton.setOnClickListener {
if (step == Steps.NETWORK_PROMPT) {
clearAllData(false)
} else {
dismiss()
}
}
contentView.clearAllDataButton.setOnClickListener {
when(step) {
Steps.INFO_PROMPT -> step = Steps.NETWORK_PROMPT
Steps.NETWORK_PROMPT -> {
clearAllData(true)
}
Steps.DELETING -> { /* do nothing intentionally */ }
}
}
builder.setView(contentView)
builder.setCancelable(false)
val result = builder.create()
@ -42,71 +68,70 @@ class ClearAllDataDialog(val deleteNetworkMessages: Boolean) : DialogFragment()
return result
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
}
private fun updateUI() {
override fun onStart() {
super.onStart()
isCancelable = false
dialog?.setCanceledOnTouchOutside(false)
}
private fun updateUI(isLoading: Boolean) {
dialog?.let { view ->
val isLoading = step == Steps.DELETING
when (step) {
Steps.INFO_PROMPT -> {
view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_explanation)
view.cancelButton.setText(R.string.cancel)
}
else -> {
view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_network_explanation)
view.cancelButton.setText(R.string.dialog_clear_all_data_local_only)
}
}
view.cancelButton.isVisible = !isLoading
view.clearAllDataButton.isVisible = !isLoading
view.progressBar.isVisible = isLoading
view.setCanceledOnTouchOutside(!isLoading)
isCancelable = !isLoading
}
}
private fun clearAllData() {
private fun clearAllData(deleteNetworkMessages: Boolean) {
if (KeyPairUtilities.hasV2KeyPair(requireContext())) {
clearJob = lifecycleScope.launch(Dispatchers.IO) {
val previousStep = step
withContext(Dispatchers.Main) {
updateUI(true)
step = Steps.DELETING
}
if (!deleteNetworkMessages) {
try {
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(requireContext()).get()
ApplicationContext.getInstance(context).clearAllData(false)
withContext(Dispatchers.Main) {
dismiss()
}
} catch (e: Exception) {
Log.e("Loki", "Failed to force sync", e)
withContext(Dispatchers.Main) {
updateUI(false)
}
}
ApplicationContext.getInstance(context).clearAllData(false)
withContext(Dispatchers.Main) {
dismiss()
}
} else {
// finish
val promises = try {
val result = try {
SnodeAPI.deleteAllMessages(requireContext()).get()
} catch (e: Exception) {
null
}
val rawResponses = promises?.map {
try {
it.get()
} catch (e: Exception) {
null
if (result == null || result.values.any { !it } || result.isEmpty()) {
// didn't succeed (at least one)
withContext(Dispatchers.Main) {
step = previousStep
}
} ?: listOf(null)
// TODO: process the responses here
if (rawResponses.all { it != null }) {
} else if (result.values.all { it }) {
// don't force sync because all the messages are deleted?
ApplicationContext.getInstance(context).clearAllData(false)
withContext(Dispatchers.Main) {
dismiss()
}
} else if (rawResponses.any { it == null || it["failed"] as? Boolean == true }) {
// didn't succeed (at least one)
withContext(Dispatchers.Main) {
updateUI(false)
}
}
}
}

View File

@ -185,16 +185,6 @@
android:textStyle="bold"
android:gravity="center"
android:text="@string/activity_settings_clear_all_data_button_title" />
<TextView
android:id="@+id/clearAllDataAndNetworkButton"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:background="@drawable/setting_button_background"
android:textColor="@color/destructive"
android:textSize="@dimen/medium_font_size"
android:textStyle="bold"
android:gravity="center"
android:text="@string/activity_settings_clear_all_data_and_network_button_title" />
<View
android:layout_width="match_parent"

View File

@ -21,7 +21,7 @@
android:textSize="@dimen/medium_font_size" />
<TextView
android:id="@+id/seedTextView"
android:id="@+id/dialogDescriptionText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/large_spacing"
@ -37,7 +37,6 @@
android:orientation="horizontal">
<Button
tools:visibility="gone"
style="@style/Widget.Session.Button.Dialog.Unimportant"
android:id="@+id/cancelButton"
android:layout_width="0dp"
@ -46,7 +45,6 @@
android:text="@string/cancel" />
<Button
tools:visibility="gone"
style="@style/Widget.Session.Button.Dialog.Destructive"
android:id="@+id/clearAllDataButton"
android:layout_width="0dp"

View File

@ -778,6 +778,8 @@
<string name="dialog_clear_all_data_title">Clear All Data</string>
<string name="dialog_clear_all_data_explanation">This will permanently delete your messages, sessions, and contacts.</string>
<string name="dialog_clear_all_data_network_explanation">Do you also want to clear all your data from the network?</string>
<string name="dialog_clear_all_data_local_only">Delete Local Only</string>
<string name="activity_qr_code_title">QR Code</string>
<string name="activity_qr_code_view_my_qr_code_tab_title">View My QR Code</string>

View File

@ -107,9 +107,9 @@ object SnodeAPI {
val parameters = mapOf(
"method" to "get_n_service_nodes",
"params" to mapOf(
"active_only" to true,
"limit" to 256,
"fields" to mapOf( "public_ip" to true, "storage_port" to true, "pubkey_x25519" to true, "pubkey_ed25519" to true )
"active_only" to true,
"limit" to 256,
"fields" to mapOf("public_ip" to true, "storage_port" to true, "pubkey_x25519" to true, "pubkey_ed25519" to true)
)
)
val deferred = deferred<Snode, Exception>()
@ -185,8 +185,8 @@ object SnodeAPI {
val base64EncodedNameHash = Base64.encodeBytes(nameHash)
// Ask 3 different snodes for the Session ID associated with the given name hash
val parameters = mapOf(
"endpoint" to "ons_resolve",
"params" to mapOf( "type" to 0, "name_hash" to base64EncodedNameHash )
"endpoint" to "ons_resolve",
"params" to mapOf( "type" to 0, "name_hash" to base64EncodedNameHash )
)
val promises = (1..validationCount).map {
getRandomSnode().bind { snode ->
@ -293,7 +293,7 @@ object SnodeAPI {
fun getNetworkTime(snode: Snode): Promise<Pair<Snode,Long>, Exception> {
return invoke(Snode.Method.Info, snode, null, emptyMap()).map { rawResponse ->
val timestamp = rawResponse["timestamp"] as Long
val timestamp = rawResponse["timestamp"] as? Long ?: -1
snode to timestamp
}
}
@ -312,39 +312,6 @@ object SnodeAPI {
}
}
fun deleteAllMessages(context: Context): Promise<Set<RawResponsePromise>, Exception> {
return retryIfNeeded(maxRetryCount) {
// considerations: timestamp off in retrying logic, not being able to re-sign with latest timestamp? do we just not retry this as it will be synchronous
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
val destination = if (useTestnet) userPublicKey.removing05PrefixIfNeeded() else userPublicKey
getSwarm(destination).map { swarm ->
val promise = swarm.first().let { snode ->
retryIfNeeded(maxRetryCount) {
getNetworkTime(snode).bind { (_, timestamp) ->
val signature = ByteArray(Sign.BYTES)
val data = (Snode.Method.DeleteAll.rawValue + timestamp.toString()).toByteArray()
sodium.cryptoSignDetached(signature, data, data.size.toLong(), userED25519KeyPair.secretKey.asBytes)
val deleteMessageParams = mapOf(
"pubkey" to userPublicKey,
"pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString,
"timestamp" to timestamp,
"signature" to Base64.encodeBytes(signature)
)
invoke(Snode.Method.DeleteAll, snode, destination, deleteMessageParams).map { rawResponse -> parseDeletions(timestamp, rawResponse) }.fail { e ->
Log.e("Loki", "Failed to clear data",e)
}
}
}
}
setOf(promise)
}
}
}
// Parsing
private fun parseSnodes(rawResponse: Any): List<Snode> {
val json = rawResponse as? Map<*, *>
@ -370,6 +337,34 @@ object SnodeAPI {
}
}
fun deleteAllMessages(context: Context): Promise<Map<String,Boolean>, Exception> {
return retryIfNeeded(maxRetryCount) {
// considerations: timestamp off in retrying logic, not being able to re-sign with latest timestamp? do we just not retry this as it will be synchronous
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
getSingleTargetSnode(userPublicKey).bind { snode ->
retryIfNeeded(maxRetryCount) {
getNetworkTime(snode).bind { (_, timestamp) ->
val signature = ByteArray(Sign.BYTES)
val data = (Snode.Method.DeleteAll.rawValue + timestamp.toString()).toByteArray()
sodium.cryptoSignDetached(signature, data, data.size.toLong(), userED25519KeyPair.secretKey.asBytes)
val deleteMessageParams = mapOf(
"pubkey" to userPublicKey,
"pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString,
"timestamp" to timestamp,
"signature" to Base64.encodeBytes(signature)
)
invoke(Snode.Method.DeleteAll, snode, userPublicKey, deleteMessageParams).map { rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse) }.fail { e ->
Log.e("Loki", "Failed to clear data", e)
}
}
}
}
}
}
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String): List<SignalServiceProtos.Envelope> {
val messages = rawResponse["messages"] as? List<*>
return if (messages != null) {
@ -429,10 +424,11 @@ object SnodeAPI {
}
@Suppress("UNCHECKED_CAST")
private fun parseDeletions(timestamp: Long, rawResponse: RawResponse): Map<String, Any> {
val swarms = rawResponse["swarms"] as? Map<String,Any> ?: return mapOf()
private fun parseDeletions(userPublicKey: String, timestamp: Long, rawResponse: RawResponse): Map<String, Boolean> {
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return mapOf()
val swarmResponsesValid = swarms.mapNotNull { (nodePubKeyHex, rawMap) ->
val map = rawMap as? Map<String,Any> ?: return@mapNotNull null
val map = rawMap as? Map<String, Any> ?: return@mapNotNull null
/** Deletes all messages owned by the given pubkey on this SN and broadcasts the delete request to
* all other swarm members.
* Returns dict of:
@ -448,17 +444,16 @@ object SnodeAPI {
val reason = map["reason"] as? String
nodePubKeyHex to if (failed) {
// return error probs
Log.e("Loki", "Failed to delete all from $nodePubKeyHex with error code $code and reason $reason")
false
} else {
// success
val deleted = map["deleted"] as List<String> // list of deleted hashes
Log.d("Loki", "node $nodePubKeyHex deleted ${deleted.size} messages")
val signature = map["signature"] as String
val nodePubKeyBytes = Hex.fromStringCondensed(nodePubKeyHex)
val nodePubKey = Key.fromHexString(nodePubKeyHex)
// signature of ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )
val message = (signature + timestamp + deleted.fold("") { a, v -> a+v }).toByteArray()
sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, nodePubKeyBytes)
val message = (userPublicKey + timestamp.toString() + deleted.fold("") { a, v -> a + v }).toByteArray()
sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, nodePubKey.asBytes)
}
}
return swarmResponsesValid.toMap()