mirror of
https://github.com/home-assistant/android
synced 2024-10-04 23:29:31 +00:00
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
This commit is contained in:
parent
5cd74c0179
commit
346ef3da5f
|
@ -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`
|
||||||
- `io.homeassistant.companion.android.minimal.debug`
|
- `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.
|
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.
|
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'**.
|
8. If the build is successful, you can run the app by doing the following: click **Run** -> **Run 'app'**.
|
||||||
|
|
|
@ -143,9 +143,6 @@ dependencies {
|
||||||
implementation("org.altbeacon:android-beacon-library:2.19.5")
|
implementation("org.altbeacon:android-beacon-library:2.19.5")
|
||||||
implementation("com.maltaisn:icondialog:3.3.0")
|
implementation("com.maltaisn:icondialog:3.3.0")
|
||||||
implementation("com.maltaisn:iconpack-community-material:5.3.45")
|
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-stdlib-jdk8:1.7.20")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect: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-location:21.0.1")
|
||||||
"fullImplementation"("com.google.android.gms:play-services-home:16.0.0")
|
"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"("com.google.firebase:firebase-messaging")
|
||||||
"fullImplementation"("io.sentry:sentry-android:6.11.0")
|
"fullImplementation"("io.sentry:sentry-android:6.11.0")
|
||||||
"fullImplementation"("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4")
|
"fullImplementation"("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4")
|
||||||
|
|
|
@ -13,7 +13,6 @@ import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.Color
|
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
|
@ -29,7 +28,6 @@ import android.os.PowerManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.speech.tts.TextToSpeech
|
import android.speech.tts.TextToSpeech
|
||||||
import android.speech.tts.UtteranceProgressListener
|
import android.speech.tts.UtteranceProgressListener
|
||||||
import android.text.Spanned
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
|
@ -41,22 +39,27 @@ import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.app.RemoteInput
|
import androidx.core.app.RemoteInput
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.text.HtmlCompat
|
|
||||||
import androidx.core.text.isDigitsOnly
|
import androidx.core.text.isDigitsOnly
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
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 dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import io.homeassistant.companion.android.R
|
import io.homeassistant.companion.android.R
|
||||||
import io.homeassistant.companion.android.common.data.authentication.AuthenticationRepository
|
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.integration.IntegrationRepository
|
||||||
import io.homeassistant.companion.android.common.data.url.UrlRepository
|
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.sensors.BluetoothSensorManager
|
||||||
import io.homeassistant.companion.android.common.util.cancel
|
import io.homeassistant.companion.android.common.util.cancel
|
||||||
import io.homeassistant.companion.android.common.util.cancelGroupIfNeeded
|
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.common.util.getActiveNotification
|
||||||
import io.homeassistant.companion.android.database.notification.NotificationDao
|
import io.homeassistant.companion.android.database.notification.NotificationDao
|
||||||
import io.homeassistant.companion.android.database.notification.NotificationItem
|
import io.homeassistant.companion.android.database.notification.NotificationItem
|
||||||
|
@ -86,7 +89,6 @@ import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.util.Locale
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import io.homeassistant.companion.android.common.R as commonR
|
import io.homeassistant.companion.android.common.R as commonR
|
||||||
|
@ -111,25 +113,17 @@ class MessagingManager @Inject constructor(
|
||||||
const val SETTINGS_PREFIX = "settings://"
|
const val SETTINGS_PREFIX = "settings://"
|
||||||
const val NOTIFICATION_HISTORY = "notification_history"
|
const val NOTIFICATION_HISTORY = "notification_history"
|
||||||
|
|
||||||
const val TITLE = "title"
|
|
||||||
const val MESSAGE = "message"
|
|
||||||
const val SUBJECT = "subject"
|
const val SUBJECT = "subject"
|
||||||
const val IMPORTANCE = "importance"
|
|
||||||
const val TIMEOUT = "timeout"
|
const val TIMEOUT = "timeout"
|
||||||
const val IMAGE_URL = "image"
|
const val IMAGE_URL = "image"
|
||||||
const val ICON_URL = "icon_url"
|
const val ICON_URL = "icon_url"
|
||||||
const val VIDEO_URL = "video"
|
const val VIDEO_URL = "video"
|
||||||
const val VISIBILITY = "visibility"
|
const val VISIBILITY = "visibility"
|
||||||
const val LED_COLOR = "ledColor"
|
|
||||||
const val VIBRATION_PATTERN = "vibrationPattern"
|
|
||||||
const val PERSISTENT = "persistent"
|
const val PERSISTENT = "persistent"
|
||||||
const val CHRONOMETER = "chronometer"
|
const val CHRONOMETER = "chronometer"
|
||||||
const val WHEN = "when"
|
const val WHEN = "when"
|
||||||
const val GROUP_PREFIX = "group_"
|
|
||||||
const val KEY_TEXT_REPLY = "key_text_reply"
|
const val KEY_TEXT_REPLY = "key_text_reply"
|
||||||
const val ALERT_ONCE = "alert_once"
|
|
||||||
const val INTENT_CLASS_NAME = "intent_class_name"
|
const val INTENT_CLASS_NAME = "intent_class_name"
|
||||||
const val NOTIFICATION_ICON = "notification_icon"
|
|
||||||
const val URI = "URI"
|
const val URI = "URI"
|
||||||
const val REPLY = "REPLY"
|
const val REPLY = "REPLY"
|
||||||
const val BLE_ADVERTISE = "ble_advertise"
|
const val BLE_ADVERTISE = "ble_advertise"
|
||||||
|
@ -138,7 +132,6 @@ class MessagingManager @Inject constructor(
|
||||||
const val PACKAGE_NAME = "package_name"
|
const val PACKAGE_NAME = "package_name"
|
||||||
const val COMMAND = "command"
|
const val COMMAND = "command"
|
||||||
const val TTS_TEXT = "tts_text"
|
const val TTS_TEXT = "tts_text"
|
||||||
const val CHANNEL = "channel"
|
|
||||||
const val CONFIRMATION = "confirmation"
|
const val CONFIRMATION = "confirmation"
|
||||||
|
|
||||||
// special intent constants
|
// special intent constants
|
||||||
|
@ -186,16 +179,6 @@ class MessagingManager @Inject constructor(
|
||||||
const val RM_SILENT = "silent"
|
const val RM_SILENT = "silent"
|
||||||
const val RM_VIBRATE = "vibrate"
|
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
|
// Enable/Disable Commands
|
||||||
const val TURN_ON = "turn_on"
|
const val TURN_ON = "turn_on"
|
||||||
const val TURN_OFF = "turn_off"
|
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 DND_COMMANDS = listOf(DND_ALARMS_ONLY, DND_ALL, DND_NONE, DND_PRIORITY_ONLY)
|
||||||
val RM_COMMANDS = listOf(RM_NORMAL, RM_SILENT, RM_VIBRATE)
|
val RM_COMMANDS = listOf(RM_NORMAL, RM_SILENT, RM_VIBRATE)
|
||||||
val CHANNEL_VOLUME_STREAM = listOf(
|
val CHANNEL_VOLUME_STREAM = listOf(
|
||||||
ALARM_STREAM, MUSIC_STREAM, NOTIFICATION_STREAM, RING_STREAM, CALL_STREAM,
|
NotificationData.ALARM_STREAM,
|
||||||
SYSTEM_STREAM, DTMF_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 ENABLE_COMMANDS = listOf(TURN_OFF, TURN_ON)
|
||||||
val FORCE_COMMANDS = listOf(FORCE_OFF, FORCE_ON)
|
val FORCE_COMMANDS = listOf(FORCE_OFF, FORCE_ON)
|
||||||
|
@ -315,7 +303,7 @@ class MessagingManager @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
val jsonObject = JSONObject(jsonData)
|
val jsonObject = JSONObject(jsonData)
|
||||||
val notificationRow =
|
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)
|
notificationId = notificationDao.add(notificationRow)
|
||||||
|
|
||||||
val confirmation = jsonData[CONFIRMATION]?.toBoolean() ?: false
|
val confirmation = jsonData[CONFIRMATION]?.toBoolean() ?: false
|
||||||
|
@ -331,25 +319,25 @@ class MessagingManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
jsonData[MESSAGE] == REQUEST_LOCATION_UPDATE -> {
|
jsonData[NotificationData.MESSAGE] == REQUEST_LOCATION_UPDATE -> {
|
||||||
Log.d(TAG, "Request location update")
|
Log.d(TAG, "Request location update")
|
||||||
requestAccurateLocationUpdate()
|
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"]}")
|
Log.d(TAG, "Clearing notification with tag: ${jsonData["tag"]}")
|
||||||
clearNotification(jsonData["tag"]!!)
|
clearNotification(jsonData["tag"]!!)
|
||||||
}
|
}
|
||||||
jsonData[MESSAGE] == REMOVE_CHANNEL && !jsonData[CHANNEL].isNullOrBlank() -> {
|
jsonData[NotificationData.MESSAGE] == REMOVE_CHANNEL && !jsonData[NotificationData.CHANNEL].isNullOrBlank() -> {
|
||||||
Log.d(TAG, "Removing Notification channel ${jsonData[CHANNEL]}")
|
Log.d(TAG, "Removing Notification channel ${jsonData[NotificationData.CHANNEL]}")
|
||||||
removeNotificationChannel(jsonData[CHANNEL]!!)
|
removeNotificationChannel(jsonData[NotificationData.CHANNEL]!!)
|
||||||
}
|
}
|
||||||
jsonData[MESSAGE] == TTS -> {
|
jsonData[NotificationData.MESSAGE] == TTS -> {
|
||||||
Log.d(TAG, "Sending notification title to TTS")
|
Log.d(TAG, "Sending notification title to TTS")
|
||||||
speakNotification(jsonData)
|
speakNotification(jsonData)
|
||||||
}
|
}
|
||||||
jsonData[MESSAGE] in DEVICE_COMMANDS -> {
|
jsonData[NotificationData.MESSAGE] in DEVICE_COMMANDS -> {
|
||||||
Log.d(TAG, "Processing device command")
|
Log.d(TAG, "Processing device command")
|
||||||
when (jsonData[MESSAGE]) {
|
when (jsonData[NotificationData.MESSAGE]) {
|
||||||
COMMAND_DND -> {
|
COMMAND_DND -> {
|
||||||
if (jsonData[COMMAND] in DND_COMMANDS) {
|
if (jsonData[COMMAND] in DND_COMMANDS) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||||
|
@ -667,7 +655,7 @@ class MessagingManager @Inject constructor(
|
||||||
if (it == TextToSpeech.SUCCESS) {
|
if (it == TextToSpeech.SUCCESS) {
|
||||||
val listener = object : UtteranceProgressListener() {
|
val listener = object : UtteranceProgressListener() {
|
||||||
override fun onStart(p0: String?) {
|
override fun onStart(p0: String?) {
|
||||||
if (data[MEDIA_STREAM] == ALARM_STREAM_MAX)
|
if (data[MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX)
|
||||||
audioManager?.setStreamVolume(
|
audioManager?.setStreamVolume(
|
||||||
AudioManager.STREAM_ALARM,
|
AudioManager.STREAM_ALARM,
|
||||||
maxAlarmVolume!!,
|
maxAlarmVolume!!,
|
||||||
|
@ -678,7 +666,7 @@ class MessagingManager @Inject constructor(
|
||||||
override fun onDone(p0: String?) {
|
override fun onDone(p0: String?) {
|
||||||
textToSpeech?.stop()
|
textToSpeech?.stop()
|
||||||
textToSpeech?.shutdown()
|
textToSpeech?.shutdown()
|
||||||
if (data[MEDIA_STREAM] == ALARM_STREAM_MAX)
|
if (data[MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX)
|
||||||
audioManager?.setStreamVolume(
|
audioManager?.setStreamVolume(
|
||||||
AudioManager.STREAM_ALARM,
|
AudioManager.STREAM_ALARM,
|
||||||
currentAlarmVolume!!,
|
currentAlarmVolume!!,
|
||||||
|
@ -689,7 +677,7 @@ class MessagingManager @Inject constructor(
|
||||||
override fun onError(p0: String?) {
|
override fun onError(p0: String?) {
|
||||||
textToSpeech?.stop()
|
textToSpeech?.stop()
|
||||||
textToSpeech?.shutdown()
|
textToSpeech?.shutdown()
|
||||||
if (data[MEDIA_STREAM] == ALARM_STREAM_MAX)
|
if (data[MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX)
|
||||||
audioManager?.setStreamVolume(
|
audioManager?.setStreamVolume(
|
||||||
AudioManager.STREAM_ALARM,
|
AudioManager.STREAM_ALARM,
|
||||||
currentAlarmVolume!!,
|
currentAlarmVolume!!,
|
||||||
|
@ -698,7 +686,7 @@ class MessagingManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop(utteranceId: String?, interrupted: Boolean) {
|
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?.setStreamVolume(
|
||||||
AudioManager.STREAM_ALARM,
|
AudioManager.STREAM_ALARM,
|
||||||
currentAlarmVolume!!,
|
currentAlarmVolume!!,
|
||||||
|
@ -707,7 +695,7 @@ class MessagingManager @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
textToSpeech?.setOnUtteranceProgressListener(listener)
|
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()
|
val audioAttributes = AudioAttributes.Builder()
|
||||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
.setUsage(AudioAttributes.USAGE_ALARM)
|
.setUsage(AudioAttributes.USAGE_ALARM)
|
||||||
|
@ -729,7 +717,7 @@ class MessagingManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDeviceCommands(data: Map<String, String>) {
|
private fun handleDeviceCommands(data: Map<String, String>) {
|
||||||
val message = data[MESSAGE]
|
val message = data[NotificationData.MESSAGE]
|
||||||
val command = data[COMMAND]
|
val command = data[COMMAND]
|
||||||
when (message) {
|
when (message) {
|
||||||
COMMAND_DND -> {
|
COMMAND_DND -> {
|
||||||
|
@ -737,7 +725,7 @@ class MessagingManager @Inject constructor(
|
||||||
val notificationManager =
|
val notificationManager =
|
||||||
context.getSystemService<NotificationManager>()
|
context.getSystemService<NotificationManager>()
|
||||||
if (notificationManager?.isNotificationPolicyAccessGranted == false) {
|
if (notificationManager?.isNotificationPolicyAccessGranted == false) {
|
||||||
notifyMissingPermission(data[MESSAGE].toString())
|
notifyMissingPermission(message.toString())
|
||||||
} else {
|
} else {
|
||||||
when (command) {
|
when (command) {
|
||||||
DND_ALARMS_ONLY -> notificationManager?.setInterruptionFilter(
|
DND_ALARMS_ONLY -> notificationManager?.setInterruptionFilter(
|
||||||
|
@ -761,7 +749,7 @@ class MessagingManager @Inject constructor(
|
||||||
val notificationManager =
|
val notificationManager =
|
||||||
context.getSystemService<NotificationManager>()
|
context.getSystemService<NotificationManager>()
|
||||||
if (notificationManager?.isNotificationPolicyAccessGranted == false) {
|
if (notificationManager?.isNotificationPolicyAccessGranted == false) {
|
||||||
notifyMissingPermission(data[MESSAGE].toString())
|
notifyMissingPermission(message.toString())
|
||||||
} else {
|
} else {
|
||||||
processRingerMode(audioManager!!, command)
|
processRingerMode(audioManager!!, command)
|
||||||
}
|
}
|
||||||
|
@ -800,7 +788,7 @@ class MessagingManager @Inject constructor(
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
val notificationManager = context.getSystemService<NotificationManager>()
|
val notificationManager = context.getSystemService<NotificationManager>()
|
||||||
if (notificationManager?.isNotificationPolicyAccessGranted == false) {
|
if (notificationManager?.isNotificationPolicyAccessGranted == false) {
|
||||||
notifyMissingPermission(data[MESSAGE].toString())
|
notifyMissingPermission(message.toString())
|
||||||
} else {
|
} else {
|
||||||
processStreamVolume(
|
processStreamVolume(
|
||||||
audioManager!!,
|
audioManager!!,
|
||||||
|
@ -825,7 +813,7 @@ class MessagingManager @Inject constructor(
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.e(TAG, "Missing Bluetooth permissions, notifying user to grant permissions")
|
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 -> {
|
COMMAND_ACTIVITY -> {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (!Settings.canDrawOverlays(context))
|
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) {
|
else if (ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED && data["tag"] == Intent.ACTION_CALL) {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
|
@ -933,7 +921,7 @@ class MessagingManager @Inject constructor(
|
||||||
COMMAND_WEBVIEW -> {
|
COMMAND_WEBVIEW -> {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (!Settings.canDrawOverlays(context))
|
if (!Settings.canDrawOverlays(context))
|
||||||
notifyMissingPermission(data[MESSAGE].toString())
|
notifyMissingPermission(message.toString())
|
||||||
else
|
else
|
||||||
openWebview(command)
|
openWebview(command)
|
||||||
} else
|
} else
|
||||||
|
@ -963,7 +951,7 @@ class MessagingManager @Inject constructor(
|
||||||
if (!NotificationManagerCompat.getEnabledListenerPackages(context)
|
if (!NotificationManagerCompat.getEnabledListenerPackages(context)
|
||||||
.contains(context.packageName)
|
.contains(context.packageName)
|
||||||
)
|
)
|
||||||
notifyMissingPermission(data[MESSAGE].toString())
|
notifyMissingPermission(message.toString())
|
||||||
else {
|
else {
|
||||||
processMediaCommand(data)
|
processMediaCommand(data)
|
||||||
}
|
}
|
||||||
|
@ -972,7 +960,7 @@ class MessagingManager @Inject constructor(
|
||||||
COMMAND_LAUNCH_APP -> {
|
COMMAND_LAUNCH_APP -> {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (!Settings.canDrawOverlays(context))
|
if (!Settings.canDrawOverlays(context))
|
||||||
notifyMissingPermission(data[MESSAGE].toString())
|
notifyMissingPermission(message.toString())
|
||||||
else
|
else
|
||||||
launchApp(data)
|
launchApp(data)
|
||||||
} else
|
} else
|
||||||
|
@ -990,7 +978,7 @@ class MessagingManager @Inject constructor(
|
||||||
if (!processScreenCommands(data))
|
if (!processScreenCommands(data))
|
||||||
mainScope.launch { sendNotification(data) }
|
mainScope.launch { sendNotification(data) }
|
||||||
} else
|
} else
|
||||||
notifyMissingPermission(data[MESSAGE].toString())
|
notifyMissingPermission(message.toString())
|
||||||
} else if (!processScreenCommands(data))
|
} else if (!processScreenCommands(data))
|
||||||
mainScope.launch { sendNotification(data) }
|
mainScope.launch { sendNotification(data) }
|
||||||
}
|
}
|
||||||
|
@ -1063,24 +1051,24 @@ class MessagingManager @Inject constructor(
|
||||||
var previousGroup = ""
|
var previousGroup = ""
|
||||||
var previousGroupId = 0
|
var previousGroupId = 0
|
||||||
if (!group.isNullOrBlank()) {
|
if (!group.isNullOrBlank()) {
|
||||||
group = GROUP_PREFIX + group
|
group = NotificationData.GROUP_PREFIX + group
|
||||||
groupId = group.hashCode()
|
groupId = group.hashCode()
|
||||||
} else {
|
} else {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
|
||||||
val notification = notificationManagerCompat.getActiveNotification(tag, messageId)
|
val notification = notificationManagerCompat.getActiveNotification(tag, messageId)
|
||||||
if (notification != null && notification.isGroup) {
|
if (notification != null && notification.isGroup) {
|
||||||
previousGroup = GROUP_PREFIX + notification.tag
|
previousGroup = NotificationData.GROUP_PREFIX + notification.tag
|
||||||
previousGroupId = previousGroup.hashCode()
|
previousGroupId = previousGroup.hashCode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val channelId = handleChannel(notificationManagerCompat, data)
|
val channelId = handleChannel(context, notificationManagerCompat, data)
|
||||||
|
|
||||||
val notificationBuilder = NotificationCompat.Builder(context, channelId)
|
val notificationBuilder = NotificationCompat.Builder(context, channelId)
|
||||||
|
|
||||||
handleSmallIcon(notificationBuilder, data)
|
handleSmallIcon(context, notificationBuilder, data)
|
||||||
|
|
||||||
handleSound(notificationBuilder, data)
|
handleSound(notificationBuilder, data)
|
||||||
|
|
||||||
|
@ -1088,11 +1076,11 @@ class MessagingManager @Inject constructor(
|
||||||
|
|
||||||
handleLargeIcon(notificationBuilder, data)
|
handleLargeIcon(notificationBuilder, data)
|
||||||
|
|
||||||
handleGroup(notificationBuilder, group, data[ALERT_ONCE].toBoolean())
|
handleGroup(notificationBuilder, group, data[NotificationData.ALERT_ONCE].toBoolean())
|
||||||
|
|
||||||
handleTimeout(notificationBuilder, data)
|
handleTimeout(notificationBuilder, data)
|
||||||
|
|
||||||
handleColor(notificationBuilder, data)
|
handleColor(context, notificationBuilder, data)
|
||||||
|
|
||||||
handleSticky(notificationBuilder, data)
|
handleSticky(notificationBuilder, data)
|
||||||
|
|
||||||
|
@ -1127,7 +1115,7 @@ class MessagingManager @Inject constructor(
|
||||||
notify(tag, messageId, notificationBuilder.build())
|
notify(tag, messageId, notificationBuilder.build())
|
||||||
if (!group.isNullOrBlank()) {
|
if (!group.isNullOrBlank()) {
|
||||||
Log.d(TAG, "Show group notification with tag \"$group\" and id \"$groupId\"")
|
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 {
|
} else {
|
||||||
if (!previousGroup.isBlank()) {
|
if (!previousGroup.isBlank()) {
|
||||||
Log.d(
|
Log.d(
|
||||||
|
@ -1163,22 +1151,6 @@ class MessagingManager @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSmallIcon(
|
|
||||||
builder: NotificationCompat.Builder,
|
|
||||||
data: Map<String, String>
|
|
||||||
) {
|
|
||||||
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(
|
private fun handleContentIntent(
|
||||||
builder: NotificationCompat.Builder,
|
builder: NotificationCompat.Builder,
|
||||||
messageId: Int,
|
messageId: Int,
|
||||||
|
@ -1226,34 +1198,11 @@ class MessagingManager @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getGroupNotificationBuilder(
|
|
||||||
channelId: String,
|
|
||||||
group: String,
|
|
||||||
data: Map<String, String>
|
|
||||||
): 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(
|
private fun handleSound(
|
||||||
builder: NotificationCompat.Builder,
|
builder: NotificationCompat.Builder,
|
||||||
data: Map<String, String>
|
data: Map<String, String>
|
||||||
) {
|
) {
|
||||||
if (data[CHANNEL] == ALARM_STREAM) {
|
if (data[NotificationData.CHANNEL] == NotificationData.ALARM_STREAM) {
|
||||||
builder.setCategory(Notification.CATEGORY_ALARM)
|
builder.setCategory(Notification.CATEGORY_ALARM)
|
||||||
builder.setSound(
|
builder.setSound(
|
||||||
RingtoneManager.getActualDefaultRingtoneUri(
|
RingtoneManager.getActualDefaultRingtoneUri(
|
||||||
|
@ -1269,38 +1218,17 @@ class MessagingManager @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
builder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
|
builder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
|
||||||
}
|
}
|
||||||
if (data[ALERT_ONCE].toBoolean())
|
if (data[NotificationData.ALERT_ONCE].toBoolean())
|
||||||
builder.setOnlyAlertOnce(true)
|
builder.setOnlyAlertOnce(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleColor(
|
|
||||||
builder: NotificationCompat.Builder,
|
|
||||||
data: Map<String, String>
|
|
||||||
) {
|
|
||||||
|
|
||||||
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(
|
private fun handleLegacyLedColor(
|
||||||
builder: NotificationCompat.Builder,
|
builder: NotificationCompat.Builder,
|
||||||
data: Map<String, String>
|
data: Map<String, String>
|
||||||
) {
|
) {
|
||||||
val ledColor = data[LED_COLOR]
|
val ledColor = data[NotificationData.LED_COLOR]
|
||||||
if (!ledColor.isNullOrBlank()) {
|
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,
|
builder: NotificationCompat.Builder,
|
||||||
data: Map<String, String>
|
data: Map<String, String>
|
||||||
) {
|
) {
|
||||||
val vibrationPattern = data[VIBRATION_PATTERN]
|
val vibrationPattern = data[NotificationData.VIBRATION_PATTERN]
|
||||||
if (!vibrationPattern.isNullOrBlank()) {
|
if (!vibrationPattern.isNullOrBlank()) {
|
||||||
val arrVibrationPattern = parseVibrationPattern(vibrationPattern)
|
val arrVibrationPattern = parseVibrationPattern(vibrationPattern)
|
||||||
if (arrVibrationPattern.isNotEmpty()) {
|
if (arrVibrationPattern.isNotEmpty()) {
|
||||||
|
@ -1323,7 +1251,7 @@ class MessagingManager @Inject constructor(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// Use importance property for legacy priority support
|
// Use importance property for legacy priority support
|
||||||
val priority = data[IMPORTANCE]
|
val priority = data[NotificationData.IMPORTANCE]
|
||||||
|
|
||||||
when (priority) {
|
when (priority) {
|
||||||
"high" -> {
|
"high" -> {
|
||||||
|
@ -1344,32 +1272,6 @@ class MessagingManager @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
|
||||||
private fun handleImportance(
|
|
||||||
data: Map<String, String>
|
|
||||||
): 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(
|
private fun handleTimeout(
|
||||||
builder: NotificationCompat.Builder,
|
builder: NotificationCompat.Builder,
|
||||||
data: Map<String, String>
|
data: Map<String, String>
|
||||||
|
@ -1407,29 +1309,6 @@ class MessagingManager @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleText(
|
|
||||||
builder: NotificationCompat.Builder,
|
|
||||||
data: Map<String, String>
|
|
||||||
) {
|
|
||||||
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 <br>
|
|
||||||
var brText = text.replace("(\r\n|\r|\n)|(\\\\r\\\\n|\\\\r|\\\\n)".toRegex(), "<br>")
|
|
||||||
var emojiParsedText = EmojiParser.parseToUnicode(brText)
|
|
||||||
return HtmlCompat.fromHtml(emojiParsedText, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun handleLargeIcon(
|
private suspend fun handleLargeIcon(
|
||||||
builder: NotificationCompat.Builder,
|
builder: NotificationCompat.Builder,
|
||||||
data: Map<String, String>
|
data: Map<String, String>
|
||||||
|
@ -1509,11 +1388,11 @@ class MessagingManager @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data[TITLE]?.let { rawTitle ->
|
data[NotificationData.TITLE]?.let { rawTitle ->
|
||||||
remoteViewFlipper.setTextViewText(R.id.title, rawTitle)
|
remoteViewFlipper.setTextViewText(R.id.title, rawTitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
data[MESSAGE]?.let { rawMessage ->
|
data[NotificationData.MESSAGE]?.let { rawMessage ->
|
||||||
remoteViewFlipper.setTextViewText(R.id.info, rawMessage)
|
remoteViewFlipper.setTextViewText(R.id.info, rawMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1777,114 +1656,6 @@ class MessagingManager @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleChannel(
|
|
||||||
notificationManagerCompat: NotificationManagerCompat,
|
|
||||||
data: Map<String, String>
|
|
||||||
): 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<String, String>,
|
|
||||||
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<String, String>,
|
|
||||||
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<Long>()
|
|
||||||
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)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
private fun requestDNDPermission() {
|
private fun requestDNDPermission() {
|
||||||
val intent =
|
val intent =
|
||||||
|
@ -2002,13 +1773,13 @@ class MessagingManager @Inject constructor(
|
||||||
|
|
||||||
private fun processStreamVolume(audioManager: AudioManager, stream: String, volume: Int) {
|
private fun processStreamVolume(audioManager: AudioManager, stream: String, volume: Int) {
|
||||||
when (stream) {
|
when (stream) {
|
||||||
ALARM_STREAM -> adjustVolumeStream(AudioManager.STREAM_ALARM, volume, audioManager)
|
NotificationData.ALARM_STREAM -> adjustVolumeStream(AudioManager.STREAM_ALARM, volume, audioManager)
|
||||||
MUSIC_STREAM -> adjustVolumeStream(AudioManager.STREAM_MUSIC, volume, audioManager)
|
NotificationData.MUSIC_STREAM -> adjustVolumeStream(AudioManager.STREAM_MUSIC, volume, audioManager)
|
||||||
NOTIFICATION_STREAM -> adjustVolumeStream(AudioManager.STREAM_NOTIFICATION, volume, audioManager)
|
NotificationData.NOTIFICATION_STREAM -> adjustVolumeStream(AudioManager.STREAM_NOTIFICATION, volume, audioManager)
|
||||||
RING_STREAM -> adjustVolumeStream(AudioManager.STREAM_RING, volume, audioManager)
|
NotificationData.RING_STREAM -> adjustVolumeStream(AudioManager.STREAM_RING, volume, audioManager)
|
||||||
CALL_STREAM -> adjustVolumeStream(AudioManager.STREAM_VOICE_CALL, volume, audioManager)
|
NotificationData.CALL_STREAM -> adjustVolumeStream(AudioManager.STREAM_VOICE_CALL, volume, audioManager)
|
||||||
SYSTEM_STREAM -> adjustVolumeStream(AudioManager.STREAM_SYSTEM, volume, audioManager)
|
NotificationData.SYSTEM_STREAM -> adjustVolumeStream(AudioManager.STREAM_SYSTEM, volume, audioManager)
|
||||||
DTMF_STREAM -> adjustVolumeStream(AudioManager.STREAM_DTMF, volume, audioManager)
|
NotificationData.DTMF_STREAM -> adjustVolumeStream(AudioManager.STREAM_DTMF, volume, audioManager)
|
||||||
else -> Log.d(TAG, "Skipping command due to invalid channel stream")
|
else -> Log.d(TAG, "Skipping command due to invalid channel stream")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2160,12 +1931,12 @@ class MessagingManager @Inject constructor(
|
||||||
val contentResolver = context.contentResolver
|
val contentResolver = context.contentResolver
|
||||||
val success = Settings.System.putInt(
|
val success = Settings.System.putInt(
|
||||||
contentResolver,
|
contentResolver,
|
||||||
when (data[MESSAGE].toString()) {
|
when (data[NotificationData.MESSAGE].toString()) {
|
||||||
COMMAND_SCREEN_BRIGHTNESS_LEVEL -> Settings.System.SCREEN_BRIGHTNESS
|
COMMAND_SCREEN_BRIGHTNESS_LEVEL -> Settings.System.SCREEN_BRIGHTNESS
|
||||||
COMMAND_AUTO_SCREEN_BRIGHTNESS -> Settings.System.SCREEN_BRIGHTNESS_MODE
|
COMMAND_AUTO_SCREEN_BRIGHTNESS -> Settings.System.SCREEN_BRIGHTNESS_MODE
|
||||||
else -> Settings.System.SCREEN_OFF_TIMEOUT
|
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_SCREEN_BRIGHTNESS_LEVEL -> command!!.toInt().coerceIn(0, 255)
|
||||||
COMMAND_AUTO_SCREEN_BRIGHTNESS -> {
|
COMMAND_AUTO_SCREEN_BRIGHTNESS -> {
|
||||||
if (command == TURN_ON)
|
if (command == TURN_ON)
|
||||||
|
@ -2188,7 +1959,7 @@ class MessagingManager @Inject constructor(
|
||||||
if (context.applicationInfo.processName == item.processName) {
|
if (context.applicationInfo.processName == item.processName) {
|
||||||
if (item.importance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
|
if (item.importance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
|
||||||
val data =
|
val data =
|
||||||
mutableMapOf(MESSAGE to context.getString(commonR.string.missing_command_permission))
|
mutableMapOf(NotificationData.MESSAGE to context.getString(commonR.string.missing_command_permission))
|
||||||
runBlocking {
|
runBlocking {
|
||||||
sendNotification(data)
|
sendNotification(data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,4 +77,8 @@ dependencies {
|
||||||
|
|
||||||
implementation("com.mikepenz:iconics-core:5.4.0")
|
implementation("com.mikepenz:iconics-core:5.4.0")
|
||||||
implementation("com.mikepenz:community-material-typeface:7.0.96.0-kotlin@aar")
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,5 +3,6 @@ package io.homeassistant.companion.android.common.data.integration
|
||||||
data class DeviceRegistration(
|
data class DeviceRegistration(
|
||||||
val appVersion: String? = null,
|
val appVersion: String? = null,
|
||||||
val deviceName: String? = null,
|
val deviceName: String? = null,
|
||||||
var pushToken: String? = null
|
var pushToken: String? = null,
|
||||||
|
var pushWebsocket: Boolean = true
|
||||||
)
|
)
|
||||||
|
|
|
@ -759,7 +759,7 @@ class IntegrationRepositoryImpl @Inject constructor(
|
||||||
val oldDeviceRegistration = getRegistration()
|
val oldDeviceRegistration = getRegistration()
|
||||||
val pushToken = deviceRegistration.pushToken ?: oldDeviceRegistration.pushToken
|
val pushToken = deviceRegistration.pushToken ?: oldDeviceRegistration.pushToken
|
||||||
|
|
||||||
val appData = mutableMapOf<String, Any>("push_websocket_channel" to true)
|
val appData = mutableMapOf<String, Any>("push_websocket_channel" to deviceRegistration.pushWebsocket)
|
||||||
if (!pushToken.isNullOrBlank()) {
|
if (!pushToken.isNullOrBlank()) {
|
||||||
appData["push_url"] = PUSH_URL
|
appData["push_url"] = PUSH_URL
|
||||||
appData["push_token"] = pushToken
|
appData["push_token"] = pushToken
|
||||||
|
|
|
@ -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, String>
|
||||||
|
): 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<String, String>
|
||||||
|
): 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<String, String>,
|
||||||
|
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<String, String>,
|
||||||
|
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<Long>()
|
||||||
|
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<String, String>
|
||||||
|
) {
|
||||||
|
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<String, String>
|
||||||
|
): 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 <br>
|
||||||
|
val brText = text.replace("(\r\n|\r|\n)|(\\\\r\\\\n|\\\\r|\\\\n)".toRegex(), "<br>")
|
||||||
|
val emojiParsedText = EmojiParser.parseToUnicode(brText)
|
||||||
|
return HtmlCompat.fromHtml(emojiParsedText, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleColor(
|
||||||
|
context: Context,
|
||||||
|
builder: NotificationCompat.Builder,
|
||||||
|
data: Map<String, String>
|
||||||
|
) {
|
||||||
|
val colorString = data["color"]
|
||||||
|
val color = parseColor(context, colorString, R.color.colorPrimary)
|
||||||
|
builder.color = color
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleText(
|
||||||
|
builder: NotificationCompat.Builder,
|
||||||
|
data: Map<String, String>
|
||||||
|
) {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ plugins {
|
||||||
id("com.github.triplet.play")
|
id("com.github.triplet.play")
|
||||||
kotlin("kapt")
|
kotlin("kapt")
|
||||||
id("dagger.hilt.android.plugin")
|
id("dagger.hilt.android.plugin")
|
||||||
|
id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -127,4 +128,7 @@ dependencies {
|
||||||
implementation("androidx.wear.watchface:watchface-complications-data-source-ktx:1.1.1")
|
implementation("androidx.wear.watchface:watchface-complications-data-source-ktx:1.1.1")
|
||||||
|
|
||||||
implementation("androidx.health:health-services-client:1.0.0-beta02")
|
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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,6 +170,14 @@
|
||||||
android:path="/updateTemplateTile" />
|
android:path="/updateTemplateTile" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".notifications.FirebaseCloudMessagingService"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -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.EntityRegistryResponse
|
||||||
import io.homeassistant.companion.android.common.data.websocket.impl.entities.EntityRegistryUpdatedEvent
|
import io.homeassistant.companion.android.common.data.websocket.impl.entities.EntityRegistryUpdatedEvent
|
||||||
import io.homeassistant.companion.android.data.SimplifiedEntity
|
import io.homeassistant.companion.android.data.SimplifiedEntity
|
||||||
|
import io.homeassistant.companion.android.onboarding.getMessagingToken
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -179,7 +180,8 @@ class HomePresenterImpl @Inject constructor(
|
||||||
DeviceRegistration(
|
DeviceRegistration(
|
||||||
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
||||||
null,
|
null,
|
||||||
null
|
getMessagingToken(),
|
||||||
|
false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, String>, 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<String, String>, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import dagger.hilt.android.qualifiers.ActivityContext
|
||||||
import io.homeassistant.companion.android.BuildConfig
|
import io.homeassistant.companion.android.BuildConfig
|
||||||
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
|
import io.homeassistant.companion.android.common.data.integration.DeviceRegistration
|
||||||
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
|
||||||
|
import io.homeassistant.companion.android.onboarding.getMessagingToken
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -28,7 +29,9 @@ class MobileAppIntegrationPresenterImpl @Inject constructor(
|
||||||
private suspend fun createRegistration(deviceName: String): DeviceRegistration {
|
private suspend fun createRegistration(deviceName: String): DeviceRegistration {
|
||||||
return DeviceRegistration(
|
return DeviceRegistration(
|
||||||
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
||||||
deviceName
|
deviceName,
|
||||||
|
getMessagingToken(),
|
||||||
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.database.wear.replaceAll
|
||||||
import io.homeassistant.companion.android.home.HomeActivity
|
import io.homeassistant.companion.android.home.HomeActivity
|
||||||
import io.homeassistant.companion.android.home.HomePresenterImpl
|
import io.homeassistant.companion.android.home.HomePresenterImpl
|
||||||
|
import io.homeassistant.companion.android.onboarding.getMessagingToken
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -124,7 +125,9 @@ class PhoneSettingsListener : WearableListenerService(), DataClient.OnDataChange
|
||||||
integrationUseCase.registerDevice(
|
integrationUseCase.registerDevice(
|
||||||
DeviceRegistration(
|
DeviceRegistration(
|
||||||
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
||||||
deviceName
|
deviceName,
|
||||||
|
getMessagingToken(),
|
||||||
|
false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue