Răsfoiți Sursa

crash handler & keep screen on for android

Milan Jurkulak 1 an în urmă
părinte
comite
78956d1392

+ 2 - 2
composeApp/build.gradle.kts

@@ -125,8 +125,8 @@ kotlin {
                 implementation(libs.androidx.core.splashscreen)
                 // charts
                 implementation(libs.charts)
-                // emoji
-                implementation(libs.androidx.emoji2)
+                // permissions
+                implementation(libs.accompanist.permissions)
             }
         }
         // wasm dependencies

+ 19 - 3
composeApp/src/androidMain/AndroidManifest.xml

@@ -19,10 +19,26 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
         tools:ignore="ScopedStorage" />
     <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
-    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
-    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"
+        tools:ignore="SelectedPhotoAccess" />
+    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"
+        tools:ignore="SelectedPhotoAccess" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
+    <uses-permission android:name="android.permission.SET_ALARM" />
+    <uses-permission android:name="android.permission.USE_EXACT_ALARM" />
+
+    <queries>
+        <package android:name="com.example.store" />
+        <package android:name="com.example.services" />
+        <package android:name="com.sec.android.app.sbrowser" />
+        <intent>
+            <action android:name="android.intent.action.MAIN" />
+        </intent>
+    </queries>
 
     <application
+        android:name=".app.App"
         android:allowBackup="true"
         android:banner="@mipmap/ic_banner"
         android:icon="@mipmap/ic_launcher"
@@ -34,7 +50,7 @@
         android:usesCleartextTraffic="true">
 
         <activity
-            android:name=".MainActivity"
+            android:name=".activity.MainActivity"
             android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
             android:exported="true">
 

+ 44 - 28
composeApp/src/androidMain/kotlin/org/mjdev/desktop/MainActivity.kt → composeApp/src/androidMain/kotlin/org/mjdev/desktop/activity/MainActivity.kt

@@ -1,6 +1,8 @@
-package org.mjdev.desktop
+package org.mjdev.desktop.activity
 
+import android.graphics.Color
 import android.os.Bundle
+import android.view.WindowManager
 import androidx.activity.ComponentActivity
 import androidx.activity.SystemBarStyle
 import androidx.activity.compose.setContent
@@ -15,50 +17,63 @@ import androidx.compose.foundation.layout.windowInsetsPadding
 import androidx.compose.material.Scaffold
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
-import androidx.core.provider.FontRequest
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
 import androidx.core.view.WindowCompat
 import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
-import androidx.emoji2.text.EmojiCompat
-import androidx.emoji2.text.FontRequestEmojiCompatConfig
-import org.mjdev.desktop.context.DesktopContext.Companion.rememberDesktopContext
+import androidx.core.view.WindowInsetsControllerCompat
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import org.mjdev.desktop.MainView
+import org.mjdev.desktop.context.DesktopContext
 import org.mjdev.desktop.context.LocalDesktopContext
+import org.mjdev.desktop.helpers.permission.rememberPermissionManager
 import org.mjdev.desktop.helpers.theme.DesktopTheme
