diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c571bfd..92213c5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/net/buzzert/kordophonedroid/MainActivity.kt b/app/src/main/java/net/buzzert/kordophonedroid/MainActivity.kt
index f92b989..5473810 100644
--- a/app/src/main/java/net/buzzert/kordophonedroid/MainActivity.kt
+++ b/app/src/main/java/net/buzzert/kordophonedroid/MainActivity.kt
@@ -1,16 +1,31 @@
package net.buzzert.kordophonedroid
+import android.Manifest
+import android.content.Intent
+import android.content.pm.PackageManager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.core.app.ActivityCompat
import dagger.hilt.android.AndroidEntryPoint
import net.buzzert.kordophonedroid.ui.KordophoneApp
+
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ // Ask for notifications
+ val hasPermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
+ if (hasPermission != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1234)
+ }
+
+ // Start update monitor service
+ val intent = Intent(this, UpdateMonitorService::class.java)
+ startService(intent)
+
setContent {
KordophoneApp()
}
diff --git a/app/src/main/java/net/buzzert/kordophonedroid/UpdateMonitorService.kt b/app/src/main/java/net/buzzert/kordophonedroid/UpdateMonitorService.kt
new file mode 100644
index 0000000..4d790db
--- /dev/null
+++ b/app/src/main/java/net/buzzert/kordophonedroid/UpdateMonitorService.kt
@@ -0,0 +1,138 @@
+package net.buzzert.kordophonedroid
+
+import android.Manifest
+import android.R
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationChannelGroup
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Color
+import android.os.IBinder
+import android.util.Log
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import net.buzzert.kordophone.backend.model.Message
+import net.buzzert.kordophone.backend.server.ChatRepository
+import javax.inject.Inject
+
+const val PUSH_CHANNEL_ID = "net.buzzert.kordophone.persistentNotification"
+const val NEW_MESSAGE_CHANNEL_ID = "net.buzzert.kordophone.newMessage"
+
+const val UPDATER_LOG = "UpdateService"
+
+@AndroidEntryPoint
+class UpdateMonitorService: Service()
+{
+ @Inject lateinit var chatRepository: ChatRepository
+
+ private var newMessageID: Int = 0
+ private val job = SupervisorJob()
+ private val scope = CoroutineScope(Dispatchers.IO + job)
+
+ private fun createNotificationChannel(channelId: String, channelName: String) {
+ val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT)
+ chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
+ chan.lightColor = Color.BLUE
+
+ val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ service.createNotificationChannel(chan)
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ createNotificationChannel(NEW_MESSAGE_CHANNEL_ID, "New Messages")
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ createNotificationChannel(PUSH_CHANNEL_ID, "Update Monitor Service")
+
+ val notificationIntent = Intent(this, MainActivity::class.java)
+ val pendingIntent = PendingIntent.getActivity(
+ this,
+ 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val notification: Notification = NotificationCompat.Builder(this, PUSH_CHANNEL_ID)
+ .setContentTitle("Kordophone Connected")
+ .setContentText("Kordophone is listening for new messages.")
+ .setSmallIcon(R.drawable.sym_action_chat)
+ .setContentIntent(pendingIntent)
+ .setShowWhen(false)
+ .setSilent(true)
+ .setOngoing(true)
+ .build()
+
+ startForeground(5738, notification)
+
+ // Connect to monitor
+ chatRepository.beginWatchingForUpdates(scope)
+
+ // Connect to new message flow for notifications
+ scope.launch {
+ chatRepository.newMessages.collectLatest(::onReceiveNewMessage)
+ }
+
+ // Restart if we get killed
+ return START_STICKY
+ }
+
+ private fun onReceiveNewMessage(message: Message) {
+ val hasPermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
+ if (hasPermission != PackageManager.PERMISSION_GRANTED) {
+ Log.e(UPDATER_LOG, "No permissions to post notifications.")
+ return
+ }
+
+ if (message.conversation.unreadCount == 0) {
+ // Not unread.
+ return
+ }
+
+ if (message.sender == null) {
+ // From me.
+ return
+ }
+
+ if (message.conversation.isGroupChat) {
+ // For now, since these can be noisy and there's no UI for changing it, ignore group chats.
+ return
+ }
+
+ val groupId = message.conversation.guid
+ val notification = NotificationCompat.Builder(this, NEW_MESSAGE_CHANNEL_ID)
+ .setContentTitle(message.sender)
+ .setContentText(message.text)
+ .setSmallIcon(R.drawable.stat_notify_chat)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setGroup(groupId)
+ .build()
+
+ val manager = NotificationManagerCompat.from(this)
+ manager.notify(newMessageID++, notification)
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ // no binding
+ return null
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+ chatRepository.stopWatchingForUpdates()
+ job.cancel()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListViewModel.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListViewModel.kt
index 56b0afe..3c0f3e6 100644
--- a/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListViewModel.kt
+++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListViewModel.kt
@@ -56,11 +56,6 @@ class ConversationListViewModel @Inject constructor(
}
}
}
-
- // Start watching for updates
- viewModelScope.launch {
- chatRepository.beginWatchingForUpdates(this)
- }
}
suspend fun refresh() {
diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt
index 9107eb4..9ab435c 100644
--- a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt
+++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt
@@ -28,6 +28,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCompositionContext
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -40,6 +42,7 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import androidx.core.app.NotificationManagerCompat
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.SubcomposeAsyncImage
@@ -84,8 +87,14 @@ fun MessageListScreen(
viewModel.conversationGUID = conversationGUID
// Synchronize on launch
+ val context = LocalContext.current
LaunchedEffect(Unit) {
- Log.d("MessageListScreen", "Launched effect")
+ // Clear notifications for this conversation
+ with(NotificationManagerCompat.from(context)) {
+ // Not sure how to cancel individual notifications, or groups yet...
+ cancelAll()
+ }
+
viewModel.markAsRead()
viewModel.synchronize()
}
diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt b/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt
index 35b609b..bcc0c7f 100644
--- a/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt
+++ b/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt
@@ -10,6 +10,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.onEach
@@ -53,16 +54,20 @@ class ChatRepository(
// Channel that's signaled when an outgoing message is delivered.
val messageDeliveredChannel: SharedFlow
- get() = _messageDeliveredChannel
+ get() = _messageDeliveredChannel.asSharedFlow()
// Changes Flow
val conversationChanges: Flow>
get() = database.conversationChanges
.onEach { Log.d(REPO_LOG, "Got database conversations changed") }
+ // New Messages
+ val newMessages: SharedFlow
+ get() = _newMessageChannel.asSharedFlow()
+
// Errors channel
val errorEncounteredChannel: SharedFlow
- get() = _errorEncounteredChannel
+ get() = _errorEncounteredChannel.asSharedFlow()
fun messagesChanged(conversation: Conversation): Flow> =
database.messagesChanged(conversation)
@@ -91,6 +96,7 @@ class ChatRepository(
private var outgoingMessageThread: Thread? = null
private val _messageDeliveredChannel = MutableSharedFlow()
private val _errorEncounteredChannel = MutableSharedFlow()
+ private val _newMessageChannel = MutableSharedFlow()
private var updateMonitor = UpdateMonitor(apiClient)
private var updateWatchJob: Job? = null
@@ -218,17 +224,18 @@ class ChatRepository(
.onEach { it.conversation = conversation }
}
- private fun handleConversationChangedUpdate(conversation: Conversation) {
+ private suspend fun handleConversationChangedUpdate(conversation: Conversation) {
Log.d(REPO_LOG, "Handling conversation changed update")
database.writeConversations(listOf(conversation))
}
- private fun handleMessageAddedUpdate(message: Message) {
+ private suspend fun handleMessageAddedUpdate(message: Message) {
Log.d(REPO_LOG, "Handling messages added update")
database.writeMessages(listOf(message), message.conversation)
+ _newMessageChannel.emit(message)
}
- private fun handleMessageDelivered(event: MessageDeliveredEvent) {
+ private suspend fun handleMessageDelivered(event: MessageDeliveredEvent) {
Log.d(REPO_LOG, "Handling successful delivery event")
// Unfortunate protocol reality: the server doesn't tell us about new messages that are from us,