From 346ef3da5f09d6e11ca1bd20f0d35446967ea0d6 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Fri, 20 Jan 2023 11:02:03 -0800 Subject: [PATCH] Initial commit of notification support on Wear OS (#3221) * Initial commit of wear OS notifications * Update to allow message and title * Move constants to object to reduce imports * Split up logic to match phone app * Update readme and set websocket to false for wear OS * Bump firebase BOM --- README.md | 2 +- app/build.gradle.kts | 5 +- .../android/notifications/MessagingManager.kt | 361 ++++-------------- common/build.gradle.kts | 4 + .../data/integration/DeviceRegistration.kt | 3 +- .../impl/IntegrationRepositoryImpl.kt | 2 +- .../notifications/NotificationFunctions.kt | 271 +++++++++++++ wear/build.gradle.kts | 4 + wear/src/main/AndroidManifest.xml | 8 + .../android/home/HomePresenterImpl.kt | 4 +- .../FirebaseCloudMessagingService.kt | 66 ++++ .../android/notifications/MessagingManager.kt | 89 +++++ .../android/onboarding/MessagingToken.kt | 14 + .../MobileAppIntegrationPresenterImpl.kt | 5 +- .../android/phone/PhoneSettingsListener.kt | 5 +- 15 files changed, 538 insertions(+), 305 deletions(-) create mode 100755 common/src/main/java/io/homeassistant/companion/android/common/notifications/NotificationFunctions.kt create mode 100755 wear/src/main/java/io/homeassistant/companion/android/notifications/FirebaseCloudMessagingService.kt create mode 100755 wear/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt create mode 100755 wear/src/main/java/io/homeassistant/companion/android/onboarding/MessagingToken.kt diff --git a/README.md b/README.md index 1e98f26e2..176d400e9 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are looking for documentation around the companion applications check out - `io.homeassistant.companion.android.minimal` - `io.homeassistant.companion.android.minimal.debug` -5. Now download the `google-services.json` file and put it in the _home-assistant-Android/app_ folder. This file contains the configuration of the whole project (all four applications). ([You can also use the mock services file instead of generating your own](/.github/mock-google-services.json). The file should contain client IDs for all packages listed above for debugging to work properly. **If you do not generate your own file FCM push notification will never work, only websocket notifications will**) +5. Now download the `google-services.json` file and put it in the _home-assistant-Android/app_ and _home-assistant-Android/wear_ folder. This file contains the configuration of the whole project (all four applications). ([You can also use the mock services file instead of generating your own](/.github/mock-google-services.json). The file should contain client IDs for all packages listed above for debugging to work properly. **If you do not generate your own file FCM push notification will never work, only websocket notifications will**) 6. Start Android Studio, open your source code folder and check if the Gradle build will be successful using Build/Make Module "App". You might have to install the right Android SDK via Tools/SDK Manager first. 7. Run `gradlew assembleDebug` to build all debug versions, this might take a while. 8. If the build is successful, you can run the app by doing the following: click **Run** -> **Run 'app'**. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index caff8425f..02fd4a3b1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -143,9 +143,6 @@ dependencies { implementation("org.altbeacon:android-beacon-library:2.19.5") implementation("com.maltaisn:icondialog:3.3.0") implementation("com.maltaisn:iconpack-community-material:5.3.45") - implementation("com.vdurmont:emoji-java:5.1.1") { - exclude(group = "org.json", module = "json") - } implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.20") implementation("org.jetbrains.kotlin:kotlin-reflect:1.7.20") @@ -171,7 +168,7 @@ dependencies { "fullImplementation"("com.google.android.gms:play-services-location:21.0.1") "fullImplementation"("com.google.android.gms:play-services-home:16.0.0") - "fullImplementation"(platform("com.google.firebase:firebase-bom:30.4.1")) + "fullImplementation"(platform("com.google.firebase:firebase-bom:31.1.1")) "fullImplementation"("com.google.firebase:firebase-messaging") "fullImplementation"("io.sentry:sentry-android:6.11.0") "fullImplementation"("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4") diff --git a/app/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt b/app/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt index e269796b7..6b56bd82c 100644 --- a/app/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt +++ b/app/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt @@ -13,7 +13,6 @@ import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.graphics.Color import android.media.AudioAttributes import android.media.AudioManager import android.media.MediaMetadataRetriever @@ -29,7 +28,6 @@ import android.os.PowerManager import android.provider.Settings import android.speech.tts.TextToSpeech import android.speech.tts.UtteranceProgressListener -import android.text.Spanned import android.util.Log import android.view.KeyEvent import android.widget.RemoteViews @@ -41,22 +39,27 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.app.RemoteInput import androidx.core.content.ContextCompat import androidx.core.content.getSystemService -import androidx.core.text.HtmlCompat import androidx.core.text.isDigitsOnly import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue -import com.mikepenz.iconics.IconicsDrawable -import com.mikepenz.iconics.utils.toAndroidIconCompat -import com.vdurmont.emoji.EmojiParser import dagger.hilt.android.qualifiers.ApplicationContext import io.homeassistant.companion.android.R import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository import io.homeassistant.companion.android.common.data.integration.IntegrationRepository import io.homeassistant.companion.android.common.data.url.UrlRepository +import io.homeassistant.companion.android.common.notifications.NotificationData +import io.homeassistant.companion.android.common.notifications.createChannelID +import io.homeassistant.companion.android.common.notifications.getGroupNotificationBuilder +import io.homeassistant.companion.android.common.notifications.handleChannel +import io.homeassistant.companion.android.common.notifications.handleColor +import io.homeassistant.companion.android.common.notifications.handleSmallIcon +import io.homeassistant.companion.android.common.notifications.handleText +import io.homeassistant.companion.android.common.notifications.parseColor +import io.homeassistant.companion.android.common.notifications.parseVibrationPattern +import io.homeassistant.companion.android.common.notifications.prepareText import io.homeassistant.companion.android.common.sensors.BluetoothSensorManager import io.homeassistant.companion.android.common.util.cancel import io.homeassistant.companion.android.common.util.cancelGroupIfNeeded -import io.homeassistant.companion.android.common.util.generalChannel import io.homeassistant.companion.android.common.util.getActiveNotification import io.homeassistant.companion.android.database.notification.NotificationDao import io.homeassistant.companion.android.database.notification.NotificationItem @@ -86,7 +89,6 @@ import java.io.File import java.io.FileOutputStream import java.net.URL import java.net.URLDecoder -import java.util.Locale import java.util.UUID import javax.inject.Inject import io.homeassistant.companion.android.common.R as commonR @@ -111,25 +113,17 @@ class MessagingManager @Inject constructor( const val SETTINGS_PREFIX = "settings://" const val NOTIFICATION_HISTORY = "notification_history" - const val TITLE = "title" - const val MESSAGE = "message" const val SUBJECT = "subject" - const val IMPORTANCE = "importance" const val TIMEOUT = "timeout" const val IMAGE_URL = "image" const val ICON_URL = "icon_url" const val VIDEO_URL = "video" const val VISIBILITY = "visibility" - const val LED_COLOR = "ledColor" - const val VIBRATION_PATTERN = "vibrationPattern" const val PERSISTENT = "persistent" const val CHRONOMETER = "chronometer" const val WHEN = "when" - const val GROUP_PREFIX = "group_" const val KEY_TEXT_REPLY = "key_text_reply" - const val ALERT_ONCE = "alert_once" const val INTENT_CLASS_NAME = "intent_class_name" - const val NOTIFICATION_ICON = "notification_icon" const val URI = "URI" const val REPLY = "REPLY" const val BLE_ADVERTISE = "ble_advertise" @@ -138,7 +132,6 @@ class MessagingManager @Inject constructor( const val PACKAGE_NAME = "package_name" const val COMMAND = "command" const val TTS_TEXT = "tts_text" - const val CHANNEL = "channel" const val CONFIRMATION = "confirmation" // special intent constants @@ -186,16 +179,6 @@ class MessagingManager @Inject constructor( const val RM_SILENT = "silent" const val RM_VIBRATE = "vibrate" - // Channel streams - const val ALARM_STREAM = "alarm_stream" - const val ALARM_STREAM_MAX = "alarm_stream_max" - const val MUSIC_STREAM = "music_stream" - const val NOTIFICATION_STREAM = "notification_stream" - const val RING_STREAM = "ring_stream" - const val SYSTEM_STREAM = "system_stream" - const val CALL_STREAM = "call_stream" - const val DTMF_STREAM = "dtmf_stream" - // Enable/Disable Commands const val TURN_ON = "turn_on" const val TURN_OFF = "turn_off" @@ -266,8 +249,13 @@ class MessagingManager @Inject constructor( val DND_COMMANDS = listOf(DND_ALARMS_ONLY, DND_ALL, DND_NONE, DND_PRIORITY_ONLY) val RM_COMMANDS = listOf(RM_NORMAL, RM_SILENT, RM_VIBRATE) val CHANNEL_VOLUME_STREAM = listOf( - ALARM_STREAM, MUSIC_STREAM, NOTIFICATION_STREAM, RING_STREAM, CALL_STREAM, - SYSTEM_STREAM, DTMF_STREAM + NotificationData.ALARM_STREAM, + NotificationData.MUSIC_STREAM, + NotificationData.NOTIFICATION_STREAM, + NotificationData.RING_STREAM, + NotificationData.CALL_STREAM, + NotificationData.SYSTEM_STREAM, + NotificationData.DTMF_STREAM ) val ENABLE_COMMANDS = listOf(TURN_OFF, TURN_ON) val FORCE_COMMANDS = listOf(FORCE_OFF, FORCE_ON) @@ -315,7 +303,7 @@ class MessagingManager @Inject constructor( } else { val jsonObject = JSONObject(jsonData) val notificationRow = - NotificationItem(0, now, jsonData[MESSAGE].toString(), jsonObject.toString(), source) + NotificationItem(0, now, jsonData[NotificationData.MESSAGE].toString(), jsonObject.toString(), source) notificationId = notificationDao.add(notificationRow) val confirmation = jsonData[CONFIRMATION]?.toBoolean() ?: false @@ -331,25 +319,25 @@ class MessagingManager @Inject constructor( } when { - jsonData[MESSAGE] == REQUEST_LOCATION_UPDATE -> { + jsonData[NotificationData.MESSAGE] == REQUEST_LOCATION_UPDATE -> { Log.d(TAG, "Request location update") requestAccurateLocationUpdate() } - jsonData[MESSAGE] == CLEAR_NOTIFICATION && !jsonData["tag"].isNullOrBlank() -> { + jsonData[NotificationData.MESSAGE] == CLEAR_NOTIFICATION && !jsonData["tag"].isNullOrBlank() -> { Log.d(TAG, "Clearing notification with tag: ${jsonData["tag"]}") clearNotification(jsonData["tag"]!!) } - jsonData[MESSAGE] == REMOVE_CHANNEL && !jsonData[CHANNEL].isNullOrBlank() -> { - Log.d(TAG, "Removing Notification channel ${jsonData[CHANNEL]}") - removeNotificationChannel(jsonData[CHANNEL]!!) + jsonData[NotificationData.MESSAGE] == REMOVE_CHANNEL && !jsonData[NotificationData.CHANNEL].isNullOrBlank() -> { + Log.d(TAG, "Removing Notification channel ${jsonData[NotificationData.CHANNEL]}") + removeNotificationChannel(jsonData[NotificationData.CHANNEL]!!) } - jsonData[MESSAGE] == TTS -> { + jsonData[NotificationData.MESSAGE] == TTS -> { Log.d(TAG, "Sending notification title to TTS") speakNotification(jsonData) } - jsonData[MESSAGE] in DEVICE_COMMANDS -> { + jsonData[NotificationData.MESSAGE] in DEVICE_COMMANDS -> { Log.d(TAG, "Processing device command") - when (jsonData[MESSAGE]) { + when (jsonData[NotificationData.MESSAGE]) { COMMAND_DND -> { if (jsonData[COMMAND] in DND_COMMANDS) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) @@ -667,7 +655,7 @@ class MessagingManager @Inject constructor( if (it == TextToSpeech.SUCCESS) { val listener = object : UtteranceProgressListener() { override fun onStart(p0: String?) { - if (data[MEDIA_STREAM] == ALARM_STREAM_MAX) + if (data[MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX) audioManager?.setStreamVolume( AudioManager.STREAM_ALARM, maxAlarmVolume!!, @@ -678,7 +666,7 @@ class MessagingManager @Inject constructor( override fun onDone(p0: String?) { textToSpeech?.stop() textToSpeech?.shutdown() - if (data[MEDIA_STREAM] == ALARM_STREAM_MAX) + if (data[MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX) audioManager?.setStreamVolume( AudioManager.STREAM_ALARM, currentAlarmVolume!!, @@ -689,7 +677,7 @@ class MessagingManager @Inject constructor( override fun onError(p0: String?) { textToSpeech?.stop() textToSpeech?.shutdown() - if (data[MEDIA_STREAM] == ALARM_STREAM_MAX) + if (data[MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX) audioManager?.setStreamVolume( AudioManager.STREAM_ALARM, currentAlarmVolume!!, @@ -698,7 +686,7 @@ class MessagingManager @Inject constructor( } override fun onStop(utteranceId: String?, interrupted: Boolean) { - if (data[MEDIA_STREAM] == ALARM_STREAM_MAX) + if (data[MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX) audioManager?.setStreamVolume( AudioManager.STREAM_ALARM, currentAlarmVolume!!, @@ -707,7 +695,7 @@ class MessagingManager @Inject constructor( } } textToSpeech?.setOnUtteranceProgressListener(listener) - if (data[MEDIA_STREAM] == ALARM_STREAM || data[MEDIA_STREAM] == ALARM_STREAM_MAX) { + if (data[MEDIA_STREAM] == NotificationData.ALARM_STREAM || data[MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX) { val audioAttributes = AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_ALARM) @@ -729,7 +717,7 @@ class MessagingManager @Inject constructor( } private fun handleDeviceCommands(data: Map) { - val message = data[MESSAGE] + val message = data[NotificationData.MESSAGE] val command = data[COMMAND] when (message) { COMMAND_DND -> { @@ -737,7 +725,7 @@ class MessagingManager @Inject constructor( val notificationManager = context.getSystemService() if (notificationManager?.isNotificationPolicyAccessGranted == false) { - notifyMissingPermission(data[MESSAGE].toString()) + notifyMissingPermission(message.toString()) } else { when (command) { DND_ALARMS_ONLY -> notificationManager?.setInterruptionFilter( @@ -761,7 +749,7 @@ class MessagingManager @Inject constructor( val notificationManager = context.getSystemService() if (notificationManager?.isNotificationPolicyAccessGranted == false) { - notifyMissingPermission(data[MESSAGE].toString()) + notifyMissingPermission(message.toString()) } else { processRingerMode(audioManager!!, command) } @@ -800,7 +788,7 @@ class MessagingManager @Inject constructor( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val notificationManager = context.getSystemService() if (notificationManager?.isNotificationPolicyAccessGranted == false) { - notifyMissingPermission(data[MESSAGE].toString()) + notifyMissingPermission(message.toString()) } else { processStreamVolume( audioManager!!, @@ -825,7 +813,7 @@ class MessagingManager @Inject constructor( } else -> { Log.e(TAG, "Missing Bluetooth permissions, notifying user to grant permissions") - notifyMissingPermission(data[MESSAGE].toString()) + notifyMissingPermission(message.toString()) } } } @@ -910,7 +898,7 @@ class MessagingManager @Inject constructor( COMMAND_ACTIVITY -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!Settings.canDrawOverlays(context)) - notifyMissingPermission(data[MESSAGE].toString()) + notifyMissingPermission(message.toString()) else if (ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED && data["tag"] == Intent.ACTION_CALL) { Handler(Looper.getMainLooper()).post { Toast.makeText( @@ -933,7 +921,7 @@ class MessagingManager @Inject constructor( COMMAND_WEBVIEW -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!Settings.canDrawOverlays(context)) - notifyMissingPermission(data[MESSAGE].toString()) + notifyMissingPermission(message.toString()) else openWebview(command) } else @@ -963,7 +951,7 @@ class MessagingManager @Inject constructor( if (!NotificationManagerCompat.getEnabledListenerPackages(context) .contains(context.packageName) ) - notifyMissingPermission(data[MESSAGE].toString()) + notifyMissingPermission(message.toString()) else { processMediaCommand(data) } @@ -972,7 +960,7 @@ class MessagingManager @Inject constructor( COMMAND_LAUNCH_APP -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!Settings.canDrawOverlays(context)) - notifyMissingPermission(data[MESSAGE].toString()) + notifyMissingPermission(message.toString()) else launchApp(data) } else @@ -990,7 +978,7 @@ class MessagingManager @Inject constructor( if (!processScreenCommands(data)) mainScope.launch { sendNotification(data) } } else - notifyMissingPermission(data[MESSAGE].toString()) + notifyMissingPermission(message.toString()) } else if (!processScreenCommands(data)) mainScope.launch { sendNotification(data) } } @@ -1063,24 +1051,24 @@ class MessagingManager @Inject constructor( var previousGroup = "" var previousGroupId = 0 if (!group.isNullOrBlank()) { - group = GROUP_PREFIX + group + group = NotificationData.GROUP_PREFIX + group groupId = group.hashCode() } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val notification = notificationManagerCompat.getActiveNotification(tag, messageId) if (notification != null && notification.isGroup) { - previousGroup = GROUP_PREFIX + notification.tag + previousGroup = NotificationData.GROUP_PREFIX + notification.tag previousGroupId = previousGroup.hashCode() } } } - val channelId = handleChannel(notificationManagerCompat, data) + val channelId = handleChannel(context, notificationManagerCompat, data) val notificationBuilder = NotificationCompat.Builder(context, channelId) - handleSmallIcon(notificationBuilder, data) + handleSmallIcon(context, notificationBuilder, data) handleSound(notificationBuilder, data) @@ -1088,11 +1076,11 @@ class MessagingManager @Inject constructor( handleLargeIcon(notificationBuilder, data) - handleGroup(notificationBuilder, group, data[ALERT_ONCE].toBoolean()) + handleGroup(notificationBuilder, group, data[NotificationData.ALERT_ONCE].toBoolean()) handleTimeout(notificationBuilder, data) - handleColor(notificationBuilder, data) + handleColor(context, notificationBuilder, data) handleSticky(notificationBuilder, data) @@ -1127,7 +1115,7 @@ class MessagingManager @Inject constructor( notify(tag, messageId, notificationBuilder.build()) if (!group.isNullOrBlank()) { Log.d(TAG, "Show group notification with tag \"$group\" and id \"$groupId\"") - notify(group, groupId, getGroupNotificationBuilder(channelId, group, data).build()) + notify(group, groupId, getGroupNotificationBuilder(context, channelId, group, data).build()) } else { if (!previousGroup.isBlank()) { Log.d( @@ -1163,22 +1151,6 @@ class MessagingManager @Inject constructor( } } - private fun handleSmallIcon( - builder: NotificationCompat.Builder, - data: Map - ) { - if (data[NOTIFICATION_ICON]?.startsWith("mdi:") == true && !data[NOTIFICATION_ICON]?.substringAfter("mdi:").isNullOrBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val iconName = data[NOTIFICATION_ICON]!!.split(":")[1] - val iconDrawable = - IconicsDrawable(context, "cmd-$iconName") - if (iconDrawable.icon != null) - builder.setSmallIcon(iconDrawable.toAndroidIconCompat()) - else - builder.setSmallIcon(commonR.drawable.ic_stat_ic_notification) - } else - builder.setSmallIcon(commonR.drawable.ic_stat_ic_notification) - } - private fun handleContentIntent( builder: NotificationCompat.Builder, messageId: Int, @@ -1226,34 +1198,11 @@ class MessagingManager @Inject constructor( } } - private fun getGroupNotificationBuilder( - channelId: String, - group: String, - data: Map - ): NotificationCompat.Builder { - - val groupNotificationBuilder = NotificationCompat.Builder(context, channelId) - .setStyle( - NotificationCompat.BigTextStyle() - .setSummaryText( - prepareText(group.substring(GROUP_PREFIX.length)) - ) - ) - .setGroup(group) - .setGroupSummary(true) - - if (data[ALERT_ONCE].toBoolean()) - groupNotificationBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) - handleColor(groupNotificationBuilder, data) - handleSmallIcon(groupNotificationBuilder, data) - return groupNotificationBuilder - } - private fun handleSound( builder: NotificationCompat.Builder, data: Map ) { - if (data[CHANNEL] == ALARM_STREAM) { + if (data[NotificationData.CHANNEL] == NotificationData.ALARM_STREAM) { builder.setCategory(Notification.CATEGORY_ALARM) builder.setSound( RingtoneManager.getActualDefaultRingtoneUri( @@ -1269,38 +1218,17 @@ class MessagingManager @Inject constructor( } else { builder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) } - if (data[ALERT_ONCE].toBoolean()) + if (data[NotificationData.ALERT_ONCE].toBoolean()) builder.setOnlyAlertOnce(true) } - private fun handleColor( - builder: NotificationCompat.Builder, - data: Map - ) { - - val colorString = data["color"] - val color = parseColor(colorString, commonR.color.colorPrimary) - builder.color = color - } - - private fun parseColor(colorString: String?, default: Int): Int { - if (!colorString.isNullOrBlank()) { - try { - return Color.parseColor(colorString) - } catch (e: Exception) { - Log.e(TAG, "Unable to parse color", e) - } - } - return ContextCompat.getColor(context, default) - } - private fun handleLegacyLedColor( builder: NotificationCompat.Builder, data: Map ) { - val ledColor = data[LED_COLOR] + val ledColor = data[NotificationData.LED_COLOR] if (!ledColor.isNullOrBlank()) { - builder.setLights(parseColor(ledColor, commonR.color.colorPrimary), 3000, 3000) + builder.setLights(parseColor(context, ledColor, commonR.color.colorPrimary), 3000, 3000) } } @@ -1308,7 +1236,7 @@ class MessagingManager @Inject constructor( builder: NotificationCompat.Builder, data: Map ) { - val vibrationPattern = data[VIBRATION_PATTERN] + val vibrationPattern = data[NotificationData.VIBRATION_PATTERN] if (!vibrationPattern.isNullOrBlank()) { val arrVibrationPattern = parseVibrationPattern(vibrationPattern) if (arrVibrationPattern.isNotEmpty()) { @@ -1323,7 +1251,7 @@ class MessagingManager @Inject constructor( ) { // Use importance property for legacy priority support - val priority = data[IMPORTANCE] + val priority = data[NotificationData.IMPORTANCE] when (priority) { "high" -> { @@ -1344,32 +1272,6 @@ class MessagingManager @Inject constructor( } } - @RequiresApi(Build.VERSION_CODES.N) - private fun handleImportance( - data: Map - ): Int { - - val importance = data[IMPORTANCE] - - when (importance) { - "high" -> { - return NotificationManager.IMPORTANCE_HIGH - } - "low" -> { - return NotificationManager.IMPORTANCE_LOW - } - "max" -> { - return NotificationManager.IMPORTANCE_MAX - } - "min" -> { - return NotificationManager.IMPORTANCE_MIN - } - else -> { - return NotificationManager.IMPORTANCE_DEFAULT - } - } - } - private fun handleTimeout( builder: NotificationCompat.Builder, data: Map @@ -1407,29 +1309,6 @@ class MessagingManager @Inject constructor( } } - private fun handleText( - builder: NotificationCompat.Builder, - data: Map - ) { - data[TITLE]?.let { - builder.setContentTitle(prepareText(it)) - } - data[MESSAGE]?.let { - val text = prepareText(it) - builder.setContentText(text) - builder.setStyle(NotificationCompat.BigTextStyle().bigText(text)) - } - } - - private fun prepareText( - text: String - ): Spanned { - // Replace control char \r\n, \r, \n and also \r\n, \r, \n as text literals in strings to
- var brText = text.replace("(\r\n|\r|\n)|(\\\\r\\\\n|\\\\r|\\\\n)".toRegex(), "
") - var emojiParsedText = EmojiParser.parseToUnicode(brText) - return HtmlCompat.fromHtml(emojiParsedText, HtmlCompat.FROM_HTML_MODE_LEGACY) - } - private suspend fun handleLargeIcon( builder: NotificationCompat.Builder, data: Map @@ -1509,11 +1388,11 @@ class MessagingManager @Inject constructor( ) } - data[TITLE]?.let { rawTitle -> + data[NotificationData.TITLE]?.let { rawTitle -> remoteViewFlipper.setTextViewText(R.id.title, rawTitle) } - data[MESSAGE]?.let { rawMessage -> + data[NotificationData.MESSAGE]?.let { rawMessage -> remoteViewFlipper.setTextViewText(R.id.info, rawMessage) } @@ -1777,114 +1656,6 @@ class MessagingManager @Inject constructor( } } - private fun handleChannel( - notificationManagerCompat: NotificationManagerCompat, - data: Map - ): String { - // Define some values for a default channel - var channelID = generalChannel - var channelName = "General" - - if (!data[CHANNEL].isNullOrEmpty()) { - channelID = createChannelID(data[CHANNEL].toString()) - channelName = data[CHANNEL].toString().trim() - } - - // Since android Oreo notification channel is needed. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - channelID, - channelName, - handleImportance(data) - ) - - if (channelName == ALARM_STREAM) - handleChannelSound(channel) - - setChannelLedColor(data, channel) - setChannelVibrationPattern(data, channel) - notificationManagerCompat.createNotificationChannel(channel) - } - return channelID - } - - private fun setChannelLedColor( - data: Map, - channel: NotificationChannel - ) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val ledColor = data[LED_COLOR] - if (!ledColor.isNullOrBlank()) { - channel.enableLights(true) - channel.lightColor = parseColor(ledColor, commonR.color.colorPrimary) - } - } - } - - private fun setChannelVibrationPattern( - data: Map, - channel: NotificationChannel - ) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val vibrationPattern = data[VIBRATION_PATTERN] - val arrVibrationPattern = parseVibrationPattern(vibrationPattern) - if (arrVibrationPattern.isNotEmpty()) { - channel.vibrationPattern = arrVibrationPattern - } - } - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun handleChannelSound( - channel: NotificationChannel - ) { - val audioAttributes = AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) - .setLegacyStreamType(AudioManager.STREAM_ALARM) - .setUsage(AudioAttributes.USAGE_ALARM) - .build() - channel.setSound( - RingtoneManager.getActualDefaultRingtoneUri( - context, - RingtoneManager.TYPE_ALARM - ) - ?: RingtoneManager.getActualDefaultRingtoneUri( - context, - RingtoneManager.TYPE_RINGTONE - ), - audioAttributes - ) - } - - private fun parseVibrationPattern( - vibrationPattern: String? - ): LongArray { - if (!vibrationPattern.isNullOrBlank()) { - val pattern = vibrationPattern.split(",").toTypedArray() - val list = mutableListOf() - pattern.forEach { it -> - val ms = it.trim().toLongOrNull() - if (ms != null) { - list.add(ms) - } - } - if (list.count() > 0) { - return list.toLongArray() - } - } - return LongArray(0) - } - - private fun createChannelID( - channelName: String - ): String { - return channelName - .trim() - .toLowerCase(Locale.ROOT) - .replace(" ", "_") - } - @RequiresApi(Build.VERSION_CODES.M) private fun requestDNDPermission() { val intent = @@ -2002,13 +1773,13 @@ class MessagingManager @Inject constructor( private fun processStreamVolume(audioManager: AudioManager, stream: String, volume: Int) { when (stream) { - ALARM_STREAM -> adjustVolumeStream(AudioManager.STREAM_ALARM, volume, audioManager) - MUSIC_STREAM -> adjustVolumeStream(AudioManager.STREAM_MUSIC, volume, audioManager) - NOTIFICATION_STREAM -> adjustVolumeStream(AudioManager.STREAM_NOTIFICATION, volume, audioManager) - RING_STREAM -> adjustVolumeStream(AudioManager.STREAM_RING, volume, audioManager) - CALL_STREAM -> adjustVolumeStream(AudioManager.STREAM_VOICE_CALL, volume, audioManager) - SYSTEM_STREAM -> adjustVolumeStream(AudioManager.STREAM_SYSTEM, volume, audioManager) - DTMF_STREAM -> adjustVolumeStream(AudioManager.STREAM_DTMF, volume, audioManager) + NotificationData.ALARM_STREAM -> adjustVolumeStream(AudioManager.STREAM_ALARM, volume, audioManager) + NotificationData.MUSIC_STREAM -> adjustVolumeStream(AudioManager.STREAM_MUSIC, volume, audioManager) + NotificationData.NOTIFICATION_STREAM -> adjustVolumeStream(AudioManager.STREAM_NOTIFICATION, volume, audioManager) + NotificationData.RING_STREAM -> adjustVolumeStream(AudioManager.STREAM_RING, volume, audioManager) + NotificationData.CALL_STREAM -> adjustVolumeStream(AudioManager.STREAM_VOICE_CALL, volume, audioManager) + NotificationData.SYSTEM_STREAM -> adjustVolumeStream(AudioManager.STREAM_SYSTEM, volume, audioManager) + NotificationData.DTMF_STREAM -> adjustVolumeStream(AudioManager.STREAM_DTMF, volume, audioManager) else -> Log.d(TAG, "Skipping command due to invalid channel stream") } } @@ -2160,12 +1931,12 @@ class MessagingManager @Inject constructor( val contentResolver = context.contentResolver val success = Settings.System.putInt( contentResolver, - when (data[MESSAGE].toString()) { + when (data[NotificationData.MESSAGE].toString()) { COMMAND_SCREEN_BRIGHTNESS_LEVEL -> Settings.System.SCREEN_BRIGHTNESS COMMAND_AUTO_SCREEN_BRIGHTNESS -> Settings.System.SCREEN_BRIGHTNESS_MODE else -> Settings.System.SCREEN_OFF_TIMEOUT }, - when (data[MESSAGE].toString()) { + when (data[NotificationData.MESSAGE].toString()) { COMMAND_SCREEN_BRIGHTNESS_LEVEL -> command!!.toInt().coerceIn(0, 255) COMMAND_AUTO_SCREEN_BRIGHTNESS -> { if (command == TURN_ON) @@ -2188,7 +1959,7 @@ class MessagingManager @Inject constructor( if (context.applicationInfo.processName == item.processName) { if (item.importance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { val data = - mutableMapOf(MESSAGE to context.getString(commonR.string.missing_command_permission)) + mutableMapOf(NotificationData.MESSAGE to context.getString(commonR.string.missing_command_permission)) runBlocking { sendNotification(data) } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 09663a8e1..7e567766c 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -77,4 +77,8 @@ dependencies { implementation("com.mikepenz:iconics-core:5.4.0") implementation("com.mikepenz:community-material-typeface:7.0.96.0-kotlin@aar") + + implementation("com.vdurmont:emoji-java:5.1.1") { + exclude(group = "org.json", module = "json") + } } diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/integration/DeviceRegistration.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/DeviceRegistration.kt index 6ba5d9658..90a3e16f8 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/integration/DeviceRegistration.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/DeviceRegistration.kt @@ -3,5 +3,6 @@ package io.homeassistant.companion.android.common.data.integration data class DeviceRegistration( val appVersion: String? = null, val deviceName: String? = null, - var pushToken: String? = null + var pushToken: String? = null, + var pushWebsocket: Boolean = true ) diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationRepositoryImpl.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationRepositoryImpl.kt index 72643bfed..c3f7f9ead 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationRepositoryImpl.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/integration/impl/IntegrationRepositoryImpl.kt @@ -759,7 +759,7 @@ class IntegrationRepositoryImpl @Inject constructor( val oldDeviceRegistration = getRegistration() val pushToken = deviceRegistration.pushToken ?: oldDeviceRegistration.pushToken - val appData = mutableMapOf("push_websocket_channel" to true) + val appData = mutableMapOf("push_websocket_channel" to deviceRegistration.pushWebsocket) if (!pushToken.isNullOrBlank()) { appData["push_url"] = PUSH_URL appData["push_token"] = pushToken diff --git a/common/src/main/java/io/homeassistant/companion/android/common/notifications/NotificationFunctions.kt b/common/src/main/java/io/homeassistant/companion/android/common/notifications/NotificationFunctions.kt new file mode 100755 index 000000000..eb4456801 --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/notifications/NotificationFunctions.kt @@ -0,0 +1,271 @@ +package io.homeassistant.companion.android.common.notifications + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.graphics.Color +import android.media.AudioAttributes +import android.media.AudioManager +import android.media.RingtoneManager +import android.os.Build +import android.text.Spanned +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import androidx.core.text.HtmlCompat +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.utils.toAndroidIconCompat +import com.vdurmont.emoji.EmojiParser +import io.homeassistant.companion.android.common.R +import io.homeassistant.companion.android.common.util.generalChannel +import java.util.Locale + +object NotificationData { + const val TAG = "MessagingService" + const val TITLE = "title" + const val MESSAGE = "message" + const val GROUP_PREFIX = "group_" + const val CHANNEL = "channel" + const val IMPORTANCE = "importance" + const val LED_COLOR = "ledColor" + const val VIBRATION_PATTERN = "vibrationPattern" + const val NOTIFICATION_ICON = "notification_icon" + const val ALERT_ONCE = "alert_once" + + // Channel streams + const val ALARM_STREAM = "alarm_stream" + const val ALARM_STREAM_MAX = "alarm_stream_max" + const val MUSIC_STREAM = "music_stream" + const val NOTIFICATION_STREAM = "notification_stream" + const val RING_STREAM = "ring_stream" + const val SYSTEM_STREAM = "system_stream" + const val CALL_STREAM = "call_stream" + const val DTMF_STREAM = "dtmf_stream" +} + +fun createChannelID( + channelName: String +): String { + return channelName + .trim() + .lowercase(Locale.ROOT) + .replace(" ", "_") +} + +fun handleChannel( + context: Context, + notificationManagerCompat: NotificationManagerCompat, + data: Map +): String { + // Define some values for a default channel + var channelID = generalChannel + var channelName = "General" + + if (!data[NotificationData.CHANNEL].isNullOrEmpty()) { + channelID = createChannelID(data[NotificationData.CHANNEL].toString()) + channelName = data[NotificationData.CHANNEL].toString().trim() + } + + // Since android Oreo notification channel is needed. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + channelID, + channelName, + handleImportance(data) + ) + + if (channelName == NotificationData.ALARM_STREAM) + handleChannelSound(context, channel) + + setChannelLedColor(context, data, channel) + setChannelVibrationPattern(data, channel) + notificationManagerCompat.createNotificationChannel(channel) + } + return channelID +} + +@RequiresApi(Build.VERSION_CODES.N) +fun handleImportance( + data: Map +): Int { + + when (data[NotificationData.IMPORTANCE]) { + "high" -> { + return NotificationManager.IMPORTANCE_HIGH + } + "low" -> { + return NotificationManager.IMPORTANCE_LOW + } + "max" -> { + return NotificationManager.IMPORTANCE_MAX + } + "min" -> { + return NotificationManager.IMPORTANCE_MIN + } + else -> { + return NotificationManager.IMPORTANCE_DEFAULT + } + } +} + +@RequiresApi(Build.VERSION_CODES.O) +fun handleChannelSound( + context: Context, + channel: NotificationChannel +) { + val audioAttributes = AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) + .setLegacyStreamType(AudioManager.STREAM_ALARM) + .setUsage(AudioAttributes.USAGE_ALARM) + .build() + channel.setSound( + RingtoneManager.getActualDefaultRingtoneUri( + context, + RingtoneManager.TYPE_ALARM + ) + ?: RingtoneManager.getActualDefaultRingtoneUri( + context, + RingtoneManager.TYPE_RINGTONE + ), + audioAttributes + ) +} + +fun setChannelLedColor( + context: Context, + data: Map, + channel: NotificationChannel +) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val ledColor = data[NotificationData.LED_COLOR] + if (!ledColor.isNullOrBlank()) { + channel.enableLights(true) + channel.lightColor = parseColor(context, ledColor, R.color.colorPrimary) + } + } +} + +fun setChannelVibrationPattern( + data: Map, + channel: NotificationChannel +) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val vibrationPattern = data[NotificationData.VIBRATION_PATTERN] + val arrVibrationPattern = parseVibrationPattern(vibrationPattern) + if (arrVibrationPattern.isNotEmpty()) { + channel.vibrationPattern = arrVibrationPattern + } + } +} + +fun parseVibrationPattern( + vibrationPattern: String? +): LongArray { + if (!vibrationPattern.isNullOrBlank()) { + val pattern = vibrationPattern.split(",").toTypedArray() + val list = mutableListOf() + pattern.forEach { + val ms = it.trim().toLongOrNull() + if (ms != null) { + list.add(ms) + } + } + if (list.isNotEmpty()) { + return list.toLongArray() + } + } + return LongArray(0) +} + +fun parseColor( + context: Context, + colorString: String?, + default: Int +): Int { + if (!colorString.isNullOrBlank()) { + try { + return Color.parseColor(colorString) + } catch (e: Exception) { + Log.e(NotificationData.TAG, "Unable to parse color", e) + } + } + return ContextCompat.getColor(context, default) +} + +fun handleSmallIcon( + context: Context, + builder: NotificationCompat.Builder, + data: Map +) { + val notificationIcon = data[NotificationData.NOTIFICATION_ICON] ?: "" + if (notificationIcon.startsWith("mdi:") && notificationIcon.substringAfter("mdi:").isNotBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val iconName = notificationIcon.split(":")[1] + val iconDrawable = + IconicsDrawable(context, "cmd-$iconName") + if (iconDrawable.icon != null) + builder.setSmallIcon(iconDrawable.toAndroidIconCompat()) + else + builder.setSmallIcon(R.drawable.ic_stat_ic_notification) + } else + builder.setSmallIcon(R.drawable.ic_stat_ic_notification) +} + +fun getGroupNotificationBuilder( + context: Context, + channelId: String, + group: String, + data: Map +): NotificationCompat.Builder { + + val groupNotificationBuilder = NotificationCompat.Builder(context, channelId) + .setStyle( + NotificationCompat.BigTextStyle() + .setSummaryText( + prepareText(group.substring(NotificationData.GROUP_PREFIX.length)) + ) + ) + .setGroup(group) + .setGroupSummary(true) + + if (data[NotificationData.ALERT_ONCE].toBoolean()) + groupNotificationBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + handleColor(context, groupNotificationBuilder, data) + handleSmallIcon(context, groupNotificationBuilder, data) + return groupNotificationBuilder +} + +fun prepareText( + text: String +): Spanned { + // Replace control char \r\n, \r, \n and also \r\n, \r, \n as text literals in strings to
+ val brText = text.replace("(\r\n|\r|\n)|(\\\\r\\\\n|\\\\r|\\\\n)".toRegex(), "
") + val emojiParsedText = EmojiParser.parseToUnicode(brText) + return HtmlCompat.fromHtml(emojiParsedText, HtmlCompat.FROM_HTML_MODE_LEGACY) +} + +fun handleColor( + context: Context, + builder: NotificationCompat.Builder, + data: Map +) { + val colorString = data["color"] + val color = parseColor(context, colorString, R.color.colorPrimary) + builder.color = color +} + +fun handleText( + builder: NotificationCompat.Builder, + data: Map +) { + data[NotificationData.TITLE]?.let { + builder.setContentTitle(prepareText(it)) + } + data[NotificationData.MESSAGE]?.let { + val text = prepareText(it) + builder.setContentText(text) + builder.setStyle(NotificationCompat.BigTextStyle().bigText(text)) + } +} diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 63032c3df..b1ee1a94f 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("com.github.triplet.play") kotlin("kapt") id("dagger.hilt.android.plugin") + id("com.google.gms.google-services") } android { @@ -127,4 +128,7 @@ dependencies { implementation("androidx.wear.watchface:watchface-complications-data-source-ktx:1.1.1") implementation("androidx.health:health-services-client:1.0.0-beta02") + + implementation(platform("com.google.firebase:firebase-bom:31.1.1")) + implementation("com.google.firebase:firebase-messaging") } diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 21e7c58a3..641a273d7 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -170,6 +170,14 @@ android:path="/updateTemplateTile" /> + + + + + + \ No newline at end of file diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt index c1761048d..620645f67 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt @@ -16,6 +16,7 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.De import io.homeassistant.companion.android.common.data.websocket.impl.entities.EntityRegistryResponse import io.homeassistant.companion.android.common.data.websocket.impl.entities.EntityRegistryUpdatedEvent import io.homeassistant.companion.android.data.SimplifiedEntity +import io.homeassistant.companion.android.onboarding.getMessagingToken import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -179,7 +180,8 @@ class HomePresenterImpl @Inject constructor( DeviceRegistration( "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", null, - null + getMessagingToken(), + false ) ) } catch (e: Exception) { diff --git a/wear/src/main/java/io/homeassistant/companion/android/notifications/FirebaseCloudMessagingService.kt b/wear/src/main/java/io/homeassistant/companion/android/notifications/FirebaseCloudMessagingService.kt new file mode 100755 index 000000000..e6594f382 --- /dev/null +++ b/wear/src/main/java/io/homeassistant/companion/android/notifications/FirebaseCloudMessagingService.kt @@ -0,0 +1,66 @@ +package io.homeassistant.companion.android.notifications + +import android.util.Log +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import dagger.hilt.android.AndroidEntryPoint +import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository +import io.homeassistant.companion.android.common.data.authentication.SessionState +import io.homeassistant.companion.android.common.data.integration.DeviceRegistration +import io.homeassistant.companion.android.common.data.integration.IntegrationRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class FirebaseCloudMessagingService : FirebaseMessagingService() { + companion object { + private const val TAG = "FCMService" + private const val SOURCE = "FCM" + } + + @Inject + lateinit var integrationUseCase: IntegrationRepository + + @Inject + lateinit var authenticationUseCase: AuthenticationRepository + + @Inject + lateinit var messagingManager: MessagingManager + + private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job()) + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + Log.d(TAG, "From: ${remoteMessage.from} and data: ${remoteMessage.data}") + + messagingManager.handleMessage(remoteMessage.data, SOURCE) + } + + /** + * Called if InstanceID token is updated. This may occur if the security of + * the previous token had been compromised. Note that this is called when the InstanceID token + * is initially generated so this is where you would retrieve the token. + */ + override fun onNewToken(token: String) { + mainScope.launch { + Log.d(TAG, "Refreshed token: $token") + if (authenticationUseCase.getSessionState() == SessionState.ANONYMOUS) { + Log.d(TAG, "Not trying to update registration since we aren't authenticated.") + return@launch + } + try { + integrationUseCase.updateRegistration( + DeviceRegistration( + pushToken = token, + pushWebsocket = false + ) + ) + } catch (e: Exception) { + // TODO: Store for update later + Log.e(TAG, "Issue updating token", e) + } + } + } +} diff --git a/wear/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt b/wear/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt new file mode 100755 index 000000000..3277e342c --- /dev/null +++ b/wear/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt @@ -0,0 +1,89 @@ +package io.homeassistant.companion.android.notifications + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.homeassistant.companion.android.common.notifications.NotificationData +import io.homeassistant.companion.android.common.notifications.getGroupNotificationBuilder +import io.homeassistant.companion.android.common.notifications.handleChannel +import io.homeassistant.companion.android.common.notifications.handleSmallIcon +import io.homeassistant.companion.android.common.notifications.handleText +import io.homeassistant.companion.android.common.util.cancelGroupIfNeeded +import io.homeassistant.companion.android.common.util.getActiveNotification +import io.homeassistant.companion.android.database.AppDatabase +import io.homeassistant.companion.android.database.notification.NotificationItem +import org.json.JSONObject +import javax.inject.Inject + +class MessagingManager @Inject constructor( + @ApplicationContext val context: Context, +) { + + companion object { + const val TAG = "MessagingManager" + } + + fun handleMessage(notificationData: Map, source: String) { + + val notificationDao = AppDatabase.getInstance(context).notificationDao() + val now = System.currentTimeMillis() + + val jsonObject = (notificationData as Map<*, *>?)?.let { JSONObject(it) } + val notificationRow = + NotificationItem(0, now, notificationData[NotificationData.MESSAGE].toString(), jsonObject.toString(), source) + notificationDao.add(notificationRow) + + sendNotification(notificationData, now) + } + + @SuppressLint("MissingPermission") + private fun sendNotification(data: Map, received: Long? = null) { + val notificationManagerCompat = NotificationManagerCompat.from(context) + + val tag = data["tag"] + val messageId = tag?.hashCode() ?: received?.toInt() ?: System.currentTimeMillis().toInt() + + var group = data["group"] + var groupId = 0 + var previousGroup = "" + var previousGroupId = 0 + if (!group.isNullOrBlank()) { + group = NotificationData.GROUP_PREFIX + group + groupId = group.hashCode() + } else { + val notification = notificationManagerCompat.getActiveNotification(tag, messageId) + if (notification != null && notification.isGroup) { + previousGroup = NotificationData.GROUP_PREFIX + notification.tag + previousGroupId = previousGroup.hashCode() + } + } + + val channelId = handleChannel(context, notificationManagerCompat, data) + + val notificationBuilder = NotificationCompat.Builder(context, channelId) + + handleSmallIcon(context, notificationBuilder, data) + + handleText(notificationBuilder, data) + + notificationManagerCompat.apply { + Log.d(TAG, "Show notification with tag \"$tag\" and id \"$messageId\"") + notify(tag, messageId, notificationBuilder.build()) + if (!group.isNullOrBlank()) { + Log.d(TAG, "Show group notification with tag \"$group\" and id \"$groupId\"") + notify(group, groupId, getGroupNotificationBuilder(context, channelId, group, data).build()) + } else { + if (previousGroup.isNotBlank()) { + Log.d( + TAG, + "Remove group notification with tag \"$previousGroup\" and id \"$previousGroupId\"" + ) + notificationManagerCompat.cancelGroupIfNeeded(previousGroup, previousGroupId) + } + } + } + } +} diff --git a/wear/src/main/java/io/homeassistant/companion/android/onboarding/MessagingToken.kt b/wear/src/main/java/io/homeassistant/companion/android/onboarding/MessagingToken.kt new file mode 100755 index 000000000..36bfe3ea6 --- /dev/null +++ b/wear/src/main/java/io/homeassistant/companion/android/onboarding/MessagingToken.kt @@ -0,0 +1,14 @@ +package io.homeassistant.companion.android.onboarding + +import android.util.Log +import com.google.firebase.messaging.FirebaseMessaging +import kotlinx.coroutines.tasks.await + +suspend fun getMessagingToken(): String { + return try { + FirebaseMessaging.getInstance().token.await() + } catch (e: Exception) { + Log.e("MessagingToken", "Issue getting token", e) + "" + } +} diff --git a/wear/src/main/java/io/homeassistant/companion/android/onboarding/integration/MobileAppIntegrationPresenterImpl.kt b/wear/src/main/java/io/homeassistant/companion/android/onboarding/integration/MobileAppIntegrationPresenterImpl.kt index 150a91d4c..f962d209d 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/onboarding/integration/MobileAppIntegrationPresenterImpl.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/onboarding/integration/MobileAppIntegrationPresenterImpl.kt @@ -6,6 +6,7 @@ import dagger.hilt.android.qualifiers.ActivityContext import io.homeassistant.companion.android.BuildConfig import io.homeassistant.companion.android.common.data.integration.DeviceRegistration import io.homeassistant.companion.android.common.data.integration.IntegrationRepository +import io.homeassistant.companion.android.onboarding.getMessagingToken import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -28,7 +29,9 @@ class MobileAppIntegrationPresenterImpl @Inject constructor( private suspend fun createRegistration(deviceName: String): DeviceRegistration { return DeviceRegistration( "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", - deviceName + deviceName, + getMessagingToken(), + false ) } diff --git a/wear/src/main/java/io/homeassistant/companion/android/phone/PhoneSettingsListener.kt b/wear/src/main/java/io/homeassistant/companion/android/phone/PhoneSettingsListener.kt index d96c771b6..3bd27951d 100755 --- a/wear/src/main/java/io/homeassistant/companion/android/phone/PhoneSettingsListener.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/phone/PhoneSettingsListener.kt @@ -24,6 +24,7 @@ import io.homeassistant.companion.android.database.wear.getAll import io.homeassistant.companion.android.database.wear.replaceAll import io.homeassistant.companion.android.home.HomeActivity import io.homeassistant.companion.android.home.HomePresenterImpl +import io.homeassistant.companion.android.onboarding.getMessagingToken import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -124,7 +125,9 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange integrationUseCase.registerDevice( DeviceRegistration( "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", - deviceName + deviceName, + getMessagingToken(), + false ) )