From 5f6418ca5be9cb341b1aec89fa007b9e69686174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Przemys=C5=82aw=20Pluta?= Date: Thu, 14 May 2020 20:53:56 +0200 Subject: [PATCH] Introduce Dagger2 --- app/build.gradle | 5 ++ app/src/main/AndroidManifest.xml | 1 + .../java/io/bartek/ttsserver/MainActivity.kt | 7 ++- .../io/bartek/ttsserver/TTSApplication.kt | 11 ++++ .../io/bartek/ttsserver/di/AndroidModule.kt | 24 ++++++++ .../io/bartek/ttsserver/di/AppComponent.kt | 23 +++++++ .../java/io/bartek/ttsserver/di/TTSModule.kt | 60 +++++++++++++++++++ .../bartek/ttsserver/network/NetworkUtil.kt | 4 +- .../ttsserver/service/ForegroundService.kt | 26 +++++--- .../io/bartek/ttsserver/sonos/SonosQueue.kt | 29 +++++++-- .../main/java/io/bartek/ttsserver/tts/TTS.kt | 12 +++- .../bartek/ttsserver/tts/TTSStatusHolder.kt | 22 +++++++ .../java/io/bartek/ttsserver/web/Endpoints.kt | 2 +- .../java/io/bartek/ttsserver/web/WebServer.kt | 60 +++++++++++-------- .../bartek/ttsserver/web/WebServerFactory.kt | 17 ++++++ 15 files changed, 257 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/io/bartek/ttsserver/TTSApplication.kt create mode 100644 app/src/main/java/io/bartek/ttsserver/di/AndroidModule.kt create mode 100644 app/src/main/java/io/bartek/ttsserver/di/AppComponent.kt create mode 100644 app/src/main/java/io/bartek/ttsserver/di/TTSModule.kt create mode 100644 app/src/main/java/io/bartek/ttsserver/tts/TTSStatusHolder.kt create mode 100644 app/src/main/java/io/bartek/ttsserver/web/WebServerFactory.kt 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