diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e5da728..0db3c9e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + @@ -20,7 +22,8 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" - android:usesCleartextTraffic="true"> + android:usesCleartextTraffic="true" + tools:targetApi="m"> diff --git a/app/src/main/java/io/bartek/ttsserver/network/NetworkUtil.kt b/app/src/main/java/io/bartek/ttsserver/network/NetworkUtil.kt new file mode 100644 index 0000000..c6d2c6f --- /dev/null +++ b/app/src/main/java/io/bartek/ttsserver/network/NetworkUtil.kt @@ -0,0 +1,22 @@ +package io.bartek.ttsserver.network + +import android.content.Context +import android.content.Context.WIFI_SERVICE +import android.net.wifi.WifiManager +import java.net.InetAddress + + +object NetworkUtil { + fun getIpAddress(context: Context): String { + return (context.getApplicationContext().getSystemService(WIFI_SERVICE) as WifiManager).let { + inetAddress(it.dhcpInfo.ipAddress).toString().substring(1) + } + } + + private fun inetAddress(hostAddress: Int) = byteArrayOf( + (0xff and hostAddress).toByte(), + (0xff and (hostAddress shr 8)).toByte(), + (0xff and (hostAddress shr 16)).toByte(), + (0xff and (hostAddress shr 24)).toByte() + ).let { InetAddress.getByAddress(it) } +} \ No newline at end of file diff --git a/app/src/main/java/io/bartek/ttsserver/sonos/SonosController.kt b/app/src/main/java/io/bartek/ttsserver/sonos/SonosController.kt new file mode 100644 index 0000000..835190e --- /dev/null +++ b/app/src/main/java/io/bartek/ttsserver/sonos/SonosController.kt @@ -0,0 +1,17 @@ +package io.bartek.ttsserver.sonos + +import com.vmichalak.sonoscontroller.SonosDiscovery +import java.io.File + +class SonosController(private val host: String, private val port: Int) { + fun clip(wave: File, zone: String, volume: Int) { + SonosDiscovery.discover().firstOrNull { it.zoneGroupState.name == zone }?.let { + val filename = wave.name + val url = "http://$host:$port/sonos/$filename" + val currentVolume = it.volume + it.volume = volume + it.clip(url, "") + it.volume = currentVolume + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/bartek/ttsserver/web/SonosTTSRequestData.kt b/app/src/main/java/io/bartek/ttsserver/web/SonosTTSRequestData.kt new file mode 100644 index 0000000..fe884ca --- /dev/null +++ b/app/src/main/java/io/bartek/ttsserver/web/SonosTTSRequestData.kt @@ -0,0 +1,30 @@ +package io.bartek.ttsserver.web + +import fi.iki.elonen.NanoHTTPD +import fi.iki.elonen.NanoHTTPD.ResponseException +import org.json.JSONObject +import java.util.* + +data class SonosTTSRequestData(val text: String, val language: Locale, val zone: String, val volume: Int) { + companion object { + fun fromJSON(json: String): SonosTTSRequestData { + val root = JSONObject(json) + + val language = root.optString("language") + .takeIf { it.isNotBlank() } + ?.let { Locale(it) } + ?: Locale.US + val text = root.optString("text") ?: throw ResponseException( + NanoHTTPD.Response.Status.BAD_REQUEST, + "" + ) + val zone = root.optString("zone") ?: throw ResponseException( + NanoHTTPD.Response.Status.BAD_REQUEST, + "" + ) + val volume = root.optInt("volume", 50) + + return SonosTTSRequestData(text, language, zone, volume) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/bartek/ttsserver/web/TTSRequestData.kt b/app/src/main/java/io/bartek/ttsserver/web/TTSRequestData.kt new file mode 100644 index 0000000..95d6202 --- /dev/null +++ b/app/src/main/java/io/bartek/ttsserver/web/TTSRequestData.kt @@ -0,0 +1,25 @@ +package io.bartek.ttsserver.web + +import fi.iki.elonen.NanoHTTPD +import fi.iki.elonen.NanoHTTPD.ResponseException +import org.json.JSONObject +import java.util.* + +data class TTSRequestData(val text: String, val language: Locale) { + companion object { + fun fromJSON(json: String): TTSRequestData { + val root = JSONObject(json) + + val language = root.optString("language") + .takeIf { it.isNotBlank() } + ?.let { Locale(it) } + ?: Locale.US + val text = root.optString("text") ?: throw ResponseException( + NanoHTTPD.Response.Status.BAD_REQUEST, + "" + ) + + return TTSRequestData(text, language) + } + } +} \ No newline at end of file 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 b1adcef..5f622e9 100644 --- a/app/src/main/java/io/bartek/ttsserver/web/WebServer.kt +++ b/app/src/main/java/io/bartek/ttsserver/web/WebServer.kt @@ -8,17 +8,15 @@ 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.SonosController import io.bartek.ttsserver.tts.TTS -import org.json.JSONObject import java.io.BufferedInputStream import java.io.File import java.io.FileInputStream -import java.util.* - -private data class TTSRequestData(val text: String, val language: Locale) class WebServer(port: Int, private val context: Context) : NanoHTTPD(port), @@ -26,6 +24,7 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port), private val preferences = PreferenceManager.getDefaultSharedPreferences(context) private val tts = TTS(context, this) private val endpoints = Endpoints() + private val sonos = SonosController(NetworkUtil.getIpAddress(context), port) override fun serve(session: IHTTPSession?): Response { try { @@ -47,9 +46,10 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port), } } - private fun sonos(session: IHTTPSession): Response { -// SonosDiscovery.discover().firstOrNull { it.zoneGroupState.name == "Salon" } -// ?.play() + private fun say(session: IHTTPSession): Response { + if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) { + throw ResponseException(NOT_FOUND, "") + } if (session.method != Method.POST) { throw ResponseException(METHOD_NOT_ALLOWED, "") @@ -59,9 +59,52 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port), throw ResponseException(BAD_REQUEST, "") } - val (text, language) = getRequestData(session) + val (text, language) = extractBody(session) { TTSRequestData.fromJSON(it) } + + tts.performTTS(text, language) + return newFixedLengthResponse(OK, MIME_PLAINTEXT, "") + } + + private fun extractBody(session: IHTTPSession, provider: (String) -> T): T { + return mutableMapOf().let { + session.parseBody(it) + provider(it["postData"] ?: "{}") + } + } + + private fun wave(session: IHTTPSession): Response { + if (!preferences.getBoolean(PreferenceKey.ENABLE_WAVE_ENDPOINT, true)) { + throw ResponseException(NOT_FOUND, "") + } + + if (session.method != Method.POST) { + throw ResponseException(METHOD_NOT_ALLOWED, "") + } + + if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) { + throw ResponseException(BAD_REQUEST, "") + } + + val (text, language) = extractBody(session) { TTSRequestData.fromJSON(it) } + + val (stream, size) = tts.fetchTTSStream(text, language) + return newFixedLengthResponse(OK, MIME_WAVE, stream, size) + } + + private fun sonos(session: IHTTPSession): Response { + if (session.method != Method.POST) { + throw ResponseException(METHOD_NOT_ALLOWED, "") + } + + if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) { + throw ResponseException(BAD_REQUEST, "") + } + + val (text, language, zone, volume) = extractBody(session) { SonosTTSRequestData.fromJSON(it) } + val file = tts.createTTSFile(text, language) - return newFixedLengthResponse(file.toString()) + sonos.clip(file, zone, volume) + return newFixedLengthResponse("") } private fun sonosCache(session: IHTTPSession): Response { @@ -81,54 +124,6 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port), return newFixedLengthResponse(OK, MIME_WAVE, stream, size) } - private fun wave(session: IHTTPSession): Response { - if (!preferences.getBoolean(PreferenceKey.ENABLE_WAVE_ENDPOINT, true)) { - throw ResponseException(NOT_FOUND, "") - } - - if (session.method != Method.POST) { - throw ResponseException(METHOD_NOT_ALLOWED, "") - } - - if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) { - throw ResponseException(BAD_REQUEST, "") - } - - val (text, language) = getRequestData(session) - val (stream, size) = tts.fetchTTSStream(text, language) - return newFixedLengthResponse(OK, MIME_WAVE, stream, size) - } - - private fun say(session: IHTTPSession): Response { - if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) { - throw ResponseException(NOT_FOUND, "") - } - - if (session.method != Method.POST) { - throw ResponseException(METHOD_NOT_ALLOWED, "") - } - - if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) { - throw ResponseException(BAD_REQUEST, "") - } - - val (text, language) = getRequestData(session) - tts.performTTS(text, language) - return newFixedLengthResponse(OK, MIME_PLAINTEXT, "") - } - - private fun getRequestData(session: IHTTPSession): TTSRequestData { - val map = mutableMapOf() - session.parseBody(map) - val json = JSONObject(map["postData"] ?: "{}") - val language = json.optString("language") - .takeIf { it.isNotBlank() } - ?.let { Locale(it) } - ?: Locale.US - val text = json.optString("text") ?: throw ResponseException(BAD_REQUEST, "") - return TTSRequestData(text, language) - } - override fun onInit(status: Int) = start() override fun start() {