[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 <aclansley@gmail.com>
This commit is contained in:
AL-Session 2024-07-02 16:42:49 +10:00 committed by GitHub
parent 0da949c8e6
commit a30f00104e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 89 additions and 56 deletions

View File

@ -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()) { if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive()) {
firebaseInstanceIdJob.cancel(null); firebaseInstanceIdJob.cancel(null);
} }
@ -515,9 +522,11 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit(); getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) { if (!deleteDatabase(SQLCipherOpenHelper.DATABASE_NAME)) {
Log.d("Loki", "Failed to delete database."); Log.d("Loki", "Failed to delete database.");
return false;
} }
configFactory.keyPairChanged(); configFactory.keyPairChanged();
Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200)); Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200));
return true;
} }
public void restartApplication() { public void restartApplication() {

View File

@ -4,12 +4,13 @@ import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import androidx.core.view.isGone import android.widget.Toast
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -24,17 +25,26 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
class ClearAllDataDialog : DialogFragment() { class ClearAllDataDialog : DialogFragment() {
private val TAG = "ClearAllDataDialog"
private lateinit var binding: DialogClearAllDataBinding private lateinit var binding: DialogClearAllDataBinding
enum class Steps { private enum class Steps {
INFO_PROMPT, INFO_PROMPT,
NETWORK_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) { set(value) {
field = value field = value
updateUI() updateUI()
@ -46,8 +56,8 @@ class ClearAllDataDialog : DialogFragment() {
private fun createView(): View { private fun createView(): View {
binding = DialogClearAllDataBinding.inflate(LayoutInflater.from(requireContext())) binding = DialogClearAllDataBinding.inflate(LayoutInflater.from(requireContext()))
val device = radioOption("deviceOnly", R.string.dialog_clear_all_data_clear_device_only) val device = radioOption("deviceOnly", R.string.clearDeviceOnly)
val network = radioOption("deviceAndNetwork", R.string.dialog_clear_all_data_clear_device_and_network) val network = radioOption("deviceAndNetwork", R.string.clearDeviceAndNetwork)
var selectedOption: RadioOption<String> = device var selectedOption: RadioOption<String> = device
val optionAdapter = RadioOptionAdapter { selectedOption = it } val optionAdapter = RadioOptionAdapter { selectedOption = it }
binding.recyclerView.apply { binding.recyclerView.apply {
@ -57,18 +67,21 @@ class ClearAllDataDialog : DialogFragment() {
setHasFixedSize(true) setHasFixedSize(true)
} }
optionAdapter.submitList(listOf(device, network)) optionAdapter.submitList(listOf(device, network))
binding.cancelButton.setOnClickListener { binding.cancelButton.setOnClickListener {
dismiss() dismiss()
} }
binding.clearAllDataButton.setOnClickListener { binding.clearAllDataButton.setOnClickListener {
when(step) { when (step) {
Steps.INFO_PROMPT -> if (selectedOption == network) { Steps.INFO_PROMPT -> if (selectedOption == network) {
step = Steps.NETWORK_PROMPT step = Steps.NETWORK_PROMPT
} else { } else {
clearAllData(false) clearAllData(DeletionScope.DeleteLocalDataOnly)
} }
Steps.NETWORK_PROMPT -> clearAllData(true) Steps.NETWORK_PROMPT -> clearAllData(DeletionScope.DeleteBothLocalAndNetworkData)
Steps.DELETING -> { /* do nothing intentionally */ } Steps.DELETING -> { /* do nothing intentionally */ }
Steps.RETRY_LOCAL_DELETE_ONLY_PROMPT -> clearAllData(DeletionScope.DeleteLocalDataOnly)
} }
} }
return binding.root return binding.root
@ -86,8 +99,13 @@ class ClearAllDataDialog : DialogFragment() {
binding.dialogDescriptionText.setText(R.string.dialog_clear_all_data_clear_device_and_network_confirmation) binding.dialogDescriptionText.setText(R.string.dialog_clear_all_data_clear_device_and_network_confirmation)
} }
Steps.DELETING -> { /* do nothing intentionally */ } 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.cancelButton.isVisible = !isLoading
binding.clearAllDataButton.isVisible = !isLoading binding.clearAllDataButton.isVisible = !isLoading
binding.progressBar.isVisible = isLoading binding.progressBar.isVisible = isLoading
@ -97,45 +115,55 @@ class ClearAllDataDialog : DialogFragment() {
} }
} }
private fun clearAllData(deleteNetworkMessages: Boolean) { private suspend fun performDeleteLocalDataOnlyStep() {
clearJob = lifecycleScope.launch(Dispatchers.IO) {
val previousStep = step
withContext(Dispatchers.Main) {
step = Steps.DELETING
}
if (!deleteNetworkMessages) {
try { try {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext()).get() ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext()).get()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Loki", "Failed to force sync", e) 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()
} }
ApplicationContext.getInstance(context).clearAllData(false) return
withContext(Dispatchers.Main) { }
ApplicationContext.getInstance(context).clearAllData(false).let { success ->
withContext(Main) {
if (success) {
dismiss() dismiss()
}
} else { } else {
// finish Toast.makeText(ApplicationContext.getInstance(requireContext()), R.string.errorUnknown, Toast.LENGTH_LONG).show()
val result = try { }
}
}
}
private fun clearAllData(deletionScope: DeletionScope) {
step = Steps.DELETING
clearJob = lifecycleScope.launch(Dispatchers.IO) {
when (deletionScope) {
DeletionScope.DeleteLocalDataOnly -> {
performDeleteLocalDataOnlyStep()
}
DeletionScope.DeleteBothLocalAndNetworkData -> {
val deletionResultMap: Map<String, Boolean>? = try {
val openGroups = DatabaseComponent.get(requireContext()).lokiThreadDatabase().getAllOpenGroups() val openGroups = DatabaseComponent.get(requireContext()).lokiThreadDatabase().getAllOpenGroups()
openGroups.map { it.value.server }.toSet().forEach { server -> openGroups.map { it.value.server }.toSet().forEach { server ->
OpenGroupApi.deleteAllInboxMessages(server).get() OpenGroupApi.deleteAllInboxMessages(server).get()
} }
SnodeAPI.deleteAllMessages().get() SnodeAPI.deleteAllMessages().get()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to delete network messages - offering user option to delete local data only.", e)
null null
} }
if (result == null || result.values.any { !it } || result.isEmpty()) { // If one or more deletions failed then inform the user and allow them to clear the device only if they wish..
// didn't succeed (at least one) if (deletionResultMap == null || deletionResultMap.values.any { !it } || deletionResultMap.isEmpty()) {
withContext(Dispatchers.Main) { withContext(Main) { step = Steps.RETRY_LOCAL_DELETE_ONLY_PROMPT }
step = previousStep
} }
} else if (result.values.all { it }) { else if (deletionResultMap.values.all { it }) {
// don't force sync because all the messages are deleted? // ..otherwise if the network data deletion was successful proceed to delete the local data as well.
ApplicationContext.getInstance(context).clearAllData(false) ApplicationContext.getInstance(context).clearAllData(false)
withContext(Dispatchers.Main) { withContext(Main) { dismiss() }
dismiss()
} }
} }
} }

View File

@ -657,8 +657,6 @@
<string name="dialog_clear_all_data_explanation">این گزینه به طور دائم پیام‌ها، جلسات و مخاطبین شما را حذف می‌کند.</string> <string name="dialog_clear_all_data_explanation">این گزینه به طور دائم پیام‌ها، جلسات و مخاطبین شما را حذف می‌کند.</string>
<string name="dialog_clear_all_data_network_explanation">آیا فقط می‌خواهید این دستگاه را پاک کنید یا می‌خواهید کل اکانت را پاک کنید؟</string> <string name="dialog_clear_all_data_network_explanation">آیا فقط می‌خواهید این دستگاه را پاک کنید یا می‌خواهید کل اکانت را پاک کنید؟</string>
<string name="dialog_clear_all_data_message">این کار پیام‌ها و مخاطبین شما را برای همیشه حذف می‌کند. آیا می‌خواهید فقط این دستگاه را پاک کنید یا داتا خود را از شبکه نیز حذف کنید?</string> <string name="dialog_clear_all_data_message">این کار پیام‌ها و مخاطبین شما را برای همیشه حذف می‌کند. آیا می‌خواهید فقط این دستگاه را پاک کنید یا داتا خود را از شبکه نیز حذف کنید?</string>
<string name="dialog_clear_all_data_clear_device_only">فقط پاک کردن دستگاه</string>
<string name="dialog_clear_all_data_clear_device_and_network">پاک کردن دستگاه و شبکه</string>
<string name="dialog_clear_all_data_clear_device_and_network_confirmation">آیا مطمئن هستید که می خواهید داتا های خود را از شبکه حذف کنید؟ اگر ادامه دهید، نمی‌توانید پیام‌ها یا مخاطبین خود را بازیابی کنید.</string> <string name="dialog_clear_all_data_clear_device_and_network_confirmation">آیا مطمئن هستید که می خواهید داتا های خود را از شبکه حذف کنید؟ اگر ادامه دهید، نمی‌توانید پیام‌ها یا مخاطبین خود را بازیابی کنید.</string>
<string name="dialog_clear_all_data_clear">پاک</string> <string name="dialog_clear_all_data_clear">پاک</string>
<string name="dialog_clear_all_data_local_only">فقط حذف شود</string> <string name="dialog_clear_all_data_local_only">فقط حذف شود</string>

View File

@ -660,8 +660,6 @@
<string name="dialog_clear_all_data_explanation">Cela supprimera définitivement vos messages, vos sessions et vos contacts.</string> <string name="dialog_clear_all_data_explanation">Cela supprimera définitivement vos messages, vos sessions et vos contacts.</string>
<string name="dialog_clear_all_data_network_explanation">Souhaitez-vous effacer seulement cet appareil ou supprimer l\'ensemble de votre compte ?</string> <string name="dialog_clear_all_data_network_explanation">Souhaitez-vous effacer seulement cet appareil ou supprimer l\'ensemble de votre compte ?</string>
<string name="dialog_clear_all_data_message">Cela supprimera définitivement vos messages, sessions et contacts. Voulez-vous uniquement effacer cet appareil ou supprimer l\'intégralité de votre compte ?</string> <string name="dialog_clear_all_data_message">Cela supprimera définitivement vos messages, sessions et contacts. Voulez-vous uniquement effacer cet appareil ou supprimer l\'intégralité de votre compte ?</string>
<string name="dialog_clear_all_data_clear_device_only">Effacer l\'appareil uniquement</string>
<string name="dialog_clear_all_data_clear_device_and_network">Effacer l\'appareil et le réseau</string>
<string name="dialog_clear_all_data_clear_device_and_network_confirmation">Ê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.</string> <string name="dialog_clear_all_data_clear_device_and_network_confirmation">Ê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.</string>
<string name="dialog_clear_all_data_clear">Effacer</string> <string name="dialog_clear_all_data_clear">Effacer</string>
<string name="dialog_clear_all_data_local_only">Effacer seulement</string> <string name="dialog_clear_all_data_local_only">Effacer seulement</string>

View File

@ -660,8 +660,6 @@
<string name="dialog_clear_all_data_explanation">Cela supprimera définitivement vos messages, vos sessions et vos contacts.</string> <string name="dialog_clear_all_data_explanation">Cela supprimera définitivement vos messages, vos sessions et vos contacts.</string>
<string name="dialog_clear_all_data_network_explanation">Souhaitez-vous effacer seulement cet appareil ou supprimer l\'ensemble de votre compte ?</string> <string name="dialog_clear_all_data_network_explanation">Souhaitez-vous effacer seulement cet appareil ou supprimer l\'ensemble de votre compte ?</string>
<string name="dialog_clear_all_data_message">Cela supprimera définitivement vos messages, sessions et contacts. Voulez-vous uniquement effacer cet appareil ou supprimer l\'intégralité de votre compte ?</string> <string name="dialog_clear_all_data_message">Cela supprimera définitivement vos messages, sessions et contacts. Voulez-vous uniquement effacer cet appareil ou supprimer l\'intégralité de votre compte ?</string>
<string name="dialog_clear_all_data_clear_device_only">Effacer l\'appareil uniquement</string>
<string name="dialog_clear_all_data_clear_device_and_network">Effacer l\'appareil et le réseau</string>
<string name="dialog_clear_all_data_clear_device_and_network_confirmation">Ê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.</string> <string name="dialog_clear_all_data_clear_device_and_network_confirmation">Ê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.</string>
<string name="dialog_clear_all_data_clear">Effacer</string> <string name="dialog_clear_all_data_clear">Effacer</string>
<string name="dialog_clear_all_data_local_only">Effacer seulement</string> <string name="dialog_clear_all_data_local_only">Effacer seulement</string>

View File

@ -842,8 +842,6 @@
<string name="dialog_clear_all_data_explanation">This will permanently delete your messages, sessions, and contacts.</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">Would you like to clear only this device, or delete your entire account?</string> <string name="dialog_clear_all_data_network_explanation">Would you like to clear only this device, or delete your entire account?</string>
<string name="dialog_clear_all_data_message">This will permanently delete your messages, sessions, and contacts. Would you like to clear only this device, or delete your entire account?</string> <string name="dialog_clear_all_data_message">This will permanently delete your messages, sessions, and contacts. Would you like to clear only this device, or delete your entire account?</string>
<string name="dialog_clear_all_data_clear_device_only">Clear Device Only</string>
<string name="dialog_clear_all_data_clear_device_and_network">Clear Device and Network</string>
<string name="dialog_clear_all_data_clear_device_and_network_confirmation">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.</string> <string name="dialog_clear_all_data_clear_device_and_network_confirmation">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.</string>
<string name="dialog_clear_all_data_clear">Clear</string> <string name="dialog_clear_all_data_clear">Clear</string>
<string name="dialog_clear_all_data_local_only">Delete Only</string> <string name="dialog_clear_all_data_local_only">Delete Only</string>

View File

@ -25,7 +25,6 @@ import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.recover import org.session.libsignal.utilities.recover
import org.session.libsignal.utilities.toHexString import org.session.libsignal.utilities.toHexString
import java.util.Date
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.set import kotlin.collections.set

View File

@ -73,4 +73,10 @@
<string name="ConversationItem_group_action_left">%1$s has left the group.</string> <string name="ConversationItem_group_action_left">%1$s has left the group.</string>
<!-- RecipientProvider --> <!-- RecipientProvider -->
<string name="RecipientProvider_unnamed_group">Unnamed group</string> <string name="RecipientProvider_unnamed_group">Unnamed group</string>
<string name="clearDataErrorDescriptionGeneric">An unknown error occurred and your data was not deleted. Do you want to delete your data from just this device instead?</string>
<string name="errorUnknown">An unknown error occurred.</string>
<string name="clearDevice">Clear Device</string>
<string name="clearDeviceOnly">Clear device only</string>
<string name="clearDeviceAndNetwork">Clear device and network</string>
</resources> </resources>

View File

@ -4,7 +4,6 @@ import android.os.Process
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.SynchronousQueue
import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext