Implements notifications
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@@ -32,6 +35,8 @@
|
||||
android:name="android.app.lib_name"
|
||||
android:value="" />
|
||||
</activity>
|
||||
|
||||
<service android:name=".UpdateMonitorService" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -56,11 +56,6 @@ class ConversationListViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start watching for updates
|
||||
viewModelScope.launch {
|
||||
chatRepository.beginWatchingForUpdates(this)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun refresh() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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<MessageDeliveredEvent>
|
||||
get() = _messageDeliveredChannel
|
||||
get() = _messageDeliveredChannel.asSharedFlow()
|
||||
|
||||
// Changes Flow
|
||||
val conversationChanges: Flow<List<Conversation>>
|
||||
get() = database.conversationChanges
|
||||
.onEach { Log.d(REPO_LOG, "Got database conversations changed") }
|
||||
|
||||
// New Messages
|
||||
val newMessages: SharedFlow<Message>
|
||||
get() = _newMessageChannel.asSharedFlow()
|
||||
|
||||
// Errors channel
|
||||
val errorEncounteredChannel: SharedFlow<Error>
|
||||
get() = _errorEncounteredChannel
|
||||
get() = _errorEncounteredChannel.asSharedFlow()
|
||||
|
||||
fun messagesChanged(conversation: Conversation): Flow<List<Message>> =
|
||||
database.messagesChanged(conversation)
|
||||
@@ -91,6 +96,7 @@ class ChatRepository(
|
||||
private var outgoingMessageThread: Thread? = null
|
||||
private val _messageDeliveredChannel = MutableSharedFlow<MessageDeliveredEvent>()
|
||||
private val _errorEncounteredChannel = MutableSharedFlow<Error>()
|
||||
private val _newMessageChannel = MutableSharedFlow<Message>()
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user