From a30f00104effa8c9e07c4e6048cbdaa315688c55 Mon Sep 17 00:00:00 2001 From: AL-Session <160798022+AL-Session@users.noreply.github.com> Date: Tue, 2 Jul 2024 16:42:49 +1000 Subject: [PATCH] [SS-54] Add dialog to allow local deletion if network deletion fails (#1526) * WIP * Push before attempting some HTTPRequestFailedException rate limiting * Functionality now works * Merging dev resulted in some subproject commit change so pushing that * Fixes #1525 * Addressed Andy PR feedback * Addressed further PR feedback from Andy --------- Co-authored-by: alansley --- .../securesms/ApplicationContext.java | 11 +- .../preferences/ClearAllDataDialog.kt | 118 +++++++++++------- app/src/main/res/values-fa-rIR/strings.xml | 2 - app/src/main/res/values-fr-rFR/strings.xml | 2 - app/src/main/res/values-fr/strings.xml | 2 - app/src/main/res/values/strings.xml | 2 - .../libsession/snode/OnionRequestAPI.kt | 1 - libsession/src/main/res/values/strings.xml | 6 + .../libsignal/utilities/ThreadUtils.kt | 1 - 9 files changed, 89 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 03b56d6b61..8d11a10925 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -501,7 +501,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO }); } - public void clearAllData(boolean isMigratingToV2KeyPair) { + // Method to clear the local data - returns true on success otherwise false + + /** + * Clear all local profile data and message history then restart the app after a brief delay. + * @param isMigratingToV2KeyPair whether we're upgrading to a more recent V2 key pair or not. + * @return true on success, false otherwise. + */ + public boolean clearAllData(boolean isMigratingToV2KeyPair) { if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) { firebaseInstanceIdJob.cancel(null); } @@ -515,9 +522,11 @@ public class ApplicationContext extends Application implements DefaultLifecycleO getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit(); if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) { Log.d("Loki", "Failed to delete database."); + return false; } configFactory.keyPairChanged(); Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200)); + return true; } public void restartApplication() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt index 31f2782f8f..d03ae98030 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt @@ -4,12 +4,13 @@ import android.app.Dialog import android.os.Bundle import android.view.LayoutInflater import android.view.View -import androidx.core.view.isGone +import android.widget.Toast import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -24,17 +25,26 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities class ClearAllDataDialog : DialogFragment() { + private val TAG = "ClearAllDataDialog" + private lateinit var binding: DialogClearAllDataBinding - enum class Steps { + private enum class Steps { INFO_PROMPT, NETWORK_PROMPT, - DELETING + DELETING, + RETRY_LOCAL_DELETE_ONLY_PROMPT } - var clearJob: Job? = null + // Rather than passing a bool around we'll use an enum to clarify our intent + private enum class DeletionScope { + DeleteLocalDataOnly, + DeleteBothLocalAndNetworkData + } - var step = Steps.INFO_PROMPT + private var clearJob: Job? = null + + private var step = Steps.INFO_PROMPT set(value) { field = value updateUI() @@ -46,8 +56,8 @@ class ClearAllDataDialog : DialogFragment() { private fun createView(): View { binding = DialogClearAllDataBinding.inflate(LayoutInflater.from(requireContext())) - val device = radioOption("deviceOnly", R.string.dialog_clear_all_data_clear_device_only) - val network = radioOption("deviceAndNetwork", R.string.dialog_clear_all_data_clear_device_and_network) + val device = radioOption("deviceOnly", R.string.clearDeviceOnly) + val network = radioOption("deviceAndNetwork", R.string.clearDeviceAndNetwork) var selectedOption: RadioOption = device val optionAdapter = RadioOptionAdapter { selectedOption = it } binding.recyclerView.apply { @@ -57,18 +67,21 @@ class ClearAllDataDialog : DialogFragment() { setHasFixedSize(true) } optionAdapter.submitList(listOf(device, network)) + binding.cancelButton.setOnClickListener { dismiss() } + binding.clearAllDataButton.setOnClickListener { - when(step) { + when (step) { Steps.INFO_PROMPT -> if (selectedOption == network) { step = Steps.NETWORK_PROMPT } else { - clearAllData(false) + clearAllData(DeletionScope.DeleteLocalDataOnly) } - Steps.NETWORK_PROMPT -> clearAllData(true) + Steps.NETWORK_PROMPT -> clearAllData(DeletionScope.DeleteBothLocalAndNetworkData) Steps.DELETING -> { /* do nothing intentionally */ } + Steps.RETRY_LOCAL_DELETE_ONLY_PROMPT -> clearAllData(DeletionScope.DeleteLocalDataOnly) } } return binding.root @@ -86,8 +99,13 @@ class ClearAllDataDialog : DialogFragment() { binding.dialogDescriptionText.setText(R.string.dialog_clear_all_data_clear_device_and_network_confirmation) } Steps.DELETING -> { /* do nothing intentionally */ } + Steps.RETRY_LOCAL_DELETE_ONLY_PROMPT -> { + binding.dialogDescriptionText.setText(R.string.clearDataErrorDescriptionGeneric) + binding.clearAllDataButton.text = getString(R.string.clearDevice) + } } - binding.recyclerView.isGone = step == Steps.NETWORK_PROMPT + + binding.recyclerView.isVisible = step == Steps.INFO_PROMPT binding.cancelButton.isVisible = !isLoading binding.clearAllDataButton.isVisible = !isLoading binding.progressBar.isVisible = isLoading @@ -97,45 +115,55 @@ class ClearAllDataDialog : DialogFragment() { } } - private fun clearAllData(deleteNetworkMessages: Boolean) { - clearJob = lifecycleScope.launch(Dispatchers.IO) { - val previousStep = step - withContext(Dispatchers.Main) { - step = Steps.DELETING + private suspend fun performDeleteLocalDataOnlyStep() { + try { + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext()).get() + } catch (e: Exception) { + Log.e(TAG, "Failed to force sync when deleting data", e) + withContext(Main) { + Toast.makeText(ApplicationContext.getInstance(requireContext()), R.string.errorUnknown, Toast.LENGTH_LONG).show() } - - if (!deleteNetworkMessages) { - try { - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext()).get() - } catch (e: Exception) { - Log.e("Loki", "Failed to force sync", e) - } - ApplicationContext.getInstance(context).clearAllData(false) - withContext(Dispatchers.Main) { + return + } + ApplicationContext.getInstance(context).clearAllData(false).let { success -> + withContext(Main) { + if (success) { dismiss() + } else { + Toast.makeText(ApplicationContext.getInstance(requireContext()), R.string.errorUnknown, Toast.LENGTH_LONG).show() } - } else { - // finish - val result = try { - val openGroups = DatabaseComponent.get(requireContext()).lokiThreadDatabase().getAllOpenGroups() - openGroups.map { it.value.server }.toSet().forEach { server -> - OpenGroupApi.deleteAllInboxMessages(server).get() - } - SnodeAPI.deleteAllMessages().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 + private fun clearAllData(deletionScope: DeletionScope) { + step = Steps.DELETING + + clearJob = lifecycleScope.launch(Dispatchers.IO) { + when (deletionScope) { + DeletionScope.DeleteLocalDataOnly -> { + performDeleteLocalDataOnlyStep() + } + DeletionScope.DeleteBothLocalAndNetworkData -> { + val deletionResultMap: Map? = try { + val openGroups = DatabaseComponent.get(requireContext()).lokiThreadDatabase().getAllOpenGroups() + openGroups.map { it.value.server }.toSet().forEach { server -> + OpenGroupApi.deleteAllInboxMessages(server).get() + } + SnodeAPI.deleteAllMessages().get() + } catch (e: Exception) { + Log.e(TAG, "Failed to delete network messages - offering user option to delete local data only.", e) + 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() + + // If one or more deletions failed then inform the user and allow them to clear the device only if they wish.. + if (deletionResultMap == null || deletionResultMap.values.any { !it } || deletionResultMap.isEmpty()) { + withContext(Main) { step = Steps.RETRY_LOCAL_DELETE_ONLY_PROMPT } + } + else if (deletionResultMap.values.all { it }) { + // ..otherwise if the network data deletion was successful proceed to delete the local data as well. + ApplicationContext.getInstance(context).clearAllData(false) + withContext(Main) { dismiss() } } } } diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index a4467b5f6e..4b16ccd655 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -657,8 +657,6 @@ این گزینه به طور دائم پیام‌ها، جلسات و مخاطبین شما را حذف می‌کند. آیا فقط می‌خواهید این دستگاه را پاک کنید یا می‌خواهید کل اکانت را پاک کنید؟ این کار پیام‌ها و مخاطبین شما را برای همیشه حذف می‌کند. آیا می‌خواهید فقط این دستگاه را پاک کنید یا داتا خود را از شبکه نیز حذف کنید? - فقط پاک کردن دستگاه - پاک کردن دستگاه و شبکه آیا مطمئن هستید که می خواهید داتا های خود را از شبکه حذف کنید؟ اگر ادامه دهید، نمی‌توانید پیام‌ها یا مخاطبین خود را بازیابی کنید. پاک فقط حذف شود diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 3aeeba0550..4d76fa8e5e 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -660,8 +660,6 @@ Cela supprimera définitivement vos messages, vos sessions et vos contacts. Souhaitez-vous effacer seulement cet appareil ou supprimer l\'ensemble de votre compte ? Cela supprimera définitivement vos messages, sessions et contacts. Voulez-vous uniquement effacer cet appareil ou supprimer l\'intégralité de votre compte ? - Effacer l\'appareil uniquement - Effacer l\'appareil et le réseau Êtes-vous sûr de vouloir supprimer vos données du réseau ? Si vous continuez, vous ne pourrez pas restaurer vos messages ou vos contacts. Effacer Effacer seulement diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3aeeba0550..4d76fa8e5e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -660,8 +660,6 @@ Cela supprimera définitivement vos messages, vos sessions et vos contacts. Souhaitez-vous effacer seulement cet appareil ou supprimer l\'ensemble de votre compte ? Cela supprimera définitivement vos messages, sessions et contacts. Voulez-vous uniquement effacer cet appareil ou supprimer l\'intégralité de votre compte ? - Effacer l\'appareil uniquement - Effacer l\'appareil et le réseau Êtes-vous sûr de vouloir supprimer vos données du réseau ? Si vous continuez, vous ne pourrez pas restaurer vos messages ou vos contacts. Effacer Effacer seulement diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 640b9f005d..9852cb5fa9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -842,8 +842,6 @@ This will permanently delete your messages, sessions, and contacts. Would you like to clear only this device, or delete your entire account? This will permanently delete your messages, sessions, and contacts. Would you like to clear only this device, or delete your entire account? - Clear Device Only - Clear Device and Network Are you sure you want to delete your data from the network? If you continue you will not be able to restore your messages or contacts. Clear Delete Only diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt index 04b0f722c1..cf43c7b14a 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -25,7 +25,6 @@ import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.recover import org.session.libsignal.utilities.toHexString -import java.util.Date import java.util.concurrent.atomic.AtomicReference import kotlin.collections.set diff --git a/libsession/src/main/res/values/strings.xml b/libsession/src/main/res/values/strings.xml index 7e4ffed396..f0d20aedf0 100644 --- a/libsession/src/main/res/values/strings.xml +++ b/libsession/src/main/res/values/strings.xml @@ -73,4 +73,10 @@ %1$s has left the group. Unnamed group + + An unknown error occurred and your data was not deleted. Do you want to delete your data from just this device instead? + An unknown error occurred. + Clear Device + Clear device only + Clear device and network diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/ThreadUtils.kt b/libsignal/src/main/java/org/session/libsignal/utilities/ThreadUtils.kt index 6485babe80..e3560e3f4f 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/ThreadUtils.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/ThreadUtils.kt @@ -4,7 +4,6 @@ import android.os.Process import kotlinx.coroutines.Dispatchers import java.util.concurrent.ExecutorService import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.SynchronousQueue import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit import kotlin.coroutines.EmptyCoroutineContext