mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
[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:
parent
0da949c8e6
commit
a30f00104e
@ -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() {
|
||||||
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user