diff --git a/app/build.gradle b/app/build.gradle
index d1800cc..f3b9c06 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 97aa911..1d4be1c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -17,7 +17,7 @@
-
+ android:name=".ui.log.LogActivity"
+ android:parentActivityName=".ui.main.MainActivity" />
+
+
+ android:parentActivityName=".ui.main.MainActivity" />
+
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/common/db/converter/InstantConverter.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/common/db/converter/InstantConverter.kt
new file mode 100644
index 0000000..18a2b2d
--- /dev/null
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/common/db/converter/InstantConverter.kt
@@ -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()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/converter/LogLevelConverter.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/converter/LogLevelConverter.kt
new file mode 100644
index 0000000..617f704
--- /dev/null
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/converter/LogLevelConverter.kt
@@ -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
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/dao/LogDao.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/dao/LogDao.kt
new file mode 100644
index 0000000..4c56926
--- /dev/null
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/dao/LogDao.kt
@@ -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
+
+ @Query("DELETE FROM logentry")
+ fun clear()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/database/LogDatabase.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/database/LogDatabase.kt
new file mode 100644
index 0000000..4cd40ee
--- /dev/null
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/database/LogDatabase.kt
@@ -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
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/model/entity/LogEntry.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/model/entity/LogEntry.kt
new file mode 100644
index 0000000..3193e6f
--- /dev/null
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/model/entity/LogEntry.kt
@@ -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
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/model/enumeration/LogLevel.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/model/enumeration/LogLevel.kt
new file mode 100644
index 0000000..ac5afe9
--- /dev/null
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/model/enumeration/LogLevel.kt
@@ -0,0 +1,9 @@
+package com.bartlomiejpluta.ttsserver.core.log.model.enumeration
+
+enum class LogLevel {
+ DEBUG,
+ INFO,
+ WARN,
+ ERROR,
+ FATAL
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/service/LogService.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/service/LogService.kt
new file mode 100644
index 0000000..051067b
--- /dev/null
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/log/service/LogService.kt
@@ -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
+ get() = dao.getAll()
+
+ fun clearLogs() = dao.clear()
+
+ companion object {
+ const val UPDATE_LOGS = "UPDATE_LOGS"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/lib/LogLibrary.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/lib/LogLibrary.kt
new file mode 100644
index 0000000..1f70a72
--- /dev/null
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/lib/LogLibrary.kt
@@ -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
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/loader/ConfigLoader.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/loader/ConfigLoader.kt
index bb3ac24..3bb971f 100644
--- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/loader/ConfigLoader.kt
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/loader/ConfigLoader.kt
@@ -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() {
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/loader/EndpointLoader.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/loader/EndpointLoader.kt
index 89e0c27..8e386f2 100644
--- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/loader/EndpointLoader.kt
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/loader/EndpointLoader.kt
@@ -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 {
+ 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(", ")
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/sandbox/SandboxFactory.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/sandbox/SandboxFactory.kt
index 83a94e1..1111707 100644
--- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/sandbox/SandboxFactory.kt
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/lua/sandbox/SandboxFactory.kt
@@ -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"
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServer.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServer.kt
index 489377d..8106c0f 100644
--- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServer.kt
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServer.kt
@@ -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
+ private val endpoints: List,
+ 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
}
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServerFactory.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServerFactory.kt
index 401c5e9..8a1b3af 100644
--- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServerFactory.kt
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServerFactory.kt
@@ -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
)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/AndroidModule.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/AndroidModule.kt
index bb3748a..d992e48 100644
--- a/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/AndroidModule.kt
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/AndroidModule.kt
@@ -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
}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/LuaModule.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/LuaModule.kt
index de69ca0..06fc9a4 100644
--- a/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/LuaModule.kt
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/LuaModule.kt
@@ -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()
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/TTSModule.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/TTSModule.kt
index 15b494d..ba05d64 100644
--- a/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/TTSModule.kt
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/TTSModule.kt
@@ -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)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/service/foreground/ForegroundService.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/service/foreground/ForegroundService.kt
index ce4a4fe..5a782e0 100644
--- a/app/src/main/java/com/bartlomiejpluta/ttsserver/service/foreground/ForegroundService.kt
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/service/foreground/ForegroundService.kt
@@ -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"
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/log/LogActivity.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/log/LogActivity.kt
new file mode 100644
index 0000000..60a4a51
--- /dev/null
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/log/LogActivity.kt
@@ -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) = logs.joinToString("
") {
+ String.format(
+ "%s %s [ %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 = ""
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/main/MainActivity.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/main/MainActivity.kt
index 83e1961..e31f619 100644
--- a/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/main/MainActivity.kt
+++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/main/MainActivity.kt
@@ -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)
diff --git a/app/src/main/res/layout/activity_log.xml b/app/src/main/res/layout/activity_log.xml
new file mode 100644
index 0000000..bfc611f
--- /dev/null
+++ b/app/src/main/res/layout/activity_log.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_logs.xml b/app/src/main/res/menu/menu_logs.xml
new file mode 100644
index 0000000..021af56
--- /dev/null
+++ b/app/src/main/res/menu/menu_logs.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
index dd93f37..633bcd1 100644
--- a/app/src/main/res/menu/menu_main.xml
+++ b/app/src/main/res/menu/menu_main.xml
@@ -3,4 +3,5 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
+
\ No newline at end of file
diff --git a/app/src/main/res/raw/cache.lua b/app/src/main/res/raw/cache.lua
index a16aa17..9a07d5d 100644
--- a/app/src/main/res/raw/cache.lua
+++ b/app/src/main/res/raw/cache.lua
@@ -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,
diff --git a/app/src/main/res/raw/config.lua b/app/src/main/res/raw/config.lua
index 2ee9097..a0b6085 100644
--- a/app/src/main/res/raw/config.lua
+++ b/app/src/main/res/raw/config.lua
@@ -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
diff --git a/app/src/main/res/raw/file.lua b/app/src/main/res/raw/file.lua
index e4fa5e8..1899805 100644
--- a/app/src/main/res/raw/file.lua
+++ b/app/src/main/res/raw/file.lua
@@ -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,
diff --git a/app/src/main/res/raw/say.lua b/app/src/main/res/raw/say.lua
index 4a0a8a3..3851fed 100644
--- a/app/src/main/res/raw/say.lua
+++ b/app/src/main/res/raw/say.lua
@@ -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
}
\ No newline at end of file
diff --git a/app/src/main/res/raw/sonos.lua b/app/src/main/res/raw/sonos.lua
index d6c19d0..1589b12 100644
--- a/app/src/main/res/raw/sonos.lua
+++ b/app/src/main/res/raw/sonos.lua
@@ -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
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 88794a3..215a751 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -31,9 +31,14 @@
Settings
Help
+ Server logs
+ Clear logs
Skipping %1$s file because of error:\n%2$s
+ Confirmation
+ Do you want to clear logs?
+
Error
Debug