Force 3-characters wide spacing and refactor & reformat code accordingly to the new rules

This commit is contained in:
2020-05-10 17:21:28 +02:00
parent 76ed48eaf3
commit b0b1ca994f
14 changed files with 427 additions and 425 deletions

View File

@@ -3,6 +3,12 @@
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<codeStyleSettings language="JAVA">
<indentOptions>
<option name="INDENT_SIZE" value="3" />
<option name="TAB_SIZE" value="3" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML"> <codeStyleSettings language="XML">
<indentOptions> <indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" /> <option name="CONTINUATION_INDENT_SIZE" value="4" />
@@ -117,6 +123,10 @@
</codeStyleSettings> </codeStyleSettings>
<codeStyleSettings language="kotlin"> <codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<indentOptions>
<option name="INDENT_SIZE" value="3" />
<option name="TAB_SIZE" value="3" />
</indentOptions>
</codeStyleSettings> </codeStyleSettings>
</code_scheme> </code_scheme>
</component> </component>

View File

@@ -1,13 +1,11 @@
package io.bartek package io.bartek
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.Assert.*
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
* *
@@ -15,10 +13,10 @@ import org.junit.Assert.*
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
@Test @Test
fun useAppContext() { fun useAppContext() {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.bartek", appContext.packageName) assertEquals("io.bartek", appContext.packageName)
} }
} }

View File

@@ -20,88 +20,84 @@ import io.bartek.service.ServiceState
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var controlServerButton: AppCompatImageButton private lateinit var serverControlButton: AppCompatImageButton
private lateinit var promptText: TextView private lateinit var promptText: TextView
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?.let { (intent?.getStringExtra(ForegroundService.STATE) ?: ServiceState.STOPPED.name)
updateViewAccordingToServiceState( .let { ServiceState.valueOf(it) }
ServiceState.valueOf( .let { updateViewAccordingToServiceState(it) }
it.getStringExtra(ForegroundService.STATE) ?: ServiceState.STOPPED.name }
) }
)
}
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_main, menu) menuInflater.inflate(R.menu.menu_main, menu)
return true return true
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.open_preferences -> startActivity(Intent(this, PreferencesActivity::class.java)) R.id.open_preferences -> startActivity(Intent(this, PreferencesActivity::class.java))
R.id.open_help -> startActivity(Intent(this, HelpActivity::class.java)) R.id.open_help -> startActivity(Intent(this, HelpActivity::class.java))
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
private fun updateViewAccordingToServiceState(newState: ServiceState) { private fun updateViewAccordingToServiceState(newState: ServiceState) {
controlServerButton.isEnabled = true serverControlButton.isEnabled = true
when (newState) { when (newState) {
ServiceState.STOPPED -> { ServiceState.STOPPED -> {
controlServerButton.setImageResource(R.drawable.ic_power_off) serverControlButton.setImageResource(R.drawable.ic_power_off)
promptText.text = getString(R.string.main_activity_prompt_to_run) promptText.text = getString(R.string.main_activity_prompt_to_run)
} }
ServiceState.RUNNING -> { ServiceState.RUNNING -> {
controlServerButton.setImageResource(R.drawable.ic_power_on) serverControlButton.setImageResource(R.drawable.ic_power_on)
promptText.text = getString(R.string.main_activity_prompt_to_stop) promptText.text = getString(R.string.main_activity_prompt_to_stop)
} }
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
controlServerButton = findViewById(R.id.control_server_button) serverControlButton = findViewById(R.id.server_control_button)
promptText = findViewById(R.id.prompt_text) promptText = findViewById(R.id.prompt_text)
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
LocalBroadcastManager LocalBroadcastManager
.getInstance(this) .getInstance(this)
.registerReceiver(receiver, IntentFilter(ForegroundService.CHANGE_STATE)) .registerReceiver(receiver, IntentFilter(ForegroundService.CHANGE_STATE))
updateViewAccordingToServiceState(ForegroundService.state) updateViewAccordingToServiceState(ForegroundService.state)
} }
override fun onPause() { override fun onPause() {
LocalBroadcastManager LocalBroadcastManager
.getInstance(this) .getInstance(this)
.unregisterReceiver(receiver) .unregisterReceiver(receiver)
super.onPause() super.onPause()
} }
fun controlServer(view: View) { fun controlServer(view: View) {
controlServerButton.isEnabled = false serverControlButton.isEnabled = false
when (ForegroundService.state) { when (ForegroundService.state) {
ServiceState.STOPPED -> actionOnService(ForegroundService.START) ServiceState.STOPPED -> actionOnService(ForegroundService.START)
ServiceState.RUNNING -> actionOnService(ForegroundService.STOP) ServiceState.RUNNING -> actionOnService(ForegroundService.STOP)
} }
} }
private fun actionOnService(action: String) { private fun actionOnService(action: String) {
Intent(this, ForegroundService::class.java).also { Intent(this, ForegroundService::class.java).also {
it.action = action it.action = action
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(it) startForegroundService(it)
return return
} }
startService(it) startService(it)
} }
} }
} }

