Force 3-characters wide spacing and refactor & reformat code accordingly to the new rules
This commit is contained in:
10
.idea/codeStyles/Project.xml
generated
10
.idea/codeStyles/Project.xml
generated
@@ -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>
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) }
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package io.bartek.service
|
package io.bartek.service
|
||||||
|
|
||||||
enum class ServiceState {
|
enum class ServiceState {
|
||||||
RUNNING,
|
RUNNING,
|
||||||
STOPPED
|
STOPPED
|
||||||
}
|
}
|
||||||
@@ -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?) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user