diff --git a/.github/config/configuration.json b/.github/config/configuration.json new file mode 100644 index 0000000..be6002f --- /dev/null +++ b/.github/config/configuration.json @@ -0,0 +1,54 @@ +{ + "categories": [ + { + "title": "## ๐Ÿš€ Features", + "labels": [ + "feat" + ] + }, + { + "title": "## ๐Ÿ› Fixes", + "labels": [ + "fix" + ] + }, + { + "title": "๐Ÿงฐ Maintenance", + "labels": [ + "chore" + ] + }, + { + "title": "## ๐Ÿงช Tests", + "labels": [ + "test" + ] + }, + { + "title": "## ๐Ÿ–๏ธ Documentation", + "labels": [ + "doc" + ] + }, + { + "title": "## ๐Ÿ“ฆ Dependencies", + "labels": [ + "dependencies" + ] + } + ], + "sort": "ASC", + "template": "${{CHANGELOG}}\n\n
\nUncategorized\n\n${{UNCATEGORIZED}}\n
", + "pr_template": "${{TITLE}}", + "empty_template": "- no changes", + "label_extractor": [ + { + "pattern": "(.+): (.+)", + "target": "$1" + } + ], + "exclude_merge_branches": [ + "merge pull request", + "Merge pull request" + ] +} \ No newline at end of file diff --git a/.github/config/labels.yml b/.github/config/labels.yml new file mode 100644 index 0000000..24a6530 --- /dev/null +++ b/.github/config/labels.yml @@ -0,0 +1,51 @@ +- name: bug + description: Something isn't working + color: d73a4a +- name: doc + description: Improvements to documentation + color: d4c5f9 +- name: duplicate + description: This issue or pull request already exists + color: cfd3d7 +- name: feature + color: 1d76db + description: New features +- name: enhancement + description: Enhancement of existing functionality + color: 84b6eb +- name: deprecated + description: Deprecating API + color: f4c21d +- name: removed + description: Removing API + color: e4b21d +- name: tests + description: Enhancement of tests + color: 0e8a16 +- name: java + description: Java/JDK changes + color: 03d0d6 +- name: gradle + description: Gradle changes + color: d0d603 +- name: maven + description: Maven changes + color: d60366 +- name: compose + description: Jetbrains Compose issues + color: 3cdc84 +- name: help + description: Help Wanted + color: 0e8a16 +- name: question + description: Questions and discussions + color: cc317c +- name: dependencies + description: Changes that affect dependencies + color: 5319e7 +- name: docker + description: Container changes + color: e99695 +- name: github-actions + description: Github action changes + color: ff7619 \ No newline at end of file diff --git a/.github/config/release-drafter.yml b/.github/config/release-drafter.yml new file mode 100644 index 0000000..3e13770 --- /dev/null +++ b/.github/config/release-drafter.yml @@ -0,0 +1,39 @@ +name-template: 'v$RESOLVED_VERSION ๐ŸŒˆ' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: '๐Ÿš€ Features' + labels: + - 'feat' + - 'feature' + - 'enhancement' + - title: '๐Ÿ› Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '๐Ÿงฐ Maintenance' + label: 'chore' + - title: "๐Ÿ“ Documentation" + labels: + - 'doc' + - 'documentation' + - title: "๐Ÿงช Tests" + labels: + - 'test' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +template: | + ## Changes + + $CHANGES \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..9d5241e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,139 @@ +name: Logvue Build + +env: + GITHUB_DEPLOY: 'false' + +on: + + pull_request: + branches: + - main + + workflow_dispatch: + repository_dispatch: + types: [ app-release ] + +defaults: + run: + shell: bash + +jobs: + build: + name: Build Package + timeout-minutes: 15 + continue-on-error: false + # if: github.event_name == 'pull_request' + + runs-on: ${{ matrix.os }} + environment: Production + env: + SENTRY_ENDPOINT: ${{ secrets.SENTRY_ENDPOINT }} + SENTRY_DEBUG: ${{ secrets.SENTRY_DEBUG }} + strategy: + fail-fast: true + matrix: + os: [ arm64, ubuntu-latest, macos-latest, windows-latest ] + jdk: [ 18 ] + + steps: + - name: Check out the source code + uses: actions/checkout@v2 + + - name: Download ${{ matrix.os }} OpenJDK ${{ matrix.jdk }} + id: download-jdk + uses: sormuras/download-jdk@v1 + with: + feature: ${{ matrix.jdk }} + + - name: Set up OpenJDK ${{ matrix.jdk }} + id: setup-java + uses: actions/setup-java@v2 + if: always() && steps.download-jdk.outcome == 'success' + with: + distribution: jdkfile + java-version: ${{ env.JDK_VERSION }} + jdkFile: ${{ env.JDK_FILE }} + + - name: Cache Gradle dependencies + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Deploy to GitHub Packages (Linux) + id: gradle-deploy + if: env.GITHUB_DEPLOY == 'true' && runner.os == 'Linux' + run: | + ./gradlew deploy + env: + GITHUB_USER: ${{ github.repository_owner }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Gradle Build + id: gradle-build + run: ./gradlew packageUberJarForCurrentOS package -DSENTRY_ENDPOINT=env.SENTRY_ENDPOINT -DSENTRY_DEBUG=env.SENTRY_DEBUG + + - name: Uploading ${{ matrix.os }} uber jar + if: steps.gradle-build.outcome == 'success' + uses: actions/upload-artifact@v2 + with: + name: ${{ steps.gradle-build.outputs.uber_jar_name }} + path: | + ${{ steps.gradle-build.outputs.uber_jar_path }} + if-no-files-found: error + + - name: Uploading ${{ matrix.os }} native package + if: steps.gradle-build.outcome == 'success' + uses: actions/upload-artifact@v2 + with: + name: ${{ steps.gradle-build.outputs.app_pkg_name }} + path: | + ${{ steps.gradle-build.outputs.app_pkg_path }} + if-no-files-found: error + + + release: + name: Release new version. + needs: [ build ] + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - name: Check out the source code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - uses: ffurrer2/extract-release-notes@v1.10.0 + id: extract_release_notes + if: ${{ false }} + + - name: Build Changelog + id: github_release + uses: mikepenz/release-changelog-builder-action@v2 + with: + configuration: ".github/config/configuration.json" + commitMode: true + ignorePreReleases: ${{ !contains(github.ref, '-') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Download all the build artifacts + uses: actions/download-artifact@v2 + with: + path: release-artifacts + + - name: Github Release + uses: softprops/action-gh-release@v1 + with: + body: ${{ steps.github_release.outputs.changelog }} + prerelease: ${{ contains(github.event.inputs.version, '-rc') || contains(github.event.inputs.version, '-b') || contains(github.event.inputs.version, '-a') }} + files: | + ${{ github.workspace }}/release-artifacts/** + fail_on_unmatched_files: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/gradle-wrapper.yml b/.github/workflows/gradle-wrapper.yml new file mode 100644 index 0000000..ed37f78 --- /dev/null +++ b/.github/workflows/gradle-wrapper.yml @@ -0,0 +1,10 @@ +name: "Validate Gradle Wrapper" +on: [ push, pull_request ] + +jobs: + validation: + name: "Validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 \ No newline at end of file diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..607a46d --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,19 @@ +name: Release Drafter + +on: + push: + branches: + - main + pull_request: + types: [ opened, reopened, synchronize ] + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + - name: Drafts next Release notes + uses: release-drafter/release-drafter@v5 + with: + config-name: config/release-drafter.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml new file mode 100644 index 0000000..6533746 --- /dev/null +++ b/.github/workflows/sync-labels.yml @@ -0,0 +1,19 @@ +name: Sync labels + +on: + push: + branches: + - main + paths: + - .github/config/labels.yml + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: micnncim/action-label-syncer@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + manifest: .github/config/labels.yml \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 5dd598b..da33323 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,10 +5,11 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") version "1.6.10" id("org.jetbrains.compose") version "1.0.1" + id("com.github.gmazzo.buildconfig") version "3.0.3" } -group = "com.gi" -version = "1.0" +group = "com.voxfinite" +version = "1.0.0" repositories { google() @@ -28,14 +29,14 @@ dependencies { // embedded database implementation("org.mapdb:mapdb:3.0.8") implementation("org.snakeyaml:snakeyaml-engine:2.3") - // https://mvnrepository.com/artifact/io.netty/netty-resolver-dns-native-macos - runtimeOnly("io.netty:netty-resolver-dns-native-macos:4.1.72.Final") // not sure if needed now +// runtimeOnly("io.netty:netty-resolver-dns-native-macos:4.1.72.Final") // not sure if needed now implementation("com.android.tools.ddms:ddmlib:30.2.0-alpha06") implementation("com.google.code.gson:gson:2.8.9") // https://mvnrepository.com/artifact/com.googlecode.cqengine/cqengine implementation("com.googlecode.cqengine:cqengine:3.6.0") implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.10") - implementation("com.halilibo.compose-richtext:richtext-ui-material:0.10.0") + + implementation("io.sentry:sentry-log4j2:5.5.2") } tasks.test { @@ -55,8 +56,57 @@ compose.desktop { mainClass = "MainKt" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "logvue" - packageVersion = "1.0.0" + packageName = project.name + packageVersion = "${project.version}" + description = "Local Analytics" + linux { + debMaintainer = "kapoor.aman22@gmail.com" + iconFile.set(project.file("logo_icon.png")) + } + macOS { + bundleID = "${project.group}.${project.name}" + iconFile.set(project.file("logo_icon.icns")) + } + windows { + upgradeUuid = "8AEBC8BF-9C94-4D02-ACA8-AF543E0CEB98" + iconFile.set(project.file("logo_icon.ico")) + } } } } + +buildConfig { + className("AppBuildConfig") + useKotlinOutput { topLevelConstants = true } + buildConfigField("String", "APP_NAME", "\"${project.name}\"") + buildConfigField("String", "APP_VERSION", "\"${project.version}\"") +} + +/** + * Sets the Github Action output as package name and path to use in other steps. + */ +gradle.buildFinished { + val pkgFormat = + compose.desktop.application.nativeDistributions.targetFormats.firstOrNull { it.isCompatibleWithCurrentOS } + val nativePkg = buildDir.resolve("compose/binaries").findPkg(pkgFormat?.fileExt) + val jarPkg = buildDir.resolve("compose/jars").findPkg(".jar") + nativePkg.ghActionOutput("app_pkg") + jarPkg.ghActionOutput("uber_jar") +} + +fun File.findPkg(format: String?) = when (format != null) { + true -> walk().firstOrNull { it.isFile && it.name.endsWith(format, ignoreCase = true) } + else -> null +} + +fun File?.ghActionOutput(prefix: String) = this?.let { + when (System.getenv("GITHUB_ACTIONS").toBoolean()) { + true -> println( + """ + ::set-output name=${prefix}_name::${it.name} + ::set-output name=${prefix}_path::${it.absolutePath} + """.trimIndent() + ) + else -> println("$prefix: $this") + } +} diff --git a/logo_icon.icns b/logo_icon.icns new file mode 100644 index 0000000..db8a1d2 Binary files /dev/null and b/logo_icon.icns differ diff --git a/logo_icon.ico b/logo_icon.ico new file mode 100644 index 0000000..f5acaf6 Binary files /dev/null and b/logo_icon.ico differ diff --git a/logo_icon.png b/logo_icon.png new file mode 100644 index 0000000..eb4e226 Binary files /dev/null and b/logo_icon.png differ diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index e2644c9..7030ee7 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -21,13 +21,13 @@ import storage.Db import ui.AppTheme import ui.CustomTheme import ui.components.BodyPanel -import ui.components.IntroDialog import ui.components.SideNavigation -import utils.APP_NAME -import utils.AppLog -import utils.Helpers +import ui.components.dialogs.CrashDialog +import ui.components.dialogs.IntroDialog +import utils.* import java.awt.Desktop + @Composable @Preview fun App() { @@ -49,20 +49,32 @@ fun App() { AdbHelper.init() } LaunchIntroIfNeeded() + LaunchCrashDialogIfNeeded() } } @Composable fun LaunchIntroIfNeeded() { - var introLaunched by remember { mutableStateOf(Db.configs["isIntroLaunched"].toBoolean()) } + var introLaunched by remember { mutableStateOf(AppSettings.getFlag("isIntroLaunched")) } if (!introLaunched) { IntroDialog { - Db.configs["isIntroLaunched"] = "true" + AppSettings.setFlag("isIntroLaunched", true) introLaunched = true } } } +@Composable +fun LaunchCrashDialogIfNeeded() { + if (!CustomExceptionHandler.isLastTimeCrashed()) return + var launched by remember { mutableStateOf(true) } + if (launched) { + CrashDialog { + launched = false + } + } +} + @OptIn(ExperimentalComposeUiApi::class) fun main() = application(false) { fun onClose(source: String) { @@ -78,11 +90,10 @@ fun main() = application(false) { onClose("User Close") exitApplication() } + Thread.setDefaultUncaughtExceptionHandler(CustomExceptionHandler()) + SentryHelper.init() val windowState = rememberWindowState(WindowPlacement.Floating, size = DpSize(1440.dp, 1024.dp)) - Window(onCloseRequest = onCloseRequest, title = APP_NAME, state = windowState) { -// window.exceptionHandler = WindowExceptionHandler { -// println(it) -// } + Window(onCloseRequest = onCloseRequest, title = CustomTheme.strings.appName, state = windowState) { App() } } diff --git a/src/main/kotlin/inputs/adb/CancelException.kt b/src/main/kotlin/inputs/adb/CancelException.kt deleted file mode 100644 index 56db764..0000000 --- a/src/main/kotlin/inputs/adb/CancelException.kt +++ /dev/null @@ -1,8 +0,0 @@ -package inputs.adb - -class CancelException : Exception { - constructor() : super("Logging is Cancelled") - constructor(message: String) : super(message) - constructor(message: String, cause: Throwable) : super(message, cause) - constructor(cause: Throwable) : super("Logging is Cancelled", cause) -} diff --git a/src/main/kotlin/inputs/adb/ddmlib/AdbHelper.kt b/src/main/kotlin/inputs/adb/ddmlib/AdbHelper.kt index c08f20a..5bd9416 100644 --- a/src/main/kotlin/inputs/adb/ddmlib/AdbHelper.kt +++ b/src/main/kotlin/inputs/adb/ddmlib/AdbHelper.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import models.LogCatMessage2 import utils.Either +import utils.reportException import java.io.File import java.util.* import java.util.concurrent.TimeUnit @@ -50,7 +51,7 @@ object AdbHelper { try { AndroidDebugBridge.terminate() } catch (e: Exception) { - // ignore + e.reportException() } } @@ -81,7 +82,6 @@ object AdbHelper { clientPid = client.clientData.pid } if (clientPid < 0) { - println("Client is null") send(Either.Left(LogErrorPackageIssue)) close() awaitClose() @@ -127,6 +127,7 @@ object AdbHelper { val androidEnvHome: File? = try { System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT") } catch (e: SecurityException) { + e.reportException() null }?.let { File(it) } diff --git a/src/main/kotlin/inputs/adb/ddmlib/Devices.kt b/src/main/kotlin/inputs/adb/ddmlib/Devices.kt index 3793c29..bb11d3b 100644 --- a/src/main/kotlin/inputs/adb/ddmlib/Devices.kt +++ b/src/main/kotlin/inputs/adb/ddmlib/Devices.kt @@ -10,7 +10,7 @@ class Devices : AndroidDebugBridge.IDeviceChangeListener { companion object { private val _devicesFlow: MutableStateFlow> = MutableStateFlow(emptyList()) val devicesFlow: MutableStateFlow> = _devicesFlow - private val devices: HashSet = hashSetOf() + private val connectedDevices: HashSet = hashSetOf() private val _currentDeviceFlow: MutableStateFlow = MutableStateFlow(null) val currentDeviceFlow = _currentDeviceFlow @@ -24,23 +24,23 @@ class Devices : AndroidDebugBridge.IDeviceChangeListener { override fun deviceConnected(device: IDevice) { val details2 = DeviceDetails2(device) - devices.add(details2) + connectedDevices.add(details2) _devicesFlow.value = currentDevices() } override fun deviceDisconnected(device: IDevice) { val details2 = DeviceDetails2(device) - devices.remove(details2) + connectedDevices.remove(details2) _devicesFlow.value = currentDevices() } override fun deviceChanged(device: IDevice, changeMask: Int) { var details2 = DeviceDetails2(device) - devices.remove(details2) + connectedDevices.remove(details2) details2 = DeviceDetails2(device) - devices.add(details2) + connectedDevices.add(details2) _devicesFlow.value = currentDevices() } - private fun currentDevices() = devices.toList().sortedBy { it.sortKey() } + private fun currentDevices() = connectedDevices.toList().sortedBy { it.sortKey() } } diff --git a/src/main/kotlin/inputs/adb/ddmlib/LogCatRunner.kt b/src/main/kotlin/inputs/adb/ddmlib/LogCatRunner.kt index 5ec8af6..a4370a9 100644 --- a/src/main/kotlin/inputs/adb/ddmlib/LogCatRunner.kt +++ b/src/main/kotlin/inputs/adb/ddmlib/LogCatRunner.kt @@ -4,9 +4,9 @@ import com.android.ddmlib.* import com.android.ddmlib.logcat.LogCatMessageParser import models.LogCatHeader2 import models.LogCatMessage2 +import utils.reportException import java.io.IOException import java.time.Instant -import java.util.* import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import javax.annotation.concurrent.GuardedBy @@ -61,9 +61,11 @@ class LogCatRunner( } catch (e: TimeoutException) { notifyListeners(arrayListOf(sConnectionTimeoutMsg)) } catch (ignored: AdbCommandRejectedException) { + ignored.reportException() // will not be thrown as long as the shell supports logcat } catch (ignored: ShellCommandUnresponsiveException) { // this will not be thrown since the last argument is 0 + ignored.reportException() } catch (e: IOException) { notifyListeners(arrayListOf(sConnectionErrorMsg)) } diff --git a/src/main/kotlin/processor/MainProcessor.kt b/src/main/kotlin/processor/MainProcessor.kt index dc23438..1eb39ed 100644 --- a/src/main/kotlin/processor/MainProcessor.kt +++ b/src/main/kotlin/processor/MainProcessor.kt @@ -4,6 +4,8 @@ import com.android.ddmlib.Log import inputs.adb.AndroidLogStreamer import inputs.adb.LogCatErrors import inputs.adb.LogErrorNoSession +import io.sentry.Sentry +import io.sentry.SpanStatus import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -11,10 +13,10 @@ import kotlinx.coroutines.withContext import models.LogItem import models.SessionInfo import storage.Db -import utils.AppLog import utils.Helpers import utils.failureOrNull import utils.getOrNull +import utils.reportException class MainProcessor { @@ -125,11 +127,17 @@ class MainProcessor { if (!isNewStream) { indexedCollection.clear() } + val sentryTransaction = Sentry.startTransaction("filterLogs", "filter", true) + sentryTransaction.setData("query", filterQuery ?: "") try { filterLogs(indexedCollection, list, parser, fQuery) } catch (e: Exception) { - e.printStackTrace() + e.reportException() + sentryTransaction.throwable = e + sentryTransaction.status = SpanStatus.INTERNAL_ERROR listOf(LogItem.errorContent("Error in query\n${e.message}")) + } finally { + sentryTransaction.finish() } } if (filterResult.isEmpty() && !isNewStream) { @@ -141,10 +149,6 @@ class MainProcessor { } fun pause() { - try { - streamer.stop() - } catch (e: Exception) { - AppLog.d("unnecessary", "keeping exception for now in pause") - } + streamer.stop() } } diff --git a/src/main/kotlin/processor/QueryHelper.kt b/src/main/kotlin/processor/QueryHelper.kt index 2ec2838..4d7628d 100644 --- a/src/main/kotlin/processor/QueryHelper.kt +++ b/src/main/kotlin/processor/QueryHelper.kt @@ -11,6 +11,7 @@ import com.googlecode.cqengine.index.radixreversed.ReversedRadixTreeIndex import com.googlecode.cqengine.query.parser.sql.SQLParser import models.LogItem import utils.AppLog +import utils.reportException import kotlin.reflect.KProperty1 import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue @@ -48,7 +49,6 @@ fun filterLogs( ): List { indexedCollection.addAll(list) registerPropertiesInParser(list, parser, indexedCollection) - println("Filtering logs") val filterResult = measureTimedValue { parser.retrieve(indexedCollection, filterQuery) } @@ -119,7 +119,7 @@ fun registerAndAddIndex( addIndex(ReversedRadixTreeIndex.onAttribute(att)) parser.registerAttribute(att) } catch (e: Exception) { - // TODO: Make sure to log these exceptions somewhere so that we can analyse them + e.reportException() addGenericAttribute(key, value, parser) } } @@ -127,9 +127,10 @@ fun registerAndAddIndex( try { val att: ParameterizedAttribute> = ParameterizedAttribute(key, value.javaClass) addIndex(HashIndex.onAttribute(att)) - addIndex(NavigableIndex.onAttribute(att)) +// addIndex(NavigableIndex.onAttribute(att)) // TODO: break it to specific types parser.registerAttribute(att) } catch (e: Exception) { + e.reportException() addGenericAttribute(key, value, parser) } } diff --git a/src/main/kotlin/storage/Db.kt b/src/main/kotlin/storage/Db.kt index a9593ae..6d6858d 100644 --- a/src/main/kotlin/storage/Db.kt +++ b/src/main/kotlin/storage/Db.kt @@ -101,9 +101,6 @@ object Db { newCatalog.remove(it) } db.nameCatalogSave(newCatalog) - db.getAllNames().forEach { - println(it) - } db.commit() } diff --git a/src/main/kotlin/storage/serializer/ObjectSerializer.kt b/src/main/kotlin/storage/serializer/ObjectSerializer.kt index ee1bfb1..c95e9fc 100644 --- a/src/main/kotlin/storage/serializer/ObjectSerializer.kt +++ b/src/main/kotlin/storage/serializer/ObjectSerializer.kt @@ -6,7 +6,8 @@ import org.mapdb.DataOutput2 import org.mapdb.serializer.GroupSerializerObjectArray import java.io.* -class ObjectSerializer(val classLoader: ClassLoader = Thread.currentThread().contextClassLoader) : GroupSerializerObjectArray() { +class ObjectSerializer(val classLoader: ClassLoader = Thread.currentThread().contextClassLoader) : + GroupSerializerObjectArray() { override fun serialize(out: DataOutput2, value: T) { val out2 = ObjectOutputStream(out as OutputStream) diff --git a/src/main/kotlin/ui/components/BodyHeader.kt b/src/main/kotlin/ui/components/BodyHeader.kt index 3bf3afb..6133299 100644 --- a/src/main/kotlin/ui/components/BodyHeader.kt +++ b/src/main/kotlin/ui/components/BodyHeader.kt @@ -17,6 +17,8 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import processor.QUERY_PREFIX import ui.CustomTheme +import ui.components.dialogs.FilterFaqDialog +import ui.components.dialogs.SettingsDialog @Composable fun BodyHeader( diff --git a/src/main/kotlin/ui/components/BodyPanel.kt b/src/main/kotlin/ui/components/BodyPanel.kt index b3ebed3..0201691 100644 --- a/src/main/kotlin/ui/components/BodyPanel.kt +++ b/src/main/kotlin/ui/components/BodyPanel.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import inputs.adb.CancelException import inputs.adb.LogCatErrors import inputs.adb.ddmlib.Devices import inputs.adb.logcatErrorString @@ -95,7 +94,7 @@ fun BodyPanel( if (isOpen) { val sessionInfo = processor.getSessionInfo(sessionId.orEmpty()) if (sessionInfo != null) { - ExportDialog(sessionInfo, logItems) { + ui.components.dialogs.ExportDialog(sessionInfo, logItems) { isOpen = false } } @@ -298,9 +297,5 @@ private fun fetchOldData( } private fun pauseProcessor(processor: MainProcessor) { - try { - processor.pause() - } catch (ex: CancelException) { - println(ex.message) - } + processor.pause() } diff --git a/src/main/kotlin/ui/components/NewSessionBox.kt b/src/main/kotlin/ui/components/NewSessionBox.kt index d9150de..08a00ac 100644 --- a/src/main/kotlin/ui/components/NewSessionBox.kt +++ b/src/main/kotlin/ui/components/NewSessionBox.kt @@ -21,6 +21,7 @@ import inputs.adb.ddmlib.Devices import models.DeviceDetails2 import models.SessionInfo import ui.CustomTheme +import ui.components.dialogs.StyledCustomVerticalDialog @OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @Composable diff --git a/src/main/kotlin/ui/components/SideNavigation.kt b/src/main/kotlin/ui/components/SideNavigation.kt index d74b2c8..f3a875a 100644 --- a/src/main/kotlin/ui/components/SideNavigation.kt +++ b/src/main/kotlin/ui/components/SideNavigation.kt @@ -1,6 +1,5 @@ package ui.components -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Divider @@ -8,15 +7,12 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import inputs.adb.ddmlib.Devices import kotlinx.coroutines.launch import processor.MainProcessor import ui.CustomTheme -import utils.APP_NAME +import ui.components.common.AppLogo @Composable fun SideNavigation( @@ -82,13 +78,3 @@ private fun SideNavHeader(header: String) { style = CustomTheme.typography.headings.h3 ) } - -@Composable -fun AppLogo(modifier: Modifier = Modifier) { - Image( - painterResource("icons/logo.svg"), APP_NAME, - modifier, - colorFilter = ColorFilter.tint(CustomTheme.colors.highContrast), - contentScale = ContentScale.FillWidth - ) -} diff --git a/src/main/kotlin/ui/components/BasicComponents.kt b/src/main/kotlin/ui/components/common/BasicComponents.kt similarity index 78% rename from src/main/kotlin/ui/components/BasicComponents.kt rename to src/main/kotlin/ui/components/common/BasicComponents.kt index 5ad5006..354b56d 100644 --- a/src/main/kotlin/ui/components/BasicComponents.kt +++ b/src/main/kotlin/ui/components/common/BasicComponents.kt @@ -1,25 +1,38 @@ -package ui.components +package ui.components.common +import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.Icon -import androidx.compose.material.RadioButton -import androidx.compose.material.Switch -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import models.MarkupText +import models.SocialIcons import ui.CustomTheme +import ui.components.dialogs.openBrowser import ui.views.DarkToggleButton +@Composable +fun AppLogo(modifier: Modifier = Modifier) { + Image( + painterResource("icons/logo.svg"), CustomTheme.strings.appName, + modifier, + colorFilter = ColorFilter.tint(CustomTheme.colors.highContrast), + contentScale = ContentScale.FillWidth + ) +} + @Composable fun DarkModeSwitchItem( isDarkMode: Boolean, @@ -181,3 +194,32 @@ fun MultiLineRadioButton( } } } + +@Composable +fun WebLinkButton( + socialIcons: SocialIcons, text: String, modifier: Modifier = Modifier +) { + val buttonColors = ButtonDefaults.textButtonColors( + contentColor = CustomTheme.colors.mediumContrast + ) + TextButton({ openBrowser(socialIcons.url) }, modifier, colors = buttonColors) { + Icon(painterResource(socialIcons.icon), socialIcons.name) + Spacer(Modifier.width(4.dp)) + Text(text, style = CustomTheme.typography.bodySmall) + } +} + +@Composable +fun WebLinkButtonFilled( + socialIcons: SocialIcons, text: String, modifier: Modifier = Modifier +) { + val buttonColors = ButtonDefaults.buttonColors( + backgroundColor = CustomTheme.colors.mediumContrast + ) + val elevation = ButtonDefaults.elevation(defaultElevation = 0.dp) + Button({ openBrowser(socialIcons.url) }, modifier, colors = buttonColors, elevation = elevation) { + Icon(painterResource(socialIcons.icon), socialIcons.name) + Spacer(Modifier.width(4.dp)) + Text(text, style = CustomTheme.typography.bodySmall) + } +} diff --git a/src/main/kotlin/ui/components/dialogs/CrashDialog.kt b/src/main/kotlin/ui/components/dialogs/CrashDialog.kt new file mode 100644 index 0000000..853cc20 --- /dev/null +++ b/src/main/kotlin/ui/components/dialogs/CrashDialog.kt @@ -0,0 +1,34 @@ +package ui.components.dialogs + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import models.SocialIcons +import ui.CustomTheme +import ui.components.common.WebLinkButtonFilled +import utils.CustomExceptionHandler + +@Composable +fun CrashDialog(onDismissRequest: () -> Unit) { + CustomExceptionHandler.setLastCrashConsumed() + SimpleVerticalDialog("Share crash", onDismissRequest) { + Image( + painterResource("icons/crash_illustration.xml"), "Crashed", + Modifier.fillMaxWidth(0.9f), contentScale = ContentScale.FillWidth + ) + Spacer(Modifier.height(16.dp)) + Text(CustomTheme.strings.appCrashText, textAlign = TextAlign.Center, style = CustomTheme.typography.body) + Spacer(Modifier.height(16.dp)) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) { + WebLinkButtonFilled(SocialIcons.GithubIssues, "Github Issue", Modifier.weight(0.5f)) + WebLinkButtonFilled(SocialIcons.Email, "Mail Us", Modifier.weight(0.5f)) + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/ui/components/CustomDialog.kt b/src/main/kotlin/ui/components/dialogs/CustomDialog.kt similarity index 98% rename from src/main/kotlin/ui/components/CustomDialog.kt rename to src/main/kotlin/ui/components/dialogs/CustomDialog.kt index 335577a..f84a189 100644 --- a/src/main/kotlin/ui/components/CustomDialog.kt +++ b/src/main/kotlin/ui/components/dialogs/CustomDialog.kt @@ -1,4 +1,4 @@ -package ui.components +package ui.components.dialogs import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -91,7 +91,7 @@ fun CustomDialog( Floats.constrainToRange(backgroundAlpha, 0.0f, 1.0f) Floats.constrainToRange(dialogWidthRatio, 0.1f, 1.0f) Floats.constrainToRange(dialogHeightRatio, 0.1f, 1.0f) - with(UndecoratedWindowAlertDialogProvider) { + with(PopupAlertDialogProvider) { AlertDialog(onDismissRequest) { Dialog( onCloseRequest = onDismissRequest, diff --git a/src/main/kotlin/ui/components/ExportDialog.kt b/src/main/kotlin/ui/components/dialogs/ExportDialog.kt similarity index 97% rename from src/main/kotlin/ui/components/ExportDialog.kt rename to src/main/kotlin/ui/components/dialogs/ExportDialog.kt index 3ed60f7..2e167e0 100644 --- a/src/main/kotlin/ui/components/ExportDialog.kt +++ b/src/main/kotlin/ui/components/dialogs/ExportDialog.kt @@ -1,4 +1,4 @@ -package ui.components +package ui.components.dialogs import androidx.compose.foundation.layout.* import androidx.compose.material.Button @@ -15,6 +15,8 @@ import kotlinx.coroutines.launch import models.* import processor.Exporter import storage.Db +import ui.components.common.MultiLineRadioButton +import ui.components.common.SwitchItem import utils.Helpers import java.nio.file.Path import kotlin.io.path.absolutePathString diff --git a/src/main/kotlin/ui/components/FaqDialog.kt b/src/main/kotlin/ui/components/dialogs/FaqDialog.kt similarity index 97% rename from src/main/kotlin/ui/components/FaqDialog.kt rename to src/main/kotlin/ui/components/dialogs/FaqDialog.kt index 10382dd..664ebf2 100644 --- a/src/main/kotlin/ui/components/FaqDialog.kt +++ b/src/main/kotlin/ui/components/dialogs/FaqDialog.kt @@ -1,4 +1,4 @@ -package ui.components +package ui.components.dialogs import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -13,6 +13,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import models.Faq import ui.CustomTheme +import ui.components.common.SimpleListItem @Composable fun FaqDialog( diff --git a/src/main/kotlin/ui/components/FileDialog.kt b/src/main/kotlin/ui/components/dialogs/FileDialog.kt similarity index 97% rename from src/main/kotlin/ui/components/FileDialog.kt rename to src/main/kotlin/ui/components/dialogs/FileDialog.kt index 7020ddd..f8f5227 100644 --- a/src/main/kotlin/ui/components/FileDialog.kt +++ b/src/main/kotlin/ui/components/dialogs/FileDialog.kt @@ -1,4 +1,4 @@ -package ui.components +package ui.components.dialogs import androidx.compose.runtime.Composable import androidx.compose.ui.window.AwtWindow diff --git a/src/main/kotlin/ui/components/IntroDialog.kt b/src/main/kotlin/ui/components/dialogs/IntroDialog.kt similarity index 97% rename from src/main/kotlin/ui/components/IntroDialog.kt rename to src/main/kotlin/ui/components/dialogs/IntroDialog.kt index bc9465a..05e0b51 100644 --- a/src/main/kotlin/ui/components/IntroDialog.kt +++ b/src/main/kotlin/ui/components/dialogs/IntroDialog.kt @@ -1,4 +1,4 @@ -package ui.components +package ui.components.dialogs import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import ui.CustomTheme +import ui.components.common.AppLogo @Composable fun IntroDialog(onDismissRequest: () -> Unit) { diff --git a/src/main/kotlin/ui/components/SettingsDialog.kt b/src/main/kotlin/ui/components/dialogs/SettingsDialog.kt similarity index 89% rename from src/main/kotlin/ui/components/SettingsDialog.kt rename to src/main/kotlin/ui/components/dialogs/SettingsDialog.kt index 3f58af5..ea6a8ce 100644 --- a/src/main/kotlin/ui/components/SettingsDialog.kt +++ b/src/main/kotlin/ui/components/dialogs/SettingsDialog.kt @@ -1,7 +1,9 @@ -package ui.components +package ui.components.dialogs import androidx.compose.foundation.layout.* -import androidx.compose.material.* +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -14,12 +16,13 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import models.SocialIcons import ui.CustomTheme +import ui.components.ItemHeader +import ui.components.common.* import utils.AppSettings import utils.Helpers @Composable fun SettingsDialog(onDismissRequest: () -> Unit) { - SimpleVerticalDialog(header = "Settings", onDismissRequest = onDismissRequest) { GeneralSettingBlock(Modifier.fillMaxWidth()) Spacer(Modifier.height(16.dp)) @@ -103,7 +106,6 @@ fun OtherSettingBlock(modifier: Modifier = Modifier) { } Row(Modifier.padding(start = 32.dp)) { SocialIcons.DefaultIcons.forEach { - println(it) SocialIcon(it) } } @@ -111,16 +113,6 @@ fun OtherSettingBlock(modifier: Modifier = Modifier) { } } -@Composable -private fun WebLinkButton(socialIcons: SocialIcons, text: String) { - val buttonColors = ButtonDefaults.textButtonColors(contentColor = CustomTheme.colors.mediumContrast) - TextButton({ openBrowser(socialIcons.url) }, colors = buttonColors) { - Icon(painterResource(socialIcons.icon), socialIcons.name) - Spacer(Modifier.width(4.dp)) - Text(text, style = CustomTheme.typography.bodySmall) - } -} - @Composable private fun SocialIcon(icon: SocialIcons) { IconButton({ openBrowser(icon.url) }) { diff --git a/src/main/kotlin/utils/AppLog.kt b/src/main/kotlin/utils/AppLog.kt index b074c05..3a400df 100644 --- a/src/main/kotlin/utils/AppLog.kt +++ b/src/main/kotlin/utils/AppLog.kt @@ -1,5 +1,6 @@ package utils +import io.sentry.Sentry import java.util.logging.Level import java.util.logging.LogRecord import java.util.logging.SimpleFormatter @@ -17,3 +18,11 @@ object AppLog { d("Debug", msg) } } + +fun Throwable?.reportException() { + if (this == null) { + Sentry.captureException(UnsupportedOperationException("Throwable should not be null")) + return + } + Sentry.captureException(this) +} diff --git a/src/main/kotlin/utils/AppResources.kt b/src/main/kotlin/utils/AppResources.kt index 2e821cd..68439ca 100644 --- a/src/main/kotlin/utils/AppResources.kt +++ b/src/main/kotlin/utils/AppResources.kt @@ -1,15 +1,17 @@ package utils -const val APP_NAME = "LogVue" - //TODO: Move all strings here to support languages in future interface StringRes { val appName: String val filterFaqTitle: String + val appCrashText: String } class EnglishStringRes : StringRes { - override val appName: String = APP_NAME + override val appName: String = "LogVue" override val filterFaqTitle: String = "Filter FAQโ€™s" + override val appCrashText: String = "Unfortunately the app was crashed last time. " + + "While we look into this, you can also report this issue to us on github or through " + + "mail describing the scenario that caused this crash." } diff --git a/src/main/kotlin/utils/CustomExceptionHandler.kt b/src/main/kotlin/utils/CustomExceptionHandler.kt new file mode 100644 index 0000000..5762ff2 --- /dev/null +++ b/src/main/kotlin/utils/CustomExceptionHandler.kt @@ -0,0 +1,23 @@ +package utils + +class CustomExceptionHandler : Thread.UncaughtExceptionHandler { + + override fun uncaughtException(t: Thread?, e: Throwable?) { + e?.printStackTrace() + setCrashed() + } + + companion object { + private const val DB_KEY = "lastTimeException" + + private fun setCrashed() { + AppSettings.setFlag(DB_KEY, true) + } + + fun setLastCrashConsumed() { + AppSettings.setFlag(DB_KEY, false) + } + + fun isLastTimeCrashed() = AppSettings.getFlag(DB_KEY) + } +} \ No newline at end of file diff --git a/src/main/kotlin/utils/Helpers.kt b/src/main/kotlin/utils/Helpers.kt index 22a7a01..0bae37f 100644 --- a/src/main/kotlin/utils/Helpers.kt +++ b/src/main/kotlin/utils/Helpers.kt @@ -191,7 +191,7 @@ object Helpers { val dump = Dump(settings) dump.dumpToString(properties) } catch (e: Exception) { - AppLog.d("YamlConverter", e.localizedMessage) + e.reportException() null } } @@ -222,7 +222,7 @@ object Helpers { val dump = Dump(settings) dump.dump(properties, YamlWriter(printWriter)) } catch (e: Exception) { - AppLog.d("YamlConverter", e.localizedMessage) + e.reportException() } } @@ -231,7 +231,7 @@ object Helpers { val dump = Dump(settings) dump.dump(properties, streamDataWriter) } catch (e: Exception) { - AppLog.d("YamlConverter", e.localizedMessage) + e.reportException() } } @@ -292,7 +292,7 @@ object Helpers { } Runtime.getRuntime().exec(command) } catch (e: Exception) { - AppLog.d("failed to open file manager") + e.reportException() } } diff --git a/src/main/kotlin/utils/ItemObjectMapper.kt b/src/main/kotlin/utils/ItemObjectMapper.kt index 3516366..2c83af3 100644 --- a/src/main/kotlin/utils/ItemObjectMapper.kt +++ b/src/main/kotlin/utils/ItemObjectMapper.kt @@ -22,8 +22,7 @@ class ItemObjectMapper { } } } catch (e: Exception) { - LOGGER.error("Unexpected exception!", e) - println("Unexpected Exception! (item=$item e = ${e.message}") + Exception("Item mapping failed for item=$item", e).reportException() item.stringRepresentation } } @@ -31,7 +30,6 @@ class ItemObjectMapper { private fun parseObject(item: ObjectItem): HashMap { val map = hashMapOf() item.getAttributes().forEach { entry -> -// println("parsing for field: $entry") val stringRepresentation = entry.value.stringRepresentation ?: "" val key = entry.key.removePrefix("{").removeSuffix("}") diff --git a/src/main/kotlin/utils/SentryHelper.kt b/src/main/kotlin/utils/SentryHelper.kt new file mode 100644 index 0000000..a3736bf --- /dev/null +++ b/src/main/kotlin/utils/SentryHelper.kt @@ -0,0 +1,33 @@ +package utils + +import com.voxfinite.logvue.APP_VERSION +import io.sentry.Breadcrumb +import io.sentry.Sentry +import io.sentry.SentryOptions + +object SentryHelper { + + private const val SAMPLE_RATE = 0.3 + + fun init() { + Sentry.init { options: SentryOptions -> + options.dsn = System.getProperty("SENTRY_ENDPOINT").orEmpty() + // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring. + // We recommend adjusting this value in production. + options.tracesSampleRate = SAMPLE_RATE + // When first trying Sentry it's good to see what the SDK is doing: + options.setDebug(System.getProperty("SENTRY_DEBUG").toBoolean()) + } + Sentry.configureScope { scope -> + scope.setTag("os.name", System.getProperty("os.name")) + scope.setTag("os.arch", System.getProperty("os.arch")) + scope.setTag("os.version", System.getProperty("os.version")) + scope.setTag("build.version", APP_VERSION) + } + } + + fun breadcrumb(breadcrumb: Breadcrumb) { + Sentry.addBreadcrumb(breadcrumb) + } + +} \ No newline at end of file diff --git a/src/main/resources/icons/crash_illustration.xml b/src/main/resources/icons/crash_illustration.xml new file mode 100644 index 0000000..034bea8 --- /dev/null +++ b/src/main/resources/icons/crash_illustration.xml @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index f6fa73b..5db49b8 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -1,14 +1,14 @@ - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + \ No newline at end of file