Skip to content

Commit

Permalink
Implement new notification features
Browse files Browse the repository at this point in the history
Signed-off-by: mueller-ma <[email protected]>
  • Loading branch information
mueller-ma committed Jul 8, 2024
1 parent f81eff0 commit 325dac6
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import kotlinx.coroutines.runBlocking
import org.openhab.habdroid.model.CloudNotification
import org.openhab.habdroid.model.toCloudNotificationAction
import org.openhab.habdroid.model.toOH2IconResource
import org.openhab.habdroid.util.map
import org.openhab.habdroid.util.toJsonArrayOrNull

class FcmMessageListenerService : FirebaseMessagingService() {
private lateinit var notifHelper: NotificationHelper
Expand All @@ -42,15 +45,23 @@ class FcmMessageListenerService : FirebaseMessagingService() {

when (messageType) {
"notification" -> {
val actions = data["actions"]
?.toJsonArrayOrNull()
?.map { it.toCloudNotificationAction() }
?.filterNotNull()
val cloudNotification = CloudNotification(
data["persistedId"].orEmpty(),
data["message"].orEmpty(),
id = data["persistedId"].orEmpty(),
title = data["title"].orEmpty(),
message = data["message"].orEmpty(),
// Older versions of openhab-cloud didn't send the notification generation
// timestamp, so use the (undocumented) google.sent_time as a time reference
// in that case. If that also isn't present, don't show time at all.
data["timestamp"]?.toLong() ?: message.sentTime,
data["icon"].toOH2IconResource(),
data["severity"]
createdTimestamp = data["timestamp"]?.toLongOrNull() ?: message.sentTime,
icon = data["icon"].toOH2IconResource(),
severity = data["severity"],
actions = actions,
onClickAction = data["TODO"].toCloudNotificationAction(),
mediaAttachmentUrl = data["TODO"]
)

runBlocking {
Expand All @@ -67,3 +78,4 @@ class FcmMessageListenerService : FirebaseMessagingService() {
private val TAG = FcmMessageListenerService::class.java.simpleName
}
}

3 changes: 3 additions & 0 deletions mobile/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,9 @@
<receiver
android:name=".core.NotificationDismissedReceiver"
android:exported="false" />
<receiver
android:name=".core.NotificationActionReceiver"
android:exported="false" />

<activity
android:name=".background.NfcReceiveActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import org.openhab.habdroid.background.tiles.AbstractTileService
import org.openhab.habdroid.background.tiles.TileData
import org.openhab.habdroid.core.CloudMessagingHelper
import org.openhab.habdroid.core.OpenHabApplication
import org.openhab.habdroid.model.CloudNotificationAction
import org.openhab.habdroid.model.NfcTag
import org.openhab.habdroid.ui.TaskerItemPickerActivity
import org.openhab.habdroid.ui.homescreenwidget.ItemUpdateWidget
Expand Down Expand Up @@ -279,6 +280,7 @@ class BackgroundTasksManager : BroadcastReceiver() {
const val WORKER_TAG_PREFIX_TASKER = "tasker-"
const val WORKER_TAG_PREFIX_WIDGET = "widget-"
const val WORKER_TAG_PREFIX_TILE = "tile-"
const val WORKER_TAG_PREFIX_NOTIFICATION = "notification-"
const val WORKER_TAG_PREFIX_TILE_ID = "tile_id-"
const val WORKER_TAG_VOICE_COMMAND = "voiceCommand"

Expand Down Expand Up @@ -428,6 +430,21 @@ class BackgroundTasksManager : BroadcastReceiver() {
)
}

fun enqueueNotificationAction(context: Context, action: CloudNotificationAction) {
val split = action.action.split(":", limit = 3)
enqueueItemUpload(
context,
WORKER_TAG_PREFIX_NOTIFICATION + split[1],
split[1],
null,
ItemUpdateWorker.ValueWithInfo(split[2]),
isImportant = true,
showToast = true,
asCommand = true,
forceUpdate = true
)
}

fun buildVoiceRecognitionIntent(context: Context, fromBackground: Boolean): Intent {
val callbackIntent = Intent(context, BackgroundTasksManager::class.java).apply {
action = ACTION_VOICE_RESULT
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/

package org.openhab.habdroid.core

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.net.toUri
import org.openhab.habdroid.background.BackgroundTasksManager
import org.openhab.habdroid.core.NotificationHelper.Companion.NOTIFICATION_ACTION_ACTION
import org.openhab.habdroid.core.NotificationHelper.Companion.NOTIFICATION_ACTION_LABEL
import org.openhab.habdroid.core.NotificationHelper.Companion.NOTIFICATION_ID_EXTRA
import org.openhab.habdroid.model.CloudNotificationAction
import org.openhab.habdroid.util.openInBrowser

class NotificationActionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "onReceive(): $intent")
val notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, 0)
if (notificationId == 0) {
return
}
val rawAction = intent.getStringExtra(NOTIFICATION_ACTION_ACTION) ?: return
val label = intent.getStringExtra(NOTIFICATION_ACTION_LABEL) ?: return
val action = CloudNotificationAction(label, rawAction)
Log.d(TAG, "Received action from $notificationId: $action")
executeAction(context, action)
}

companion object {
private val TAG = NotificationActionReceiver::class.java.simpleName

fun executeAction(context: Context, action: CloudNotificationAction) {
when {
action.action.startsWith("command:") -> {
BackgroundTasksManager.enqueueNotificationAction(context, action)
}
action.action.startsWith("http://") || action.action.startsWith("https://") -> {
action.action.toUri().openInBrowser(context)
} else -> {
// TODO
Log.e(TAG, "Not yet implemented")
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import org.openhab.habdroid.R
import org.openhab.habdroid.background.NotificationUpdateObserver
import org.openhab.habdroid.core.connection.ConnectionFactory
import org.openhab.habdroid.model.CloudNotification
import org.openhab.habdroid.model.CloudNotificationAction
import org.openhab.habdroid.model.IconResource
import org.openhab.habdroid.ui.MainActivity
import org.openhab.habdroid.util.HttpClient
Expand Down Expand Up @@ -94,6 +95,19 @@ class NotificationHelper(private val context: Context) {
)
}

private fun createActionIntent(action: CloudNotificationAction, notificationId: Int): PendingIntent {
val intent = Intent(context, NotificationActionReceiver::class.java)
intent.putExtra(NOTIFICATION_ID_EXTRA, notificationId)
intent.putExtra(NOTIFICATION_ACTION_LABEL, action.label)
intent.putExtra(NOTIFICATION_ACTION_ACTION, action.action)
return PendingIntent.getBroadcast(
context,
notificationId + action.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent_Immutable
)
}

private fun createChannelForSeverity(severity: String?) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
Expand Down Expand Up @@ -130,7 +144,11 @@ class NotificationHelper(private val context: Context) {
): Notification {
val iconBitmap = getNotificationIcon(message.icon)

val contentIntent = makeNotificationClickIntent(message.id, notificationId)
val contentIntent = if (message.onClickAction == null){
makeNotificationClickIntent(message.id, notificationId)
} else {
createActionIntent(message.onClickAction, notificationId)
}
val channelId = getChannelId(message.severity)

val publicText = context.resources.getQuantityString(R.plurals.summary_notification_text, 1, 1)
Expand All @@ -140,17 +158,24 @@ class NotificationHelper(private val context: Context) {
.setContentIntent(contentIntent)
.build()

return makeNotificationBuilder(channelId, message.createdTimestamp)
val builder = makeNotificationBuilder(channelId, message.createdTimestamp)
.setLargeIcon(iconBitmap)
.setStyle(NotificationCompat.BigTextStyle().bigText(message.message))
.setSound(context.getPrefs().getNotificationTone())
.setContentTitle(message.title)
.setContentText(message.message)
.setSubText(message.severity)
.setContentIntent(contentIntent)
.setDeleteIntent(deleteIntent)
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.setPublicVersion(publicVersion)
.build()

message.actions?.forEach {
val action = NotificationCompat.Action(null, it.label, createActionIntent(it, notificationId))
builder.addAction(action)
}

return builder.build()
}

private suspend fun getNotificationIcon(icon: IconResource?): Bitmap? {
Expand Down Expand Up @@ -248,6 +273,8 @@ class NotificationHelper(private val context: Context) {
companion object {
private val TAG = NotificationHelper::class.java.simpleName
const val NOTIFICATION_ID_EXTRA = "notification_id"
const val NOTIFICATION_ACTION_LABEL = "notification_action_label"
const val NOTIFICATION_ACTION_ACTION = "notification_action_action"

private fun getChannelId(severity: String?) = if (severity.isNullOrEmpty()) {
NotificationUpdateObserver.CHANNEL_ID_MESSAGE_DEFAULT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

package org.openhab.habdroid.model

import android.content.Context
import android.graphics.Bitmap
import android.os.Parcelable
import java.text.ParseException
import java.text.SimpleDateFormat
Expand All @@ -21,17 +23,40 @@ import java.util.TimeZone
import kotlinx.parcelize.Parcelize
import org.json.JSONException
import org.json.JSONObject
import org.openhab.habdroid.core.connection.Connection
import org.openhab.habdroid.util.IconBackground
import org.openhab.habdroid.util.ImageConversionPolicy
import org.openhab.habdroid.util.getIconFallbackColor
import org.openhab.habdroid.util.map
import org.openhab.habdroid.util.optStringOrNull

@Parcelize
data class CloudNotification internal constructor(
val id: String,
val title: String,
val message: String,
val createdTimestamp: Long,
val icon: IconResource?,
val severity: String?
val severity: String?,
val actions: List<CloudNotificationAction>?,
val onClickAction: CloudNotificationAction?,
val mediaAttachmentUrl: String?
) : Parcelable {
val idHash get() = id.hashCode()

suspend fun loadImage(connection: Connection, context: Context, size: Int): Bitmap? {
mediaAttachmentUrl ?: return null
//if (mediaAttachmentUrl.startsWith("item:")) {
// val itemName = mediaAttachmentUrl.removePrefix("item:")
// val item = ItemClient.loadItem(connection, itemName)
//
//}
val fallbackColor = context.getIconFallbackColor(IconBackground.APP_THEME)
return connection.httpClient
.get(mediaAttachmentUrl)
.asBitmap(size, fallbackColor, ImageConversionPolicy.PreferTargetSize)
.response
}
}

@Throws(JSONException::class)
Expand All @@ -47,11 +72,36 @@ fun JSONObject.toCloudNotification(): CloudNotification {
}
}

val payload = optJSONObject("payload")
return CloudNotification(
getString("_id"),
getString("message"),
created,
optStringOrNull("icon").toOH2IconResource(),
optStringOrNull("severity")
id = getString("_id"),
title = payload?.optString("title").orEmpty(),
message = payload?.getString("message") ?: getString("message"),
createdTimestamp = created,
icon = payload?.optStringOrNull("icon").toOH2IconResource() ?: optStringOrNull("icon").toOH2IconResource(),
severity = payload?.optStringOrNull("severity") ?: optStringOrNull("severity"),
actions = payload?.optJSONArray("actions")?.map { it.toCloudNotificationAction() }?.filterNotNull(),
onClickAction = payload?.optStringOrNull("on-click").toCloudNotificationAction(),
mediaAttachmentUrl = payload?.optStringOrNull("media-attachment-url")
)
}

@Parcelize
data class CloudNotificationAction internal constructor(
val label: String,
val action: String
) : Parcelable

fun String?.toCloudNotificationAction(): CloudNotificationAction? {
val split = this?.split("=", limit = 2)
if (split?.size != 2) {
return null
}
return CloudNotificationAction(split.component1(), split.component2())
}

fun JSONObject?.toCloudNotificationAction(): CloudNotificationAction? {
val action = this?.optStringOrNull("action") ?: return null
val title = optStringOrNull("title") ?: return null
return CloudNotificationAction(title, action)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import android.widget.TextView
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import java.util.ArrayList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.openhab.habdroid.R
import org.openhab.habdroid.core.connection.ConnectionFactory
import org.openhab.habdroid.model.CloudNotification
Expand Down Expand Up @@ -96,9 +100,11 @@ class CloudNotificationAdapter(context: Context, private val loadMoreListener: (

class NotificationViewHolder(inflater: LayoutInflater, parent: ViewGroup) :
RecyclerView.ViewHolder(inflater.inflate(R.layout.notificationlist_item, parent, false)) {
private val createdView: TextView = itemView.findViewById(R.id.notificationCreated)
private val titleView: TextView = itemView.findViewById(R.id.notificationTitle)
private val messageView: TextView = itemView.findViewById(R.id.notificationMessage)
private val iconView: WidgetImageView = itemView.findViewById(R.id.notificationImage)
private val createdView: TextView = itemView.findViewById(R.id.notificationCreated)
private val iconView: WidgetImageView = itemView.findViewById(R.id.notificationIcon)
private val imageView: WidgetImageView = itemView.findViewById(R.id.notificationImage)
private val severityView: TextView = itemView.findViewById(R.id.notificationSeverity)

fun bind(notification: CloudNotification) {
Expand All @@ -109,20 +115,33 @@ class CloudNotificationAdapter(context: Context, private val loadMoreListener: (
DateUtils.WEEK_IN_MILLIS,
0
)
titleView.text = notification.title
titleView.isVisible = notification.title.isNotEmpty()
messageView.text = notification.message
messageView.isVisible = notification.message.isNotEmpty()

val conn = ConnectionFactory.activeCloudConnection?.connection
if (notification.icon != null && conn != null) {
iconView.setImageUrl(
conn,
notification.icon.toUrl(
itemView.context,
itemView.context.determineDataUsagePolicy(conn).loadIconsWithState
),
timeoutMillis = 2000
)
} else {
if (conn == null) {
iconView.applyFallbackDrawable()
imageView.isVisible = false
} else {
if (notification.icon != null) {
iconView.setImageUrl(
conn,
notification.icon.toUrl(
itemView.context,
itemView.context.determineDataUsagePolicy(conn).loadIconsWithState
),
timeoutMillis = 2000
)
}
imageView.isVisible = notification.mediaAttachmentUrl != null
CoroutineScope(Dispatchers.IO + Job()).launch {
val bitmap = notification.loadImage(conn, itemView.context, itemView.width)
withContext(Dispatchers.Main) {
imageView.setImageBitmap(bitmap)
}
}
}
severityView.text = notification.severity
severityView.isGone = notification.severity.isNullOrEmpty()
Expand Down
Loading

0 comments on commit 325dac6

Please sign in to comment.