mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-17 12:18:25 +00:00
feat: clear all data dialog with local and network only options
This commit is contained in:
parent
05b0e5f308
commit
1df6fa46a4
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user