View File

@@ -7,24 +7,24 @@ import io.bartek.R
import java.util.* import java.util.*
class HelpActivity : AppCompatActivity() { class HelpActivity : AppCompatActivity() {
private lateinit var helpView: WebView private lateinit var helpView: WebView
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_help) setContentView(R.layout.activity_help)
helpView = findViewById(R.id.help_view) helpView = findViewById(R.id.help_view)
loadHelp() loadHelp()
} }
private fun loadHelp() { private fun loadHelp() {
val lang = Locale.getDefault().language val lang = Locale.getDefault().language
val file = HELP_FILE.format(".$lang") val file = HELP_FILE.format(".$lang")
.takeIf { resources.assets.list("help")?.contains(it) == true } .takeIf { resources.assets.list("help")?.contains(it) == true }
?: HELP_FILE.format("") ?: HELP_FILE.format("")
helpView.loadUrl("file:///android_asset/help/${file}") helpView.loadUrl("file:///android_asset/help/${file}")
} }
companion object { companion object {
private const val HELP_FILE = "help%s.html" private const val HELP_FILE = "help%s.html"
} }
} }

View File

@@ -3,30 +3,31 @@ package io.bartek.preference
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import java.lang.Integer.parseInt
class IntEditTextPreference : EditTextPreference { class IntEditTextPreference : EditTextPreference {
constructor( constructor(
context: Context?, context: Context?,
attrs: AttributeSet?, attrs: AttributeSet?,
defStyleAttr: Int, defStyleAttr: Int,
defStyleRes: Int defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes) ) : super(context, attrs, defStyleAttr, defStyleRes)
constructor( constructor(
context: Context?, context: Context?,
attrs: AttributeSet?, attrs: AttributeSet?,
defStyleAttr: Int defStyleAttr: Int
) : super(context, attrs, defStyleAttr) ) : super(context, attrs, defStyleAttr)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?) : super(context) constructor(context: Context?) : super(context)
override fun getPersistedString(defaultReturnValue: String?) =
getPersistedInt(Integer.valueOf(defaultReturnValue ?: "-1")).toString()
override fun persistString(value: String?) = persistInt(Integer.valueOf(value ?: "-1"))
override fun getPersistedString(defaultReturnValue: String?) = (defaultReturnValue ?: "-1")
.let { Integer.valueOf(it) }
.let { getPersistedInt(it) }
.toString()
override fun persistString(value: String?) = (value ?: "-1")
.let { Integer.valueOf(it) }
.let { persistInt(it) }
} }

View File

@@ -2,8 +2,8 @@ package io.bartek.preference
object PreferenceKey { object PreferenceKey {
const val PORT = "preference_port" const val PORT = "preference_port"
const val ENABLE_SAY_ENDPOINT = "preference_enable_say_endpoint" const val ENABLE_SAY_ENDPOINT = "preference_enable_say_endpoint"
const val ENABLE_WAVE_ENDPOINT = "preference_enable_wave_endpoint" const val ENABLE_WAVE_ENDPOINT = "preference_enable_wave_endpoint"
const val TTS = "preference_tts" const val TTS = "preference_tts"
} }

