From 60f76fcd454e947ecb3521b15cccdae8b50ea4d2 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Tue, 31 Jan 2023 11:18:58 -0800 Subject: [PATCH] Add TTS notifications to Wear OS (#3266) --- .../android/notifications/MessagingManager.kt | 110 ++---------------- .../notifications/NotificationFunctions.kt | 3 + .../util/NotificationManagerExtensions.kt | 12 +- .../android/common/util/TextToSpeech.kt | 109 +++++++++++++++++ .../android/notifications/MessagingManager.kt | 8 +- 5 files changed, 134 insertions(+), 108 deletions(-) create mode 100755 common/src/main/java/io/homeassistant/companion/android/common/util/TextToSpeech.kt diff --git a/app/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt b/app/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt index e9fb9b5c5..54277ddfa 100644 --- a/app/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt +++ b/app/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt @@ -13,7 +13,6 @@ import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.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, 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) { - var tts = data[TTS_TEXT] - val audioManager = context.getSystemService() - 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) { 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)) { diff --git a/common/src/main/java/io/homeassistant/companion/android/common/notifications/NotificationFunctions.kt b/common/src/main/java/io/homeassistant/companion/android/common/notifications/NotificationFunctions.kt index 7a9ef3e99..260fa0396 100755 --- a/common/src/main/java/io/homeassistant/companion/android/common/notifications/NotificationFunctions.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/notifications/NotificationFunctions.kt @@ -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( diff --git a/common/src/main/java/io/homeassistant/companion/android/common/util/NotificationManagerExtensions.kt b/common/src/main/java/io/homeassistant/companion/android/common/util/NotificationManagerExtensions.kt index abc98d48f..b345dad1a 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/util/NotificationManagerExtensions.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/util/NotificationManagerExtensions.kt @@ -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 diff --git a/common/src/main/java/io/homeassistant/companion/android/common/util/TextToSpeech.kt b/common/src/main/java/io/homeassistant/companion/android/common/util/TextToSpeech.kt new file mode 100755 index 000000000..aa6b172d5 --- /dev/null +++ b/common/src/main/java/io/homeassistant/companion/android/common/util/TextToSpeech.kt @@ -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 +) { + Log.d(TAG, "Sending text to TTS") + var tts = data[TextToSpeechData.TTS_TEXT] + val audioManager = context.getSystemService() + 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() +} diff --git a/wear/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt b/wear/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt index 6db45b167..916ac47ab 100755 --- a/wear/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/notifications/MessagingManager.kt @@ -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) } }