+import org.mjdev.desktop.helpers.WakeLockHelper
 
 class MainActivity : ComponentActivity() {
+    val wakeLockHelper by lazy { WakeLockHelper(this) }
+
+    @Suppress("OVERRIDE_DEPRECATION")
+    override fun onBackPressed() {
+        // Disable back button
+    }
+
+    @OptIn(ExperimentalPermissionsApi::class)
     override fun onCreate(savedInstanceState: Bundle?) {
-        initEmojiCompat()
+        wakeLockHelper.acquireWakeLock()
         installSplashScreen()
+        setKeepScreenOn()
         super.onCreate(savedInstanceState)
         enableEdgeToEdge(
-            statusBarStyle = SystemBarStyle.dark(
-                android.graphics.Color.TRANSPARENT,
+            statusBarStyle = SystemBarStyle.Companion.dark(
+                Color.TRANSPARENT,
             ),
-            navigationBarStyle = SystemBarStyle.dark(
-                android.graphics.Color.TRANSPARENT,
+            navigationBarStyle = SystemBarStyle.Companion.dark(
+                Color.TRANSPARENT,
             )
         )
         WindowCompat.getInsetsController(window, window.decorView).apply {
             hide(WindowInsetsCompat.Type.statusBars())
             hide(WindowInsetsCompat.Type.navigationBars())
-            systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+            systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
         }
         setContent {
+            val permissionManager = rememberPermissionManager()
             CompositionLocalProvider(
-                LocalDesktopContext provides rememberDesktopContext(baseContext)
+                LocalDesktopContext provides DesktopContext.Companion.rememberDesktopContext(
+                    baseContext
+                )
             ) {
                 DesktopTheme {
                     Scaffold(
-                        modifier = Modifier.fillMaxSize()
+                        modifier = Modifier.Companion.fillMaxSize()
                     ) { paddingValues ->
                         Box(
-                            modifier = Modifier
+                            modifier = Modifier.Companion
                                 .fillMaxSize()
                                 .padding(paddingValues)
                                 .consumeWindowInsets(paddingValues)
                                 .windowInsetsPadding(
-                                    WindowInsets.safeDrawing
+                                    WindowInsets.Companion.safeDrawing
                                 )
                         ) {
                             MainView()
@@ -69,16 +84,17 @@ class MainActivity : ComponentActivity() {
         }
     }
 
-    private fun initEmojiCompat() {
-        EmojiCompat.init(
-            FontRequestEmojiCompatConfig(
-                this, FontRequest(
-                    "com.google.android.gms.fonts",
-                    "com.google.android.gms",
-                    "Noto Color Emoji Compat",
-                    emptyList()
-                )
-            )
-        )
+    override fun onDestroy() {
+        super.onDestroy()
+        resetKeepScreenOn()
+        wakeLockHelper.releaseWakeLock()
+    }
+
+    private fun ComponentActivity.setKeepScreenOn() {
+        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+    }
+
+    private fun ComponentActivity.resetKeepScreenOn() {
+        window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
     }
-}
+}

+ 11 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/app/App.kt

@@ -0,0 +1,11 @@
+package org.mjdev.desktop.app
+
+import android.app.Application
+import org.mjdev.desktop.helpers.crash.CrashHandler
+
+class App : Application() {
+    override fun onCreate() {
+        super.onCreate()
+        CrashHandler.register(this, 200L)
+    }
+}

+ 32 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/WakeLockHelper.kt

@@ -0,0 +1,32 @@
+package org.mjdev.desktop.helpers
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.PowerManager
+import android.os.PowerManager.WakeLock
+
+class WakeLockHelper(
+    val context: Context
+) {
+    private var wakeLock: WakeLock? = null
+    private val powerManager
+        get() = context.getSystemService(Context.POWER_SERVICE) as? PowerManager
+
+    @SuppressLint("WakelockTimeout")
+    fun acquireWakeLock() {
+        wakeLock = powerManager?.newWakeLock(
+            PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
+            "MyApp:KeepScreenOnTag"
+        )
+        if (wakeLock?.isHeld != true) {
+            wakeLock?.acquire()
+        }
+    }
+
+    fun releaseWakeLock() {
+        if (wakeLock?.isHeld == true) {
+            wakeLock?.release()
+        }
+        wakeLock = null
+    }
+}

+ 58 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/crash/CrashHandler.kt

@@ -0,0 +1,58 @@
+package org.mjdev.desktop.helpers.crash
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import kotlin.system.exitProcess
+
+class CrashHandler(
+    val context: Context,
+    val restartMillis:Long = 1000
+) : Thread.UncaughtExceptionHandler {
+    private val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
+    val alarmMgr
+        get() = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+
+    @Suppress("KotlinUnreachableCode")
+    override fun uncaughtException(thread: Thread, ex: Throwable) {
+        Log.e(TAG, "Fatal: ", ex)
+        val intent = context.packageManager
+            .getLaunchIntentForPackage(context.packageName)
+        if (intent != null) {
+            intent.addFlags(
+                Intent.FLAG_ACTIVITY_CLEAR_TOP or
+                        Intent.FLAG_ACTIVITY_CLEAR_TASK or
+                        Intent.FLAG_ACTIVITY_NEW_TASK
+            )
+            val pendingIntent = PendingIntent.getActivity(
+                context,
+                0,
+                intent,
+                PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+            )
+            alarmMgr.set(
+                AlarmManager.RTC,
+                System.currentTimeMillis() + restartMillis,
+                pendingIntent
+            )
+        } else {
+            Log.e(TAG, "No intent to restart app.")
+        }
+        exitProcess(10)
+        defaultExceptionHandler?.uncaughtException(thread, ex)
+    }
+
+    companion object {
+        private val TAG = CrashHandler::class.simpleName
+
+        fun register(
+            context: Context,
+            restartMillis:Long = 1000L
+        ) {
+            Thread.setDefaultUncaughtExceptionHandler(CrashHandler(context, restartMillis))
+            Log.d(TAG, "Crash handler registered.")
+        }
+    }
+}

+ 90 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/permission/PermissonManager.kt

@@ -0,0 +1,90 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2025.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+@file:Suppress("MemberVisibilityCanBePrivate")
+
+package org.mjdev.desktop.helpers.permission
+
+import android.annotation.SuppressLint
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.MultiplePermissionsState
+import com.google.accompanist.permissions.rememberMultiplePermissionsState
+
+@Composable
+fun manifestPermissions(): List<String> {
+    val context = LocalContext.current
+    return rememberSaveable(context) {
+        try {
+            val packageManager = context.packageManager
+            arrayListOf<String>().apply {
+                val pkgInfo: PackageInfo = packageManager.getPackageInfo(
+                    context.packageName,
+                    PackageManager.GET_PERMISSIONS
+                )
+                pkgInfo.requestedPermissions?.forEach { p -> add(p) }
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+            emptyList()
+        }
+    }
+}
+
+@OptIn(ExperimentalPermissionsApi::class)
+@Suppress("LocalVariableName")
+@SuppressLint("ComposableNaming")
+@Composable
+fun rememberPermissionManager(
+    permissions: List<String> = manifestPermissions(),
+    onNeedToGrant: (MultiplePermissionsState.(permissions: List<String>) -> Unit)? = null
+) {
+    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
+        var _ps: MultiplePermissionsState? = null
+        val lifecycleOwner = LocalLifecycleOwner.current
+        val onPermissionsResult: (Map<String, Boolean>) -> Unit = { map ->
+            map.filter { p ->
+                !p.value
+            }.filter { p ->
+                permissions.contains(p.key)
+            }.also { ps ->
+                _ps?.also { mps ->
+                    onNeedToGrant?.invoke(mps, ps.map { p -> p.key })
+                }
+            }
+        }
+        val permissionsState = rememberMultiplePermissionsState(
+            permissions = permissions,
+            onPermissionsResult = onPermissionsResult
+        )
+        _ps = permissionsState
+        DisposableEffect(
+            key1 = lifecycleOwner,
+            effect = {
+                val observer = LifecycleEventObserver { _, event ->
+                    if (event == Lifecycle.Event.ON_RESUME) {
+                        permissionsState.launchMultiplePermissionRequest()
+                    }
+                }
+                lifecycleOwner.lifecycle.addObserver(observer)
+                onDispose {
+                    lifecycleOwner.lifecycle.removeObserver(observer)
+                }
+            }
+        )
+    }
+}

+ 9 - 13
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/apps/AppsManager.kt

@@ -37,21 +37,25 @@ class AppsManager(
         (context as? DesktopContext)?.context
     }
     private val userManger by lazy {
-        androidContext?.getSystemService(Context.USER_SERVICE) as UserManager
+        runCatching {
+            androidContext?.getSystemService(Context.USER_SERVICE) as UserManager
+        }.getOrNull()
     }
     private val launcherApps by lazy {
-        androidContext?.getSystemService(LauncherApps::class.java)
+        runCatching {
+            androidContext?.getSystemService(LauncherApps::class.java)
+        }.getOrNull()
     }
 
-    val userHandles: List<UserHandle>
-        get() = userManger.getUserProfiles()
+    val userHandles: List<UserHandle>?
+        get() = userManger?.getUserProfiles()
 
     override val currentLocale: ILocale
         get() = Locale.getDefault().toILocale()
 
     private val allAppsState = mutableStateListFlow {
         mutableListOf<App>().apply {
-            userHandles.forEach { handle ->
+            userHandles?.forEach { handle ->
                 launcherApps?.getActivityList(null, handle)?.map { aInfo ->
                     App(androidContext, aInfo)
                 }?.also { apps ->
@@ -66,14 +70,6 @@ class AppsManager(
             allAppsState.firstOrNull() ?: emptyList()
         }
 
-    val backgrounds by lazy {
-        Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
-            .absolutePath
-            .toPath()
-            .filesOnly
-            .sortedByName()
-    }
-
     override val categories
         get() = allApps.asSequence().flatMap { app ->
             app.categories

+ 1 - 1
composeApp/src/commonMain/kotlin/org/mjdev/desktop/data/DesktopConfig.kt

@@ -55,7 +55,7 @@ class DesktopConfig(
 
         private val DEFAULT = DesktopConfig {
 //            addBackground(ProviderSmug())
-//            addBackground(ProviderErzvo())
+            addBackground(ProviderErzvo())
         }
 
         // todo load from user settings

+ 1 - 0
composeApp/src/commonMain/kotlin/org/mjdev/desktop/providers/background/ProviderSmug.kt

@@ -15,6 +15,7 @@ import org.mjdev.desktop.helpers.compose.ImagesProvider
 class ProviderSmug(
     private val loadCount: Int = 10
 ) : ImagesProvider {
+    // error 521 HTTP
     private val list = listOf(
         "https://wallpapersmug.com/u/c78d6a/rainy-night-of-city-dark.jpeg",
         "https://wallpapersmug.com/u/db1982/vegeta-artwork.jpg",

+ 13 - 11
gradle/libs.versions.toml

@@ -15,7 +15,7 @@ android-min-sdk = "24"
 android-target-sdk = "35"
 # lib versions
 gradle = "8.9.1"
-gradle-kotlinter = "4.3.0"
+#gradle-kotlinter = "4.3.0"
 androidx-activity-compose = "1.10.1"
 androidx-lifecycle = "2.8.4"
 androidx-ui-tooling = "1.7.8"
@@ -28,28 +28,29 @@ gettext-lib = "1.11.0"
 google-gson = "2.13.0"
 charts = "Beta-0.0.5"
 human-readable = "1.11.0"
-i18n4k-core = "0.9.0"
+#i18n4k-core = "0.9.0"
 ini4j = "0.5.4"
 jetlime = "3.0.1"
 kotlin = "2.1.20"
 kotlinx-coroutines = "1.10.2"
 kotlinx-date-time = "0.6.2"
-logback = "1.5.7"
+#logback = "1.5.7"
 okhttp3 = "4.12.0"
 okio = "3.11.0"
 qrose = "1.0.1"
 tts = "3.0.0-beta.1"
 ychat = "1.4.1"
-generativeai = "0.9.0"
+#generativeai = "0.9.0"
 alert-kmp = "2.0.0"
-emoji2 = "1.5.0"
+#emoji2 = "1.5.0"
+accompanist-permissions = "0.37.2"
 
 [libraries]
 # gradle
-gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
-gradle-api = { module = "com.android.tools.build:gradle-api", version.ref = "gradle" }
-gradle-kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
-gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version.ref = "gradle-kotlinter" }
+#gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
+#gradle-api = { module = "com.android.tools.build:gradle-api", version.ref = "gradle" }
+#gradle-kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+#gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version.ref = "gradle-kotlinter" }
 # androidx
 androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
 androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
@@ -91,12 +92,13 @@ ini4j = { module = "org.ini4j:ini4j", version.ref = "ini4j" }
 tts = { module = "nl.marc-apps:tts", version.ref = "tts" }
 # ai
 google-generative-ai = { module = "dev.shreyaspatil.generativeai:generativeai-google-jvm", version.ref = "generative-ai" }
-generativeai = { group = "com.google.ai.client.generativeai", name = "generativeai", version.ref = "generativeai" }
+#generativeai = { group = "com.google.ai.client.generativeai", name = "generativeai", version.ref = "generativeai" }
 # strings
 human-readable = { module = "nl.jacobras:Human-Readable", version.ref = "human-readable" }
 # alerts
 alert-kmp = { module = "io.github.khubaibkhan4:alert-kmp", version.ref="alert-kmp" }
-androidx-emoji2 = { group = "androidx.emoji2", name = "emoji2", version.ref = "emoji2" }
+#androidx-emoji2 = { group = "androidx.emoji2", name = "emoji2", version.ref = "emoji2" }
+accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist-permissions" }
 
 [plugins]
 android-application = { id = "com.android.application", version.ref = "gradle" }