Implements notifications
This commit is contained in:
@@ -2,6 +2,9 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@@ -32,6 +35,8 @@
|
|||||||
android:name="android.app.lib_name"
|
android:name="android.app.lib_name"
|
||||||
android:value="" />
|
android:value="" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service android:name=".UpdateMonitorService" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,16 +1,31 @@
|
|||||||
package net.buzzert.kordophonedroid
|
package net.buzzert.kordophonedroid
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import net.buzzert.kordophonedroid.ui.KordophoneApp
|
import net.buzzert.kordophonedroid.ui.KordophoneApp
|
||||||
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
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 {
|
setContent {
|
||||||
KordophoneApp()
|
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() {
|
suspend fun refresh() {
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
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.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
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.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil.compose.SubcomposeAsyncImage
|
import coil.compose.SubcomposeAsyncImage
|
||||||
@@ -84,8 +87,14 @@ fun MessageListScreen(
|
|||||||
viewModel.conversationGUID = conversationGUID
|
viewModel.conversationGUID = conversationGUID
|
||||||
|
|
||||||
// Synchronize on launch
|
// Synchronize on launch
|
||||||
|
val context = LocalContext.current
|
||||||
LaunchedEffect(Unit) {
|
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.markAsRead()
|
||||||
viewModel.synchronize()
|
viewModel.synchronize()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@@ -53,16 +54,20 @@ class ChatRepository(
|
|||||||
|
|
||||||
// Channel that's signaled when an outgoing message is delivered.
|
// Channel that's signaled when an outgoing message is delivered.
|
||||||
val messageDeliveredChannel: SharedFlow<MessageDeliveredEvent>
|
val messageDeliveredChannel: SharedFlow<MessageDeliveredEvent>
|
||||||
get() = _messageDeliveredChannel
|
get() = _messageDeliveredChannel.asSharedFlow()
|
||||||
|
|
||||||
// Changes Flow
|
// Changes Flow
|
||||||
val conversationChanges: Flow<List<Conversation>>
|
val conversationChanges: Flow<List<Conversation>>
|
||||||
get() = database.conversationChanges
|
get() = database.conversationChanges
|
||||||
.onEach { Log.d(REPO_LOG, "Got database conversations changed") }
|
.onEach { Log.d(REPO_LOG, "Got database conversations changed") }
|
||||||
|
|
||||||
|
// New Messages
|
||||||
|
val newMessages: SharedFlow<Message>
|
||||||
|
get() = _newMessageChannel.asSharedFlow()
|
||||||
|
|
||||||
// Errors channel
|
// Errors channel
|
||||||
val errorEncounteredChannel: SharedFlow<Error>
|
val errorEncounteredChannel: SharedFlow<Error>
|
||||||
get() = _errorEncounteredChannel
|
get() = _errorEncounteredChannel.asSharedFlow()
|
||||||
|
|
||||||
fun messagesChanged(conversation: Conversation): Flow<List<Message>> =
|
fun messagesChanged(conversation: Conversation): Flow<List<Message>> =
|
||||||
database.messagesChanged(conversation)
|
database.messagesChanged(conversation)
|
||||||
@@ -91,6 +96,7 @@ class ChatRepository(
|
|||||||
private var outgoingMessageThread: Thread? = null
|
private var outgoingMessageThread: Thread? = null
|
||||||
private val _messageDeliveredChannel = MutableSharedFlow<MessageDeliveredEvent>()
|
private val _messageDeliveredChannel = MutableSharedFlow<MessageDeliveredEvent>()
|
||||||
private val _errorEncounteredChannel = MutableSharedFlow<Error>()
|
private val _errorEncounteredChannel = MutableSharedFlow<Error>()
|
||||||
|
private val _newMessageChannel = MutableSharedFlow<Message>()
|
||||||
|
|
||||||
private var updateMonitor = UpdateMonitor(apiClient)
|
private var updateMonitor = UpdateMonitor(apiClient)
|
||||||
private var updateWatchJob: Job? = null
|
private var updateWatchJob: Job? = null
|
||||||
@@ -218,17 +224,18 @@ class ChatRepository(
|
|||||||
.onEach { it.conversation = conversation }
|
.onEach { it.conversation = conversation }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleConversationChangedUpdate(conversation: Conversation) {
|
private suspend fun handleConversationChangedUpdate(conversation: Conversation) {
|
||||||
Log.d(REPO_LOG, "Handling conversation changed update")
|
Log.d(REPO_LOG, "Handling conversation changed update")
|
||||||
database.writeConversations(listOf(conversation))
|
database.writeConversations(listOf(conversation))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMessageAddedUpdate(message: Message) {
|
private suspend fun handleMessageAddedUpdate(message: Message) {
|
||||||
Log.d(REPO_LOG, "Handling messages added update")
|
Log.d(REPO_LOG, "Handling messages added update")
|
||||||
database.writeMessages(listOf(message), message.conversation)
|
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")
|
Log.d(REPO_LOG, "Handling successful delivery event")
|
||||||
|
|
||||||
// Unfortunate protocol reality: the server doesn't tell us about new messages that are from us,
|
// Unfortunate protocol reality: the server doesn't tell us about new messages that are from us,
|
||||||
|
|||||||
Reference in New Issue
Block a user