Introduce Dagger2

This commit is contained in:
2020-05-14 20:53:56 +02:00
parent c8f4f99687
commit 5f6418ca5b
15 changed files with 257 additions and 46 deletions

View File

@@ -1,6 +1,7 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion 29 compileSdkVersion 29
@@ -34,6 +35,10 @@ dependencies {
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.preference:preference:1.1.1'
implementation 'com.github.vmichalak:sonos-controller:v.0.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' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

View File

@@ -16,6 +16,7 @@
<uses-permission android:name="io.bartek.permission.TTS_HTTP_SERVICE" /> <uses-permission android:name="io.bartek.permission.TTS_HTTP_SERVICE" />
<application <application
android:name=".ttsserver.TTSApplication"
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"

View File

@@ -13,17 +13,22 @@ import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatImageButton import androidx.appcompat.widget.AppCompatImageButton
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import dagger.android.support.DaggerAppCompatActivity
import io.bartek.R import io.bartek.R
import io.bartek.ttsserver.help.HelpActivity import io.bartek.ttsserver.help.HelpActivity
import io.bartek.ttsserver.preference.PreferencesActivity import io.bartek.ttsserver.preference.PreferencesActivity
import io.bartek.ttsserver.service.ForegroundService import io.bartek.ttsserver.service.ForegroundService
import io.bartek.ttsserver.service.ServiceState import io.bartek.ttsserver.service.ServiceState
import javax.inject.Inject
class MainActivity : AppCompatActivity() { class MainActivity : DaggerAppCompatActivity() {
private lateinit var serverControlButton: AppCompatImageButton private lateinit var serverControlButton: AppCompatImageButton
private lateinit var promptText: TextView private lateinit var promptText: TextView
@Inject
lateinit var context: Context
private val receiver = object : BroadcastReceiver() { private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
(intent?.getStringExtra(ForegroundService.STATE) ?: ServiceState.STOPPED.name) (intent?.getStringExtra(ForegroundService.STATE) ?: ServiceState.STOPPED.name)

View File

@@ -0,0 +1,11 @@
package io.bartek.ttsserver
import dagger.android.support.DaggerApplication
import io.bartek.ttsserver.di.DaggerAppComponent
class TTSApplication : DaggerApplication() {
override fun applicationInjector() = DaggerAppComponent.builder().create(this).let {
it.inject(this)
it
}
}

View File

@@ -0,0 +1,24 @@
package io.bartek.ttsserver.di
import dagger.Module
import dagger.android.ContributesAndroidInjector
import io.bartek.ttsserver.MainActivity
import io.bartek.ttsserver.help.HelpActivity
import io.bartek.ttsserver.preference.PreferencesActivity
import io.bartek.ttsserver.service.ForegroundService
@Module
abstract class AndroidModule {
@ContributesAndroidInjector
abstract fun mainActivity(): MainActivity
@ContributesAndroidInjector
abstract fun helpActivity(): HelpActivity
@ContributesAndroidInjector
abstract fun preferencesActivity(): PreferencesActivity
@ContributesAndroidInjector
abstract fun foregroundService(): ForegroundService
}

View File

@@ -0,0 +1,23 @@
package io.bartek.ttsserver.di
import android.content.Context
import dagger.BindsInstance
import dagger.Component
import dagger.android.AndroidInjector
import dagger.android.support.AndroidSupportInjectionModule
import io.bartek.ttsserver.TTSApplication
import javax.inject.Singleton
@Singleton
@Component(modules = [AndroidSupportInjectionModule::class, AndroidModule::class, TTSModule::class])
interface AppComponent : AndroidInjector<TTSApplication> {
@Component.Builder
abstract class Builder : AndroidInjector.Builder<TTSApplication>() {
@BindsInstance
abstract fun appContext(context: Context)
override fun seedInstance(instance: TTSApplication) = appContext(instance)
}
}

View File

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

View File

@@ -6,8 +6,8 @@ import android.net.wifi.WifiManager
import java.net.InetAddress import java.net.InetAddress
object NetworkUtil { class NetworkUtil(private val context: Context) {
fun getIpAddress(context: Context): String { fun getIpAddress(): String {
return (context.getApplicationContext().getSystemService(WIFI_SERVICE) as WifiManager).let { return (context.getApplicationContext().getSystemService(WIFI_SERVICE) as WifiManager).let {
inetAddress(it.dhcpInfo.ipAddress).toString().substring(1) inetAddress(it.dhcpInfo.ipAddress).toString().substring(1)
} }

View File

@@ -1,29 +1,35 @@
package io.bartek.ttsserver.service package io.bartek.ttsserver.service
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.PowerManager import android.os.PowerManager
import androidx.preference.PreferenceManager import dagger.android.DaggerService
import io.bartek.ttsserver.preference.PreferenceKey import io.bartek.ttsserver.preference.PreferenceKey
import io.bartek.ttsserver.web.WebServer import io.bartek.ttsserver.web.WebServer
import io.bartek.ttsserver.web.WebServerFactory
import javax.inject.Inject
class ForegroundService : Service() { class ForegroundService : DaggerService() {
private lateinit var preferences: SharedPreferences
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false private var isServiceStarted = false
private var webServer: WebServer? = null private var webServer: WebServer? = null
private val port: Int private val port: Int
get() = preferences.getInt(PreferenceKey.PORT, 8080) 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() { override fun onCreate() {
super.onCreate() super.onCreate()
preferences = PreferenceManager.getDefaultSharedPreferences(this)
startForeground(1, notificationFactory.createForegroundNotification(port)) startForeground(1, notificationFactory.createForegroundNotification(port))
} }
@@ -41,6 +47,7 @@ class ForegroundService : Service() {
} }
override fun onDestroy() { override fun onDestroy() {
webServer?.stop()
webServer = null webServer = null
} }
@@ -54,8 +61,11 @@ class ForegroundService : Service() {
acquire() acquire()
} }
} }
webServer = WebServer(port, this) webServer = webServerFactory.createWebServer()
state = ServiceState.RUNNING webServer?.let {
state = ServiceState.RUNNING
it.start()
}
} }
private fun stopService() { private fun stopService() {

View File

@@ -1,6 +1,9 @@
package io.bartek.ttsserver.sonos package io.bartek.ttsserver.sonos
import android.content.SharedPreferences
import com.vmichalak.sonoscontroller.SonosDiscovery 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.ForegroundService
import io.bartek.ttsserver.service.ServiceState import io.bartek.ttsserver.service.ServiceState
import io.bartek.ttsserver.tts.TTS 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 { SonosDiscovery.discover().firstOrNull { it.zoneGroupState.name == data.zone }?.let {
val file = tts.createTTSFile(data.text, data.language) val file = tts.createTTSFile(data.text, data.language)
val filename = file.name val filename = file.name
@@ -34,16 +37,30 @@ private class Consumer(
it.clip(url, "") it.clip(url, "")
it.volume = currentVolume 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<SonosTTSRequestData> = LinkedBlockingQueue() private val queue: BlockingQueue<SonosTTSRequestData> = LinkedBlockingQueue()
private val consumer = Thread(Consumer(tts, host, port, queue)).also { private val host: String
it.name = "SONOS_QUEUE" 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) fun push(data: SonosTTSRequestData) = queue.add(data)
} }

View File

@@ -13,10 +13,16 @@ import java.util.*
data class SpeechData(val stream: InputStream, val size: Long) data class SpeechData(val stream: InputStream, val size: Long)
class TTS(private val context: Context, initListener: TextToSpeech.OnInitListener) { class TTS(
private val tts = TextToSpeech(context, initListener) private val context: Context,
private val tts: TextToSpeech,
private val ttsStausHolder: TTSStatusHolder
) {
private val messageDigest = MessageDigest.getInstance("SHA-256") private val messageDigest = MessageDigest.getInstance("SHA-256")
val status: TTSStatus
get() = ttsStausHolder.status
fun createTTSFile(text: String, language: Locale): File { fun createTTSFile(text: String, language: Locale): File {
val digest = hash(text, language) val digest = hash(text, language)
val filename = "tts_$digest.wav" val filename = "tts_$digest.wav"
@@ -34,7 +40,7 @@ class TTS(private val context: Context, initListener: TextToSpeech.OnInitListene
lock.wait() lock.wait()
} }
if(!lock.success) { if (!lock.success) {
throw TTSException() throw TTSException()
} }

View File

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

View File

@@ -15,7 +15,7 @@ enum class Endpoint(val uri: String, val id: Int) {
} }
} }
class Endpoints { object Endpoints {
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init { init {

View File

@@ -2,47 +2,56 @@ package io.bartek.ttsserver.web
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.speech.tts.TextToSpeech
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import fi.iki.elonen.NanoHTTPD import fi.iki.elonen.NanoHTTPD
import fi.iki.elonen.NanoHTTPD.Response.Status.* import fi.iki.elonen.NanoHTTPD.Response.Status.*
import io.bartek.ttsserver.network.NetworkUtil
import io.bartek.ttsserver.preference.PreferenceKey import io.bartek.ttsserver.preference.PreferenceKey
import io.bartek.ttsserver.service.ForegroundService import io.bartek.ttsserver.service.ForegroundService
import io.bartek.ttsserver.service.ServiceState import io.bartek.ttsserver.service.ServiceState
import io.bartek.ttsserver.sonos.SonosQueue import io.bartek.ttsserver.sonos.SonosQueue
import io.bartek.ttsserver.tts.TTS import io.bartek.ttsserver.tts.TTS
import io.bartek.ttsserver.tts.TTSStatus
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
class WebServer(port: Int, private val context: Context) : NanoHTTPD(port), class WebServer(
TextToSpeech.OnInitListener { port: Int,
private val preferences = PreferenceManager.getDefaultSharedPreferences(context) private val context: Context,
private val tts = TTS(context, this) private val preferences: SharedPreferences,
private val endpoints = Endpoints() private val tts: TTS,
private val sonos = SonosQueue(tts, NetworkUtil.getIpAddress(context), port) private val sonos: SonosQueue
) : NanoHTTPD(port) {
override fun serve(session: IHTTPSession?): Response { override fun serve(session: IHTTPSession?): Response {
try { try {
assertThatTTSIsReady()
session?.let { session?.let {
return when (endpoints.match(it.uri)) { return dispatch(it)
Endpoint.SAY -> say(it)
Endpoint.WAVE -> wave(it)
Endpoint.SONOS -> sonos(it)
Endpoint.SONOS_CACHE -> sonosCache(it)
Endpoint.UNKNOWN -> throw ResponseException(NOT_FOUND, "")
}
} }
throw ResponseException(BAD_REQUEST, "") throw ResponseException(BAD_REQUEST, "")
} catch (e: ResponseException) { }
throw e catch (e: ResponseException) { throw e }
} catch (e: Exception) { catch (e: Exception) { throw ResponseException(INTERNAL_ERROR, e.toString(), e) }
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, "") 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) val file = File(context.cacheDir, filename)
if(!file.exists()) { if (!file.exists()) {
throw ResponseException(NOT_FOUND, "") 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) return newFixedLengthResponse(OK, MIME_WAVE, stream, size)
} }
override fun onInit(status: Int) = start()
override fun start() { override fun start() {
super.start() super.start()
sonos.run()
LocalBroadcastManager LocalBroadcastManager
.getInstance(context) .getInstance(context)
.sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also { .sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also {
@@ -145,6 +154,7 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
override fun stop() { override fun stop() {
super.stop() super.stop()
sonos.stop()
LocalBroadcastManager LocalBroadcastManager
.getInstance(context) .getInstance(context)
.sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also { .sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also {

View File

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