diff --git a/app/build.gradle b/app/build.gradle
index d097304..810bd8d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 29
@@ -34,6 +35,10 @@ dependencies {
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'com.github.vmichalak:sonos-controller:v.0.1'
+ implementation 'com.google.dagger:dagger-android:2.15'
+ implementation 'com.google.dagger:dagger-android-support:2.15'
+ kapt 'com.google.dagger:dagger-android-processor:2.15'
+ kapt 'com.google.dagger:dagger-compiler:2.15'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0db3c9e..abb92cc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -16,6 +16,7 @@
{
+
+ @Component.Builder
+ abstract class Builder : AndroidInjector.Builder() {
+
+ @BindsInstance
+ abstract fun appContext(context: Context)
+
+ override fun seedInstance(instance: TTSApplication) = appContext(instance)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/bartek/ttsserver/di/TTSModule.kt b/app/src/main/java/io/bartek/ttsserver/di/TTSModule.kt
new file mode 100644
index 0000000..9959740
--- /dev/null
+++ b/app/src/main/java/io/bartek/ttsserver/di/TTSModule.kt
@@ -0,0 +1,60 @@
+package io.bartek.ttsserver.di
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.speech.tts.TextToSpeech
+import androidx.preference.PreferenceManager
+import dagger.Module
+import dagger.Provides
+import io.bartek.ttsserver.network.NetworkUtil
+import io.bartek.ttsserver.service.ForegroundNotificationFactory
+import io.bartek.ttsserver.sonos.SonosQueue
+import io.bartek.ttsserver.tts.TTS
+import io.bartek.ttsserver.tts.TTSStatusHolder
+import io.bartek.ttsserver.web.WebServerFactory
+import javax.inject.Singleton
+
+@Module
+class TTSModule {
+
+ @Provides
+ @Singleton
+ fun ttsStatusHolder() = TTSStatusHolder()
+
+ @Provides
+ @Singleton
+ fun textToSpeech(context: Context, ttsStatusHolder: TTSStatusHolder) =
+ TextToSpeech(context, ttsStatusHolder)
+
+ @Provides
+ @Singleton
+ fun tts(context: Context, textToSpeech: TextToSpeech, ttsStatusHolder: TTSStatusHolder) =
+ TTS(context, textToSpeech, ttsStatusHolder)
+
+ @Provides
+ @Singleton
+ fun webServerFactory(
+ preferences: SharedPreferences,
+ context: Context,
+ tts: TTS,
+ sonos: SonosQueue
+ ) =
+ WebServerFactory(preferences, context, tts, sonos)
+
+ @Provides
+ @Singleton
+ fun preferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context)
+
+ @Provides
+ @Singleton
+ fun networkUtil(context: Context) = NetworkUtil(context)
+
+ @Provides
+ @Singleton
+ fun sonosQueue(tts: TTS, networkUtil: NetworkUtil, preferences: SharedPreferences) =
+ SonosQueue(tts, networkUtil, preferences)
+
+ @Provides
+ @Singleton
+ fun foregroundNotificationFactory(context: Context) = ForegroundNotificationFactory(context)
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/bartek/ttsserver/network/NetworkUtil.kt b/app/src/main/java/io/bartek/ttsserver/network/NetworkUtil.kt
index c6d2c6f..10db98e 100644
--- a/app/src/main/java/io/bartek/ttsserver/network/NetworkUtil.kt
+++ b/app/src/main/java/io/bartek/ttsserver/network/NetworkUtil.kt
@@ -6,8 +6,8 @@ import android.net.wifi.WifiManager
import java.net.InetAddress
-object NetworkUtil {
- fun getIpAddress(context: Context): String {
+class NetworkUtil(private val context: Context) {
+ fun getIpAddress(): String {
return (context.getApplicationContext().getSystemService(WIFI_SERVICE) as WifiManager).let {
inetAddress(it.dhcpInfo.ipAddress).toString().substring(1)
}
diff --git a/app/src/main/java/io/bartek/ttsserver/service/ForegroundService.kt b/app/src/main/java/io/bartek/ttsserver/service/ForegroundService.kt
index 7fd20f8..a8915e3 100644
--- a/app/src/main/java/io/bartek/ttsserver/service/ForegroundService.kt
+++ b/app/src/main/java/io/bartek/ttsserver/service/ForegroundService.kt
@@ -1,29 +1,35 @@
package io.bartek.ttsserver.service
import android.annotation.SuppressLint
-import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.PowerManager
-import androidx.preference.PreferenceManager
+import dagger.android.DaggerService
import io.bartek.ttsserver.preference.PreferenceKey
import io.bartek.ttsserver.web.WebServer
+import io.bartek.ttsserver.web.WebServerFactory
+import javax.inject.Inject
-class ForegroundService : Service() {
- private lateinit var preferences: SharedPreferences
+class ForegroundService : DaggerService() {
private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false
private var webServer: WebServer? = null
private val port: Int
get() = preferences.getInt(PreferenceKey.PORT, 8080)
- private val notificationFactory = ForegroundNotificationFactory(this)
+ @Inject
+ lateinit var preferences: SharedPreferences
+
+ @Inject
+ lateinit var webServerFactory: WebServerFactory
+
+ @Inject
+ lateinit var notificationFactory: ForegroundNotificationFactory
override fun onCreate() {
super.onCreate()
- preferences = PreferenceManager.getDefaultSharedPreferences(this)
startForeground(1, notificationFactory.createForegroundNotification(port))
}
@@ -41,6 +47,7 @@ class ForegroundService : Service() {
}
override fun onDestroy() {
+ webServer?.stop()
webServer = null
}
@@ -54,8 +61,11 @@ class ForegroundService : Service() {
acquire()
}
}
- webServer = WebServer(port, this)
- state = ServiceState.RUNNING
+ webServer = webServerFactory.createWebServer()
+ webServer?.let {
+ state = ServiceState.RUNNING
+ it.start()
+ }
}
private fun stopService() {
diff --git a/app/src/main/java/io/bartek/ttsserver/sonos/SonosQueue.kt b/app/src/main/java/io/bartek/ttsserver/sonos/SonosQueue.kt
index 40311e4..ae32409 100644
--- a/app/src/main/java/io/bartek/ttsserver/sonos/SonosQueue.kt
+++ b/app/src/main/java/io/bartek/ttsserver/sonos/SonosQueue.kt
@@ -1,6 +1,9 @@
package io.bartek.ttsserver.sonos
+import android.content.SharedPreferences
import com.vmichalak.sonoscontroller.SonosDiscovery
+import io.bartek.ttsserver.network.NetworkUtil
+import io.bartek.ttsserver.preference.PreferenceKey
import io.bartek.ttsserver.service.ForegroundService
import io.bartek.ttsserver.service.ServiceState
import io.bartek.ttsserver.tts.TTS
@@ -24,7 +27,7 @@ private class Consumer(
}
- private fun consume(data: SonosTTSRequestData) {
+ private fun consume(data: SonosTTSRequestData) =
SonosDiscovery.discover().firstOrNull { it.zoneGroupState.name == data.zone }?.let {
val file = tts.createTTSFile(data.text, data.language)
val filename = file.name
@@ -34,16 +37,30 @@ private class Consumer(
it.clip(url, "")
it.volume = currentVolume
}
- }
}
-class SonosQueue(tts: TTS, host: String, port: Int) {
+class SonosQueue(
+ private val tts: TTS,
+ private val networkUtil: NetworkUtil,
+ private val preferences: SharedPreferences
+) {
private val queue: BlockingQueue = LinkedBlockingQueue()
- private val consumer = Thread(Consumer(tts, host, port, queue)).also {
- it.name = "SONOS_QUEUE"
+ private val host: String
+ get() = networkUtil.getIpAddress()
+ private val port: Int
+ get() = preferences.getInt(PreferenceKey.PORT, 8080)
+ private var consumer: Thread? = null
+
+ fun run() {
+ consumer?.interrupt()
+ consumer = Thread(Consumer(tts, host, port, queue)).also { it.name = "SonosQueue" }
+ consumer?.start()
}
- init { consumer.start() }
+ fun stop() {
+ consumer?.interrupt()
+ consumer = null
+ }
fun push(data: SonosTTSRequestData) = queue.add(data)
}
\ No newline at end of file
diff --git a/app/src/main/java/io/bartek/ttsserver/tts/TTS.kt b/app/src/main/java/io/bartek/ttsserver/tts/TTS.kt
index e673f84..301c543 100644
--- a/app/src/main/java/io/bartek/ttsserver/tts/TTS.kt
+++ b/app/src/main/java/io/bartek/ttsserver/tts/TTS.kt
@@ -13,10 +13,16 @@ import java.util.*
data class SpeechData(val stream: InputStream, val size: Long)
-class TTS(private val context: Context, initListener: TextToSpeech.OnInitListener) {
- private val tts = TextToSpeech(context, initListener)
+class TTS(
+ private val context: Context,
+ private val tts: TextToSpeech,
+ private val ttsStausHolder: TTSStatusHolder
+) {
private val messageDigest = MessageDigest.getInstance("SHA-256")
+ val status: TTSStatus
+ get() = ttsStausHolder.status
+
fun createTTSFile(text: String, language: Locale): File {
val digest = hash(text, language)
val filename = "tts_$digest.wav"
@@ -34,7 +40,7 @@ class TTS(private val context: Context, initListener: TextToSpeech.OnInitListene
lock.wait()
}
- if(!lock.success) {
+ if (!lock.success) {
throw TTSException()
}
diff --git a/app/src/main/java/io/bartek/ttsserver/tts/TTSStatusHolder.kt b/app/src/main/java/io/bartek/ttsserver/tts/TTSStatusHolder.kt
new file mode 100644
index 0000000..18a73bf
--- /dev/null
+++ b/app/src/main/java/io/bartek/ttsserver/tts/TTSStatusHolder.kt
@@ -0,0 +1,22 @@
+package io.bartek.ttsserver.tts
+
+import android.speech.tts.TextToSpeech
+
+enum class TTSStatus(private val status: Int) {
+ READY(TextToSpeech.SUCCESS),
+ ERROR(TextToSpeech.ERROR),
+ UNLOADED(1);
+
+ companion object {
+ fun of(status: Int) = values().firstOrNull { it.status == status } ?: UNLOADED
+ }
+}
+
+class TTSStatusHolder : TextToSpeech.OnInitListener {
+ var status = TTSStatus.UNLOADED
+ private set
+
+ override fun onInit(status: Int) {
+ this.status = TTSStatus.of(status)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/bartek/ttsserver/web/Endpoints.kt b/app/src/main/java/io/bartek/ttsserver/web/Endpoints.kt
index 7f213fc..8d888ec 100644
--- a/app/src/main/java/io/bartek/ttsserver/web/Endpoints.kt
+++ b/app/src/main/java/io/bartek/ttsserver/web/Endpoints.kt
@@ -15,7 +15,7 @@ enum class Endpoint(val uri: String, val id: Int) {
}
}
-class Endpoints {
+object Endpoints {
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init {
diff --git a/app/src/main/java/io/bartek/ttsserver/web/WebServer.kt b/app/src/main/java/io/bartek/ttsserver/web/WebServer.kt
index e56391b..09890ac 100644
--- a/app/src/main/java/io/bartek/ttsserver/web/WebServer.kt
+++ b/app/src/main/java/io/bartek/ttsserver/web/WebServer.kt
@@ -2,47 +2,56 @@ package io.bartek.ttsserver.web
import android.content.Context
import android.content.Intent
+import android.content.SharedPreferences
import android.net.Uri
-import android.speech.tts.TextToSpeech
import androidx.localbroadcastmanager.content.LocalBroadcastManager
-import androidx.preference.PreferenceManager
import fi.iki.elonen.NanoHTTPD
import fi.iki.elonen.NanoHTTPD.Response.Status.*
-import io.bartek.ttsserver.network.NetworkUtil
import io.bartek.ttsserver.preference.PreferenceKey
import io.bartek.ttsserver.service.ForegroundService
import io.bartek.ttsserver.service.ServiceState
import io.bartek.ttsserver.sonos.SonosQueue
import io.bartek.ttsserver.tts.TTS
+import io.bartek.ttsserver.tts.TTSStatus
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
-class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
- TextToSpeech.OnInitListener {
- private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
- private val tts = TTS(context, this)
- private val endpoints = Endpoints()
- private val sonos = SonosQueue(tts, NetworkUtil.getIpAddress(context), port)
-
+class WebServer(
+ port: Int,
+ private val context: Context,
+ private val preferences: SharedPreferences,
+ private val tts: TTS,
+ private val sonos: SonosQueue
+) : NanoHTTPD(port) {
override fun serve(session: IHTTPSession?): Response {
try {
+ assertThatTTSIsReady()
+
session?.let {
- return when (endpoints.match(it.uri)) {
- Endpoint.SAY -> say(it)
- Endpoint.WAVE -> wave(it)
- Endpoint.SONOS -> sonos(it)
- Endpoint.SONOS_CACHE -> sonosCache(it)
- Endpoint.UNKNOWN -> throw ResponseException(NOT_FOUND, "")
- }
+ return dispatch(it)
}
throw ResponseException(BAD_REQUEST, "")
- } catch (e: ResponseException) {
- throw e
- } catch (e: Exception) {
- throw ResponseException(INTERNAL_ERROR, e.toString(), e)
+ }
+ catch (e: ResponseException) { throw e }
+ catch (e: Exception) { throw ResponseException(INTERNAL_ERROR, e.toString(), e) }
+ }
+
+ private fun dispatch(it: IHTTPSession): Response {
+ return when (Endpoints.match(it.uri)) {
+ Endpoint.SAY -> say(it)
+ Endpoint.WAVE -> wave(it)
+ Endpoint.SONOS -> sonos(it)
+ Endpoint.SONOS_CACHE -> sonosCache(it)
+ Endpoint.UNKNOWN -> throw ResponseException(NOT_FOUND, "")
+ }
+ }
+
+ private fun assertThatTTSIsReady() {
+ if (tts.status != TTSStatus.READY) {
+ throw ResponseException(NOT_ACCEPTABLE, "Server is not ready yet")
}
}
@@ -120,10 +129,11 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
throw ResponseException(METHOD_NOT_ALLOWED, "")
}
- val filename = Uri.parse(session.uri).lastPathSegment ?: throw ResponseException(BAD_REQUEST, "")
+ val filename =
+ Uri.parse(session.uri).lastPathSegment ?: throw ResponseException(BAD_REQUEST, "")
val file = File(context.cacheDir, filename)
- if(!file.exists()) {
+ if (!file.exists()) {
throw ResponseException(NOT_FOUND, "")
}
@@ -132,10 +142,9 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
return newFixedLengthResponse(OK, MIME_WAVE, stream, size)
}
- override fun onInit(status: Int) = start()
-
override fun start() {
super.start()
+ sonos.run()
LocalBroadcastManager
.getInstance(context)
.sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also {
@@ -145,6 +154,7 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
override fun stop() {
super.stop()
+ sonos.stop()
LocalBroadcastManager
.getInstance(context)
.sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also {
diff --git a/app/src/main/java/io/bartek/ttsserver/web/WebServerFactory.kt b/app/src/main/java/io/bartek/ttsserver/web/WebServerFactory.kt
new file mode 100644
index 0000000..1162134
--- /dev/null
+++ b/app/src/main/java/io/bartek/ttsserver/web/WebServerFactory.kt
@@ -0,0 +1,17 @@
+package io.bartek.ttsserver.web
+
+import android.content.Context
+import android.content.SharedPreferences
+import io.bartek.ttsserver.preference.PreferenceKey
+import io.bartek.ttsserver.sonos.SonosQueue
+import io.bartek.ttsserver.tts.TTS
+
+class WebServerFactory(
+ private val preferences: SharedPreferences,
+ private val context: Context,
+ private val tts: TTS,
+ private val sonos: SonosQueue
+) {
+ fun createWebServer() =
+ WebServer(preferences.getInt(PreferenceKey.PORT, 8080), context, preferences, tts, sonos)
+}
\ No newline at end of file