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: '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'

View File

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

View File

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

View File

@@ -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)
webServer = webServerFactory.createWebServer()
webServer?.let {
state = ServiceState.RUNNING
it.start()
}
}
private fun stopService() {

View File

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

View File

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

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

View File

@@ -2,34 +2,45 @@ 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)) {
return dispatch(it)
}
throw ResponseException(BAD_REQUEST, "")
}
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)
@@ -38,11 +49,9 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
}
}
throw ResponseException(BAD_REQUEST, "")
} catch (e: ResponseException) {
throw e
} catch (e: Exception) {
throw ResponseException(INTERNAL_ERROR, e.toString(), e)
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 {

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