View File

@@ -6,13 +6,13 @@ import io.bartek.R
class PreferencesActivity : AppCompatActivity() { class PreferencesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_preferences) setContentView(R.layout.activity_preferences)
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
.replace(R.id.preferences, PreferencesFragment()) .replace(R.id.preferences, PreferencesFragment())
.commit() .commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
} }
} }

View File

@@ -15,57 +15,53 @@ import io.bartek.service.ForegroundService
import io.bartek.service.ServiceState import io.bartek.service.ServiceState
class PreferencesFragment : PreferenceFragmentCompat() { class PreferencesFragment : PreferenceFragmentCompat() {
private lateinit var portPreference: IntEditTextPreference private lateinit var portPreference: IntEditTextPreference
private lateinit var sayEndpointPreference: SwitchPreference private lateinit var sayEndpointPreference: SwitchPreference
private lateinit var waveEndpointPreference: SwitchPreference private lateinit var waveEndpointPreference: SwitchPreference
private lateinit var ttsEnginePreference: Preference private lateinit var ttsEnginePreference: Preference
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?.let { (intent?.getStringExtra(ForegroundService.STATE) ?: ServiceState.STOPPED.name)
updateViewAccordingToServiceState( .let { ServiceState.valueOf(it) }
ServiceState.valueOf( .let { updateViewAccordingToServiceState(it) }
it.getStringExtra(ForegroundService.STATE) ?: ServiceState.STOPPED.name }
) }
)
}
}
}
private fun updateViewAccordingToServiceState(state: ServiceState) { private fun updateViewAccordingToServiceState(state: ServiceState) {
portPreference.isEnabled = state == ServiceState.STOPPED portPreference.isEnabled = state == ServiceState.STOPPED
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
LocalBroadcastManager LocalBroadcastManager
.getInstance(context!!) .getInstance(context!!)
.registerReceiver(receiver, IntentFilter(ForegroundService.CHANGE_STATE)) .registerReceiver(receiver, IntentFilter(ForegroundService.CHANGE_STATE))
updateViewAccordingToServiceState(ForegroundService.state) updateViewAccordingToServiceState(ForegroundService.state)
} }
override fun onPause() { override fun onPause() {
LocalBroadcastManager LocalBroadcastManager
.getInstance(context!!) .getInstance(context!!)
.unregisterReceiver(receiver) .unregisterReceiver(receiver)
super.onPause() super.onPause()
} }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey) setPreferencesFromResource(R.xml.preferences, rootKey)
portPreference = findPreference(PreferenceKey.PORT)!! portPreference = findPreference(PreferenceKey.PORT)!!
portPreference.setOnBindEditTextListener { it.inputType = InputType.TYPE_CLASS_NUMBER } portPreference.setOnBindEditTextListener { it.inputType = InputType.TYPE_CLASS_NUMBER }
sayEndpointPreference = findPreference(PreferenceKey.ENABLE_SAY_ENDPOINT)!! sayEndpointPreference = findPreference(PreferenceKey.ENABLE_SAY_ENDPOINT)!!
waveEndpointPreference = findPreference(PreferenceKey.ENABLE_WAVE_ENDPOINT)!! waveEndpointPreference = findPreference(PreferenceKey.ENABLE_WAVE_ENDPOINT)!!
ttsEnginePreference = findPreference(PreferenceKey.TTS)!! ttsEnginePreference = findPreference(PreferenceKey.TTS)!!
ttsEnginePreference.setOnPreferenceClickListener { ttsEnginePreference.setOnPreferenceClickListener {
startActivity(Intent(ANDROID_TTS_SETTINGS)) startActivity(Intent(ANDROID_TTS_SETTINGS))
true true
} }
updateViewAccordingToServiceState(ForegroundService.state) updateViewAccordingToServiceState(ForegroundService.state)
} }
companion object { companion object {
private const val ANDROID_TTS_SETTINGS = "com.android.settings.TTS_SETTINGS" private const val ANDROID_TTS_SETTINGS = "com.android.settings.TTS_SETTINGS"
} }
} }

