Introduce Dagger2
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
11
app/src/main/java/io/bartek/ttsserver/TTSApplication.kt
Normal file
11
app/src/main/java/io/bartek/ttsserver/TTSApplication.kt
Normal 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
|
||||
}
|
||||
}
|
||||
24
app/src/main/java/io/bartek/ttsserver/di/AndroidModule.kt
Normal file
24
app/src/main/java/io/bartek/ttsserver/di/AndroidModule.kt
Normal 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
|
||||
}
|
||||
23
app/src/main/java/io/bartek/ttsserver/di/AppComponent.kt
Normal file
23
app/src/main/java/io/bartek/ttsserver/di/AppComponent.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
60
app/src/main/java/io/bartek/ttsserver/di/TTSModule.kt
Normal file
60
app/src/main/java/io/bartek/ttsserver/di/TTSModule.kt
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
@@ -35,15 +38,29 @@ private class Consumer(
|
||||
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)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
22
app/src/main/java/io/bartek/ttsserver/tts/TTSStatusHolder.kt
Normal file
22
app/src/main/java/io/bartek/ttsserver/tts/TTSStatusHolder.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,7 +129,8 @@ 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()) {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user