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.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'**.
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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<String, String>) {
|
||||
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<NotificationManager>()
|
||||
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<NotificationManager>()
|
||||
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<NotificationManager>()
|
||||
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<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(
|
||||
builder: NotificationCompat.Builder,
|
||||
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(
|
||||
builder: NotificationCompat.Builder,
|
||||
data: Map<String, String>
|
||||
) {
|
||||
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<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(
|
||||
builder: NotificationCompat.Builder,
|
||||
data: Map<String, String>
|
||||
) {
|
||||
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<String, String>
|
||||
) {
|
||||
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<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(
|
||||
builder: NotificationCompat.Builder,
|
||||
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(
|
||||
builder: NotificationCompat.Builder,
|
||||
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)
|
||||
}
|
||||
|
||||
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, 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)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -759,7 +759,7 @@ class IntegrationRepositoryImpl @Inject constructor(
|
|||
val oldDeviceRegistration = getRegistration()
|
||||
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()) {
|
||||
appData["push_url"] = PUSH_URL
|
||||
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")
|
||||
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")
|
||||
}
|
||||
|
|
|
@ -170,6 +170,14 @@
|
|||
android:path="/updateTemplateTile" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".notifications.FirebaseCloudMessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</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.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) {
|
||||
|
|
|
@ -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.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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in a new issue