View File

@@ -12,58 +12,55 @@ import io.bartek.MainActivity
import io.bartek.R import io.bartek.R
class ForegroundNotificationFactory(private val context: Context) { class ForegroundNotificationFactory(private val context: Context) {
private val oreo: Boolean private val oreo: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
fun createForegroundNotification(port: Int): Notification { fun createForegroundNotification(port: Int): Notification {
createNotificationChannel() createNotificationChannel()
return buildNotification(port, createPendingIntent())
}
val pendingIntent = createPendingIntent() @Suppress("DEPRECATION")
private fun buildNotification(port: Int, pendingIntent: PendingIntent?) =
provideNotificationBuilder()
.setContentTitle(context.resources.getString(R.string.service_notification_title))
.setContentText(context.resources.getString(R.string.service_notification_text, port))
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_foreground_service)
.setTicker(context.getString(R.string.service_notification_text, port))
.setPriority(Notification.PRIORITY_HIGH) // for under android 26 compatibility
.build()
return buildNotification(port, pendingIntent) @SuppressLint("NewApi")
} private fun createNotificationChannel() {
if (oreo) {
val manager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
context.resources.getString(R.string.service_notification_category_name),
NotificationManager.IMPORTANCE_HIGH
).let {
it.description =
context.resources.getString(R.string.service_notification_category_description)
it
}
manager.createNotificationChannel(channel)
}
}
@Suppress("DEPRECATION") private fun createPendingIntent() =
private fun buildNotification(port: Int, pendingIntent: PendingIntent?) = Intent(context, MainActivity::class.java).let { notificationIntent ->
provideNotificationBuilder() PendingIntent.getActivity(context, 0, notificationIntent, 0)
.setContentTitle(context.resources.getString(R.string.service_notification_title)) }
.setContentText(context.resources.getString(R.string.service_notification_text, port))
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_foreground_service)
.setTicker(context.getString(R.string.service_notification_text))
.setPriority(Notification.PRIORITY_HIGH) // for under android 26 compatibility
.build()
@SuppressLint("NewApi") @Suppress("DEPRECATION")
private fun createNotificationChannel() { @SuppressLint("NewApi")
if (oreo) { private fun provideNotificationBuilder() =
val manager = if (oreo) Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager else Notification.Builder(context)
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
context.resources.getString(R.string.service_notification_category_name),
NotificationManager.IMPORTANCE_HIGH
).let {
it.description =
context.resources.getString(R.string.service_notification_category_description)
it
}
manager.createNotificationChannel(channel)
}
}
private fun createPendingIntent() = companion object {
Intent(context, MainActivity::class.java).let { notificationIntent -> private const val NOTIFICATION_CHANNEL_ID = "TTSService.NOTIFICATION_CHANNEL"
PendingIntent.getActivity(context, 0, notificationIntent, 0) }
}
@Suppress("DEPRECATION")
@SuppressLint("NewApi")
private fun provideNotificationBuilder() =
if (oreo) Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
else Notification.Builder(context)
companion object {
private const val NOTIFICATION_CHANNEL_ID = "TTSService.NOTIFICATION_CHANNEL"
}
} }

View File

