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