diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cec573a..53f8cd9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,9 +13,9 @@ on: - 'docs/**' - '.github/config/labels.yml' - pull_request: - branches: - - main + pull_request: + branches: + - main workflow_dispatch: repository_dispatch: diff --git a/build.gradle.kts b/build.gradle.kts index 2b11bcb..83c16f7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -39,6 +39,8 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.10") implementation("io.sentry:sentry-log4j2:5.5.2") + // https://mvnrepository.com/artifact/net.harawata/appdirs + implementation("net.harawata:appdirs:1.2.1") r8("com.android.tools:r8:3.0.73") } diff --git a/detekt.yml b/detekt.yml index 095bbaa..086f894 100644 --- a/detekt.yml +++ b/detekt.yml @@ -155,8 +155,8 @@ complexity: thresholdInInterfaces: 11 thresholdInObjects: 11 thresholdInEnums: 11 - ignoreDeprecated: false - ignorePrivate: false + ignoreDeprecated: true + ignorePrivate: true ignoreOverridden: false coroutines: diff --git a/src/main/kotlin/app/Main.kt b/src/main/kotlin/app/Main.kt index e63950f..6ce6dc6 100644 --- a/src/main/kotlin/app/Main.kt +++ b/src/main/kotlin/app/Main.kt @@ -3,5 +3,8 @@ package app import androidx.compose.ui.window.application fun main() = application(false) { + val environment = System.getenv("SKIKO_RENDER_API") + val property = System.getProperty("skiko.renderApi") + println("env: $environment render: $property") appWindow() } diff --git a/src/main/kotlin/app/MainWindow.kt b/src/main/kotlin/app/MainWindow.kt index 28749ab..8d6ed8a 100644 --- a/src/main/kotlin/app/MainWindow.kt +++ b/src/main/kotlin/app/MainWindow.kt @@ -1,8 +1,6 @@ package app import androidx.compose.runtime.Composable -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp import androidx.compose.ui.window.ApplicationScope import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPlacement @@ -30,7 +28,7 @@ fun ApplicationScope.appWindow() { } Thread.setDefaultUncaughtExceptionHandler(CustomExceptionHandler()) SentryHelper.init() - val windowState = rememberWindowState(WindowPlacement.Floating, size = DpSize(1440.dp, 1024.dp)) + val windowState = rememberWindowState(WindowPlacement.Maximized) Window(onCloseRequest = onCloseRequest, title = CustomTheme.strings.appName, state = windowState) { App() } diff --git a/src/main/kotlin/inputs/adb/ddmlib/AdbHelper.kt b/src/main/kotlin/inputs/adb/ddmlib/AdbHelper.kt index 5bd9416..fa2b6d9 100644 --- a/src/main/kotlin/inputs/adb/ddmlib/AdbHelper.kt +++ b/src/main/kotlin/inputs/adb/ddmlib/AdbHelper.kt @@ -29,15 +29,17 @@ object AdbHelper { private const val PACKAGES_COMMAND = "pm list packages -3 -e" // list of lines with format : package:com.ea.games.r3_row + private const val ADB_TIMEOUT = 10L + fun init() { val options = AdbInitOptions.builder().setClientSupportEnabled(true).build() AndroidDebugBridge.init(options) AndroidDebugBridge.addDeviceChangeListener(Devices()) val adbPath = adbPath() bridge = if (!adbPath.isNullOrBlank()) { - AndroidDebugBridge.createBridge(adbPath, false, 10, TimeUnit.SECONDS) + AndroidDebugBridge.createBridge(adbPath, false, ADB_TIMEOUT, TimeUnit.SECONDS) } else { - AndroidDebugBridge.createBridge(10, TimeUnit.SECONDS) + AndroidDebugBridge.createBridge(ADB_TIMEOUT, TimeUnit.SECONDS) } AndroidDebugBridge.addDebugBridgeChangeListener { bridge = it @@ -112,18 +114,18 @@ object AdbHelper { suspend fun getPackages(device: IDevice, onValue: (packages: List) -> Unit) = withContext(Dispatchers.IO) { device.executeShellCommand( PACKAGES_COMMAND, PackagesReceiver(onValue), - 10, TimeUnit.SECONDS + ADB_TIMEOUT, TimeUnit.SECONDS ) } private fun IDevice.emptyShellCommand(command: String) { executeShellCommand( command, - EmptyReceiver, 10, TimeUnit.SECONDS + EmptyReceiver, ADB_TIMEOUT, TimeUnit.SECONDS ) } - private fun adbPath(): String? { + fun adbPath(): String? { val androidEnvHome: File? = try { System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT") } catch (e: SecurityException) { diff --git a/src/main/kotlin/storage/Db.kt b/src/main/kotlin/storage/Db.kt index 6d6858d..00f6fcf 100644 --- a/src/main/kotlin/storage/Db.kt +++ b/src/main/kotlin/storage/Db.kt @@ -2,7 +2,6 @@ package storage import models.LogItem import models.SessionInfo -import org.mapdb.DBMaker import org.mapdb.HTreeMap import org.mapdb.Serializer import storage.serializer.ObjectSerializer @@ -11,8 +10,8 @@ object Db { private const val PREFIX = "Session-" - private val db by lazy { - DBMaker.fileDB("sessions.db").fileMmapEnableIfSupported().checksumHeaderBypass().make() + private val diskDb by lazy { + StorageHelper.createDiskDb() } private var session: HTreeMap? = null @@ -20,11 +19,11 @@ object Db { private val LOCK = Any() val configs by lazy { - db.hashMap("configs", Serializer.STRING, Serializer.STRING).createOrOpen() + diskDb.hashMap("configs", Serializer.STRING, Serializer.STRING).createOrOpen() } private val sessionInfoMap by lazy { - db.hashMap("sessionInfo", Serializer.STRING, ObjectSerializer()).createOrOpen() + diskDb.hashMap("sessionInfo", Serializer.STRING, ObjectSerializer()).createOrOpen() } init { @@ -37,14 +36,14 @@ object Db { return sessionInfoMap[sessionId] } - fun getAllSessions() = db.getAllNames().filter { it.startsWith(PREFIX) }.sortedBy { getSessionNumber(it) } + fun getAllSessions() = diskDb.getAllNames().filter { it.startsWith(PREFIX) }.sortedBy { getSessionNumber(it) } - fun getLastSessionNumber(): Int { + private fun getLastSessionNumber(): Int { val lastSessionId = getAllSessions().lastOrNull() ?: sessionIdFromNumber(0) return getSessionNumber(lastSessionId) } - fun getPreviousSessionNumber(): Int { + private fun getPreviousSessionNumber(): Int { val lastDbSessionId = configs["lastSessionId"] val lastSessionId = if (lastDbSessionId.isNullOrBlank() || !lastDbSessionId.startsWith(PREFIX)) { getAllSessions().lastOrNull() ?: sessionIdFromNumber(0) @@ -54,11 +53,11 @@ object Db { return getSessionNumber(lastSessionId) } - fun getSessionNumber(sessionId: String) = sessionId.split("-").lastOrNull()?.toIntOrNull() ?: 0 + private fun getSessionNumber(sessionId: String) = sessionId.split("-").lastOrNull()?.toIntOrNull() ?: 0 - fun areNoSessionsCreated() = getAllSessions().isEmpty() + private fun areNoSessionsCreated() = getAllSessions().isEmpty() - fun isThisTheOnlySession(sessionId: String): Boolean { + private fun isThisTheOnlySession(sessionId: String): Boolean { val sessions = getAllSessions() if (sessions.size != 1) return false return sessions.first() == sessionId @@ -78,12 +77,12 @@ object Db { } else if (sessionId == sessionId()) { changeSession(null) } - val oldSession = db.hashMap(sessionId, Serializer.STRING, ObjectSerializer()) + val oldSession = diskDb.hashMap(sessionId, Serializer.STRING, ObjectSerializer()) .open() oldSession.clear() sessionInfoMap.remove(sessionId) val recIds = arrayListOf() - db.nameCatalogParamsFor(sessionId).forEach { (t, u) -> + diskDb.nameCatalogParamsFor(sessionId).forEach { (t, u) -> if (t.endsWith("rootRecids")) { u.split(",").forEach { value -> val recId = value.trim().toLongOrNull() @@ -93,15 +92,15 @@ object Db { } } recIds.forEach { - db.getStore().delete(it, Serializer.STRING) + diskDb.getStore().delete(it, Serializer.STRING) } - val newCatalog = db.nameCatalogLoad() + val newCatalog = diskDb.nameCatalogLoad() val keys = newCatalog.keys.filter { it.startsWith(sessionId) } keys.forEach { newCatalog.remove(it) } - db.nameCatalogSave(newCatalog) - db.commit() + diskDb.nameCatalogSave(newCatalog) + diskDb.commit() } fun changeSession(sessionId: String?) { @@ -123,7 +122,7 @@ object Db { } if (sessionNumber < 1) throw Exception("Session number must be greater than 1") val sessionId = sessionIdFromNumber(sessionNumber) - val session = db + val session = diskDb .hashMap(sessionId, Serializer.STRING, ObjectSerializer()) .createOrOpen() this.sessionId = sessionId @@ -143,6 +142,6 @@ object Db { } fun close() { - db.close() + diskDb.close() } } diff --git a/src/main/kotlin/storage/StorageHelper.kt b/src/main/kotlin/storage/StorageHelper.kt new file mode 100644 index 0000000..baf9983 --- /dev/null +++ b/src/main/kotlin/storage/StorageHelper.kt @@ -0,0 +1,61 @@ +package storage + +import com.voxfinite.logvue.APP_NAME +import net.harawata.appdirs.AppDirsFactory +import org.mapdb.DB +import org.mapdb.DBException +import org.mapdb.DBMaker +import utils.DbCreationException +import utils.reportException +import java.io.File +import java.io.IOException +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.attribute.PosixFilePermissions + + +object StorageHelper { + + internal fun createDiskDb(): DB { + val dbFile = getDbFile() + return try { + DBMaker.fileDB(dbFile).fileMmapEnableIfSupported().checksumHeaderBypass().make() + } catch (e: DBException.VolumeIOError) { + DbCreationException("Mmap enabled db could not be created", e).reportException() + try { + DBMaker.fileDB(dbFile).fileChannelEnable().checksumHeaderBypass().make() + } catch (ee: DBException.VolumeIOError) { + DbCreationException("file channel enabled db could not be created", ee).reportException() + DBMaker.fileDB(dbFile).checksumHeaderBypass().make() + } + } + } + + @Throws(IOException::class) + private fun getDbFile(): File { + val appDirs = AppDirsFactory.getInstance() + val dataDir = appDirs.getUserDataDir(APP_NAME, null, APP_NAME) + val folder = File(dataDir) + if (!folder.exists() || !folder.isDirectory) { + if (folder.exists()) { + folder.delete() + } + try { + val isPosix = FileSystems.getDefault().supportedFileAttributeViews().contains("posix") + if (isPosix) { + val posixAttribute = PosixFilePermissions.asFileAttribute( + PosixFilePermissions.fromString("rwxr-x---") + ) + Files.createDirectories(folder.toPath(), posixAttribute) + } else { + Files.createDirectories(folder.toPath()) + } + } catch (e: IOException) { + throw IOException("Cannot create app folder at path ${folder.canonicalPath}", e) + } + } + val dbName = "sessions.db" + return File(dataDir, dbName) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/utils/CustomExceptionHandler.kt b/src/main/kotlin/utils/CustomExceptionHandler.kt index 998b3b3..434200c 100644 --- a/src/main/kotlin/utils/CustomExceptionHandler.kt +++ b/src/main/kotlin/utils/CustomExceptionHandler.kt @@ -5,6 +5,7 @@ class CustomExceptionHandler : Thread.UncaughtExceptionHandler { override fun uncaughtException(t: Thread?, e: Throwable?) { e?.printStackTrace() setCrashed() + throw e ?: Exception("Unknown exception") } companion object { diff --git a/src/main/kotlin/utils/DbCreationException.kt b/src/main/kotlin/utils/DbCreationException.kt new file mode 100644 index 0000000..8007da9 --- /dev/null +++ b/src/main/kotlin/utils/DbCreationException.kt @@ -0,0 +1,8 @@ +package utils + +class DbCreationException : Exception { + constructor() : super() + constructor(message: String) : super(message) + constructor(message: String, cause: Throwable) : super(message, cause) + constructor(cause: Throwable) : super(cause) +} diff --git a/src/main/kotlin/utils/Helpers.kt b/src/main/kotlin/utils/Helpers.kt index 0bae37f..3823ab7 100644 --- a/src/main/kotlin/utils/Helpers.kt +++ b/src/main/kotlin/utils/Helpers.kt @@ -276,16 +276,10 @@ object Helpers { return takeValue } - fun isWindows(): Boolean { - val os = System.getProperty("os.name").lowercase(Locale.getDefault()) - // windows - return os.indexOf("win") >= 0 - } - fun openFileExplorer(path: Path) { try { val pathString = path.absolutePathString() - val command = if (isWindows()) { + val command = if (SystemTools.getOS() == OsWindows) { "Explorer.exe $pathString" } else { "open $pathString" diff --git a/src/main/kotlin/utils/Renderer.kt b/src/main/kotlin/utils/Renderer.kt new file mode 100644 index 0000000..ced6e42 --- /dev/null +++ b/src/main/kotlin/utils/Renderer.kt @@ -0,0 +1,48 @@ +package utils + +import org.jetbrains.skiko.GraphicsApi +import org.jetbrains.skiko.OS +import org.jetbrains.skiko.hostOs + +object Renderer { + +// private const val RENDER_API_KEY = "skiko.renderApi" + +// fun setRender() { +// when(SystemTools.getOS()) { +// OsWindows, OsLinux -> { +// System.setProperty(RENDER_API_KEY, "OPENGL") +// } +// OsMac -> { +// +// } +// } +// } + + internal fun parseRenderApi(text: String?): GraphicsApi { + return when (text) { + "SOFTWARE_COMPAT" -> GraphicsApi.SOFTWARE_COMPAT + "SOFTWARE_FAST", "DIRECT_SOFTWARE", "SOFTWARE" -> GraphicsApi.SOFTWARE_FAST + "OPENGL" -> GraphicsApi.OPENGL + "DIRECT3D" -> { + if (hostOs == OS.Windows) GraphicsApi.DIRECT3D + else throw UnsupportedOperationException("$hostOs does not support DirectX rendering API.") + } + "METAL" -> { + if (hostOs == OS.MacOS) GraphicsApi.METAL + else throw UnsupportedOperationException("$hostOs does not support Metal rendering API.") + } + else -> bestRenderApiForCurrentOS() + } + } + + private fun bestRenderApiForCurrentOS(): GraphicsApi { + return when (hostOs) { + OS.MacOS -> GraphicsApi.METAL + OS.Linux -> GraphicsApi.OPENGL + OS.Windows -> GraphicsApi.DIRECT3D + OS.JS, OS.Ios -> TODO("commonize me") + } + } + +} diff --git a/src/main/kotlin/utils/SystemTools.kt b/src/main/kotlin/utils/SystemTools.kt new file mode 100644 index 0000000..8189001 --- /dev/null +++ b/src/main/kotlin/utils/SystemTools.kt @@ -0,0 +1,22 @@ +package utils + +import java.util.* + +object SystemTools { + + fun getOS(): OS { + val os = System.getProperty("os.name").lowercase(Locale.getDefault()) + return when { + os.contains("window") -> OsWindows + os.contains("nix") || os.contains("nux") || os.contains("aix") -> OsLinux + os.contains("os x") || os.contains("mac") -> OsMac + else -> throw UnsupportedOperationException("Operating system $os is not supported") + } + } + +} + +sealed interface OS +object OsWindows : OS +object OsLinux : OS +object OsMac : OS