@@ -1,5 +1,6 @@
package io.bartek.service package io.bartek.service
import android.annotation.SuppressLint
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -7,78 +8,80 @@ import android.content.SharedPreferences
import android.os.PowerManager import android.os.PowerManager
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import io.bartek.preference.PreferenceKey import io.bartek.preference.PreferenceKey
import io.bartek.web.TTSServer import io.bartek.web.WebServer
class ForegroundService : Service() { class ForegroundService : Service() {
private lateinit var preferences: SharedPreferences 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 ttsServer: TTSServer? = 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) private val notificationFactory = ForegroundNotificationFactory(this)
override fun onCreate() {
super.onCreate()
preferences = PreferenceManager.getDefaultSharedPreferences(this)
startForeground(1, notificationFactory.createForegroundNotification(port))
}
override fun onBind(intent: Intent) = null override fun onCreate() {
super.onCreate()
preferences = PreferenceManager.getDefaultSharedPreferences(this)
startForeground(1, notificationFactory.createForegroundNotification(port))
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onBind(intent: Intent) = null
intent?.let {
when(it.action) { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
START -> startService() intent?.let {
STOP -> stopService() when (it.action) {
START -> startService()
STOP -> stopService()
}
}
return START_STICKY
}
override fun onDestroy() {
webServer = null
}
@SuppressLint("WakelockTimeout")
private fun startService() {
if (isServiceStarted) return
isServiceStarted = true
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply {
acquire()
} }
} }
webServer = WebServer(port, this)
state = ServiceState.RUNNING
}
return START_STICKY private fun stopService() {
} webServer?.stop()
webServer = null
wakeLock?.let {
if (it.isHeld) {
it.release()
}
override fun onDestroy() { stopForeground(true)
ttsServer = null stopSelf()
} }
state = ServiceState.STOPPED
}
private fun startService() { companion object {
if(isServiceStarted) return // Disclaimer: I don't know the better way
isServiceStarted = true // to check whether the service is already running
wakeLock = // than to place it as a static field
(getSystemService(Context.POWER_SERVICE) as PowerManager).run { var state = ServiceState.STOPPED
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply {
acquire()
}
}
ttsServer = TTSServer(port, this)
state = ServiceState.RUNNING
}
private fun stopService() { private const val WAKELOCK_TAG = "ForegroundService::lock"
ttsServer?.stop() const val CHANGE_STATE = "io.bartek.service.CHANGE_STATE"
ttsServer = null const val STATE = "STATE"
wakeLock?.let { const val START = "START"
if(it.isHeld) { const val STOP = "STOP"
it.release() }
}
stopForeground(true)
stopSelf()
}
state = ServiceState.STOPPED
}
companion object {
// Disclaimer: I don't know the better way
// to check whether the service is already running
// than to place it as a static field
var state = ServiceState.STOPPED
private const val WAKELOCK_TAG = "ForegroundService::lock"
const val CHANGE_STATE = "io.bartek.service.CHANGE_STATE"
const val STATE = "STATE"
const val START = "START"
const val STOP = "STOP"
}
} }

View File

@@ -1,6 +1,6 @@
package io.bartek.service package io.bartek.service
enum class ServiceState { enum class ServiceState {
RUNNING, RUNNING,
STOPPED STOPPED
} }

View File

