mirror of
https://github.com/home-assistant/android
synced 2024-09-06 17:23:32 +00:00
Add TTS notifications to Wear OS (#3266)
This commit is contained in:
parent
3af6b007e8
commit
60f76fcd45
|
@ -13,7 +13,6 @@ import android.content.Intent
|
|||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.media.RingtoneManager
|
||||
|
@ -26,8 +25,6 @@ import android.os.Handler
|
|||
import android.os.Looper
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.widget.RemoteViews
|
||||
|
@ -61,9 +58,12 @@ 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.util.TextToSpeechData
|
||||
import io.homeassistant.companion.android.common.util.cancel
|
||||
import io.homeassistant.companion.android.common.util.cancelGroupIfNeeded
|
||||
import io.homeassistant.companion.android.common.util.getActiveNotification
|
||||
import io.homeassistant.companion.android.common.util.speakText
|
||||
import io.homeassistant.companion.android.common.util.stopTTS
|
||||
import io.homeassistant.companion.android.database.notification.NotificationDao
|
||||
import io.homeassistant.companion.android.database.notification.NotificationItem
|
||||
import io.homeassistant.companion.android.database.sensor.SensorDao
|
||||
|
@ -131,7 +131,6 @@ class MessagingManager @Inject constructor(
|
|||
const val REPLY = "REPLY"
|
||||
const val HIGH_ACCURACY_UPDATE_INTERVAL = "high_accuracy_update_interval"
|
||||
const val PACKAGE_NAME = "package_name"
|
||||
const val TTS_TEXT = "tts_text"
|
||||
const val CONFIRMATION = "confirmation"
|
||||
|
||||
// special intent constants
|
||||
|
@ -145,7 +144,6 @@ class MessagingManager @Inject constructor(
|
|||
const val REQUEST_LOCATION_UPDATE = "request_location_update"
|
||||
const val CLEAR_NOTIFICATION = "clear_notification"
|
||||
const val REMOVE_CHANNEL = "remove_channel"
|
||||
const val TTS = "TTS"
|
||||
const val COMMAND_DND = "command_dnd"
|
||||
const val COMMAND_RINGER_MODE = "command_ringer_mode"
|
||||
const val COMMAND_BROADCAST_INTENT = "command_broadcast_intent"
|
||||
|
@ -161,7 +159,6 @@ class MessagingManager @Inject constructor(
|
|||
const val COMMAND_LAUNCH_APP = "command_launch_app"
|
||||
const val COMMAND_APP_LOCK = "command_app_lock"
|
||||
const val COMMAND_PERSISTENT_CONNECTION = "command_persistent_connection"
|
||||
const val COMMAND_STOP_TTS = "command_stop_tts"
|
||||
const val COMMAND_AUTO_SCREEN_BRIGHTNESS = "command_auto_screen_brightness"
|
||||
const val COMMAND_SCREEN_BRIGHTNESS_LEVEL = "command_screen_brightness_level"
|
||||
const val COMMAND_SCREEN_OFF_TIMEOUT = "command_screen_off_timeout"
|
||||
|
@ -188,7 +185,6 @@ class MessagingManager @Inject constructor(
|
|||
const val MEDIA_STOP = "stop"
|
||||
const val MEDIA_PACKAGE_NAME = "media_package_name"
|
||||
const val MEDIA_COMMAND = "media_command"
|
||||
const val MEDIA_STREAM = "media_stream"
|
||||
|
||||
// App-lock command parameters:
|
||||
const val APP_LOCK_ENABLED = "app_lock_enabled"
|
||||
|
@ -218,7 +214,7 @@ class MessagingManager @Inject constructor(
|
|||
COMMAND_LAUNCH_APP,
|
||||
COMMAND_APP_LOCK,
|
||||
COMMAND_PERSISTENT_CONNECTION,
|
||||
COMMAND_STOP_TTS,
|
||||
TextToSpeechData.COMMAND_STOP_TTS,
|
||||
COMMAND_AUTO_SCREEN_BRIGHTNESS,
|
||||
COMMAND_SCREEN_BRIGHTNESS_LEVEL,
|
||||
COMMAND_SCREEN_OFF_TIMEOUT
|
||||
|
@ -252,8 +248,6 @@ class MessagingManager @Inject constructor(
|
|||
|
||||
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
private var textToSpeech: TextToSpeech? = null
|
||||
|
||||
fun handleMessage(notificationData: Map<String, String>, source: String) {
|
||||
|
||||
var now = System.currentTimeMillis()
|
||||
|
@ -299,9 +293,8 @@ class MessagingManager @Inject constructor(
|
|||
Log.d(TAG, "Removing Notification channel ${jsonData[NotificationData.CHANNEL]}")
|
||||
removeNotificationChannel(jsonData[NotificationData.CHANNEL]!!)
|
||||
}
|
||||
jsonData[NotificationData.MESSAGE] == TTS -> {
|
||||
Log.d(TAG, "Sending notification title to TTS")
|
||||
speakNotification(jsonData)
|
||||
jsonData[NotificationData.MESSAGE] == TextToSpeechData.TTS -> {
|
||||
speakText(context, jsonData)
|
||||
}
|
||||
jsonData[NotificationData.MESSAGE] in DEVICE_COMMANDS -> {
|
||||
Log.d(TAG, "Processing device command")
|
||||
|
@ -356,7 +349,7 @@ class MessagingManager @Inject constructor(
|
|||
}
|
||||
}
|
||||
COMMAND_VOLUME_LEVEL -> {
|
||||
if (!jsonData[MEDIA_STREAM].isNullOrEmpty() && jsonData[MEDIA_STREAM] in CHANNEL_VOLUME_STREAM &&
|
||||
if (!jsonData[NotificationData.MEDIA_STREAM].isNullOrEmpty() && jsonData[NotificationData.MEDIA_STREAM] in CHANNEL_VOLUME_STREAM &&
|
||||
!jsonData[NotificationData.COMMAND].isNullOrEmpty() && jsonData[NotificationData.COMMAND]?.toIntOrNull() != null
|
||||
)
|
||||
handleDeviceCommands(jsonData)
|
||||
|
@ -527,7 +520,7 @@ class MessagingManager @Inject constructor(
|
|||
else -> handleDeviceCommands(jsonData)
|
||||
}
|
||||
}
|
||||
COMMAND_STOP_TTS -> handleDeviceCommands(jsonData)
|
||||
TextToSpeechData.COMMAND_STOP_TTS -> stopTTS()
|
||||
COMMAND_AUTO_SCREEN_BRIGHTNESS -> {
|
||||
if (!jsonData[NotificationData.COMMAND].isNullOrEmpty() && jsonData[NotificationData.COMMAND] in DeviceCommandData.ENABLE_COMMANDS)
|
||||
handleDeviceCommands(jsonData)
|
||||
|
@ -570,12 +563,6 @@ class MessagingManager @Inject constructor(
|
|||
notificationManagerCompat.cancel(tag, messageId, true)
|
||||
}
|
||||
|
||||
private fun stopTTS() {
|
||||
Log.d(TAG, "Stopping TTS")
|
||||
textToSpeech?.stop()
|
||||
textToSpeech?.shutdown()
|
||||
}
|
||||
|
||||
private fun removeNotificationChannel(channelName: String) {
|
||||
val notificationManagerCompat = NotificationManagerCompat.from(context)
|
||||
|
||||
|
@ -586,80 +573,6 @@ class MessagingManager @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun speakNotification(data: Map<String, String>) {
|
||||
var tts = data[TTS_TEXT]
|
||||
val audioManager = context.getSystemService<AudioManager>()
|
||||
val currentAlarmVolume = audioManager?.getStreamVolume(AudioManager.STREAM_ALARM)
|
||||
val maxAlarmVolume = audioManager?.getStreamMaxVolume(AudioManager.STREAM_ALARM)
|
||||
if (tts.isNullOrEmpty())
|
||||
tts = context.getString(commonR.string.tts_no_text)
|
||||
textToSpeech = TextToSpeech(
|
||||
context
|
||||
) {
|
||||
if (it == TextToSpeech.SUCCESS) {
|
||||
val listener = object : UtteranceProgressListener() {
|
||||
override fun onStart(p0: String?) {
|
||||
if (data[MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX)
|
||||
audioManager?.setStreamVolume(
|
||||
AudioManager.STREAM_ALARM,
|
||||
maxAlarmVolume!!,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDone(p0: String?) {
|
||||
textToSpeech?.stop()
|
||||
textToSpeech?.shutdown()
|
||||
if (data[MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX)
|
||||
audioManager?.setStreamVolume(
|
||||
AudioManager.STREAM_ALARM,
|
||||
currentAlarmVolume!!,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
override fun onError(p0: String?) {
|
||||
textToSpeech?.stop()
|
||||
textToSpeech?.shutdown()
|
||||
if (data[MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX)
|
||||
audioManager?.setStreamVolume(
|
||||
AudioManager.STREAM_ALARM,
|
||||
currentAlarmVolume!!,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStop(utteranceId: String?, interrupted: Boolean) {
|
||||
if (data[MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX)
|
||||
audioManager?.setStreamVolume(
|
||||
AudioManager.STREAM_ALARM,
|
||||
currentAlarmVolume!!,
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
textToSpeech?.setOnUtteranceProgressListener(listener)
|
||||
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)
|
||||
.build()
|
||||
textToSpeech?.setAudioAttributes(audioAttributes)
|
||||
}
|
||||
textToSpeech?.speak(tts, TextToSpeech.QUEUE_ADD, null, "")
|
||||
Log.d(TAG, "speaking notification")
|
||||
} else {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(commonR.string.tts_error, tts),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeviceCommands(data: Map<String, String>) {
|
||||
val message = data[NotificationData.MESSAGE]
|
||||
val command = data[NotificationData.COMMAND]
|
||||
|
@ -736,14 +649,14 @@ class MessagingManager @Inject constructor(
|
|||
} else {
|
||||
processStreamVolume(
|
||||
audioManager!!,
|
||||
data[MEDIA_STREAM].toString(),
|
||||
data[NotificationData.MEDIA_STREAM].toString(),
|
||||
command!!.toInt()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
processStreamVolume(
|
||||
audioManager!!,
|
||||
data[MEDIA_STREAM].toString(),
|
||||
data[NotificationData.MEDIA_STREAM].toString(),
|
||||
command!!.toInt()
|
||||
)
|
||||
}
|
||||
|
@ -853,9 +766,6 @@ class MessagingManager @Inject constructor(
|
|||
COMMAND_PERSISTENT_CONNECTION -> {
|
||||
togglePersistentConnection(data[PERSISTENT].toString())
|
||||
}
|
||||
COMMAND_STOP_TTS -> {
|
||||
stopTTS()
|
||||
}
|
||||
COMMAND_AUTO_SCREEN_BRIGHTNESS, COMMAND_SCREEN_BRIGHTNESS_LEVEL, COMMAND_SCREEN_OFF_TIMEOUT -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (Settings.System.canWrite(context)) {
|
||||
|
|
|
@ -44,6 +44,9 @@ object NotificationData {
|
|||
const val SYSTEM_STREAM = "system_stream"
|
||||
const val CALL_STREAM = "call_stream"
|
||||
const val DTMF_STREAM = "dtmf_stream"
|
||||
|
||||
const val MEDIA_STREAM = "media_stream"
|
||||
val ALARM_STREAMS = listOf(ALARM_STREAM, ALARM_STREAM_MAX)
|
||||
}
|
||||
|
||||
fun createChannelID(
|
||||
|
|
|
@ -7,7 +7,7 @@ import android.service.notification.StatusBarNotification
|
|||
import android.util.Log
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
|
||||
const val TAG = "NotifManagerCompat"
|
||||
private const val TAG = "NotifManagerCompat"
|
||||
|
||||
fun NotificationManagerCompat.getNotificationManager(): NotificationManager {
|
||||
val field = this.javaClass.declaredFields
|
||||
|
@ -29,14 +29,14 @@ fun NotificationManagerCompat.cancelGroupIfNeeded(tag: String?, id: Int): Boolea
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
Log.d(TAG, "Cancel notification with tag \"$tag\" and id \"$id\"")
|
||||
|
||||
var currentActiveNotifications = this.getNotificationManager().activeNotifications
|
||||
val currentActiveNotifications = this.getNotificationManager().activeNotifications
|
||||
|
||||
Log.d(TAG, "Check if the notification is in a group...")
|
||||
// Get group key from the current notification
|
||||
// to handle possible group deletion
|
||||
var statusBarNotification =
|
||||
val statusBarNotification =
|
||||
currentActiveNotifications.singleOrNull { s -> s.id == id && s.tag == tag && s.isGroup }
|
||||
var groupKey = statusBarNotification?.groupKey
|
||||
val groupKey = statusBarNotification?.groupKey
|
||||
|
||||
// Notification has a group?
|
||||
if (statusBarNotification != null && !groupKey.isNullOrBlank()) {
|
||||
|
@ -51,7 +51,7 @@ fun NotificationManagerCompat.cancelGroupIfNeeded(tag: String?, id: Int): Boolea
|
|||
currentActiveNotifications.filter { s -> s.groupKey == groupKey }
|
||||
|
||||
// Is the notification which should be deleted a group summary
|
||||
var isGroupSummary = statusBarNotification.notification.flags and FLAG_GROUP_SUMMARY != 0
|
||||
val isGroupSummary = statusBarNotification.notification.flags and FLAG_GROUP_SUMMARY != 0
|
||||
if (isGroupSummary) Log.d(TAG, "Notification is the group summary.")
|
||||
else Log.d(TAG, "Notification is NOT the group summary.")
|
||||
|
||||
|
@ -75,7 +75,7 @@ fun NotificationManagerCompat.cancelGroupIfNeeded(tag: String?, id: Int): Boolea
|
|||
// This group can't be canceled, but it will be canceled by canceling the last notification inside of the group
|
||||
// If the group isn't null, cancel the group
|
||||
return if (group != null) {
|
||||
var groupId = group.hashCode()
|
||||
val groupId = group.hashCode()
|
||||
Log.d(TAG, "Cancel group notification with tag \"$group\" and id \"$groupId\"")
|
||||
this.cancel(group, groupId)
|
||||
true
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
package io.homeassistant.companion.android.common.util
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.getSystemService
|
||||
import io.homeassistant.companion.android.common.R
|
||||
import io.homeassistant.companion.android.common.notifications.NotificationData
|
||||
|
||||
object TextToSpeechData {
|
||||
const val TTS = "TTS"
|
||||
const val TTS_TEXT = "tts_text"
|
||||
|
||||
const val COMMAND_STOP_TTS = "command_stop_tts"
|
||||
}
|
||||
|
||||
private const val TAG = "TextToSpeech"
|
||||
private var textToSpeech: TextToSpeech? = null
|
||||
|
||||
fun speakText(
|
||||
context: Context,
|
||||
data: Map<String, String>
|
||||
) {
|
||||
Log.d(TAG, "Sending text to TTS")
|
||||
var tts = data[TextToSpeechData.TTS_TEXT]
|
||||
val audioManager = context.getSystemService<AudioManager>()
|
||||
val currentAlarmVolume = audioManager?.getStreamVolume(AudioManager.STREAM_ALARM)
|
||||
val maxAlarmVolume = audioManager?.getStreamMaxVolume(AudioManager.STREAM_ALARM)
|
||||
if (tts.isNullOrEmpty())
|
||||
tts = context.getString(R.string.tts_no_text)
|
||||
textToSpeech = TextToSpeech(
|
||||
context
|
||||
) {
|
||||
if (it == TextToSpeech.SUCCESS) {
|
||||
val listener = object : UtteranceProgressListener() {
|
||||
override fun onStart(p0: String?) {
|
||||
if (data[NotificationData.MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX)
|
||||
audioManager?.setStreamVolume(
|
||||
AudioManager.STREAM_ALARM,
|
||||
maxAlarmVolume!!,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDone(p0: String?) {
|
||||
textToSpeech?.stop()
|
||||
textToSpeech?.shutdown()
|
||||
if (data[NotificationData.MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX)
|
||||
audioManager?.setStreamVolume(
|
||||
AudioManager.STREAM_ALARM,
|
||||
currentAlarmVolume!!,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onError(p0: String?) {
|
||||
textToSpeech?.stop()
|
||||
textToSpeech?.shutdown()
|
||||
if (data[NotificationData.MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX)
|
||||
audioManager?.setStreamVolume(
|
||||
AudioManager.STREAM_ALARM,
|
||||
currentAlarmVolume!!,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStop(utteranceId: String?, interrupted: Boolean) {
|
||||
if (data[NotificationData.MEDIA_STREAM] == NotificationData.ALARM_STREAM_MAX)
|
||||
audioManager?.setStreamVolume(
|
||||
AudioManager.STREAM_ALARM,
|
||||
currentAlarmVolume!!,
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
textToSpeech?.setOnUtteranceProgressListener(listener)
|
||||
if (data[NotificationData.MEDIA_STREAM] in NotificationData.ALARM_STREAMS) {
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_ALARM)
|
||||
.build()
|
||||
textToSpeech?.setAudioAttributes(audioAttributes)
|
||||
}
|
||||
textToSpeech?.speak(tts, TextToSpeech.QUEUE_ADD, null, "")
|
||||
Log.d(TAG, "speaking text")
|
||||
} else {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.tts_error, tts),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopTTS() {
|
||||
Log.d(TAG, "Stopping TTS")
|
||||
textToSpeech?.stop()
|
||||
textToSpeech?.shutdown()
|
||||
}
|
|
@ -14,8 +14,11 @@ import io.homeassistant.companion.android.common.notifications.getGroupNotificat
|
|||
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.TextToSpeechData
|
||||
import io.homeassistant.companion.android.common.util.cancelGroupIfNeeded
|
||||
import io.homeassistant.companion.android.common.util.getActiveNotification
|
||||
import io.homeassistant.companion.android.common.util.speakText
|
||||
import io.homeassistant.companion.android.common.util.stopTTS
|
||||
import io.homeassistant.companion.android.database.AppDatabase
|
||||
import io.homeassistant.companion.android.database.notification.NotificationItem
|
||||
import io.homeassistant.companion.android.database.sensor.SensorDao
|
||||
|
@ -57,8 +60,9 @@ class MessagingManager @Inject constructor(
|
|||
sendNotification(notificationData)
|
||||
}
|
||||
}
|
||||
else ->
|
||||
sendNotification(notificationData, now)
|
||||
TextToSpeechData.TTS -> speakText(context, notificationData)
|
||||
TextToSpeechData.COMMAND_STOP_TTS -> stopTTS()
|
||||
else -> sendNotification(notificationData, now)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue