From 11e96558d0e0e11860315cd93dfc2fc88d26ad99 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 23 Dec 2024 15:30:25 -0300 Subject: [PATCH] Try to cancel coroutine jobs, show notification instead of progress in the ui --- .../java/com/greenart7c3/citrine/Citrine.kt | 4 +- .../com/greenart7c3/citrine/MainActivity.kt | 19 +- .../citrine/service/WebSocketServerService.kt | 31 +- .../greenart7c3/citrine/ui/HomeViewModel.kt | 326 +++++++++--------- .../citrine/utils/ExportDatabaseUtils.kt | 9 +- app/src/main/res/values/strings.xml | 2 + 6 files changed, 203 insertions(+), 188 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/citrine/Citrine.kt b/app/src/main/java/com/greenart7c3/citrine/Citrine.kt index 8ff78e2..0355d8a 100644 --- a/app/src/main/java/com/greenart7c3/citrine/Citrine.kt +++ b/app/src/main/java/com/greenart7c3/citrine/Citrine.kt @@ -36,8 +36,8 @@ class Citrine : Application() { fun contentResolverFn(): ContentResolver = contentResolver fun cancelJob() { - job?.cancelChildren(null) - job?.cancel(null) + job?.cancelChildren() + job?.cancel() } suspend fun eventsToDelete(database: AppDatabase) { diff --git a/app/src/main/java/com/greenart7c3/citrine/MainActivity.kt b/app/src/main/java/com/greenart7c3/citrine/MainActivity.kt index 5c6c2c1..d79ddb4 100644 --- a/app/src/main/java/com/greenart7c3/citrine/MainActivity.kt +++ b/app/src/main/java/com/greenart7c3/citrine/MainActivity.kt @@ -307,21 +307,14 @@ class MainActivity : ComponentActivity() { TextButton( onClick = { deleteAllDialog = false + Citrine.getInstance().cancelJob() Citrine.getInstance().applicationScope.launch(Dispatchers.IO) { - homeViewModel.setLoading(true) Citrine.getInstance().job?.join() Citrine.getInstance().isImportingEvents = true - homeViewModel.setProgress("Calculating database size") - val ids = database.eventDao().getAllIds() - val size = ids.size - val batchSize = 500 - ids.chunked(batchSize).forEachIndexed { index, chunk -> - homeViewModel.setProgress("Deleting ${index * batchSize}/$size") - database.eventDao().delete(chunk) - } - + homeViewModel.setProgress("Deleting all events") + database.clearAllTables() + homeViewModel.setProgress("") Citrine.getInstance().isImportingEvents = false - homeViewModel.setLoading(false) } }, ) { @@ -432,10 +425,6 @@ class MainActivity : ComponentActivity() { ) { if (state.value.loading) { CircularProgressIndicator() - if (state.value.progress.isNotBlank()) { - Spacer(modifier = Modifier.padding(4.dp)) - Text(state.value.progress) - } } else { val isStarted = homeViewModel.state.value.service?.isStarted() ?: false val clipboardManager = LocalClipboardManager.current diff --git a/app/src/main/java/com/greenart7c3/citrine/service/WebSocketServerService.kt b/app/src/main/java/com/greenart7c3/citrine/service/WebSocketServerService.kt index 2ffc8e1..f8c42db 100644 --- a/app/src/main/java/com/greenart7c3/citrine/service/WebSocketServerService.kt +++ b/app/src/main/java/com/greenart7c3/citrine/service/WebSocketServerService.kt @@ -2,17 +2,18 @@ package com.greenart7c3.citrine.service import android.annotation.SuppressLint import android.app.Notification -import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.app.TaskStackBuilder -import android.content.Context import android.content.Intent import android.os.Binder import android.os.IBinder import android.util.Log +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationChannelGroupCompat import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import com.greenart7c3.citrine.Citrine @@ -23,6 +24,7 @@ import com.greenart7c3.citrine.server.CustomWebSocketServer import com.greenart7c3.citrine.server.EventSubscription import com.greenart7c3.citrine.server.Settings import com.greenart7c3.citrine.utils.ExportDatabaseUtils +import com.vitorpamplona.ammolite.relays.RelayPool import java.util.Timer import java.util.TimerTask import kotlin.coroutines.cancellation.CancellationException @@ -53,6 +55,11 @@ class WebSocketServerService : Service() { timer?.schedule( object : TimerTask() { override fun run() { + if (Citrine.getInstance().job == null || Citrine.getInstance().job?.isCompleted == true) { + RelayPool.disconnect() + RelayPool.unloadRelays() + } + runBlocking { Citrine.getInstance().eventsToDelete(database) } @@ -134,10 +141,19 @@ class WebSocketServerService : Service() { private fun createNotification(): Notification { Log.d(Citrine.TAG, "Creating notification") + val notificationManager = NotificationManagerCompat.from(this) val channelId = "WebSocketServerServiceChannel" - val channel = NotificationChannel(channelId, "WebSocket Server", NotificationManager.IMPORTANCE_DEFAULT) - channel.setSound(null, null) - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val groupId = "WebSocketServerServiceGroup" + val group = NotificationChannelGroupCompat.Builder(groupId) + .setName("WebSocket Server") + .build() + notificationManager.createNotificationChannelGroup(group) + + val channel = NotificationChannelCompat.Builder(channelId, NotificationManager.IMPORTANCE_DEFAULT) + .setName("WebSocket Server") + .setGroup(groupId) + .setSound(null, null) + .build() notificationManager.createNotificationChannel(channel) val copyIntent = Intent(this, ClipboardReceiver::class.java) @@ -160,11 +176,12 @@ class WebSocketServerService : Service() { } val notificationBuilder = NotificationCompat.Builder(this, "WebSocketServerServiceChannel") - .setContentTitle("Relay running at ws://${Settings.host}:${Settings.port}") + .setContentTitle(getString(R.string.relay_running_at_ws, Settings.host, Settings.port.toString())) .setSmallIcon(R.drawable.ic_notification) .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .addAction(R.drawable.ic_launcher_background, "Copy Address", copyPendingIntent) + .addAction(R.drawable.ic_launcher_background, getString(R.string.copy_address), copyPendingIntent) .setContentIntent(resultPendingIntent) + .setGroup(groupId) return notificationBuilder.build() } diff --git a/app/src/main/java/com/greenart7c3/citrine/ui/HomeViewModel.kt b/app/src/main/java/com/greenart7c3/citrine/ui/HomeViewModel.kt index 9b4e4c4..11f1053 100644 --- a/app/src/main/java/com/greenart7c3/citrine/ui/HomeViewModel.kt +++ b/app/src/main/java/com/greenart7c3/citrine/ui/HomeViewModel.kt @@ -1,15 +1,20 @@ package com.greenart7c3.citrine.ui +import android.Manifest import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection +import android.content.pm.PackageManager import android.os.IBinder import android.util.Log import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.anggrayudi.storage.file.extension import com.anggrayudi.storage.file.openInputStream import com.greenart7c3.citrine.Citrine @@ -35,6 +40,7 @@ import com.vitorpamplona.quartz.signers.NostrSignerExternal import com.vitorpamplona.quartz.utils.TimeUtils import java.util.UUID import kotlin.coroutines.cancellation.CancellationException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -46,7 +52,6 @@ data class HomeState( val bound: Boolean = false, val service: WebSocketServerService? = null, val pubKey: String = "", - val progress: String = "", ) class HomeViewModel : ViewModel() { @@ -63,32 +68,29 @@ class HomeViewModel : ViewModel() { fun loadEventsFromPubKey(database: AppDatabase) { if (state.value.pubKey.isNotBlank()) { - _state.value = _state.value.copy( - progress = "loading contact list", - loading = true, - ) + setProgress("loading contact list") - viewModelScope.launch(Dispatchers.IO) { - getAllEventsFromPubKey( - database = database, - ) - } + getAllEventsFromPubKey( + database = database, + ) } } - private suspend fun getAllEventsFromPubKey( + private fun getAllEventsFromPubKey( database: AppDatabase, ) { - var contactList = database.eventDao().getContactList(signer.pubKey) - if (contactList == null) { - fetchContactList( - onDone = { - Citrine.getInstance().applicationScope.launch { - state.value = state.value.copy( - progress = "loading relays", - ) - contactList = database.eventDao().getContactList(signer.pubKey) - viewModelScope.launch(Dispatchers.IO) { + Citrine.getInstance().cancelJob() + Citrine.getInstance().job = Citrine.getInstance().applicationScope.launch { + RelayPool.disconnect() + RelayPool.unloadRelays() + var contactList = database.eventDao().getContactList(signer.pubKey) + if (contactList == null) { + fetchContactList( + onDone = { + launch { + setProgress("loading relays") + contactList = database.eventDao().getContactList(signer.pubKey) + var generalRelays: Map? = null contactList?.let { generalRelays = (it.toEvent() as ContactListEvent).relays() @@ -129,6 +131,7 @@ class HomeViewModel : ViewModel() { } } fetchEvents( + scope = this, onAuth = { relay, challenge -> RelayAuthEvent.create( relay.url, @@ -147,104 +150,104 @@ class HomeViewModel : ViewModel() { }, ) } - } - }, - ) - } else { - state.value = state.value.copy( - progress = "loading relays", - ) - var generalRelays: Map? = null - contactList?.let { - generalRelays = (it.toEvent() as ContactListEvent).relays() - } + }, + ) + } else { + setProgress("loading relays") + var generalRelays: Map? = null + contactList?.let { + generalRelays = (it.toEvent() as ContactListEvent).relays() + } - generalRelays?.forEach { - if (RelayPool.getRelay(it.key) == null) { - RelayPool.addRelay( - Relay( - url = it.key, - read = true, - write = false, - forceProxy = false, - activeTypes = COMMON_FEED_TYPES, - ), - ) + generalRelays?.forEach { + if (RelayPool.getRelay(it.key) == null) { + RelayPool.addRelay( + Relay( + url = it.key, + read = true, + write = false, + forceProxy = false, + activeTypes = COMMON_FEED_TYPES, + ), + ) + } } - } - var advertisedRelayList = database.eventDao().getAdvertisedRelayList(signer.pubKey) - if (advertisedRelayList == null) { - fetchOutbox( - onDone = { - viewModelScope.launch(Dispatchers.IO) { - advertisedRelayList = database.eventDao().getAdvertisedRelayList(signer.pubKey) - advertisedRelayList?.let { - val relays = (it.toEvent() as AdvertisedRelayListEvent).relays() - relays.forEach { relay -> - if (RelayPool.getRelay(relay.relayUrl) == null) { - RelayPool.addRelay( - Relay( - url = relay.relayUrl, - read = true, - write = false, - forceProxy = false, - activeTypes = COMMON_FEED_TYPES, - ), - ) + var advertisedRelayList = database.eventDao().getAdvertisedRelayList(signer.pubKey) + if (advertisedRelayList == null) { + fetchOutbox( + onDone = { + launch { + advertisedRelayList = database.eventDao().getAdvertisedRelayList(signer.pubKey) + advertisedRelayList?.let { + val relays = (it.toEvent() as AdvertisedRelayListEvent).relays() + relays.forEach { relay -> + if (RelayPool.getRelay(relay.relayUrl) == null) { + RelayPool.addRelay( + Relay( + url = relay.relayUrl, + read = true, + write = false, + forceProxy = false, + activeTypes = COMMON_FEED_TYPES, + ), + ) + } } } + fetchEvents( + scope = this, + onAuth = { relay, challenge -> + RelayAuthEvent.create( + relay.url, + challenge, + signer, + onReady = { + relay.send(it) + }, + ) + }, + ) + state.value = state.value.copy( + pubKey = "", + ) + } + }, + ) + } else { + advertisedRelayList?.let { + val relays = (it.toEvent() as AdvertisedRelayListEvent).relays() + relays.forEach { relay -> + if (RelayPool.getRelay(relay.relayUrl) == null) { + RelayPool.addRelay( + Relay( + url = relay.relayUrl, + read = true, + write = false, + forceProxy = false, + activeTypes = COMMON_FEED_TYPES, + ), + ) } - fetchEvents( - onAuth = { relay, challenge -> - RelayAuthEvent.create( - relay.url, - challenge, - signer, - onReady = { - relay.send(it) - }, - ) - }, - ) - state.value = state.value.copy( - pubKey = "", - ) - } - }, - ) - } else { - advertisedRelayList?.let { - val relays = (it.toEvent() as AdvertisedRelayListEvent).relays() - relays.forEach { relay -> - if (RelayPool.getRelay(relay.relayUrl) == null) { - RelayPool.addRelay( - Relay( - url = relay.relayUrl, - read = true, - write = false, - forceProxy = false, - activeTypes = COMMON_FEED_TYPES, - ), - ) } } - } - fetchEvents( - onAuth = { relay, challenge -> - RelayAuthEvent.create( - relay.url, - challenge, - signer, - onReady = { - relay.send(it) - }, - ) - }, - ) - state.value = state.value.copy( - pubKey = "", - ) + fetchEvents( + scope = this, + onAuth = { relay, challenge -> + RelayAuthEvent.create( + relay.url, + challenge, + signer, + onReady = { + relay.send(it) + }, + ) + }, + ) + state.value = state.value.copy( + pubKey = "", + ) + } } } } @@ -436,6 +439,7 @@ class HomeViewModel : ViewModel() { } private suspend fun fetchEventsFrom( + scope: CoroutineScope, listeners: MutableMap, relayParam: Relay, until: Long, @@ -458,7 +462,7 @@ class HomeViewModel : ViewModel() { eventCount[relay.url] = eventCount[relay.url]?.plus(1) ?: 1 lastEventCreatedAt = event.createdAt - runBlocking { + scope.launch { CustomWebSocketService.server?.innerProcessEvent(event, null) } }, @@ -468,12 +472,12 @@ class HomeViewModel : ViewModel() { listeners[relay.url]?.let { relayParam.unregister(it) } - Citrine.getInstance().applicationScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { val localEventCount = eventCount[relay.url] ?: 0 if (localEventCount < limit) { onDone() } else { - fetchEventsFrom(listeners, relay, lastEventCreatedAt, onEvent, onAuth, onDone) + fetchEventsFrom(scope, listeners, relay, lastEventCreatedAt, onEvent, onAuth, onDone) } } }, @@ -524,6 +528,7 @@ class HomeViewModel : ViewModel() { private suspend fun fetchEvents( onAuth: (relay: Relay, challenge: String) -> Unit, + scope: CoroutineScope, ) { val finishedLoading = mutableMapOf() @@ -533,21 +538,21 @@ class HomeViewModel : ViewModel() { RelayPool.getAll().forEach { var count = 0 - state.value = state.value.copy( - progress = "loading events from ${it.url}", - ) + setProgress("loading events from ${it.url}") val listeners = mutableMapOf() fetchEventsFrom( + scope = scope, listeners = listeners, relayParam = it, until = TimeUtils.now(), onAuth = { relay, challenge -> onAuth(relay, challenge) - Citrine.getInstance().applicationScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { delay(5000) fetchEventsFrom( + scope = scope, listeners = listeners, relayParam = it, until = TimeUtils.now(), @@ -556,9 +561,7 @@ class HomeViewModel : ViewModel() { }, onEvent = { count++ - state.value = state.value.copy( - progress = "loading events from ${it.url} ($count)", - ) + setProgress("loading events from ${it.url} ($count)") }, onDone = { finishedLoading[it.url] = true @@ -568,9 +571,7 @@ class HomeViewModel : ViewModel() { }, onEvent = { count++ - state.value = state.value.copy( - progress = "loading events from ${it.url} ($count)", - ) + setProgress("loading events from ${it.url} ($count)") }, onDone = { finishedLoading[it.url] = true @@ -593,10 +594,7 @@ class HomeViewModel : ViewModel() { RelayPool.unloadRelays() delay(5000) - state.value = state.value.copy( - loading = false, - progress = "", - ) + setProgress("") } fun setPubKey(returnedKey: String) { @@ -610,24 +608,18 @@ class HomeViewModel : ViewModel() { database: AppDatabase, context: Context, ) { - Citrine.getInstance().applicationScope.launch(Dispatchers.IO) { - setLoading(true) - + Citrine.getInstance().cancelJob() + Citrine.getInstance().job = Citrine.getInstance().applicationScope.launch(Dispatchers.IO) { try { ExportDatabaseUtils.exportDatabase( database, context, folder, ) { - _state.value = _state.value.copy( - progress = it, - ) + setProgress(it) } } finally { - _state.value = _state.value.copy( - progress = "", - loading = false, - ) + setProgress("") } } } @@ -639,7 +631,8 @@ class HomeViewModel : ViewModel() { context: Context, onFinished: () -> Unit, ) { - Citrine.getInstance().applicationScope.launch(Dispatchers.IO) { + Citrine.getInstance().cancelJob() + Citrine.getInstance().job = Citrine.getInstance().applicationScope.launch(Dispatchers.IO) { val file = files.first() if (file.extension != "jsonl") { Citrine.getInstance().applicationScope.launch(Dispatchers.Main) { @@ -654,18 +647,14 @@ class HomeViewModel : ViewModel() { try { Citrine.getInstance().isImportingEvents = true - state.value = state.value.copy( - loading = true, - progress = context.getString(R.string.reading_file, file.name), - ) + setProgress(context.getString(R.string.reading_file, file.name)) + var linesRead = 0 val input2 = file.openInputStream(context) ?: return@launch input2.use { ip -> ip.bufferedReader().use { if (shouldDelete) { - _state.value = _state.value.copy( - progress = context.getString(R.string.deleting_all_events), - ) + setProgress(context.getString(R.string.deleting_all_events)) database.eventDao().deleteAll() } @@ -678,9 +667,7 @@ class HomeViewModel : ViewModel() { CustomWebSocketService.server?.innerProcessEvent(event, null) linesRead++ - _state.value = _state.value.copy( - progress = context.getString(R.string.imported2, linesRead.toString()), - ) + setProgress(context.getString(R.string.imported2, linesRead.toString())) } } RelayPool.disconnect() @@ -689,10 +676,7 @@ class HomeViewModel : ViewModel() { } } - _state.value = _state.value.copy( - progress = "", - loading = false, - ) + setProgress(context.getString(R.string.imported_events_successfully, linesRead)) Citrine.getInstance().isImportingEvents = false onFinished() Citrine.getInstance().applicationScope.launch(Dispatchers.Main) { @@ -713,10 +697,8 @@ class HomeViewModel : ViewModel() { Toast.LENGTH_SHORT, ).show() } - _state.value = _state.value.copy( - progress = "", - loading = false, - ) + setProgress("") + setProgress(context.getString(R.string.import_failed)) onFinished() } } @@ -734,8 +716,32 @@ class HomeViewModel : ViewModel() { } fun setProgress(message: String) { - _state.value = _state.value.copy( - progress = message, + val notificationManager = NotificationManagerCompat.from(Citrine.getInstance()) + + if (message.isBlank()) { + notificationManager.cancel(2) + return + } + + val channel = NotificationChannelCompat.Builder( + "citrine", + NotificationManagerCompat.IMPORTANCE_DEFAULT, ) + .setName("Citrine") + .build() + + notificationManager.createNotificationChannel(channel) + + val notification = NotificationCompat.Builder(Citrine.getInstance(), "citrine") + .setContentTitle("Citrine") + .setContentText(message) + .setSmallIcon(R.drawable.ic_notification) + .setOnlyAlertOnce(true) + .build() + + if (ActivityCompat.checkSelfPermission(Citrine.getInstance(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + return + } + notificationManager.notify(2, notification) } } diff --git a/app/src/main/java/com/greenart7c3/citrine/utils/ExportDatabaseUtils.kt b/app/src/main/java/com/greenart7c3/citrine/utils/ExportDatabaseUtils.kt index 124672e..cd8285a 100644 --- a/app/src/main/java/com/greenart7c3/citrine/utils/ExportDatabaseUtils.kt +++ b/app/src/main/java/com/greenart7c3/citrine/utils/ExportDatabaseUtils.kt @@ -27,10 +27,11 @@ object ExportDatabaseUtils { op?.writer().use { writer -> val events = database.eventDao().getAllIds() events.forEachIndexed { index, it -> - val event = database.eventDao().getById(it)!! - val json = event.toEvent().toJson() + "\n" - writer?.write(json) - onProgress("Exported ${index + 1}/${events.size}") + database.eventDao().getById(it)?.let { event -> + val json = event.toEvent().toJson() + "\n" + writer?.write(json) + onProgress("Exported ${index + 1}/${events.size}") + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4489b9..c8ba8a6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,5 +44,7 @@ Are you sure you want to delete all events from the database? Feed Show events + Copy Address + Relay running at ws://%1$s:%2$s