diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 03b56d6b612..8d11a109256 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 @@ private void loadEmojiSearchIndexIfNeeded() { }); } - 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 void clearAllData(boolean isMigratingToV2KeyPair) { 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 31f2782f8fd..d03ae980301 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 + } + + // Rather than passing a bool around we'll use an enum to clarify our intent + private enum class DeletionScope { + DeleteLocalDataOnly, + DeleteBothLocalAndNetworkData } - var clearJob: Job? = null + private var clearJob: Job? = null - var step = Steps.INFO_PROMPT + 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 + } + } + } + + 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 + } - if (result == null || result.values.any { !it } || result.isEmpty()) { - // didn't succeed (at least one) - withContext(Dispatchers.Main) { - step = previousStep + // 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 (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 (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 a4467b5f6e6..4b16ccd6552 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 3aeeba05508..4d76fa8e5e6 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 3aeeba05508..4d76fa8e5e6 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 640b9f005db..9852cb5fa91 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 04b0f722c10..cf43c7b14ae 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 7e4ffed3964..f0d20aedf08 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 6485babe808..e3560e3f4f6 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