Create in-app logging framework

This commit is contained in:
2020-07-27 22:19:21 +02:00
parent 9f8fd3f515
commit 7ba2ca80ef
30 changed files with 495 additions and 30 deletions

View File

@@ -27,6 +27,9 @@ android {
} }
dependencies { dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
def room_version = "2.2.5"
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
@@ -41,6 +44,9 @@ dependencies {
implementation 'com.google.dagger:dagger-android-support:2.15' implementation 'com.google.dagger:dagger-android-support:2.15'
implementation 'com.github.adrielcafe:AndroidAudioConverter:0.0.8' implementation 'com.github.adrielcafe:AndroidAudioConverter:0.0.8'
implementation 'org.luaj:luaj-jse:3.0.1' implementation 'org.luaj:luaj-jse:3.0.1'
implementation "org.threeten:threetenbp:1.4.4"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
kapt 'com.google.dagger:dagger-android-processor:2.15' kapt 'com.google.dagger:dagger-android-processor:2.15'
kapt 'com.google.dagger:dagger-compiler:2.15' kapt 'com.google.dagger:dagger-compiler:2.15'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'

View File

@@ -17,7 +17,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
android:name="com.bartlomiejpluta.ttsserver.TTSApplication" android:name=".TTSApplication"
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@@ -27,25 +27,29 @@
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="m"> tools:targetApi="m">
<activity <activity
android:name="com.bartlomiejpluta.ttsserver.ui.help.HelpActivity" android:name=".ui.log.LogActivity"
android:parentActivityName="com.bartlomiejpluta.ttsserver.ui.main.MainActivity" /> android:parentActivityName=".ui.main.MainActivity" />
<activity <activity
android:name="com.bartlomiejpluta.ttsserver.ui.preference.component.PreferencesActivity" android:name=".ui.help.HelpActivity"
android:parentActivityName=".ui.main.MainActivity" />
<activity
android:name=".ui.preference.component.PreferencesActivity"
android:label="@string/title_activity_preferences" android:label="@string/title_activity_preferences"
android:parentActivityName="com.bartlomiejpluta.ttsserver.ui.main.MainActivity" /> android:parentActivityName=".ui.main.MainActivity" />
<service <service
android:name="com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService" android:name=".service.foreground.ForegroundService"
android:enabled="true" android:enabled="true"
android:exported="true" android:exported="true"
android:permission="com.bartlomiejpluta.permission.TTS_HTTP_SERVICE" /> android:permission="com.bartlomiejpluta.permission.TTS_HTTP_SERVICE" />
<activity <activity
android:name="com.bartlomiejpluta.ttsserver.ui.main.MainActivity" android:name=".ui.main.MainActivity"
android:launchMode="singleTop"> android:launchMode="singleTop">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>

View File

@@ -0,0 +1,15 @@
package com.bartlomiejpluta.ttsserver.common.db.converter
import androidx.room.TypeConverter
import org.threeten.bp.Instant
object InstantConverter {
@TypeConverter
@JvmStatic
fun toInstant(epoch: Long?) = epoch?.let { Instant.ofEpochMilli(it) }
@TypeConverter
@JvmStatic
fun fromInstant(instant: Instant?) = instant?.toEpochMilli()
}

View File

@@ -0,0 +1,15 @@
package com.bartlomiejpluta.ttsserver.core.log.converter
import androidx.room.TypeConverter
import com.bartlomiejpluta.ttsserver.core.log.model.enumeration.LogLevel
object LogLevelConverter {
@TypeConverter
@JvmStatic
fun toLogLevel(string: String?) = string?.let { LogLevel.valueOf(it) }
@TypeConverter
@JvmStatic
fun fromLogLevel(logLevel: LogLevel?) = logLevel?.name
}

View File

@@ -0,0 +1,20 @@
package com.bartlomiejpluta.ttsserver.core.log.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import com.bartlomiejpluta.ttsserver.core.log.model.entity.LogEntry
@Dao
interface LogDao {
@Insert
fun insert(entry: LogEntry)
@Query("SELECT * FROM logentry ORDER BY date DESC LIMIT 5000")
fun getAll(): List<LogEntry>
@Query("DELETE FROM logentry")
fun clear()
}

View File

@@ -0,0 +1,11 @@
package com.bartlomiejpluta.ttsserver.core.log.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.bartlomiejpluta.ttsserver.core.log.dao.LogDao
import com.bartlomiejpluta.ttsserver.core.log.model.entity.LogEntry
@Database(entities = [LogEntry::class], version = 1)
abstract class LogDatabase : RoomDatabase() {
abstract fun dao(): LogDao
}

View File

@@ -0,0 +1,26 @@
package com.bartlomiejpluta.ttsserver.core.log.model.entity
import androidx.room.*
import com.bartlomiejpluta.ttsserver.common.db.converter.InstantConverter
import com.bartlomiejpluta.ttsserver.core.log.converter.LogLevelConverter
import com.bartlomiejpluta.ttsserver.core.log.model.enumeration.LogLevel
import org.threeten.bp.Instant
@Entity
@TypeConverters(InstantConverter::class, LogLevelConverter::class)
data class LogEntry (
@PrimaryKey
val id: Int?,
@ColumnInfo(name = "date")
val date: Instant,
@ColumnInfo(name = "level")
val level: LogLevel,
@ColumnInfo(name = "source")
val source: String,
@ColumnInfo(name = "message")
val message: String
)

View File

@@ -0,0 +1,9 @@
package com.bartlomiejpluta.ttsserver.core.log.model.enumeration
enum class LogLevel {
DEBUG,
INFO,
WARN,
ERROR,
FATAL
}

View File

@@ -0,0 +1,53 @@
package com.bartlomiejpluta.ttsserver.core.log.service
import android.content.Context
import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.bartlomiejpluta.ttsserver.core.log.database.LogDatabase
import com.bartlomiejpluta.ttsserver.core.log.model.entity.LogEntry
import com.bartlomiejpluta.ttsserver.core.log.model.enumeration.LogLevel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.threeten.bp.Instant
class LogService(private val context: Context, logDatabase: LogDatabase) {
private val dao = logDatabase.dao()
private val broadcastManager = LocalBroadcastManager.getInstance(context)
fun debug(source: String, message: String) = log(LogLevel.DEBUG, source, message)
fun info(source: String, message: String) = log(LogLevel.INFO, source, message)
fun warn(source: String, message: String) = log(LogLevel.WARN, source, message)
fun error(source: String, message: String) = log(LogLevel.ERROR, source, message)
fun fatal(source: String, message: String) = log(LogLevel.FATAL, source, message)
fun log(level: LogLevel, source: String, message: String) =
log(LogEntry(null, Instant.now(), level, source, message))
fun log(logEntry: LogEntry) {
persistLogEntry(logEntry)
broadcastLogEntry()
}
private fun persistLogEntry(logEntry: LogEntry) = runBlocking {
withContext(Dispatchers.IO + Job()) {
dao.insert(logEntry)
}
}
private fun broadcastLogEntry() = broadcastManager.sendBroadcast(Intent(UPDATE_LOGS))
val allLogs: List<LogEntry>
get() = dao.getAll()
fun clearLogs() = dao.clear()
companion object {
const val UPDATE_LOGS = "UPDATE_LOGS"
}
}

View File

@@ -0,0 +1,33 @@
package com.bartlomiejpluta.ttsserver.core.lua.lib
import com.bartlomiejpluta.ttsserver.core.log.model.enumeration.LogLevel
import com.bartlomiejpluta.ttsserver.core.log.service.LogService
import org.luaj.vm2.LuaValue
import org.luaj.vm2.lib.OneArgFunction
import org.luaj.vm2.lib.TwoArgFunction
class LogLibrary(private val logService: LogService) : TwoArgFunction() {
override fun call(modname: LuaValue, env: LuaValue): LuaValue {
val scriptName = env.get("_G").get("script").checkjstring()
val log = LuaValue.tableOf()
log.set("debug", LogFunction(logService, LogLevel.DEBUG, scriptName))
log.set("info", LogFunction(logService, LogLevel.INFO, scriptName))
log.set("warn", LogFunction(logService, LogLevel.WARN, scriptName))
log.set("error", LogFunction(logService, LogLevel.ERROR, scriptName))
log.set("fatal", LogFunction(logService, LogLevel.FATAL, scriptName))
env.set("log", log)
return env
}
class LogFunction(
private val log: LogService,
private val level: LogLevel,
private val scriptName: String
) : OneArgFunction() {
override fun call(message: LuaValue): LuaValue {
log.log(level, scriptName, message.checkjstring())
return LuaValue.NIL
}
}
}

View File

@@ -8,12 +8,18 @@ import org.luaj.vm2.lib.TwoArgFunction
import java.io.File import java.io.File
class ConfigLoader(context: Context) { class ConfigLoader(context: Context) {
private val configDirectory = context.getExternalFilesDir("config") private val configFile = File(context.getExternalFilesDir("config"), "config.lua")
private var cachedConfig: LuaTable? = null
fun refreshConfig(env: Globals) {
cachedConfig = env.loadfile(configFile.absolutePath).call().checktable()
}
fun loadConfig(env: Globals) { fun loadConfig(env: Globals) {
val configFile = File(configDirectory, "config.lua") cachedConfig
val table = env.loadfile(configFile.absolutePath).call().checktable() ?.let { ConfigLibrary(it) }
env.load(ConfigLibrary(table)) ?.let { env.load(it) }
?: error("Config has not been refreshed before loading start")
} }
class ConfigLibrary(private val table: LuaTable) : TwoArgFunction() { class ConfigLibrary(private val table: LuaTable) : TwoArgFunction() {

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.bartlomiejpluta.ttsserver.R import com.bartlomiejpluta.ttsserver.R
import com.bartlomiejpluta.ttsserver.core.log.service.LogService
import com.bartlomiejpluta.ttsserver.core.lua.sandbox.SandboxFactory import com.bartlomiejpluta.ttsserver.core.lua.sandbox.SandboxFactory
import com.bartlomiejpluta.ttsserver.core.web.endpoint.DefaultEndpoint import com.bartlomiejpluta.ttsserver.core.web.endpoint.DefaultEndpoint
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint
@@ -19,28 +20,37 @@ import java.io.File
class EndpointLoader( class EndpointLoader(
private val context: Context, private val context: Context,
private val sandboxFactory: SandboxFactory, private val sandboxFactory: SandboxFactory,
private val tasksQueueFactory: TasksQueueFactory private val tasksQueueFactory: TasksQueueFactory,
private val log: LogService
) { ) {
fun loadEndpoints(): List<Endpoint> { fun loadEndpoints(): List<Endpoint> {
sandboxFactory.refreshConfig()
log.info(TAG, "Loading endpoint scripts...")
val scripts = context.getExternalFilesDir("endpoints")?.listFiles() ?: emptyArray() val scripts = context.getExternalFilesDir("endpoints")?.listFiles() ?: emptyArray()
return scripts.mapNotNull { loadEndpoint(it) } return scripts
.mapNotNull { loadEndpoint(it) }
.also { log.info(TAG, "Loading endpoints is complete") }
} }
private fun loadEndpoint(script: File): Endpoint? { private fun loadEndpoint(script: File): Endpoint? {
try { try {
log.info(TAG, "Loading ${script.name} script...")
return sandboxFactory return sandboxFactory
.createSandbox() .createSandbox(script.name)
.loadfile(script.absolutePath) .loadfile(script.absolutePath)
.call() .call()
.checktable() .checktable()
.takeIf { parseEnabled(it) } .takeIf { parseEnabled(it) }
?.let { createEndpoint(it) } ?.let { createEndpoint(it) }
?.also { log.info(TAG, "Script ${script.name} has been loaded successfully") }
} catch (e: LuaError) { } catch (e: LuaError) {
handleError(e) handleError(e)
log.error(TAG, "Loading script ${script.name} failed: ${e.message}")
return null return null
} catch (e: Exception) { } catch (e: Exception) {
log.error(TAG, "Loading script ${script.name} failed: ${e.message}")
throw e throw e
} }
} }
@@ -91,6 +101,7 @@ class EndpointLoader(
?: throw LuaError("Invalid HTTP method. Allowed methods are: $ALLOWED_METHODS") ?: throw LuaError("Invalid HTTP method. Allowed methods are: $ALLOWED_METHODS")
companion object { companion object {
private val TAG = "@endpoints"
private val ALLOWED_METHODS = Method.values().joinToString(", ") private val ALLOWED_METHODS = Method.values().joinToString(", ")
} }
} }

View File

@@ -1,6 +1,7 @@
package com.bartlomiejpluta.ttsserver.core.lua.sandbox package com.bartlomiejpluta.ttsserver.core.lua.sandbox
import android.content.Context import android.content.Context
import com.bartlomiejpluta.ttsserver.core.log.service.LogService
import com.bartlomiejpluta.ttsserver.core.lua.lib.* import com.bartlomiejpluta.ttsserver.core.lua.lib.*
import com.bartlomiejpluta.ttsserver.core.lua.loader.ConfigLoader import com.bartlomiejpluta.ttsserver.core.lua.loader.ConfigLoader
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -18,21 +19,31 @@ import org.luaj.vm2.lib.jse.JseOsLib
class SandboxFactory( class SandboxFactory(
private val context: Context, private val context: Context,
private val log: LogService,
private val configLoader: ConfigLoader, private val configLoader: ConfigLoader,
private val threadLibrary: ThreadLibrary, private val threadLibrary: ThreadLibrary,
private val serverLibrary: ServerLibrary, private val serverLibrary: ServerLibrary,
private val logLibrary: LogLibrary,
private val httpLibrary: HTTPLibrary, private val httpLibrary: HTTPLibrary,
private val ttsLibrary: TTSLibrary, private val ttsLibrary: TTSLibrary,
private val sonosLibrary: SonosLibrary private val sonosLibrary: SonosLibrary
) { ) {
fun createSandbox() = runBlocking { fun createSandbox(scriptName: String) = createLuaGlobals(scriptName).also {
loadConfiguration(it)
}
fun refreshConfig() = runBlocking {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
createLuaGlobals() log.info(TAG, "Loading configuration file")
val configEnvironment = createLuaGlobals(":config")
configLoader.refreshConfig(configEnvironment)
} }
} }
private fun createLuaGlobals() = Globals().also { sandbox -> private fun createLuaGlobals(scriptName: String) = Globals().also { sandbox ->
log.info(TAG, "Installing sandbox for $scriptName...")
loadStandardLibraries(sandbox) loadStandardLibraries(sandbox)
sandbox.get("_G").checktable().set("script", scriptName)
loadApplicationLibraries(sandbox) loadApplicationLibraries(sandbox)
install(sandbox) install(sandbox)
loadLuaLibraries(sandbox) loadLuaLibraries(sandbox)
@@ -49,6 +60,7 @@ class SandboxFactory(
private fun loadApplicationLibraries(sandbox: Globals) { private fun loadApplicationLibraries(sandbox: Globals) {
sandbox.load(serverLibrary) sandbox.load(serverLibrary)
sandbox.load(logLibrary)
sandbox.load(threadLibrary) sandbox.load(threadLibrary)
sandbox.load(httpLibrary) sandbox.load(httpLibrary)
sandbox.load(ttsLibrary) sandbox.load(ttsLibrary)
@@ -58,6 +70,9 @@ class SandboxFactory(
private fun install(sandbox: Globals) { private fun install(sandbox: Globals) {
LoadState.install(sandbox) LoadState.install(sandbox)
LuaC.install(sandbox) LuaC.install(sandbox)
}
private fun loadConfiguration(sandbox: Globals) {
configLoader.loadConfig(sandbox) configLoader.loadConfig(sandbox)
} }
@@ -68,4 +83,8 @@ class SandboxFactory(
?.map { (name, reader) -> name.substringBeforeLast(".") to sandbox.load(reader, name) } ?.map { (name, reader) -> name.substringBeforeLast(".") to sandbox.load(reader, name) }
?.forEach { (name, value) -> sandbox.set(name, value.call()) } ?.forEach { (name, value) -> sandbox.set(name, value.call()) }
} }
companion object {
private const val TAG = "@sandbox"
}
} }

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.bartlomiejpluta.ttsserver.core.log.service.LogService
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatus import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatus
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint
@@ -22,7 +23,8 @@ class WebServer(
private val context: Context, private val context: Context,
private val preferences: SharedPreferences, private val preferences: SharedPreferences,
private val tts: TTSEngine, private val tts: TTSEngine,
private val endpoints: List<Endpoint> private val endpoints: List<Endpoint>,
private val log: LogService
) : NanoHTTPD(port) { ) : NanoHTTPD(port) {
private val queuedEndpoints = endpoints.mapNotNull { it as? QueuedEndpoint } private val queuedEndpoints = endpoints.mapNotNull { it as? QueuedEndpoint }
@@ -36,10 +38,13 @@ class WebServer(
throw WebException(BAD_REQUEST, "Unknown error") throw WebException(BAD_REQUEST, "Unknown error")
} catch (e: WebException) { } catch (e: WebException) {
log.error(TAG, "Web exception occurred: ${e.message}")
return handleWebException(e) return handleWebException(e)
} catch (e: LuaError) { } catch (e: LuaError) {
log.error(TAG, "Lua error occurred: ${e.message}")
return handleLuaError(e) return handleLuaError(e)
} catch (e: Exception) { } catch (e: Exception) {
log.fatal(TAG, "Unknown error occurred: ${e.message}")
return handleUnknownException(e) return handleUnknownException(e)
} }
} }
@@ -75,6 +80,7 @@ class WebServer(
override fun start() { override fun start() {
super.start() super.start()
log.info(TAG, "Web server is up")
LocalBroadcastManager LocalBroadcastManager
.getInstance(context) .getInstance(context)
@@ -84,6 +90,7 @@ class WebServer(
} }
override fun stop() { override fun stop() {
log.info(TAG, "Stopping web server...")
super.stop() super.stop()
queuedEndpoints.forEach { it.shutdownQueue() } queuedEndpoints.forEach { it.shutdownQueue() }
@@ -95,6 +102,7 @@ class WebServer(
} }
companion object { companion object {
private const val TAG = "@webserver"
private const val MIME_JSON = "application/json" private const val MIME_JSON = "application/json"
const val DEFAULT_PORT = 8080 const val DEFAULT_PORT = 8080
} }

View File

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

View File

@@ -2,6 +2,7 @@ package com.bartlomiejpluta.ttsserver.di.module
import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService
import com.bartlomiejpluta.ttsserver.ui.help.HelpActivity import com.bartlomiejpluta.ttsserver.ui.help.HelpActivity
import com.bartlomiejpluta.ttsserver.ui.log.LogActivity
import com.bartlomiejpluta.ttsserver.ui.main.MainActivity import com.bartlomiejpluta.ttsserver.ui.main.MainActivity
import com.bartlomiejpluta.ttsserver.ui.preference.component.PreferencesActivity import com.bartlomiejpluta.ttsserver.ui.preference.component.PreferencesActivity
import dagger.Module import dagger.Module
@@ -19,6 +20,9 @@ abstract class AndroidModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract fun preferencesActivity(): PreferencesActivity abstract fun preferencesActivity(): PreferencesActivity
@ContributesAndroidInjector
abstract fun logActivity(): LogActivity
@ContributesAndroidInjector @ContributesAndroidInjector
abstract fun foregroundService(): ForegroundService abstract fun foregroundService(): ForegroundService
} }

View File

@@ -2,6 +2,7 @@ package com.bartlomiejpluta.ttsserver.di.module
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import com.bartlomiejpluta.ttsserver.core.log.service.LogService
import com.bartlomiejpluta.ttsserver.core.lua.lib.* import com.bartlomiejpluta.ttsserver.core.lua.lib.*
import com.bartlomiejpluta.ttsserver.core.lua.loader.ConfigLoader import com.bartlomiejpluta.ttsserver.core.lua.loader.ConfigLoader
import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader
@@ -22,9 +23,10 @@ class LuaModule {
fun endpointLoader( fun endpointLoader(
context: Context, context: Context,
sandboxFactory: SandboxFactory, sandboxFactory: SandboxFactory,
tasksQueueFactory: TasksQueueFactory tasksQueueFactory: TasksQueueFactory,
logService: LogService
) = ) =
EndpointLoader(context, sandboxFactory, tasksQueueFactory) EndpointLoader(context, sandboxFactory, tasksQueueFactory, logService)
@Provides @Provides
@Singleton @Singleton
@@ -34,17 +36,21 @@ class LuaModule {
@Singleton @Singleton
fun sandboxFactory( fun sandboxFactory(
context: Context, context: Context,
logService: LogService,
configLoader: ConfigLoader, configLoader: ConfigLoader,
threadLibrary: ThreadLibrary, threadLibrary: ThreadLibrary,
serverLibrary: ServerLibrary, serverLibrary: ServerLibrary,
logLibrary: LogLibrary,
httpLibrary: HTTPLibrary, httpLibrary: HTTPLibrary,
ttsLibrary: TTSLibrary, ttsLibrary: TTSLibrary,
sonosLibrary: SonosLibrary sonosLibrary: SonosLibrary
) = SandboxFactory( ) = SandboxFactory(
context, context,
logService,
configLoader, configLoader,
threadLibrary, threadLibrary,
serverLibrary, serverLibrary,
logLibrary,
httpLibrary, httpLibrary,
ttsLibrary, ttsLibrary,
sonosLibrary sonosLibrary
@@ -63,6 +69,10 @@ class LuaModule {
fun serverLibrary(context: Context, networkUtil: NetworkUtil) = fun serverLibrary(context: Context, networkUtil: NetworkUtil) =
ServerLibrary(context, networkUtil) ServerLibrary(context, networkUtil)
@Provides
@Singleton
fun logLibrary(logService: LogService) = LogLibrary(logService)
@Provides @Provides
@Singleton @Singleton
fun httpLibrary() = HTTPLibrary() fun httpLibrary() = HTTPLibrary()

View File

@@ -4,6 +4,9 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.speech.tts.TextToSpeech import android.speech.tts.TextToSpeech
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.room.Room
import com.bartlomiejpluta.ttsserver.core.log.database.LogDatabase
import com.bartlomiejpluta.ttsserver.core.log.service.LogService
import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatusHolder import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatusHolder
@@ -43,12 +46,14 @@ class TTSModule {
preferences: SharedPreferences, preferences: SharedPreferences,
context: Context, context: Context,
tts: TTSEngine, tts: TTSEngine,
endpointLoader: EndpointLoader endpointLoader: EndpointLoader,
logService: LogService
) = WebServerFactory( ) = WebServerFactory(
preferences, preferences,
context, context,
tts, tts,
endpointLoader endpointLoader,
logService
) )
@Provides @Provides
@@ -70,4 +75,14 @@ class TTSModule {
context: Context, context: Context,
networkUtil: NetworkUtil networkUtil: NetworkUtil
) = ForegroundNotificationFactory(context, networkUtil) ) = ForegroundNotificationFactory(context, networkUtil)
@Provides
@Singleton
fun logDatabase(context: Context) = Room
.databaseBuilder(context, LogDatabase::class.java, "log.db")
.build()
@Provides
@Singleton
fun logService(context: Context, logDatabase: LogDatabase) = LogService(context, logDatabase)
} }

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.PowerManager import android.os.PowerManager
import com.bartlomiejpluta.ttsserver.core.log.service.LogService
import com.bartlomiejpluta.ttsserver.core.web.server.WebServer import com.bartlomiejpluta.ttsserver.core.web.server.WebServer
import com.bartlomiejpluta.ttsserver.core.web.server.WebServerFactory import com.bartlomiejpluta.ttsserver.core.web.server.WebServerFactory
import com.bartlomiejpluta.ttsserver.service.notification.ForegroundNotificationFactory import com.bartlomiejpluta.ttsserver.service.notification.ForegroundNotificationFactory
@@ -23,6 +24,9 @@ class ForegroundService : DaggerService() {
@Inject @Inject
lateinit var notificationFactory: ForegroundNotificationFactory lateinit var notificationFactory: ForegroundNotificationFactory
@Inject
lateinit var log: LogService
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
startForeground(1, notificationFactory.createForegroundNotification()) startForeground(1, notificationFactory.createForegroundNotification())
@@ -50,6 +54,7 @@ class ForegroundService : DaggerService() {
private fun startService() { private fun startService() {
if (isServiceStarted) return if (isServiceStarted) return
isServiceStarted = true isServiceStarted = true
log.info(TAG, "Starting service...")
wakeLock = wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run { (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
@@ -62,10 +67,12 @@ class ForegroundService : DaggerService() {
webServer?.let { webServer?.let {
state = ServiceState.RUNNING state = ServiceState.RUNNING
it.start() it.start()
log.info(TAG, "Service started successfully")
} }
} }
private fun stopService() { private fun stopService() {
log.info(TAG, "Stopping service...")
webServer?.stop() webServer?.stop()
webServer = null webServer = null
wakeLock?.let { wakeLock?.let {
@@ -77,6 +84,7 @@ class ForegroundService : DaggerService() {
stopSelf() stopSelf()
} }
state = ServiceState.STOPPED state = ServiceState.STOPPED
log.info(TAG, "Service stopped successfully")
} }
companion object { companion object {
@@ -85,6 +93,7 @@ class ForegroundService : DaggerService() {
// than to place it as a static field // than to place it as a static field
var state = ServiceState.STOPPED var state = ServiceState.STOPPED
private const val TAG = "@service"
private const val WAKELOCK_TAG = "ForegroundService::lock" private const val WAKELOCK_TAG = "ForegroundService::lock"
const val START = "START" const val START = "START"
const val STOP = "STOP" const val STOP = "STOP"

View File

@@ -0,0 +1,145 @@
package com.bartlomiejpluta.ttsserver.ui.log
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AlertDialog
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.bartlomiejpluta.ttsserver.R
import com.bartlomiejpluta.ttsserver.core.log.model.entity.LogEntry
import com.bartlomiejpluta.ttsserver.core.log.model.enumeration.LogLevel
import com.bartlomiejpluta.ttsserver.core.log.service.LogService
import dagger.android.support.DaggerAppCompatActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.threeten.bp.ZoneId
import org.threeten.bp.format.DateTimeFormatter
import org.threeten.bp.format.FormatStyle
import java.util.*
import javax.inject.Inject
class LogActivity : DaggerAppCompatActivity() {
private val formatter = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM)
.withLocale(Locale.getDefault())
.withZone(ZoneId.systemDefault())
private lateinit var logView: WebView
@Inject
lateinit var logService: LogService
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
LogService.UPDATE_LOGS -> updateLogs()
else -> throw UnsupportedOperationException("This action is not supported")
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_log)
logView = findViewById(R.id.logview)
logView.webViewClient = object : WebViewClient() {
override fun onPageCommitVisible(view: WebView?, url: String?) {
view?.scrollY = view?.contentHeight ?: 0
//view?.pageDown(true)
}
}
}
override fun onResume() {
super.onResume()
super.onResume()
val filter = IntentFilter().apply {
addAction(LogService.UPDATE_LOGS)
}
LocalBroadcastManager
.getInstance(this)
.registerReceiver(receiver, filter)
updateLogs()
}
private fun updateLogs() {
val logs = fetchLogs()
val logsHTML = logsToHTML(logs)
val html = LOGS_STYLES + logsHTML
logView.loadDataWithBaseURL("", html, "text/html", "UTF-8", "");
}
private fun fetchLogs() = runBlocking {
withContext(Dispatchers.IO + Job()) {
logService.allLogs.asReversed()
}
}
private fun logsToHTML(logs: List<LogEntry>) = logs.joinToString("<br />") {
String.format(
"%s <span style=\"color: %s\">%s</span> [ %s ] %s",
formatter.format(it.date).padEnd(23, ' ').replace(" ", "&nbsp;"),
logLevelColor(it.level),
it.level.name.padEnd(5, ' ').replace(" ", "&nbsp;"),
it.source.padEnd(15, ' ').replace(" ", "&nbsp;"),
it.message
)
}
private fun logLevelColor(level: LogLevel) = when (level) {
LogLevel.DEBUG -> "#0044FF"
LogLevel.INFO -> "#00FF00"
LogLevel.WARN -> "#FFEB3B"
LogLevel.ERROR -> "#FF7777"
LogLevel.FATAL -> "#FF0000"
}
override fun onPause() {
LocalBroadcastManager
.getInstance(this)
.unregisterReceiver(receiver)
super.onPause()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_logs, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.clear_logs -> clearLogs()
}
return super.onOptionsItemSelected(item)
}
private fun clearLogs() = AlertDialog.Builder(this)
.setTitle(getString(R.string.dialog_confirmation))
.setMessage(getString(R.string.dialog_clear_logs_confirmation))
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.yes) { _, _ ->
runBlocking {
withContext(Dispatchers.IO + Job()) {
logService.clearLogs()
}
}.also { updateLogs() }
}
.setNegativeButton(android.R.string.no, null).show()
companion object {
private const val LOGS_STYLES = "<style> * { font-family: monospace; } </style>"
}
}

View File

@@ -16,6 +16,7 @@ import com.bartlomiejpluta.ttsserver.initializer.ScriptsInitializer
import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService
import com.bartlomiejpluta.ttsserver.service.state.ServiceState import com.bartlomiejpluta.ttsserver.service.state.ServiceState
import com.bartlomiejpluta.ttsserver.ui.help.HelpActivity import com.bartlomiejpluta.ttsserver.ui.help.HelpActivity
import com.bartlomiejpluta.ttsserver.ui.log.LogActivity
import com.bartlomiejpluta.ttsserver.ui.preference.component.PreferencesActivity import com.bartlomiejpluta.ttsserver.ui.preference.component.PreferencesActivity
import dagger.android.support.DaggerAppCompatActivity import dagger.android.support.DaggerAppCompatActivity
import javax.inject.Inject import javax.inject.Inject
@@ -68,6 +69,7 @@ class MainActivity : DaggerAppCompatActivity() {
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))
R.id.open_logs -> startActivity(Intent(this, LogActivity::class.java))
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.log.LogActivity">
<WebView
android:id="@+id/logview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp" />
</LinearLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/clear_logs" app:showAsAction="never" android:title="@string/menu_clear_logs" />
</menu>

View File

@@ -3,4 +3,5 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/open_preferences" app:showAsAction="never" android:title="@string/menu_settings" /> <item android:id="@+id/open_preferences" app:showAsAction="never" android:title="@string/menu_settings" />
<item android:id="@+id/open_help" app:showAsAction="never" android:title="@string/menu_help" /> <item android:id="@+id/open_help" app:showAsAction="never" android:title="@string/menu_help" />
<item android:id="@+id/open_logs" app:showAsAction="never" android:title="@string/menu_server_logs" />
</menu> </menu>

View File

@@ -4,7 +4,10 @@ return {
consumer = function(request) consumer = function(request)
local filename = string.format("%s.%s", request.path.filename, request.path.ext) local filename = string.format("%s.%s", request.path.filename, request.path.ext)
local file = server.getCachedFile(filename) local file = server.getCachedFile(filename)
local mime = Mime[request.path.ext:upper()] local format = request.path.ext:upper()
local mime = Mime[format]
log.info("Returning the " .. format .. " file from cache...")
return { return {
mime = mime, mime = mime,

View File

@@ -18,8 +18,11 @@ function discoverSonosDevices()
local output = {} local output = {}
local devices = sonos.discover() local devices = sonos.discover()
log.info("Discovering Sonos devices...")
for _, device in ipairs(devices) do for _, device in ipairs(devices) do
output[device:getZoneGroupState():getName()] = device local name = device:getZoneGroupState():getName()
output[name] = device
log.info("Discovered '" .. name .. "' as " .. tostring(device))
end end
return output return output

View File

@@ -5,8 +5,10 @@ return {
local format = (request.path.ext or "wav"):upper() local format = (request.path.ext or "wav"):upper()
local audioFormat = AudioFormat[format] local audioFormat = AudioFormat[format]
local mime = Mime[format] local mime = Mime[format]
local language = request.query.lang or "en"
local file = tts.sayToCache(request.query.phrase, request.query.lang or "en", audioFormat) log.info("Saying to " .. format .. " file (lang: " .. language .. ")...")
local file = tts.sayToCache(request.query.phrase, language, audioFormat)
return { return {
mime = mime, mime = mime,

View File

@@ -5,8 +5,10 @@ return {
accepts = Mime.JSON, accepts = Mime.JSON,
consumer = function(request) consumer = function(request)
if(config.silenceMode()) then return end if(config.silenceMode()) then return end
local body = json.decode(request.body) local body = json.decode(request.body)
tts.say(body.text, body.language or "en") local language = body.language or "en"
log.info("Saying (lang: " .. language .. ")...")
tts.say(body.text, language)
end end
} }

View File

@@ -1,12 +1,14 @@
local snapshot local snapshot
function prepareTTSFile(phrase, language) function prepareTTSFile(phrase, language)
log.info("Saying to cache file (lang: " .. language .. ")...")
local file = tts.sayToCache(phrase, language, AudioFormat.MP3) local file = tts.sayToCache(phrase, language, AudioFormat.MP3)
return string.format("%s/cache/%s", server.url, file:getName()) return string.format("%s/cache/%s", server.url, file:getName())
end end
function updateSnapshotIfFirst(device) function updateSnapshotIfFirst(device)
if(snapshot == nil) then if(snapshot == nil) then
log.info("Dumping the Sonos state snapshot...")
snapshot = device:snapshot() snapshot = device:snapshot()
end end
end end
@@ -14,15 +16,18 @@ end
function announce(device, data, url) function announce(device, data, url)
device:stop() device:stop()
device:setVolume(data.volume) device:setVolume(data.volume)
log.info("Announcing on '" .. data.zone .. "' zone...")
device:playUri(url, "") device:playUri(url, "")
while(device:getPlayState():name() ~= "STOPPED") do while(device:getPlayState():name() ~= "STOPPED") do
thread.sleep(500) thread.sleep(500)
end end
log.info("Announcement is complete")
end end
function restoreSnapshotIfLast(queueLength) function restoreSnapshotIfLast(queueLength)
if(queueLength() == 0) then if(queueLength() == 0) then
if(snapshot ~= nil) then if(snapshot ~= nil) then
log.info("Restoring the Sonos state snapshot...")
snapshot:restore() snapshot:restore()
snapshot = nil snapshot = nil
end end

View File

@@ -31,9 +31,14 @@
<string name="menu_settings">Settings</string> <string name="menu_settings">Settings</string>
<string name="menu_help">Help</string> <string name="menu_help">Help</string>
<string name="menu_server_logs">Server logs</string>
<string name="menu_clear_logs">Clear logs</string>
<string name="error_invalid_endpoint_script">Skipping %1$s file because of error:\n%2$s</string> <string name="error_invalid_endpoint_script">Skipping %1$s file because of error:\n%2$s</string>
<string name="dialog_confirmation">Confirmation</string>
<string name="dialog_clear_logs_confirmation">Do you want to clear logs?</string>
<string name="error">Error</string> <string name="error">Error</string>
<string name="debug">Debug</string> <string name="debug">Debug</string>
</resources> </resources>