@@ -7,80 +7,81 @@ import io.bartek.exception.TTSException
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.lang.RuntimeException
import java.util.* import java.util.*
data class SpeechData(val stream: InputStream, val size: Long) data class SpeechData(val stream: InputStream, val size: Long)
class TTS(context: Context, initListener: TextToSpeech.OnInitListener) { class TTS(context: Context, initListener: TextToSpeech.OnInitListener) {
private val tts = TextToSpeech(context, initListener) private val tts = TextToSpeech(context, initListener)
fun fetchTTSStream(text: String, language: Locale): SpeechData { fun fetchTTSStream(text: String, language: Locale): SpeechData {
val file = createTempFile("tmp_tts_server", ".wav") val file = createTempFile("tmp_tts_server", ".wav")
val uuid = UUID.randomUUID().toString() val uuid = UUID.randomUUID().toString()
val lock = Lock() val lock = Lock()
tts.setOnUtteranceProgressListener(TTSProcessListener(uuid, lock)) tts.setOnUtteranceProgressListener(TTSProcessListener(uuid, lock))
synchronized(lock) { synchronized(lock) {
tts.language = language tts.language = language
tts.synthesizeToFile(text, null, file, uuid) tts.synthesizeToFile(text, null, file, uuid)
lock.wait() lock.wait()
} }
if (!lock.success) { if (!lock.success) {
throw TTSException() throw TTSException()
} }
val stream = BufferedInputStream(FileInputStream(file)) val stream = BufferedInputStream(FileInputStream(file))
val length = file.length() val length = file.length()
file.delete() file.delete()
return SpeechData(stream, length) return SpeechData(stream, length)
} }
fun performTTS(text: String, language: Locale) { fun performTTS(text: String, language: Locale) {
val uuid = UUID.randomUUID().toString() val uuid = UUID.randomUUID().toString()
val lock = Lock() val lock = Lock()
tts.setOnUtteranceProgressListener(TTSProcessListener(uuid, lock)) tts.setOnUtteranceProgressListener(TTSProcessListener(uuid, lock))
synchronized(lock) { synchronized(lock) {
tts.language = language tts.language = language
tts.speak(text, TextToSpeech.QUEUE_ADD, null, uuid) tts.speak(text, TextToSpeech.QUEUE_ADD, null, uuid)
lock.wait() lock.wait()
} }
if(!lock.success) { if (!lock.success) {
throw TTSException() throw TTSException()
} }
} }
} }
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
// TODO: Investigate the Kotlin way to achieve the same
private data class Lock(var success: Boolean = false) : Object() private data class Lock(var success: Boolean = false) : Object()
private class TTSProcessListener( private class TTSProcessListener(
private val uuid: String, private val uuid: String,
private val lock: Lock private val lock: Lock
) : UtteranceProgressListener() { ) : UtteranceProgressListener() {
override fun onDone(utteranceId: String?) { override fun onDone(utteranceId: String?) {
if (utteranceId == uuid) { if (utteranceId == uuid) {
synchronized(lock) { synchronized(lock) {
lock.success = true lock.success = true
lock.notifyAll() lock.notifyAll()
} }
} }
} }
override fun onError(utteranceId: String?) {
if (utteranceId == uuid) {
synchronized(lock) {
lock.success = false
lock.notifyAll()
}
}
}
override fun onStart(utteranceId: String?) {} override fun onError(utteranceId: String?) {
if (utteranceId == uuid) {
synchronized(lock) {
lock.success = false
lock.notifyAll()
}
}
}
override fun onStart(utteranceId: String?) {}
} }

View File

