Add TTS notifications to Wear OS (#3266)

This commit is contained in:
Daniel Shokouhi 2023-01-31 11:18:58 -08:00 committed by GitHub
parent 3af6b007e8
commit 60f76fcd45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 134 additions and 108 deletions

View file

@ -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)) {

View file

@ -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(

View file

@ -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

View file

@ -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()
}

View file

@ -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)
}
}