Create in-app logging framework
This commit is contained in:
@@ -27,6 +27,9 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
def room_version = "2.2.5"
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
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.github.adrielcafe:AndroidAudioConverter:0.0.8'
|
||||
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-compiler:2.15'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name="com.bartlomiejpluta.ttsserver.TTSApplication"
|
||||
android:name=".TTSApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -27,25 +27,29 @@
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="m">
|
||||
<activity
|
||||
android:name="com.bartlomiejpluta.ttsserver.ui.help.HelpActivity"
|
||||
android:parentActivityName="com.bartlomiejpluta.ttsserver.ui.main.MainActivity" />
|
||||
android:name=".ui.log.LogActivity"
|
||||
android:parentActivityName=".ui.main.MainActivity" />
|
||||
|
||||
<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:parentActivityName="com.bartlomiejpluta.ttsserver.ui.main.MainActivity" />
|
||||
android:parentActivityName=".ui.main.MainActivity" />
|
||||
|
||||
<service
|
||||
android:name="com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService"
|
||||
android:name=".service.foreground.ForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:permission="com.bartlomiejpluta.permission.TTS_HTTP_SERVICE" />
|
||||
|
||||
<activity
|
||||
android:name="com.bartlomiejpluta.ttsserver.ui.main.MainActivity"
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.bartlomiejpluta.ttsserver.core.log.model.enumeration
|
||||
|
||||
enum class LogLevel {
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR,
|
||||
FATAL
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,18 @@ import org.luaj.vm2.lib.TwoArgFunction
|
||||
import java.io.File
|
||||
|
||||
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) {
|
||||
val configFile = File(configDirectory, "config.lua")
|
||||
val table = env.loadfile(configFile.absolutePath).call().checktable()
|
||||
env.load(ConfigLibrary(table))
|
||||
cachedConfig
|
||||
?.let { ConfigLibrary(it) }
|
||||
?.let { env.load(it) }
|
||||
?: error("Config has not been refreshed before loading start")
|
||||
}
|
||||
|
||||
class ConfigLibrary(private val table: LuaTable) : TwoArgFunction() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
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.web.endpoint.DefaultEndpoint
|
||||
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint
|
||||
@@ -19,28 +20,37 @@ import java.io.File
|
||||
class EndpointLoader(
|
||||
private val context: Context,
|
||||
private val sandboxFactory: SandboxFactory,
|
||||
private val tasksQueueFactory: TasksQueueFactory
|
||||
private val tasksQueueFactory: TasksQueueFactory,
|
||||
private val log: LogService
|
||||
) {
|
||||
|
||||
fun loadEndpoints(): List<Endpoint> {
|
||||
sandboxFactory.refreshConfig()
|
||||
log.info(TAG, "Loading endpoint scripts...")
|
||||
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? {
|
||||
try {
|
||||
log.info(TAG, "Loading ${script.name} script...")
|
||||
return sandboxFactory
|
||||
.createSandbox()
|
||||
.createSandbox(script.name)
|
||||
.loadfile(script.absolutePath)
|
||||
.call()
|
||||
.checktable()
|
||||
.takeIf { parseEnabled(it) }
|
||||
?.let { createEndpoint(it) }
|
||||
?.also { log.info(TAG, "Script ${script.name} has been loaded successfully") }
|
||||
} catch (e: LuaError) {
|
||||
handleError(e)
|
||||
log.error(TAG, "Loading script ${script.name} failed: ${e.message}")
|
||||
return null
|
||||
} catch (e: Exception) {
|
||||
log.error(TAG, "Loading script ${script.name} failed: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -91,6 +101,7 @@ class EndpointLoader(
|
||||
?: throw LuaError("Invalid HTTP method. Allowed methods are: $ALLOWED_METHODS")
|
||||
|
||||
companion object {
|
||||
private val TAG = "@endpoints"
|
||||
private val ALLOWED_METHODS = Method.values().joinToString(", ")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.bartlomiejpluta.ttsserver.core.lua.sandbox
|
||||
|
||||
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.loader.ConfigLoader
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -18,21 +19,31 @@ import org.luaj.vm2.lib.jse.JseOsLib
|
||||
|
||||
class SandboxFactory(
|
||||
private val context: Context,
|
||||
private val log: LogService,
|
||||
private val configLoader: ConfigLoader,
|
||||
private val threadLibrary: ThreadLibrary,
|
||||
private val serverLibrary: ServerLibrary,
|
||||
private val logLibrary: LogLibrary,
|
||||
private val httpLibrary: HTTPLibrary,
|
||||
private val ttsLibrary: TTSLibrary,
|
||||
private val sonosLibrary: SonosLibrary
|
||||
) {
|
||||
fun createSandbox() = runBlocking {
|
||||
fun createSandbox(scriptName: String) = createLuaGlobals(scriptName).also {
|
||||
loadConfiguration(it)
|
||||
}
|
||||
|
||||
fun refreshConfig() = runBlocking {
|
||||
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)
|
||||
sandbox.get("_G").checktable().set("script", scriptName)
|
||||
loadApplicationLibraries(sandbox)
|
||||
install(sandbox)
|
||||
loadLuaLibraries(sandbox)
|
||||
@@ -49,6 +60,7 @@ class SandboxFactory(
|
||||
|
||||
private fun loadApplicationLibraries(sandbox: Globals) {
|
||||
sandbox.load(serverLibrary)
|
||||
sandbox.load(logLibrary)
|
||||
sandbox.load(threadLibrary)
|
||||
sandbox.load(httpLibrary)
|
||||
sandbox.load(ttsLibrary)
|
||||
@@ -58,6 +70,9 @@ class SandboxFactory(
|
||||
private fun install(sandbox: Globals) {
|
||||
LoadState.install(sandbox)
|
||||
LuaC.install(sandbox)
|
||||
}
|
||||
|
||||
private fun loadConfiguration(sandbox: Globals) {
|
||||
configLoader.loadConfig(sandbox)
|
||||
}
|
||||
|
||||
@@ -68,4 +83,8 @@ class SandboxFactory(
|
||||
?.map { (name, reader) -> name.substringBeforeLast(".") to sandbox.load(reader, name) }
|
||||
?.forEach { (name, value) -> sandbox.set(name, value.call()) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "@sandbox"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
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.status.TTSStatus
|
||||
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint
|
||||
@@ -22,7 +23,8 @@ class WebServer(
|
||||
private val context: Context,
|
||||
private val preferences: SharedPreferences,
|
||||
private val tts: TTSEngine,
|
||||
private val endpoints: List<Endpoint>
|
||||
private val endpoints: List<Endpoint>,
|
||||
private val log: LogService
|
||||
) : NanoHTTPD(port) {
|
||||
private val queuedEndpoints = endpoints.mapNotNull { it as? QueuedEndpoint }
|
||||
|
||||
@@ -36,10 +38,13 @@ class WebServer(
|
||||
|
||||
throw WebException(BAD_REQUEST, "Unknown error")
|
||||
} catch (e: WebException) {
|
||||
log.error(TAG, "Web exception occurred: ${e.message}")
|
||||
return handleWebException(e)
|
||||
} catch (e: LuaError) {
|
||||
log.error(TAG, "Lua error occurred: ${e.message}")
|
||||
return handleLuaError(e)
|
||||
} catch (e: Exception) {
|
||||
log.fatal(TAG, "Unknown error occurred: ${e.message}")
|
||||
return handleUnknownException(e)
|
||||
}
|
||||
}
|
||||
@@ -75,6 +80,7 @@ class WebServer(
|
||||
|
||||
override fun start() {
|
||||
super.start()
|
||||
log.info(TAG, "Web server is up")
|
||||
|
||||
LocalBroadcastManager
|
||||
.getInstance(context)
|
||||
@@ -84,6 +90,7 @@ class WebServer(
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
log.info(TAG, "Stopping web server...")
|
||||
super.stop()
|
||||
queuedEndpoints.forEach { it.shutdownQueue() }
|
||||
|
||||
@@ -95,6 +102,7 @@ class WebServer(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "@webserver"
|
||||
private const val MIME_JSON = "application/json"
|
||||
const val DEFAULT_PORT = 8080
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.bartlomiejpluta.ttsserver.core.web.server
|
||||
|
||||
import android.content.Context
|
||||
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.tts.engine.TTSEngine
|
||||
import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey
|
||||
@@ -10,13 +11,15 @@ class WebServerFactory(
|
||||
private val preferences: SharedPreferences,
|
||||
private val context: Context,
|
||||
private val tts: TTSEngine,
|
||||
private val endpointLoader: EndpointLoader
|
||||
private val endpointLoader: EndpointLoader,
|
||||
private val logService: LogService
|
||||
) {
|
||||
fun createWebServer() = WebServer(
|
||||
preferences.getInt(PreferenceKey.PORT, WebServer.DEFAULT_PORT),
|
||||
context,
|
||||
preferences,
|
||||
tts,
|
||||
endpointLoader.loadEndpoints()
|
||||
endpointLoader.loadEndpoints(),
|
||||
logService
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.bartlomiejpluta.ttsserver.di.module
|
||||
|
||||
import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService
|
||||
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.preference.component.PreferencesActivity
|
||||
import dagger.Module
|
||||
@@ -19,6 +20,9 @@ abstract class AndroidModule {
|
||||
@ContributesAndroidInjector
|
||||
abstract fun preferencesActivity(): PreferencesActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun logActivity(): LogActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun foregroundService(): ForegroundService
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.bartlomiejpluta.ttsserver.di.module
|
||||
|
||||
import android.content.Context
|
||||
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.loader.ConfigLoader
|
||||
import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader
|
||||
@@ -22,9 +23,10 @@ class LuaModule {
|
||||
fun endpointLoader(
|
||||
context: Context,
|
||||
sandboxFactory: SandboxFactory,
|
||||
tasksQueueFactory: TasksQueueFactory
|
||||
tasksQueueFactory: TasksQueueFactory,
|
||||
logService: LogService
|
||||
) =
|
||||
EndpointLoader(context, sandboxFactory, tasksQueueFactory)
|
||||
EndpointLoader(context, sandboxFactory, tasksQueueFactory, logService)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@@ -34,17 +36,21 @@ class LuaModule {
|
||||
@Singleton
|
||||
fun sandboxFactory(
|
||||
context: Context,
|
||||
logService: LogService,
|
||||
configLoader: ConfigLoader,
|
||||
threadLibrary: ThreadLibrary,
|
||||
serverLibrary: ServerLibrary,
|
||||
logLibrary: LogLibrary,
|
||||
httpLibrary: HTTPLibrary,
|
||||
ttsLibrary: TTSLibrary,
|
||||
sonosLibrary: SonosLibrary
|
||||
) = SandboxFactory(
|
||||
context,
|
||||
logService,
|
||||
configLoader,
|
||||
threadLibrary,
|
||||
serverLibrary,
|
||||
logLibrary,
|
||||
httpLibrary,
|
||||
ttsLibrary,
|
||||
sonosLibrary
|
||||
@@ -63,6 +69,10 @@ class LuaModule {
|
||||
fun serverLibrary(context: Context, networkUtil: NetworkUtil) =
|
||||
ServerLibrary(context, networkUtil)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun logLibrary(logService: LogService) = LogLibrary(logService)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun httpLibrary() = HTTPLibrary()
|
||||
|
||||
@@ -4,6 +4,9 @@ import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.speech.tts.TextToSpeech
|
||||
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.tts.engine.TTSEngine
|
||||
import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatusHolder
|
||||
@@ -43,12 +46,14 @@ class TTSModule {
|
||||
preferences: SharedPreferences,
|
||||
context: Context,
|
||||
tts: TTSEngine,
|
||||
endpointLoader: EndpointLoader
|
||||
endpointLoader: EndpointLoader,
|
||||
logService: LogService
|
||||
) = WebServerFactory(
|
||||
preferences,
|
||||
context,
|
||||
tts,
|
||||
endpointLoader
|
||||
endpointLoader,
|
||||
logService
|
||||
)
|
||||
|
||||
@Provides
|
||||
@@ -70,4 +75,14 @@ class TTSModule {
|
||||
context: Context,
|
||||
networkUtil: 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)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.WebServerFactory
|
||||
import com.bartlomiejpluta.ttsserver.service.notification.ForegroundNotificationFactory
|
||||
@@ -23,6 +24,9 @@ class ForegroundService : DaggerService() {
|
||||
@Inject
|
||||
lateinit var notificationFactory: ForegroundNotificationFactory
|
||||
|
||||
@Inject
|
||||
lateinit var log: LogService
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startForeground(1, notificationFactory.createForegroundNotification())
|
||||
@@ -50,6 +54,7 @@ class ForegroundService : DaggerService() {
|
||||
private fun startService() {
|
||||
if (isServiceStarted) return
|
||||
isServiceStarted = true
|
||||
log.info(TAG, "Starting service...")
|
||||
wakeLock =
|
||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||
@@ -62,10 +67,12 @@ class ForegroundService : DaggerService() {
|
||||
webServer?.let {
|
||||
state = ServiceState.RUNNING
|
||||
it.start()
|
||||
log.info(TAG, "Service started successfully")
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopService() {
|
||||
log.info(TAG, "Stopping service...")
|
||||
webServer?.stop()
|
||||
webServer = null
|
||||
wakeLock?.let {
|
||||
@@ -77,6 +84,7 @@ class ForegroundService : DaggerService() {
|
||||
stopSelf()
|
||||
}
|
||||
state = ServiceState.STOPPED
|
||||
log.info(TAG, "Service stopped successfully")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -85,6 +93,7 @@ class ForegroundService : DaggerService() {
|
||||
// than to place it as a static field
|
||||
var state = ServiceState.STOPPED
|
||||
|
||||
private const val TAG = "@service"
|
||||
private const val WAKELOCK_TAG = "ForegroundService::lock"
|
||||
const val START = "START"
|
||||
const val STOP = "STOP"
|
||||
|
||||
@@ -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(" ", " "),
|
||||
logLevelColor(it.level),
|
||||
it.level.name.padEnd(5, ' ').replace(" ", " "),
|
||||
it.source.padEnd(15, ' ').replace(" ", " "),
|
||||
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>"
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import com.bartlomiejpluta.ttsserver.initializer.ScriptsInitializer
|
||||
import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService
|
||||
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
|
||||
import com.bartlomiejpluta.ttsserver.ui.help.HelpActivity
|
||||
import com.bartlomiejpluta.ttsserver.ui.log.LogActivity
|
||||
import com.bartlomiejpluta.ttsserver.ui.preference.component.PreferencesActivity
|
||||
import dagger.android.support.DaggerAppCompatActivity
|
||||
import javax.inject.Inject
|
||||
@@ -68,6 +69,7 @@ class MainActivity : DaggerAppCompatActivity() {
|
||||
when (item.itemId) {
|
||||
R.id.open_preferences -> startActivity(Intent(this, PreferencesActivity::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)
|
||||
|
||||
15
app/src/main/res/layout/activity_log.xml
Normal file
15
app/src/main/res/layout/activity_log.xml
Normal 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>
|
||||
5
app/src/main/res/menu/menu_logs.xml
Normal file
5
app/src/main/res/menu/menu_logs.xml
Normal 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>
|
||||
@@ -3,4 +3,5 @@
|
||||
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_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>
|
||||
@@ -4,7 +4,10 @@ return {
|
||||
consumer = function(request)
|
||||
local filename = string.format("%s.%s", request.path.filename, request.path.ext)
|
||||
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 {
|
||||
mime = mime,
|
||||
|
||||
@@ -18,8 +18,11 @@ function discoverSonosDevices()
|
||||
local output = {}
|
||||
local devices = sonos.discover()
|
||||
|
||||
log.info("Discovering Sonos devices...")
|
||||
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
|
||||
|
||||
return output
|
||||
|
||||
@@ -5,8 +5,10 @@ return {
|
||||
local format = (request.path.ext or "wav"):upper()
|
||||
local audioFormat = AudioFormat[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 {
|
||||
mime = mime,
|
||||
|
||||
@@ -5,8 +5,10 @@ return {
|
||||
accepts = Mime.JSON,
|
||||
consumer = function(request)
|
||||
if(config.silenceMode()) then return end
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
local snapshot
|
||||
|
||||
function prepareTTSFile(phrase, language)
|
||||
log.info("Saying to cache file (lang: " .. language .. ")...")
|
||||
local file = tts.sayToCache(phrase, language, AudioFormat.MP3)
|
||||
return string.format("%s/cache/%s", server.url, file:getName())
|
||||
end
|
||||
|
||||
function updateSnapshotIfFirst(device)
|
||||
if(snapshot == nil) then
|
||||
log.info("Dumping the Sonos state snapshot...")
|
||||
snapshot = device:snapshot()
|
||||
end
|
||||
end
|
||||
@@ -14,15 +16,18 @@ end
|
||||
function announce(device, data, url)
|
||||
device:stop()
|
||||
device:setVolume(data.volume)
|
||||
log.info("Announcing on '" .. data.zone .. "' zone...")
|
||||
device:playUri(url, "")
|
||||
while(device:getPlayState():name() ~= "STOPPED") do
|
||||
thread.sleep(500)
|
||||
end
|
||||
log.info("Announcement is complete")
|
||||
end
|
||||
|
||||
function restoreSnapshotIfLast(queueLength)
|
||||
if(queueLength() == 0) then
|
||||
if(snapshot ~= nil) then
|
||||
log.info("Restoring the Sonos state snapshot...")
|
||||
snapshot:restore()
|
||||
snapshot = nil
|
||||
end
|
||||
|
||||
@@ -31,9 +31,14 @@
|
||||
|
||||
<string name="menu_settings">Settings</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="dialog_confirmation">Confirmation</string>
|
||||
<string name="dialog_clear_logs_confirmation">Do you want to clear logs?</string>
|
||||
|
||||
<string name="error">Error</string>
|
||||
<string name="debug">Debug</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user