@@ -17,100 +17,100 @@ import java.util.*
private data class TTSRequestData(val text: String, val language: Locale) private data class TTSRequestData(val text: String, val language: Locale)
class TTSServer(port: Int, private val context: Context) : NanoHTTPD(port), class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
TextToSpeech.OnInitListener { TextToSpeech.OnInitListener {
private val preferences = PreferenceManager.getDefaultSharedPreferences(context) private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
private val tts = TTS(context, this) private val tts = TTS(context, this)
override fun serve(session: IHTTPSession?): Response { override fun serve(session: IHTTPSession?): Response {
try { try {
session?.let { session?.let {
return when (it.uri) { return when (it.uri) {
"/wave" -> wave(it) "/wave" -> wave(it)
"/say" -> say(it) "/say" -> say(it)
else -> throw ResponseException(NOT_FOUND, "") else -> throw ResponseException(NOT_FOUND, "")
}
} }
}
throw ResponseException(BAD_REQUEST, "") throw ResponseException(BAD_REQUEST, "")
} catch (e: ResponseException) { } catch (e: ResponseException) {
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
throw ResponseException(INTERNAL_ERROR, e.toString(), e) throw ResponseException(INTERNAL_ERROR, e.toString(), e)
} }
} }
private fun wave(session: IHTTPSession): Response { private fun wave(session: IHTTPSession): Response {
if (!preferences.getBoolean(PreferenceKey.ENABLE_WAVE_ENDPOINT, true)) { if (!preferences.getBoolean(PreferenceKey.ENABLE_WAVE_ENDPOINT, true)) {
throw ResponseException(NOT_FOUND, "") throw ResponseException(NOT_FOUND, "")
} }
if (session.method != Method.POST) { if (session.method != Method.POST) {
throw ResponseException(METHOD_NOT_ALLOWED, "") throw ResponseException(METHOD_NOT_ALLOWED, "")
} }
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) { if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
throw ResponseException(BAD_REQUEST, "") throw ResponseException(BAD_REQUEST, "")
} }
val (text, language) = getRequestData(session) val (text, language) = getRequestData(session)
val (stream, size) = tts.fetchTTSStream(text, language) val (stream, size) = tts.fetchTTSStream(text, language)
return newFixedLengthResponse(OK, MIME_WAVE, stream, size) return newFixedLengthResponse(OK, MIME_WAVE, stream, size)
} }
private fun say(session: IHTTPSession): Response { private fun say(session: IHTTPSession): Response {
if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) { if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) {
throw ResponseException(NOT_FOUND, "") throw ResponseException(NOT_FOUND, "")
} }
if (session.method != Method.POST) { if (session.method != Method.POST) {
throw ResponseException(METHOD_NOT_ALLOWED, "") throw ResponseException(METHOD_NOT_ALLOWED, "")
} }
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) { if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
throw ResponseException(BAD_REQUEST, "") throw ResponseException(BAD_REQUEST, "")
} }
val (text, language) = getRequestData(session) val (text, language) = getRequestData(session)
tts.performTTS(text, language) tts.performTTS(text, language)
return newFixedLengthResponse(OK, MIME_PLAINTEXT, "") return newFixedLengthResponse(OK, MIME_PLAINTEXT, "")
} }
private fun getRequestData(session: IHTTPSession): TTSRequestData { private fun getRequestData(session: IHTTPSession): TTSRequestData {
val map = mutableMapOf<String, String>() val map = mutableMapOf<String, String>()
session.parseBody(map) session.parseBody(map)
val json = JSONObject(map["postData"] ?: "{}") val json = JSONObject(map["postData"] ?: "{}")
val language = json.optString("language") val language = json.optString("language")
.takeIf { it.isNotBlank() } .takeIf { it.isNotBlank() }
?.let { Locale(it) } ?.let { Locale(it) }
?: Locale.US ?: Locale.US
val text = json.optString("text") ?: throw ResponseException(BAD_REQUEST, "") val text = json.optString("text") ?: throw ResponseException(BAD_REQUEST, "")
return TTSRequestData(text, language) return TTSRequestData(text, language)
} }
override fun onInit(status: Int) = start() override fun onInit(status: Int) = start()
override fun start() { override fun start() {
super.start() super.start()
LocalBroadcastManager LocalBroadcastManager
.getInstance(context) .getInstance(context)
.sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also { .sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also {
it.putExtra(ForegroundService.STATE, ServiceState.RUNNING.name) it.putExtra(ForegroundService.STATE, ServiceState.RUNNING.name)
}) })
} }
override fun stop() { override fun stop() {
super.stop() super.stop()
LocalBroadcastManager LocalBroadcastManager
.getInstance(context) .getInstance(context)
.sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also { .sendBroadcast(Intent(ForegroundService.CHANGE_STATE).also {
it.putExtra(ForegroundService.STATE, ServiceState.STOPPED.name) it.putExtra(ForegroundService.STATE, ServiceState.STOPPED.name)
}) })
} }
companion object { companion object {
private const val MIME_JSON = "application/json" private const val MIME_JSON = "application/json"
private const val MIME_WAVE = "audio/x-wav" private const val MIME_WAVE = "audio/x-wav"
private const val CONTENT_TYPE = "content-type" private const val CONTENT_TYPE = "content-type"
} }
} }

View File

@@ -16,7 +16,7 @@
android:textAlignment="center" /> android:textAlignment="center" />
<ImageButton <ImageButton
android:id="@+id/control_server_button" android:id="@+id/server_control_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#00000000" android:background="#00000000"