Create working PoC

This commit is contained in:
2020-07-01 18:31:38 +02:00
parent 10a8776379
commit e4cdd29034
18 changed files with 444 additions and 169 deletions

View File

@@ -38,6 +38,7 @@ dependencies {
implementation 'com.google.dagger:dagger-android:2.15'
implementation 'com.google.dagger:dagger-android-support:2.15'
implementation 'com.github.adrielcafe:AndroidAudioConverter:0.0.8'
implementation 'org.luaj:luaj-jse:3.0.1'
kapt 'com.google.dagger:dagger-android-processor:2.15'
kapt 'com.google.dagger:dagger-compiler:2.15'
testImplementation 'junit:junit:4.12'

View File

@@ -14,6 +14,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.bartlomiejpluta.permission.TTS_HTTP_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:name="com.bartlomiejpluta.ttsserver.TTSApplication"

View File

@@ -0,0 +1,22 @@
package com.bartlomiejpluta.ttsserver.core.lua.lib
import com.bartlomiejpluta.ttsserver.core.web.endpoint.EndpointType
import fi.iki.elonen.NanoHTTPD
import org.luaj.vm2.LuaValue
import org.luaj.vm2.lib.TwoArgFunction
class HTTPLibrary : TwoArgFunction() {
override fun call(modname: LuaValue, env: LuaValue): LuaValue {
val methods = LuaValue.tableOf()
val responses = LuaValue.tableOf()
val endpoints = LuaValue.tableOf()
NanoHTTPD.Method.values().forEach { methods.set(it.name, it.name) }
NanoHTTPD.Response.Status.values().forEach { responses.set(it.name, it.requestStatus) }
EndpointType.values().forEach { endpoints.set(it.name, it.name) }
env.set("Method", methods)
env.set("Response", responses)
env.set("Endpoint", endpoints)
return methods
}
}

View File

@@ -0,0 +1,30 @@
package com.bartlomiejpluta.ttsserver.core.lua.lib
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
import org.luaj.vm2.LuaNil
import org.luaj.vm2.LuaString
import org.luaj.vm2.LuaValue
import org.luaj.vm2.lib.TwoArgFunction
import java.lang.IllegalArgumentException
import java.util.*
class TTSLibrary(private val ttsEngine: TTSEngine) : TwoArgFunction() {
override fun call(modname: LuaValue, env: LuaValue): LuaValue {
val tts = LuaValue.tableOf()
tts.set("performTTS", SayMethod(ttsEngine))
env.set("tts", tts)
return tts
}
class SayMethod(private val ttsEngine: TTSEngine) : TwoArgFunction() {
override fun call(textArg: LuaValue, languageArg: LuaValue): LuaValue {
val text = textArg as? LuaString ?: throw IllegalArgumentException("Text should be a string")
val language = textArg as? LuaString ?: throw IllegalArgumentException("Language should be a string")
ttsEngine.performTTS(text.tojstring(), Locale.forLanguageTag(language.tojstring()))
return LuaValue.NIL
}
}
}

View File

@@ -0,0 +1,63 @@
package com.bartlomiejpluta.ttsserver.core.lua.loader
import android.content.Context
import com.bartlomiejpluta.ttsserver.core.lua.sandbox.SandboxFactory
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint
import com.bartlomiejpluta.ttsserver.core.web.endpoint.EndpointType
import com.bartlomiejpluta.ttsserver.core.web.uri.UriTemplate
import fi.iki.elonen.NanoHTTPD.Method
import org.luaj.vm2.LuaClosure
import org.luaj.vm2.LuaNil
import org.luaj.vm2.LuaString
import org.luaj.vm2.LuaTable
import java.lang.IllegalArgumentException
class EndpointLoader(private val context: Context, private val sandboxFactory: SandboxFactory) {
fun loadEndpoints(): List<Endpoint> {
val scripts = context.getExternalFilesDir("Endpoints")?.listFiles() ?: emptyArray()
return scripts
.map { sandboxFactory.createSandbox().loadfile(it.absolutePath).call() }
.map { it as? LuaTable ?: throw IllegalArgumentException("Expected single table to be returned") }
.map { createEndpoint(it) }
}
private fun createEndpoint(luaTable: LuaTable) = Endpoint(
uri = parseUri(luaTable),
method = parseMethod(luaTable),
type = parseType(luaTable),
accepts = parseAccepts(luaTable),
consumer = parseConsumer(luaTable)
)
private fun parseUri(luaTable: LuaTable) = luaTable.get("uri")
.takeIf { it !is LuaNil }
?.let { it as? LuaString ?: throw IllegalArgumentException("'uri' must be of string type'") }
?.tojstring()
?.let { UriTemplate.parse(it) }
?: throw IllegalArgumentException("'uri' field is required")
private fun parseConsumer(luaTable: LuaTable) = luaTable.get("consumer")
.takeIf { it !is LuaNil }
?.let { it as? LuaClosure ?: throw IllegalArgumentException("'consumer' must be a function'") }
?: throw IllegalArgumentException("'consumer' field is required")
private fun parseAccepts(luaTable: LuaTable) = luaTable.get("accepts")
.takeIf { it !is LuaNil }
?.let { it as? LuaString ?: throw IllegalArgumentException("'accepts' must be of string type'") }
?.tojstring()
?: "text/plain"
private fun parseType(luaTable: LuaTable) = luaTable.get("type")
.takeIf { it !is LuaNil }
?.let { it as? LuaString ?: throw IllegalArgumentException("'type' must be of string type'") }
?.let { EndpointType.valueOf(it.tojstring()) }
?: EndpointType.DEFAULT
private fun parseMethod(luaTable: LuaTable) = luaTable.get("method")
.takeIf { it !is LuaNil }
?.let { it as? LuaString ?: throw IllegalArgumentException("'method' must be of string type'") }
?.let { Method.valueOf(it.tojstring()) }
?: Method.GET
}

View File

@@ -0,0 +1,31 @@
package com.bartlomiejpluta.ttsserver.core.lua.sandbox
import com.bartlomiejpluta.ttsserver.core.lua.lib.HTTPLibrary
import com.bartlomiejpluta.ttsserver.core.lua.lib.TTSLibrary
import org.luaj.vm2.Globals
import org.luaj.vm2.LoadState
import org.luaj.vm2.compiler.LuaC
import org.luaj.vm2.lib.PackageLib
import org.luaj.vm2.lib.StringLib
import org.luaj.vm2.lib.TableLib
import org.luaj.vm2.lib.jse.JseBaseLib
import org.luaj.vm2.lib.jse.JseMathLib
import org.luaj.vm2.lib.jse.JseOsLib
class SandboxFactory(
private val httpLibrary: HTTPLibrary,
private val ttsLibrary: TTSLibrary
) {
fun createSandbox() = Globals().also {
it.load(JseBaseLib())
it.load(PackageLib())
it.load(TableLib())
it.load(StringLib())
it.load(JseMathLib())
it.load(JseOsLib())
it.load(httpLibrary)
it.load(ttsLibrary)
LoadState.install(it)
LuaC.install(it)
}
}

View File

@@ -4,7 +4,7 @@ import android.content.SharedPreferences
import cafe.adriel.androidaudioconverter.model.AudioFormat
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
import com.bartlomiejpluta.ttsserver.core.web.dto.SonosDTO
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpointx
import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey
@@ -20,8 +20,8 @@ class SonosWorker(
private val preferences: SharedPreferences,
private val queue: BlockingQueue<SonosDTO>
) : Runnable {
private val gongUrl: String get() = address + Endpoint.GONG.trimmedUri
private val announcementUrl: String get() = address + Endpoint.SONOS_CACHE.trimmedUri
private val gongUrl: String get() = address + Endpointx.GONG.trimmedUri
private val announcementUrl: String get() = address + Endpointx.SONOS_CACHE.trimmedUri
private var snapshot: Snapshot? = null
override fun run() = try {

View File

@@ -1,22 +1,92 @@
package com.bartlomiejpluta.ttsserver.core.web.endpoint
enum class Endpoint(val uri: String, val id: Int) {
UNKNOWN("/", 1),
SAY("/say", 2),
WAVE("/wave", 3),
AAC("/aac", 4),
MP3("/mp3", 5),
M4A("/m4a", 6),
WMA("/wma", 7),
FLAC("/flac", 8),
SONOS("/sonos", 9),
SONOS_CACHE("/sonos/*", 10),
GONG("/gong.wav", 11);
import com.bartlomiejpluta.ttsserver.core.web.uri.UriTemplate
import fi.iki.elonen.NanoHTTPD.*
import org.luaj.vm2.LuaClosure
import org.luaj.vm2.LuaTable
import org.luaj.vm2.LuaValue
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
val trimmedUri: String
get() = uri.replace("*", "")
class Endpoint(
private val uri: UriTemplate,
private val type: EndpointType,
private val accepts: String,
private val method: Method,
private val consumer: LuaClosure
) {
companion object {
fun of(ordinal: Int) = values().firstOrNull { it.ordinal == ordinal } ?: UNKNOWN
fun hit(session: IHTTPSession): Response? {
if (session.method != method) {
return null
}
val matchingResult = uri.match(session.uri)
if (!matchingResult.matched) {
return null
}
val params = LuaValue.tableOf().also { params ->
matchingResult.variables
.map { LuaValue.valueOf(it.key) to LuaValue.valueOf(it.value) }
.forEach { params.set(it.first, it.second) }
}
val response = consumer.call(LuaValue.valueOf(extractBody(session)), params).checktable()
return parseResponse(response)
}
private fun parseResponse(response: LuaValue) = response
.let {
it as? LuaTable
?: throw IllegalArgumentException("Invalid type for response - expected table")
}
.let { provideResponse(it) }
private fun provideResponse(response: LuaTable) =
when (response.get("type").checkjstring()) {
ResponseType.TEXT.name -> getTextResponse(response)
ResponseType.FILE.name -> getFileResponse(response)
else -> throw IllegalArgumentException("Unknown value for type in response")
}
private fun getTextResponse(response: LuaTable) = newFixedLengthResponse(
getStatus(response),
getMimeType(response),
getData(response)
)
private fun getFileResponse(response: LuaTable): Response? {
val file = File(response.get("file").checkstring().tojstring())
val stream = BufferedInputStream(FileInputStream(file))
val length = file.length()
return newFixedLengthResponse(
getStatus(response),
getMimeType(response),
stream,
length
)
}
private fun getStatus(response: LuaTable): Response.Status {
val status = response.get("status").checkint()
return Response.Status
.values()
.firstOrNull { it.requestStatus == status }
?: throw IllegalArgumentException("Unsupported status: $status")
}
private fun getMimeType(response: LuaTable) = response.get("mime").checkstring().tojstring()
private fun getData(response: LuaTable) = response.get("data").checkstring().tojstring()
private fun extractBody(session: IHTTPSession): String {
return mutableMapOf<String, String>().let {
session.parseBody(it)
it["postData"] ?: ""
}
}
}

View File

@@ -7,11 +7,11 @@ object EndpointMatcher {
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init {
Endpoint.values().forEach {
Endpointx.values().forEach {
uriMatcher.addURI("", it.uri, it.ordinal)
}
}
fun match(uri: String) =
Endpoint.of(uriMatcher.match(Uri.parse("content://$uri")))
Endpointx.of(uriMatcher.match(Uri.parse("content://$uri")))
}

View File

@@ -0,0 +1,6 @@
package com.bartlomiejpluta.ttsserver.core.web.endpoint
enum class EndpointType {
DEFAULT,
QUEUE
}

View File

@@ -0,0 +1,22 @@
package com.bartlomiejpluta.ttsserver.core.web.endpoint
enum class Endpointx(val uri: String, val id: Int) {
UNKNOWN("/", 1),
SAY("/say", 2),
WAVE("/wave", 3),
AAC("/aac", 4),
MP3("/mp3", 5),
M4A("/m4a", 6),
WMA("/wma", 7),
FLAC("/flac", 8),
SONOS("/sonos", 9),
SONOS_CACHE("/sonos/*", 10),
GONG("/gong.wav", 11);
val trimmedUri: String
get() = uri.replace("*", "")
companion object {
fun of(ordinal: Int) = values().firstOrNull { it.ordinal == ordinal } ?: UNKNOWN
}
}

View File

@@ -0,0 +1,6 @@
package com.bartlomiejpluta.ttsserver.core.web.endpoint
enum class ResponseType {
TEXT,
FILE
}

View File

@@ -3,19 +3,14 @@ package com.bartlomiejpluta.ttsserver.core.web.server
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import cafe.adriel.androidaudioconverter.model.AudioFormat
import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader
import com.bartlomiejpluta.ttsserver.core.lua.sandbox.SandboxFactory
import com.bartlomiejpluta.ttsserver.core.sonos.queue.SonosQueue
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
import com.bartlomiejpluta.ttsserver.core.tts.exception.TTSException
import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatus
import com.bartlomiejpluta.ttsserver.core.web.dto.BaseDTO
import com.bartlomiejpluta.ttsserver.core.web.dto.SonosDTO
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint
import com.bartlomiejpluta.ttsserver.core.web.endpoint.EndpointMatcher
import com.bartlomiejpluta.ttsserver.core.web.exception.WebException
import com.bartlomiejpluta.ttsserver.core.web.mime.MimeType
import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey
@@ -23,10 +18,6 @@ import com.bartlomiejpluta.ttsserver.ui.preference.model.TimeRange
import fi.iki.elonen.NanoHTTPD
import fi.iki.elonen.NanoHTTPD.Response.Status.*
import org.json.JSONObject
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.util.*
class WebServer(
@@ -34,7 +25,8 @@ class WebServer(
private val context: Context,
private val preferences: SharedPreferences,
private val tts: TTSEngine,
private val sonos: SonosQueue
private val sonos: SonosQueue,
private val endpoints: List<Endpoint>
) : NanoHTTPD(port) {
private val speakersSilenceSchedulerEnabled: Boolean
get() = preferences.getBoolean(PreferenceKey.ENABLE_SPEAKERS_SILENCE_SCHEDULER, false)
@@ -72,20 +64,12 @@ class WebServer(
}
}
private fun dispatch(it: IHTTPSession): Response {
return when (EndpointMatcher.match(it.uri)) {
Endpoint.SAY -> say(it)
Endpoint.WAVE -> file(it, AudioFormat.WAV)
Endpoint.AAC -> file(it, AudioFormat.AAC)
Endpoint.MP3 -> file(it, AudioFormat.MP3)
Endpoint.M4A -> file(it, AudioFormat.M4A)
Endpoint.WMA -> file(it, AudioFormat.WMA)
Endpoint.FLAC -> file(it, AudioFormat.FLAC)
Endpoint.SONOS -> sonos(it)
Endpoint.SONOS_CACHE -> sonosCache(it)
Endpoint.GONG -> gong(it)
Endpoint.UNKNOWN -> throw WebException(NOT_FOUND)
private fun dispatch(session: IHTTPSession): Response {
for (endpoint in endpoints) {
endpoint.hit(session)?.let { return it }
}
throw WebException(NOT_FOUND)
}
private fun handleWebException(e: WebException) =
@@ -100,28 +84,28 @@ class WebServer(
return newFixedLengthResponse(INTERNAL_ERROR, MIME_PLAINTEXT, stacktrace)
}
private fun say(session: IHTTPSession): Response {
if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) {
throw WebException(NOT_FOUND)
}
if (session.method != Method.POST) {
throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
}
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
throw WebException(BAD_REQUEST, "Only JSON data is accepted")
}
if (speakersSilenceSchedulerEnabled && speakersSilenceSchedule.inRange(Calendar.getInstance())) {
return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "")
}
val dto = extractBody(session) { BaseDTO(it) }
tts.performTTS(dto.text, dto.language)
return newFixedLengthResponse(OK, MIME_JSON, SUCCESS_RESPONSE)
}
// private fun say(session: IHTTPSession): Response {
// if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) {
// throw WebException(NOT_FOUND)
// }
//
// if (session.method != Method.POST) {
// throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
// }
//
// if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
// throw WebException(BAD_REQUEST, "Only JSON data is accepted")
// }
//
// if (speakersSilenceSchedulerEnabled && speakersSilenceSchedule.inRange(Calendar.getInstance())) {
// return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "")
// }
//
// val dto = extractBody(session) { BaseDTO(it) }
//
// tts.performTTS(dto.text, dto.language)
// return newFixedLengthResponse(OK, MIME_JSON, SUCCESS_RESPONSE)
// }
private fun <T> extractBody(session: IHTTPSession, provider: (String) -> T): T {
return mutableMapOf<String, String>().let {
@@ -130,92 +114,92 @@ class WebServer(
}
}
private fun file(session: IHTTPSession, audioFormat: AudioFormat): Response {
if (!preferences.getBoolean(PreferenceKey.ENABLE_FILE_ENDPOINTS, true)) {
throw WebException(NOT_FOUND)
}
if (session.method != Method.POST) {
throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
}
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
throw WebException(BAD_REQUEST, "Only JSON data is accepted")
}
val dto = extractBody(session) { BaseDTO(it) }
val (stream, size) = tts.fetchTTSStream(dto.text, dto.language, audioFormat)
return newFixedLengthResponse(OK, MimeType.forAudioFormat(audioFormat).mimeType, stream, size)
}
private fun sonos(session: IHTTPSession): Response {
if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
throw WebException(NOT_FOUND)
}
if (session.method != Method.POST) {
throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
}
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
throw WebException(BAD_REQUEST, "Only JSON data is accepted")
}
if (sonosSilenceSchedulerEnabled && sonosSilenceSchedule.inRange(Calendar.getInstance())) {
return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "")
}
val dto = extractBody(session) { SonosDTO(it) }
sonos.push(dto)
return newFixedLengthResponse(ACCEPTED, MIME_JSON, QUEUED_RESPONSE)
}
private fun sonosCache(session: IHTTPSession): Response {
if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
throw WebException(NOT_FOUND)
}
if (session.method != Method.GET) {
throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed")
}
val filename = Uri.parse(session.uri).lastPathSegment ?: throw WebException(BAD_REQUEST)
val file = File(context.cacheDir, filename)
if (!file.exists()) {
throw WebException(NOT_FOUND)
}
val stream = BufferedInputStream(FileInputStream(file))
val size = file.length()
return newFixedLengthResponse(OK, MimeType.forFile(file).mimeType, stream, size)
}
private fun gong(session: IHTTPSession): Response {
if (!preferences.getBoolean(PreferenceKey.ENABLE_GONG, false)) {
throw WebException(NOT_FOUND)
}
if (session.method != Method.GET) {
throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed")
}
val uri = Uri.parse(
preferences.getString(PreferenceKey.GONG, null) ?: throw TTSException()
)
val size = context.contentResolver.openFileDescriptor(uri, "r")?.statSize
?: throw TTSException()
val stream = BufferedInputStream(
context.contentResolver.openInputStream(uri) ?: throw TTSException()
)
return newFixedLengthResponse(OK, MimeType.WAV.mimeType, stream, size)
}
// private fun file(session: IHTTPSession, audioFormat: AudioFormat): Response {
// if (!preferences.getBoolean(PreferenceKey.ENABLE_FILE_ENDPOINTS, true)) {
// throw WebException(NOT_FOUND)
// }
//
// if (session.method != Method.POST) {
// throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
// }
//
// if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
// throw WebException(BAD_REQUEST, "Only JSON data is accepted")
// }
//
// val dto = extractBody(session) { BaseDTO(it) }
//
// val (stream, size) = tts.fetchTTSStream(dto.text, dto.language, audioFormat)
// return newFixedLengthResponse(OK, MimeType.forAudioFormat(audioFormat).mimeType, stream, size)
// }
//
// private fun sonos(session: IHTTPSession): Response {
// if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
// throw WebException(NOT_FOUND)
// }
//
// if (session.method != Method.POST) {
// throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
// }
//
// if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
// throw WebException(BAD_REQUEST, "Only JSON data is accepted")
// }
//
// if (sonosSilenceSchedulerEnabled && sonosSilenceSchedule.inRange(Calendar.getInstance())) {
// return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "")
// }
//
// val dto = extractBody(session) { SonosDTO(it) }
//
// sonos.push(dto)
//
// return newFixedLengthResponse(ACCEPTED, MIME_JSON, QUEUED_RESPONSE)
// }
//
// private fun sonosCache(session: IHTTPSession): Response {
// if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
// throw WebException(NOT_FOUND)
// }
//
// if (session.method != Method.GET) {
// throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed")
// }
//
// val filename = Uri.parse(session.uri).lastPathSegment ?: throw WebException(BAD_REQUEST)
// val file = File(context.cacheDir, filename)
//
// if (!file.exists()) {
// throw WebException(NOT_FOUND)
// }
//
// val stream = BufferedInputStream(FileInputStream(file))
// val size = file.length()
// return newFixedLengthResponse(OK, MimeType.forFile(file).mimeType, stream, size)
// }
//
// private fun gong(session: IHTTPSession): Response {
// if (!preferences.getBoolean(PreferenceKey.ENABLE_GONG, false)) {
// throw WebException(NOT_FOUND)
// }
//
// if (session.method != Method.GET) {
// throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed")
// }
//
// val uri = Uri.parse(
// preferences.getString(PreferenceKey.GONG, null) ?: throw TTSException()
// )
//
// val size = context.contentResolver.openFileDescriptor(uri, "r")?.statSize
// ?: throw TTSException()
//
// val stream = BufferedInputStream(
// context.contentResolver.openInputStream(uri) ?: throw TTSException()
// )
//
// return newFixedLengthResponse(OK, MimeType.WAV.mimeType, stream, size)
// }
override fun start() {
super.start()

View File

@@ -2,6 +2,7 @@ package com.bartlomiejpluta.ttsserver.core.web.server
import android.content.Context
import android.content.SharedPreferences
import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader
import com.bartlomiejpluta.ttsserver.core.sonos.queue.SonosQueue
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey
@@ -10,13 +11,15 @@ class WebServerFactory(
private val preferences: SharedPreferences,
private val context: Context,
private val tts: TTSEngine,
private val sonos: SonosQueue
private val sonos: SonosQueue,
private val endpointLoader: EndpointLoader
) {
fun createWebServer() =
WebServer(
preferences.getInt(
PreferenceKey.PORT,
8080
), context, preferences, tts, sonos
)
fun createWebServer() = WebServer(
preferences.getInt(PreferenceKey.PORT, 8080),
context,
preferences,
tts,
sonos,
endpointLoader.loadEndpoints()
)
}

View File

@@ -28,10 +28,9 @@ class UriTemplate private constructor(uri: String) {
variables.add(variableBuilder.toString())
}
isVariable -> char.takeIf { it.isLetter() } ?: error(
"Only letters are allowed as template",
index + 1
).let { variableBuilder?.append(it) }
isVariable -> char.takeIf { it.isLetter() }
?.let { variableBuilder?.append(it) }
?: error("Only letters are allowed as template", index + 1)
else -> patternBuilder.append(char)
}

View File

@@ -3,6 +3,7 @@ package com.bartlomiejpluta.ttsserver.di.component
import android.content.Context
import com.bartlomiejpluta.ttsserver.TTSApplication
import com.bartlomiejpluta.ttsserver.di.module.AndroidModule
import com.bartlomiejpluta.ttsserver.di.module.LuaModule
import com.bartlomiejpluta.ttsserver.di.module.TTSModule
import dagger.BindsInstance
import dagger.Component
@@ -11,7 +12,7 @@ import dagger.android.support.AndroidSupportInjectionModule
import javax.inject.Singleton
@Singleton
@Component(modules = [AndroidSupportInjectionModule::class, AndroidModule::class, TTSModule::class])
@Component(modules = [AndroidSupportInjectionModule::class, AndroidModule::class, TTSModule::class, LuaModule::class])
interface AppComponent : AndroidInjector<TTSApplication> {
@Component.Builder

View File

@@ -0,0 +1,33 @@
package com.bartlomiejpluta.ttsserver.di.module
import android.content.Context
import com.bartlomiejpluta.ttsserver.core.lua.lib.HTTPLibrary
import com.bartlomiejpluta.ttsserver.core.lua.lib.TTSLibrary
import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader
import com.bartlomiejpluta.ttsserver.core.lua.sandbox.SandboxFactory
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class LuaModule {
@Provides
@Singleton
fun endpointLoader(context: Context, sandboxFactory: SandboxFactory) =
EndpointLoader(context, sandboxFactory)
@Provides
@Singleton
fun sandboxFactory(httpLibrary: HTTPLibrary, ttsLibrary: TTSLibrary) =
SandboxFactory(httpLibrary, ttsLibrary)
@Provides
@Singleton
fun httpLibrary() = HTTPLibrary()
@Provides
@Singleton
fun ttsLibrary(ttsEngine: TTSEngine) = TTSLibrary(ttsEngine)
}

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.content.SharedPreferences
import android.speech.tts.TextToSpeech
import androidx.preference.PreferenceManager
import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader
import com.bartlomiejpluta.ttsserver.core.sonos.queue.SonosQueue
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatusHolder
@@ -43,12 +44,14 @@ class TTSModule {
preferences: SharedPreferences,
context: Context,
tts: TTSEngine,
sonos: SonosQueue
sonos: SonosQueue,
endpointLoader: EndpointLoader
) = WebServerFactory(
preferences,
context,
tts,
sonos
sonos,
endpointLoader
)
@Provides