diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 30ba2c4..b424c68 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -7,7 +7,7 @@ plugins { ext { set("PUBLISH_GROUP_ID", Configuration.Api.artifactGroup) - if (Configuration.Api.isSnapshot || rootProject.ext["snapshot"] as Boolean) { + if (Configuration.Api.isSnapshot || rootProject.ext["snapshot"] as? Boolean == true) { set("PUBLISH_VERSION", Configuration.Api.snapshotVersionName) } else { set("PUBLISH_VERSION", Configuration.Api.versionName) diff --git a/app/src/main/kotlin/com/voxfinite/logvue/adb/AndroidLogStreamer.kt b/app/src/main/kotlin/com/voxfinite/logvue/adb/AndroidLogStreamer.kt index 3f3c251..f5912fe 100644 --- a/app/src/main/kotlin/com/voxfinite/logvue/adb/AndroidLogStreamer.kt +++ b/app/src/main/kotlin/com/voxfinite/logvue/adb/AndroidLogStreamer.kt @@ -7,8 +7,8 @@ import com.voxfinite.logvue.utils.Either class AndroidLogStreamer { - fun stream(packageName: String): Flow>> { - return AdbHelper.monitorLogs(packageName) + fun stream(packageName: String, filters: List): Flow>> { + return AdbHelper.monitorLogs(packageName, filters) } fun stop() { diff --git a/app/src/main/kotlin/com/voxfinite/logvue/adb/ddmlib/AdbHelper.kt b/app/src/main/kotlin/com/voxfinite/logvue/adb/ddmlib/AdbHelper.kt index 7a4fad1..d40c4b4 100644 --- a/app/src/main/kotlin/com/voxfinite/logvue/adb/ddmlib/AdbHelper.kt +++ b/app/src/main/kotlin/com/voxfinite/logvue/adb/ddmlib/AdbHelper.kt @@ -62,7 +62,7 @@ object AdbHelper { @OptIn(ExperimentalCoroutinesApi::class) fun monitorLogs( packageName: String, - filters: Array = arrayOf("FA", "FA-SVC", "PDTLogging") + filters: List ) = callbackFlow { stopLogs = false val currentSelectedDevice = Devices.currentDevice?.device diff --git a/app/src/main/kotlin/com/voxfinite/logvue/adb/ddmlib/LogCatRunner.kt b/app/src/main/kotlin/com/voxfinite/logvue/adb/ddmlib/LogCatRunner.kt index c86d133..52a56a1 100644 --- a/app/src/main/kotlin/com/voxfinite/logvue/adb/ddmlib/LogCatRunner.kt +++ b/app/src/main/kotlin/com/voxfinite/logvue/adb/ddmlib/LogCatRunner.kt @@ -16,7 +16,7 @@ import javax.annotation.concurrent.GuardedBy class LogCatRunner( val mDevice: IDevice, pid: Long, - filters: Array = arrayOf("FA", "FA-SVC") + filters: List ) { companion object { diff --git a/app/src/main/kotlin/com/voxfinite/logvue/app/App.kt b/app/src/main/kotlin/com/voxfinite/logvue/app/App.kt index 3c67d3e..74a937e 100644 --- a/app/src/main/kotlin/com/voxfinite/logvue/app/App.kt +++ b/app/src/main/kotlin/com/voxfinite/logvue/app/App.kt @@ -21,6 +21,7 @@ import com.voxfinite.logvue.ui.components.SideNavigation import com.voxfinite.logvue.ui.components.dialogs.CrashDialog import com.voxfinite.logvue.ui.components.dialogs.IntroDialog import com.voxfinite.logvue.utils.* +import com.voxfinite.logvue.utils.plugins.PluginsHelper @Composable @Preview diff --git a/app/src/main/kotlin/com/voxfinite/logvue/processor/MainProcessor.kt b/app/src/main/kotlin/com/voxfinite/logvue/processor/MainProcessor.kt index d708d4a..41ae4e0 100644 --- a/app/src/main/kotlin/com/voxfinite/logvue/processor/MainProcessor.kt +++ b/app/src/main/kotlin/com/voxfinite/logvue/processor/MainProcessor.kt @@ -1,6 +1,5 @@ package com.voxfinite.logvue.processor -import com.voxfinite.logvue.api.models.LogLevel2 import com.voxfinite.logvue.adb.AndroidLogStreamer import com.voxfinite.logvue.adb.LogCatErrors import com.voxfinite.logvue.adb.LogErrorNoSession @@ -17,6 +16,7 @@ import com.voxfinite.logvue.api.models.LogItem import com.voxfinite.logvue.models.SessionInfo import com.voxfinite.logvue.storage.Db import com.voxfinite.logvue.utils.* +import com.voxfinite.logvue.utils.plugins.PluginsHelper class MainProcessor { @@ -82,8 +82,9 @@ class MainProcessor { return@withContext } val packageName = sessionInfo.appPackage - val stream = streamer.stream(packageName) val parsers = PluginsHelper.parsers() + val filters = parsers.map { it.filters() }.flatten() + val stream = streamer.stream(packageName, filters) launch { val successStream = stream.filter { it.isSuccess }.map { it.getOrNull() } .filterNotNull() diff --git a/app/src/main/kotlin/com/voxfinite/logvue/storage/serializer/ObjectSerializer.kt b/app/src/main/kotlin/com/voxfinite/logvue/storage/serializer/ObjectSerializer.kt index 5ac8f0f..58c4517 100644 --- a/app/src/main/kotlin/com/voxfinite/logvue/storage/serializer/ObjectSerializer.kt +++ b/app/src/main/kotlin/com/voxfinite/logvue/storage/serializer/ObjectSerializer.kt @@ -1,5 +1,6 @@ package com.voxfinite.logvue.storage.serializer +import com.voxfinite.logvue.utils.plugins.PluginsHelper import org.mapdb.CC import org.mapdb.DataInput2 import org.mapdb.DataOutput2 @@ -53,7 +54,7 @@ class ObjectSerializer(val classLoader: ClassLoader = Thread.currentThread(). /** * Loader must be non-null; */ - (`in`: InputStream?) : ObjectInputStream(`in`) { + (inputStream: InputStream?) : ObjectInputStream(inputStream) { /** * Use the given ClassLoader rather than using the system class */ @@ -61,8 +62,8 @@ class ObjectSerializer(val classLoader: ClassLoader = Thread.currentThread(). override fun resolveClass(desc: ObjectStreamClass): Class<*> { val name = desc.name return try { - Class.forName(name, false, classLoader) - } catch (ex: ClassNotFoundException) { + PluginsHelper.tryResolveClass(name) + } catch (ignore: ClassNotFoundException) { super.resolveClass(desc) } } diff --git a/app/src/main/kotlin/com/voxfinite/logvue/ui/components/LogCard.kt b/app/src/main/kotlin/com/voxfinite/logvue/ui/components/LogCard.kt index 654f303..65ce4eb 100644 --- a/app/src/main/kotlin/com/voxfinite/logvue/ui/components/LogCard.kt +++ b/app/src/main/kotlin/com/voxfinite/logvue/ui/components/LogCard.kt @@ -20,6 +20,8 @@ import androidx.compose.ui.unit.sp import com.voxfinite.logvue.models.EventTypeNotSure import com.voxfinite.logvue.api.models.LogItem import com.voxfinite.logvue.ui.CustomTheme +import com.voxfinite.logvue.ui.components.common.CustomImage +import com.voxfinite.logvue.ui.components.common.NetworkImage import com.voxfinite.logvue.ui.views.flow.FlowRow import com.voxfinite.logvue.utils.Helpers import com.voxfinite.logvue.utils.predictedEventType @@ -94,8 +96,7 @@ fun LogTitle(logItem: LogItem, modifier: Modifier = Modifier) { fun LogIcon(logItem: LogItem, modifier: Modifier = Modifier) { Box(modifier.size(48.dp)) { val sourceIcon = logItem.source.icon - val sourceIconPainter = painterResource(sourceIcon) - Image(sourceIconPainter, logItem.source.type, Modifier.size(36.dp)) + CustomImage(sourceIcon, logItem.source.type, Modifier.size(36.dp)) val predictedEventType = logItem.predictedEventType() if (predictedEventType != EventTypeNotSure) { val typePainter = painterResource(predictedEventType.iconResource) diff --git a/app/src/main/kotlin/com/voxfinite/logvue/ui/components/common/Images.kt b/app/src/main/kotlin/com/voxfinite/logvue/ui/components/common/Images.kt new file mode 100644 index 0000000..8792074 --- /dev/null +++ b/app/src/main/kotlin/com/voxfinite/logvue/ui/components/common/Images.kt @@ -0,0 +1,132 @@ +package com.voxfinite.logvue.ui.components.common + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.loadImageBitmap +import androidx.compose.ui.res.loadSvgPainter +import androidx.compose.ui.res.loadXmlImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Density +import com.voxfinite.logvue.utils.plugins.PluginsResourceLoader +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.xml.sax.InputSource +import java.io.File +import java.io.IOException +import java.net.URL + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun CustomImage( + url: String?, + contentDescription: String, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Fit +) { + if (url.isNullOrBlank()) return + if (url.startsWith("http://") || url.startsWith("https://")) { + NetworkImage(url, contentDescription, modifier, contentScale) + } else { + Image(painterResource(url, PluginsResourceLoader()), contentDescription, modifier, contentScale = contentScale) + } +} + +@Composable +fun NetworkImage( + url: String?, + contentDescription: String, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Fit +) { + if (url.isNullOrBlank()) return + AsyncImage( + load = { loadImageBitmap(url) }, + painterFor = { remember { BitmapPainter(it) } }, + contentDescription, modifier, contentScale + ) +} + +@Composable +fun AsyncImage( + load: suspend () -> T, + painterFor: @Composable (T) -> Painter, + contentDescription: String, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Fit, +) { + // should cache image + val image: T? by produceState(null) { + value = withContext(Dispatchers.IO) { + try { + load() + } catch (e: IOException) { + // instead of printing to console, you can also write this to log, + // or show some error placeholder + e.printStackTrace() + null + } + } + } + + if (image != null) { + Image( + painter = painterFor(image!!), + contentDescription = contentDescription, + contentScale = contentScale, + modifier = modifier + ) + } +} + + +/* Loading from file with java.io API */ + +fun loadImageBitmap(file: File): ImageBitmap = + file.inputStream().buffered().use(::loadImageBitmap) + +fun loadSvgPainter(file: File, density: Density): Painter = + file.inputStream().buffered().use { loadSvgPainter(it, density) } + +fun loadXmlImageVector(file: File, density: Density): ImageVector = + file.inputStream().buffered().use { loadXmlImageVector(InputSource(it), density) } + +/* Loading from network with java.net API */ + +fun loadImageBitmap(url: String): ImageBitmap = + URL(url).openStream().buffered().use(::loadImageBitmap) + +fun loadSvgPainter(url: String, density: Density): Painter = + URL(url).openStream().buffered().use { loadSvgPainter(it, density) } + +fun loadXmlImageVector(url: String, density: Density): ImageVector = + URL(url).openStream().buffered().use { loadXmlImageVector(InputSource(it), density) } + +/* Loading from network with Ktor client API (https://ktor.io/docs/client.html). */ + +/* + +suspend fun loadImageBitmap(url: String): ImageBitmap = + urlStream(url).use(::loadImageBitmap) + +suspend fun loadSvgPainter(url: String, density: Density): Painter = + urlStream(url).use { loadSvgPainter(it, density) } + +suspend fun loadXmlImageVector(url: String, density: Density): ImageVector = + urlStream(url).use { loadXmlImageVector(InputSource(it), density) } + +@OptIn(KtorExperimentalAPI::class) +private suspend fun urlStream(url: String) = HttpClient(CIO).use { + ByteArrayInputStream(it.get(url)) +} + + */ diff --git a/app/src/main/kotlin/com/voxfinite/logvue/utils/PluginsHelper.kt b/app/src/main/kotlin/com/voxfinite/logvue/utils/plugins/PluginsHelper.kt similarity index 67% rename from app/src/main/kotlin/com/voxfinite/logvue/utils/PluginsHelper.kt rename to app/src/main/kotlin/com/voxfinite/logvue/utils/plugins/PluginsHelper.kt index 3884c15..7452ab1 100644 --- a/app/src/main/kotlin/com/voxfinite/logvue/utils/PluginsHelper.kt +++ b/app/src/main/kotlin/com/voxfinite/logvue/utils/plugins/PluginsHelper.kt @@ -1,17 +1,19 @@ -package com.voxfinite.logvue.utils +package com.voxfinite.logvue.utils.plugins import com.voxfinite.logvue.api.LogEventParser import com.voxfinite.logvue.parsers.FirebaseParser import com.voxfinite.logvue.storage.StorageHelper +import com.voxfinite.logvue.utils.AppLog import org.pf4j.CompoundPluginDescriptorFinder import org.pf4j.DefaultPluginManager import org.pf4j.ManifestPluginDescriptorFinder import org.pf4j.PropertiesPluginDescriptorFinder import java.nio.file.Path +import kotlin.jvm.Throws object PluginsHelper { - private class PluginManager(importPaths: List) : DefaultPluginManager(importPaths) { + internal class PluginManager(importPaths: List) : DefaultPluginManager(importPaths) { override fun createPluginDescriptorFinder(): CompoundPluginDescriptorFinder { return CompoundPluginDescriptorFinder() // Demo is using the Manifest file // PropertiesPluginDescriptorFinder is commented out just to avoid error log @@ -20,7 +22,7 @@ object PluginsHelper { } } - private val pluginManager by lazy { + internal val pluginManager by lazy { val pluginsPath = StorageHelper.getPluginsPath() AppLog.d(pluginsPath.toString()) PluginManager(listOf(pluginsPath)) @@ -33,7 +35,6 @@ object PluginsHelper { fun load() { // load the plugins pluginManager.loadPlugins() - // enable a disabled plugin // pluginManager.enablePlugin("welcome-plugin") @@ -49,6 +50,21 @@ object PluginsHelper { return allParsers } + @Throws(ClassCastException::class) + fun tryResolveClass(className : String) : Class<*> { + pluginManager.resolvedPlugins.forEach { + val classLoader = it.pluginClassLoader + val resolvedClass: Class<*>? + try { + resolvedClass = Class.forName(className, false, classLoader) + return resolvedClass + } catch (ignore : ClassNotFoundException) { + // ignore + } + } + return Class.forName(className, false, Thread.currentThread().contextClassLoader) + } + private fun getDefaultParsers(): MutableList = arrayListOf(FirebaseParser()) fun stop() { diff --git a/app/src/main/kotlin/com/voxfinite/logvue/utils/plugins/PluginsResourceLoader.kt b/app/src/main/kotlin/com/voxfinite/logvue/utils/plugins/PluginsResourceLoader.kt new file mode 100644 index 0000000..a359620 --- /dev/null +++ b/app/src/main/kotlin/com/voxfinite/logvue/utils/plugins/PluginsResourceLoader.kt @@ -0,0 +1,28 @@ +package com.voxfinite.logvue.utils.plugins + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.res.ResourceLoader +import java.io.InputStream + +@OptIn(ExperimentalComposeUiApi::class) +class PluginsResourceLoader : ResourceLoader { + override fun load(resourcePath: String): InputStream { + PluginsHelper.pluginManager.resolvedPlugins.forEach { + val classLoader = it.pluginClassLoader + val resource = try { + classLoader.getResourceAsStream(resourcePath) + } catch (ignore : Exception) { + null + } + if (resource != null) { + return resource + } + } + // TODO(https://github.com/JetBrains/compose-jb/issues/618): probably we shouldn't use + // contextClassLoader here, as it is not defined in threads created by non-JVM + val contextClassLoader = Thread.currentThread().contextClassLoader!! + val resource = contextClassLoader.getResourceAsStream(resourcePath) + ?: (::PluginsResourceLoader.javaClass).getResourceAsStream(resourcePath) + return requireNotNull(resource) { "Resource $resourcePath not found" } + } +}