├── .github ├── dependabot.yml └── workflows │ ├── publish-dokka-doc.yml │ └── publish-on-maven-central.yml ├── .gitignore ├── LICENSE ├── README.MD ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── platformtools ├── appmanager │ ├── build.gradle.kts │ └── src │ │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── kdroidfilter │ │ │ │ └── platformtools │ │ │ │ └── appmanager │ │ │ │ ├── AppInstaller.android.kt │ │ │ │ ├── AppRestarter.android.kt │ │ │ │ ├── InstallResultReceiver.kt │ │ │ │ ├── InstallationManager.kt │ │ │ │ └── restartappmanager │ │ │ │ ├── ProcessRestarter.kt │ │ │ │ ├── RestartManagerActivity.kt │ │ │ │ └── RestartManagerService.kt │ │ └── res │ │ │ └── xml │ │ │ └── file_paths.xml │ │ ├── commonMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ └── appmanager │ │ │ ├── AppInstaller.kt │ │ │ ├── AppRestarter.kt │ │ │ └── AppVersionChecker.kt │ │ └── jvmMain │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── kdroidfilter │ │ └── platformtools │ │ └── appmanager │ │ ├── AppInstaller.jvm.kt │ │ ├── AppRestarter.jvm.kt │ │ └── WindowsPrivilegeHelper.kt ├── core │ ├── build.gradle.kts │ └── src │ │ ├── androidJvmMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ ├── CacheProvider.kt │ │ │ └── VersionProvider.kt │ │ ├── androidMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ ├── CacheProvider.android.kt │ │ │ ├── OsProvider.android.kt │ │ │ ├── PlatformProvider.android.kt │ │ │ └── VersionProvider.android.kt │ │ ├── commonMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ ├── OsProvider.kt │ │ │ └── PlatformProvider.kt │ │ ├── iosMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ ├── OsProvider.ios.kt │ │ │ └── PlatformProvider.ios.kt │ │ ├── jsMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ ├── OsProvider.js.kt │ │ │ └── PlatformProvider.js.kt │ │ ├── jvmMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ ├── CacheProvider.jvm.kt │ │ │ ├── OsProvider.jvm.kt │ │ │ ├── PlatformProvider.jvm.kt │ │ │ └── VersionProvider.jvm.kt │ │ ├── linuxX64Main │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ ├── OsProvider.linuxX64.kt │ │ │ └── PlatformProvider.linuxX64.kt │ │ ├── macosMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ ├── OsProvider.macos.kt │ │ │ └── PlatformProvider.macos.kt │ │ ├── mingwX64Main │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ ├── OsProvider.mingwX64.kt │ │ │ └── PlatformProvider.mingwX64.kt │ │ └── wasmJsMain │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── kdroidfilter │ │ └── platformtools │ │ ├── OsProvider.wasmJs.kt │ │ └── PlatformProvider.wasmJs.kt ├── darkmodedetector │ ├── build.gradle.kts │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ └── darkmodedetector │ │ │ └── IsSystemInDarkMode.android.kt │ │ ├── commonMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ └── darkmodedetector │ │ │ └── IsSystemInDarkMode.kt │ │ ├── jsMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ └── darkmodedetector │ │ │ └── IsSystemInDarkMode.js.kt │ │ ├── jvmMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ └── darkmodedetector │ │ │ ├── IsSystemInDarkMode.jvm.kt │ │ │ ├── linux │ │ │ └── LinuxThemeDetector.kt │ │ │ ├── mac │ │ │ └── MacOSThemeDetector.kt │ │ │ └── windows │ │ │ ├── Dwmapi.kt │ │ │ └── WindowsThemeDetector.kt │ │ ├── nativeMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ └── darkmodedetector │ │ │ └── IsSystemInDarkMode.native.kt │ │ └── wasmJsMain │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── kdroidfilter │ │ └── platformtools │ │ └── darkmodedetector │ │ └── IsSystemInDarkMode.wasmJs.kt ├── permissionhandler │ ├── build.gradle.kts │ └── src │ │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── kdroidfilter │ │ │ │ └── platformtools │ │ │ │ └── permissionhandler │ │ │ │ ├── BackgroundLocation.kt │ │ │ │ ├── Bluetooth.kt │ │ │ │ ├── Camera.kt │ │ │ │ ├── Installation.android.kt │ │ │ │ ├── Location.kt │ │ │ │ ├── Notification.android.kt │ │ │ │ ├── Overlay.kt │ │ │ │ ├── ReadContacts.kt │ │ │ │ ├── ReadExternalStorage.kt │ │ │ │ ├── RecordAudio.kt │ │ │ │ ├── WriteContacts.kt │ │ │ │ └── manager │ │ │ │ ├── PermissionActivity.kt │ │ │ │ └── PermissionCallbackManager.kt │ │ └── res │ │ │ └── values │ │ │ └── styles.xml │ │ ├── appleMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ └── permissionhandler │ │ │ └── Installation.apple.kt │ │ ├── commonMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ └── permissionhandler │ │ │ ├── Installation.kt │ │ │ └── Notification.kt │ │ ├── jsMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ └── permissionhandler │ │ │ ├── Installation.js.kt │ │ │ └── Notification.js.kt │ │ ├── jvmMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ └── permissionhandler │ │ │ ├── Installation.jvm.kt │ │ │ └── Notification.jvm.kt │ │ ├── nativeMain │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── kdroidfilter │ │ │ └── platformtools │ │ │ └── permissionhandler │ │ │ └── Notification.native.kt │ │ └── wasmJsMain │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── kdroidfilter │ │ └── platformtools │ │ └── permissionhandler │ │ ├── Installation.wasmJs.kt │ │ └── Notification.wasmJs.kt └── releasefetcher │ ├── build.gradle.kts │ └── src │ └── commonMain │ └── kotlin │ └── io │ └── github │ └── kdroidfilter │ └── platformtools │ └── releasefetcher │ ├── config │ └── Client.kt │ ├── downloader │ ├── Downloader.kt │ └── ReleaseFetcherConfig.kt │ └── github │ ├── GitHubReleaseFetcher.kt │ └── model │ ├── Asset.kt │ ├── Author.kt │ ├── Release.kt │ └── Uploader.kt ├── sample ├── composeApp │ ├── build.gradle.kts │ └── src │ │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── sample │ │ │ └── app │ │ │ ├── main.kt │ │ │ └── permissions │ │ │ ├── BackgroundLocationPermissionSample.kt │ │ │ ├── BluetoothPermissionSample.kt │ │ │ ├── CameraPermissionSample.kt │ │ │ ├── ContactsPermissionSample.kt │ │ │ ├── InstallPermissionSample.kt │ │ │ ├── LocationPermissionSample.kt │ │ │ ├── OverlayPermissionSample.kt │ │ │ ├── ReadExternalStoragePermissionSample.kt │ │ │ └── RecordAudioPermissionSample.kt │ │ ├── commonMain │ │ └── kotlin │ │ │ └── sample │ │ │ └── app │ │ │ ├── App.kt │ │ │ ├── AppManagerDemo.kt │ │ │ ├── CoreDemo.kt │ │ │ ├── PermissionHandlerDemo.kt │ │ │ ├── ReleaseFetcherDemo.kt │ │ │ └── permissionHandler │ │ │ └── NotificationPermissionSample.kt │ │ └── jvmMain │ │ └── kotlin │ │ └── sample │ │ └── app │ │ └── main.kt └── terminalApp │ ├── build.gradle.kts │ └── src │ └── commonMain │ └── kotlin │ └── main.kt └── settings.gradle.kts /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/publish-dokka-doc.yml: -------------------------------------------------------------------------------- 1 | name: "Automatic deployment of Dokka doc" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | # Prevents multiple simultaneous deployments 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | # 1) Build the Dokka documentation 21 | build: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Set up Java (Temurin 17) 29 | uses: actions/setup-java@v4 30 | with: 31 | distribution: temurin 32 | java-version: 17 33 | 34 | # Generate Dokka documentation 35 | - name: Generate Dokka documentation 36 | run: | 37 | ./gradlew dokkaHtmlMultiModule 38 | 39 | # Prepare files for deployment 40 | - name: Prepare files for deployment 41 | run: | 42 | mkdir -p build/final 43 | # Copy the documentation to build/final (site root) 44 | cp -r platformtools/build/dokka/htmlMultiModule/* build/final 45 | 46 | # Upload to the "pages" artifact to make it available for the next job 47 | - name: Upload artifact for GitHub Pages 48 | uses: actions/upload-pages-artifact@v3 49 | with: 50 | path: build/final 51 | 52 | # 2) Deploy to GitHub Pages 53 | deploy: 54 | needs: build 55 | runs-on: ubuntu-latest 56 | environment: 57 | name: github-pages 58 | # The final URL will be output in page_url 59 | url: ${{ steps.deployment.outputs.page_url }} 60 | steps: 61 | - name: Deploy to GitHub Pages 62 | id: deployment 63 | uses: actions/deploy-pages@v4 64 | with: 65 | path: build/final 66 | -------------------------------------------------------------------------------- /.github/workflows/publish-on-maven-central.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Maven Central 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up JDK 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | 22 | - name: Set up Publish to Maven Central 23 | run: ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache 24 | env: 25 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVENCENTRALUSERNAME }} 26 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVENCENTRALPASSWORD }} 27 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNINGINMEMORYKEY }} 28 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNINGKEYID }} 29 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNINGPASSWORD }} 30 | 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.iml 3 | .gradle 4 | .idea 5 | .kotlin 6 | .DS_Store 7 | build 8 | */build 9 | captures 10 | .externalNativeBuild 11 | .cxx 12 | local.properties 13 | xcuserdata/ 14 | Pods/ 15 | *.jks 16 | *.gpg 17 | *yarn.lock 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Elie G. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.multiplatform).apply(false) 3 | alias(libs.plugins.android.library).apply(false) 4 | alias(libs.plugins.android.application).apply(false) 5 | alias(libs.plugins.kotlinx.serialization).apply(false) 6 | alias(libs.plugins.dokka).apply(false) 7 | alias(libs.plugins.vannitktech.maven.publish).apply(false) 8 | } 9 | 10 | subprojects { 11 | apply(plugin = "org.jetbrains.dokka") 12 | } 13 | 14 | extra["libVersion"] = "0.2.9" 15 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx4G 3 | org.gradle.caching=true 4 | org.gradle.configuration-cache=true 5 | org.gradle.daemon=true 6 | org.gradle.parallel=true 7 | 8 | #Kotlin 9 | kotlin.code.style=official 10 | kotlin.daemon.jvmargs=-Xmx4G 11 | 12 | #Android 13 | android.useAndroidX=true 14 | android.nonTransitiveRClass=true 15 | 16 | #Compose 17 | org.jetbrains.compose.experimental.jscanvas.enabled=true 18 | org.jetbrains.compose.experimental.macos.enabled=true 19 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | androidcontextprovider = "1.0.1" 4 | jfa = "1.2.0" 5 | jna = "5.17.0" 6 | kermit = "2.0.5" 7 | kotlin = "2.1.21" 8 | agp = "8.6.1" 9 | compose = "1.8.1" 10 | androidx-activityCompose = "1.10.1" 11 | kotlinxBrowserWasmJs = "0.3" 12 | kotlinxCoroutinesCore = "1.10.2" 13 | core = "1.15.0" 14 | activityKtx = "1.10.1" 15 | ktor = "3.1.3" 16 | navigationCompose = "2.8.0-alpha12" 17 | slf4jSimple = "2.0.17" 18 | 19 | 20 | [libraries] 21 | 22 | androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" } 23 | androidx-core = { group = "androidx.core", name = "core", version.ref = "core" } 24 | androidcontextprovider = { module = "io.github.kdroidfilter:androidcontextprovider", version.ref = "androidcontextprovider" } 25 | androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 26 | jfa = { module = "de.jangassen:jfa", version.ref = "jfa" } 27 | jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } 28 | jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" } 29 | kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } 30 | kotlinx-browser-wasm-js = { module = "org.jetbrains.kotlinx:kotlinx-browser-wasm-js", version.ref = "kotlinxBrowserWasmJs" } 31 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } 32 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.8.1" } 33 | ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } 34 | ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } 35 | ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } 36 | ktor-client-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktor" } 37 | ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } 38 | navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" } 39 | semver = { module = "io.github.z4kn4fein:semver", version = "2.0.0" } 40 | slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4jSimple" } 41 | 42 | [plugins] 43 | 44 | multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 45 | android-library = { id = "com.android.library", version.ref = "agp" } 46 | compose = { id = "org.jetbrains.compose", version.ref = "compose" } 47 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 48 | android-application = { id = "com.android.application", version.ref = "agp" } 49 | kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 50 | vannitktech-maven-publish = {id = "com.vanniktech.maven.publish", version = "0.32.0"} 51 | dokka = { id = "org.jetbrains.dokka" , version = "2.0.0"} -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdroidFilter/Platform-Tools/d01018a2507c6def80ca660cb699b38fa06377b2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /platformtools/appmanager/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | import org.jetbrains.dokka.gradle.DokkaTask 3 | 4 | plugins { 5 | alias(libs.plugins.multiplatform) 6 | alias(libs.plugins.android.library) 7 | alias(libs.plugins.vannitktech.maven.publish) 8 | } 9 | val libVersion : String by rootProject.extra 10 | 11 | group = "io.github.kdroidfilter.platformtools.appmanager" 12 | version = libVersion 13 | 14 | kotlin { 15 | jvmToolchain(17) 16 | 17 | androidTarget { publishLibraryVariants("release") } 18 | jvm() 19 | 20 | 21 | sourceSets { 22 | commonMain.dependencies { 23 | implementation(project(":platformtools:core")) 24 | implementation(libs.kotlinx.coroutines.core) 25 | implementation("co.touchlab:kermit:2.0.5") //Add latest version 26 | } 27 | 28 | commonTest.dependencies { 29 | implementation(kotlin("test")) 30 | } 31 | 32 | jvmMain.dependencies { 33 | implementation(libs.jna) 34 | implementation(libs.jna.platform) 35 | } 36 | 37 | androidMain.dependencies { 38 | implementation(libs.androidx.core) 39 | implementation(libs.androidx.activity.ktx) 40 | implementation(libs.androidcontextprovider) 41 | } 42 | 43 | 44 | } 45 | 46 | //https://kotlinlang.org/docs/native-objc-interop.html#export-of-kdoc-comments-to-generated-objective-c-headers 47 | targets.withType { 48 | compilations["main"].compileTaskProvider.configure { 49 | compilerOptions { 50 | freeCompilerArgs.add("-Xexport-kdoc") 51 | } 52 | } 53 | } 54 | 55 | } 56 | 57 | android { 58 | namespace = "io.github.kdroidfilter.platformtools.appmanager" 59 | compileSdk = 35 60 | 61 | defaultConfig { 62 | minSdk = 21 63 | } 64 | } 65 | 66 | mavenPublishing { 67 | coordinates( 68 | groupId = "io.github.kdroidfilter", 69 | artifactId = "platformtools.appmanager", 70 | version = version.toString() 71 | ) 72 | 73 | pom { 74 | name.set("PlatformTools AppManager") 75 | description.set("Application manager module for PlatformTools, a Kotlin Library to install and update Desktop and Android Applications") 76 | inceptionYear.set("2025") 77 | url.set("https://github.com/kdroidFilter/") 78 | 79 | licenses { 80 | license { 81 | name.set("MIT License") 82 | url.set("https://opensource.org/licenses/MIT") 83 | } 84 | } 85 | 86 | developers { 87 | developer { 88 | id.set("kdroidfilter") 89 | name.set("Elyahou Hadass") 90 | email.set("elyahou.hadass@gmail.com") 91 | } 92 | } 93 | 94 | scm { 95 | connection.set("scm:git:git://github.com/kdroidFilter/platformtools.git") 96 | developerConnection.set("scm:git:ssh://git@github.com:kdroidFilter/platformtools.git") 97 | url.set("https://github.com/kdroidFilter/platformtools") 98 | } 99 | } 100 | 101 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 102 | 103 | signAllPublications() 104 | } 105 | 106 | tasks.withType().configureEach { 107 | moduleName.set("Platforms Tools") 108 | offlineMode.set(true) 109 | } -------------------------------------------------------------------------------- /platformtools/appmanager/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 18 | 19 | 24 | 25 | 30 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /platformtools/appmanager/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/appmanager/AppRestarter.android.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.appmanager 2 | 3 | import com.kdroid.androidcontextprovider.ContextProvider 4 | import io.github.kdroidfilter.platformtools.appmanager.restartappmanager.ProcessRestarter 5 | 6 | actual fun restartApplication() { 7 | val context = ContextProvider.getContext() 8 | ProcessRestarter.triggerRebirth(context); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /platformtools/appmanager/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/appmanager/InstallResultReceiver.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.appmanager 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | import android.content.pm.PackageInstaller 8 | 9 | /** 10 | * Receives and processes the result of an app installation via a broadcast. 11 | * 12 | * This class listens for broadcast intents that contain the result of a package 13 | * installation initiated through the Android PackageInstaller API. It extracts 14 | * the status and corresponding message, logs the information, and notifies 15 | * `InstallationManager` to invoke the corresponding callback. 16 | * 17 | * Inherits from `BroadcastReceiver` and overrides the `onReceive` method to handle 18 | * the incoming intents with installation results. 19 | */ 20 | class InstallResultReceiver : BroadcastReceiver() { 21 | 22 | override fun onReceive(context: Context, intent: Intent) { 23 | val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) 24 | val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) 25 | val success = (status == PackageInstaller.STATUS_SUCCESS) 26 | 27 | Log.i("InstallResultReceiver", "Installation completed. Success: $success, message: $message") 28 | 29 | // Notify the installation manager 30 | InstallationManager.notifyInstallResult(success, message) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /platformtools/appmanager/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/appmanager/InstallationManager.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.appmanager 2 | 3 | /** 4 | * Manages installation callbacks and notifies the result of installation processes. 5 | * 6 | * This singleton object is designed to handle app installation result callbacks. 7 | * It registers a callback function that can be invoked once an installation process 8 | * completes, and ensures the callback is reset post-invocation to avoid redundant calls. 9 | */ 10 | object InstallationManager { 11 | private var installCallback: ((Boolean, String?) -> Unit)? = null 12 | 13 | /** 14 | * Registers a callback for installation results. 15 | * @param callback Function to be called with the installation result. 16 | */ 17 | fun setInstallCallback(callback: (Boolean, String?) -> Unit) { 18 | installCallback = callback 19 | } 20 | 21 | /** 22 | * Notifies the registered callback with the installation result. 23 | * @param success Indicates whether the installation was successful. 24 | * @param message Message associated with the result. 25 | */ 26 | internal fun notifyInstallResult(success: Boolean, message: String?) { 27 | installCallback?.invoke(success, message) 28 | // Reset the callback after use to avoid multiple calls 29 | installCallback = null 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /platformtools/appmanager/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/appmanager/restartappmanager/ProcessRestarter.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is inspired by the ProcessPhoenix project by Jake Wharton. 3 | * Source: https://github.com/JakeWharton/ProcessPhoenix/blob/trunk/process-phoenix/src/main/java/com/jakewharton/processphoenix/ProcessPhoenix.java 4 | */ 5 | 6 | package io.github.kdroidfilter.platformtools.appmanager.restartappmanager 7 | 8 | import android.app.Activity 9 | import android.app.ActivityManager 10 | import android.app.Service 11 | import android.content.Context 12 | import android.content.Intent 13 | import android.content.pm.PackageManager 14 | import android.os.Process 15 | import io.github.kdroidfilter.platformtools.appmanager.restartappmanager.ProcessRestarter.triggerRebirth 16 | 17 | /** 18 | * ProcessRestarter facilitates restarting your application process. This should only be used for 19 | * things like fundamental state changes in your debug builds (e.g., changing from staging to 20 | * production). 21 | * 22 | * Trigger process recreation by calling [triggerRebirth] with a [Context] instance. 23 | */ 24 | object ProcessRestarter { 25 | 26 | internal const val KEY_RESTART_INTENT = "process_restart_intent" 27 | internal const val KEY_RESTART_INTENTS = "process_restart_intents" 28 | internal const val KEY_MAIN_PROCESS_PID = "process_main_process_pid" 29 | 30 | /** 31 | * Call to restart the application process using the default activity as an intent. 32 | * 33 | * Behavior of the current process after invoking this method is undefined. 34 | */ 35 | fun triggerRebirth(context: Context) { 36 | triggerRebirth(context, getRestartIntent(context)) 37 | } 38 | 39 | /** 40 | * Call to restart the application process using the provided Activity Class. 41 | * 42 | * Behavior of the current process after invoking this method is undefined. 43 | */ 44 | fun triggerRebirth(context: Context, targetClass: Class) { 45 | val nextIntent = Intent(context, targetClass) 46 | triggerRebirth(context, nextIntent) 47 | } 48 | 49 | /** 50 | * Call to restart the application process using the specified intents. 51 | * 52 | * Behavior of the current process after invoking this method is undefined. 53 | */ 54 | fun triggerRebirth(context: Context, vararg nextIntents: Intent) { 55 | require(nextIntents.isNotEmpty()) { "Intents cannot be empty" } 56 | 57 | nextIntents[0].addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 58 | 59 | val intent = Intent(context, RestartManagerActivity::class.java).apply { 60 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 61 | putParcelableArrayListExtra(KEY_RESTART_INTENTS, ArrayList(nextIntents.toList())) 62 | putExtra(KEY_MAIN_PROCESS_PID, Process.myPid()) 63 | } 64 | 65 | context.startActivity(intent) 66 | } 67 | 68 | /** 69 | * Call to restart the application process using the provided Service Class. 70 | * 71 | * Behavior of the current process after invoking this method is undefined. 72 | */ 73 | fun triggerServiceRebirth(context: Context, targetClass: Class) { 74 | val nextIntent = Intent(context, targetClass) 75 | triggerServiceRebirth(context, nextIntent) 76 | } 77 | 78 | /** 79 | * Call to restart the application process using the specified Service intent. 80 | * 81 | * Behavior of the current process after invoking this method is undefined. 82 | */ 83 | fun triggerServiceRebirth(context: Context, nextIntent: Intent) { 84 | val intent = Intent(context, RestartManagerService::class.java).apply { 85 | putExtra(KEY_RESTART_INTENT, nextIntent) 86 | putExtra(KEY_MAIN_PROCESS_PID, Process.myPid()) 87 | } 88 | 89 | context.startService(intent) 90 | } 91 | 92 | private fun getRestartIntent(context: Context): Intent { 93 | val packageName = context.packageName 94 | val packageManager = context.packageManager 95 | 96 | val defaultIntent = if (packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) 97 | ) { 98 | packageManager.getLeanbackLaunchIntentForPackage(packageName) 99 | } else { 100 | packageManager.getLaunchIntentForPackage(packageName) 101 | } 102 | 103 | return defaultIntent ?: throw IllegalStateException( 104 | "Unable to determine default activity for $packageName. " + 105 | "Does an activity specify the DEFAULT category in its intent filter?" 106 | ) 107 | } 108 | 109 | /** 110 | * Checks if the current process is a temporary Process. 111 | * This can be used to avoid initialization of unused resources or to prevent running code that 112 | * is not multi-process ready. 113 | * 114 | * @return true if the current process is a temporary Process 115 | */ 116 | fun isTemporaryProcess(context: Context): Boolean { 117 | val currentPid = Process.myPid() 118 | val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 119 | val runningProcesses = manager.runningAppProcesses 120 | 121 | return runningProcesses?.any { processInfo -> 122 | processInfo.pid == currentPid && processInfo.processName.endsWith(":phoenix") 123 | } ?: false 124 | } 125 | } 126 | 127 | -------------------------------------------------------------------------------- /platformtools/appmanager/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/appmanager/restartappmanager/RestartManagerActivity.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is inspired by the ProcessPhoenix project by Jake Wharton. 3 | * Source: https://github.com/JakeWharton/ProcessPhoenix/blob/trunk/process-phoenix/src/main/java/com/jakewharton/processphoenix/PhoenixActivity.java 4 | */ 5 | 6 | package io.github.kdroidfilter.platformtools.appmanager.restartappmanager 7 | 8 | import android.app.Activity 9 | import android.content.Intent 10 | import android.os.Build 11 | import android.os.Bundle 12 | import android.os.Process 13 | import android.os.StrictMode 14 | 15 | class RestartManagerActivity : Activity() { 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | 20 | // Kill original main process 21 | val mainProcessPid = intent.getIntExtra(ProcessRestarter.KEY_MAIN_PROCESS_PID, -1) 22 | if (mainProcessPid != -1) { 23 | Process.killProcess(mainProcessPid) 24 | } 25 | 26 | val intents = intent.getParcelableArrayListExtra(ProcessRestarter.KEY_RESTART_INTENTS)?.toTypedArray() 27 | if (intents != null) { 28 | if (Build.VERSION.SDK_INT > 31) { 29 | // Disable strict mode complaining about out-of-process intents. Normally you save and restore 30 | // the original policy, but this process will die almost immediately after the offending call. 31 | StrictMode.setVmPolicy( 32 | StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy()) 33 | .permitUnsafeIntentLaunch() 34 | .build() 35 | ) 36 | } 37 | 38 | startActivities(intents) 39 | } 40 | 41 | finish() 42 | Runtime.getRuntime().exit(0) // Kill kill kill! 43 | } 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /platformtools/appmanager/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/appmanager/restartappmanager/RestartManagerService.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is inspired by the ProcessPhoenix project by Jake Wharton. 3 | * Source: https://github.com/JakeWharton/ProcessPhoenix/blob/trunk/process-phoenix/src/main/java/com/jakewharton/processphoenix/PhoenixService.java 4 | */ 5 | 6 | package io.github.kdroidfilter.platformtools.appmanager.restartappmanager 7 | 8 | import android.content.Context 9 | import android.content.Intent 10 | import android.os.Build 11 | import android.os.Process 12 | import android.os.StrictMode 13 | import kotlinx.coroutines.CoroutineScope 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.Job 16 | import kotlinx.coroutines.withContext 17 | import kotlin.coroutines.CoroutineContext 18 | 19 | class RestartManagerService : CoroutineScope { 20 | 21 | private val job = Job() 22 | override val coroutineContext: CoroutineContext 23 | get() = Dispatchers.IO + job 24 | 25 | suspend fun handleIntent(context: Context, intent: Intent?) { 26 | if (intent == null) { 27 | return 28 | } 29 | 30 | withContext(Dispatchers.IO) { 31 | val mainProcessPid = intent.getIntExtra(ProcessRestarter.KEY_MAIN_PROCESS_PID, -1) 32 | if (mainProcessPid != -1) { 33 | Process.killProcess(mainProcessPid) // Kill original main process 34 | } 35 | 36 | val nextIntent: Intent? = if (Build.VERSION.SDK_INT >= 33) { 37 | intent.getParcelableExtra(ProcessRestarter.KEY_RESTART_INTENT, Intent::class.java) 38 | } else { 39 | @Suppress("DEPRECATION") 40 | intent.getParcelableExtra(ProcessRestarter.KEY_RESTART_INTENT) 41 | } 42 | 43 | if (nextIntent != null) { 44 | if (Build.VERSION.SDK_INT > 31) { 45 | // Disable strict mode for out-of-process intents 46 | StrictMode.setVmPolicy( 47 | StrictMode.VmPolicy.Builder(StrictMode.getVmPolicy()) 48 | .permitUnsafeIntentLaunch() 49 | .build() 50 | ) 51 | } 52 | 53 | if (Build.VERSION.SDK_INT >= 26) { 54 | context.startForegroundService(nextIntent) 55 | } else { 56 | context.startService(nextIntent) 57 | } 58 | } 59 | 60 | Runtime.getRuntime().exit(0) // Terminate process 61 | } 62 | } 63 | 64 | fun stop() { 65 | job.cancel() 66 | } 67 | } 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /platformtools/appmanager/src/androidMain/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /platformtools/appmanager/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/appmanager/AppInstaller.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.appmanager 2 | 3 | import java.io.File 4 | 5 | interface AppInstaller { 6 | 7 | /** 8 | * Installs an application from the specified file. 9 | * 10 | * This method handles the installation of an APK file. It requires permission to install 11 | * applications from unknown sources, which should be granted before invoking this method. 12 | * The installation result is communicated via the provided callback. 13 | * 14 | * @param appFile The APK file to be installed. 15 | * @param onResult A callback invoked with the installation result. The callback 16 | * parameters are: 17 | * - [success]: Indicates whether the installation succeeded. 18 | * - [message]: An optional message providing additional context, typically in case of an error. 19 | */ 20 | suspend fun installApp(appFile: File, onResult: (success: Boolean, message: String?) -> Unit) 21 | 22 | /** 23 | * Uninstalls an application based on the provided package name. 24 | * 25 | * @param packageName The package name of the application to be uninstalled. 26 | * @param onResult A callback invoked with the uninstallation result. The callback 27 | * parameters are: 28 | * - [success]: Indicates whether the uninstallation succeeded. 29 | * - [message]: An optional message providing additional context, typically in case of an error. 30 | */ 31 | suspend fun uninstallApp(packageName: String, onResult: (success: Boolean, message: String?) -> Unit) 32 | 33 | /** 34 | * Uninstalls the actual application from the device. 35 | * 36 | * @param onResult A callback invoked with the uninstallation result. 37 | * The callback parameters are: 38 | * - [success]: Indicates whether the uninstallation succeeded. 39 | * - [message]: An optional message providing additional context, typically in case of an error. 40 | */ 41 | suspend fun uninstallApp(onResult: (success: Boolean, message: String?) -> Unit) 42 | } 43 | 44 | 45 | /** 46 | * Retrieves the instance of the `AppInstaller` interface for managing app installation tasks. 47 | * 48 | * The `AppInstaller` provides methods to check and request permissions for installing packages, 49 | * as well as functionality to install or uninstall applications. 50 | * 51 | * @return An instance of `AppInstaller` to handle app installation-related operations. 52 | */ 53 | expect fun getAppInstaller(): AppInstaller -------------------------------------------------------------------------------- /platformtools/appmanager/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/appmanager/AppRestarter.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.appmanager 2 | 3 | /** 4 | * Restarts the application. 5 | * 6 | * This function triggers a full application restart. It is generally used in scenarios where 7 | * a restart is necessary, such as after applying critical updates or configuration changes. 8 | * 9 | * Implementation is platform-specific and may utilize different mechanisms to restart the 10 | * application depending on the operating system and environment. 11 | */ 12 | expect fun restartApplication() -------------------------------------------------------------------------------- /platformtools/appmanager/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/appmanager/AppVersionChecker.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.appmanager 2 | 3 | import io.github.kdroidfilter.platformtools.getAppVersion 4 | import io.github.kdroidfilter.platformtools.getCacheDir 5 | import java.io.File 6 | 7 | private const val VERSION_FILE_NAME = "app_version.txt" 8 | 9 | /** 10 | * Checks if the app version has changed since the last execution. 11 | * @return `true` if the version has changed (updated), `false` otherwise. 12 | */ 13 | fun hasAppVersionChanged(): Boolean { 14 | val currentVersion = getAppVersion() 15 | val versionFile = File(getCacheDir(), VERSION_FILE_NAME) 16 | 17 | val oldVersion: String? = if (versionFile.exists()) { 18 | versionFile.readText().trim() 19 | } else null 20 | 21 | // 1) If this is the first installation, we store the current version and return false 22 | if (oldVersion.isNullOrEmpty()) { 23 | versionFile.writeText(currentVersion) 24 | return false 25 | } 26 | 27 | // 2) Otherwise, we compare 28 | val hasChanged = oldVersion != currentVersion 29 | 30 | // 3) If it has changed, we update the file 31 | if (hasChanged) { 32 | versionFile.writeText(currentVersion) 33 | } 34 | 35 | return hasChanged 36 | } 37 | 38 | 39 | /** 40 | * Determines if the application is being installed for the first time. 41 | * 42 | * The method checks the presence of a dedicated version file in the cache directory. 43 | * If the file does not exist, it saves the current application version in the file 44 | * and returns `true`, indicating that this is the first installation. Otherwise, it 45 | * returns `false` to indicate that the application has been installed previously. 46 | * 47 | * @return `true` if this is the first installation of the application, `false` otherwise. 48 | */ 49 | fun isFirstInstallation(): Boolean { 50 | val versionFile = File(getCacheDir(), VERSION_FILE_NAME) 51 | 52 | // If the file does not exist, this is the first install 53 | if (!versionFile.exists()) { 54 | // Write the current version (or any content) 55 | versionFile.writeText(getAppVersion()) 56 | return true 57 | } 58 | 59 | // Otherwise, not the first installation 60 | return false 61 | } -------------------------------------------------------------------------------- /platformtools/appmanager/src/jvmMain/kotlin/io/github/kdroidfilter/platformtools/appmanager/AppInstaller.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.appmanager 2 | 3 | import co.touchlab.kermit.Logger 4 | import co.touchlab.kermit.Logger.Companion.setMinSeverity 5 | import co.touchlab.kermit.Severity 6 | import io.github.kdroidfilter.platformtools.OperatingSystem 7 | import io.github.kdroidfilter.platformtools.appmanager.WindowsPrivilegeHelper.installOnWindows 8 | import io.github.kdroidfilter.platformtools.getOperatingSystem 9 | import java.io.File 10 | 11 | val logger = Logger.withTag("AppInstaller").apply { setMinSeverity(Severity.Warn) } 12 | 13 | 14 | actual fun getAppInstaller(): AppInstaller = DesktopInstaller() 15 | 16 | /** 17 | * The `DesktopInstaller` class is responsible for handling the installation of applications 18 | * on desktop operating systems such as Windows, Linux, and macOS. This class implements the 19 | * `AppInstaller` interface and provides platform-specific installation logic for each supported 20 | * operating system. 21 | */ 22 | class DesktopInstaller : AppInstaller { 23 | 24 | /** 25 | * Installs a given application file based on the detected operating system. 26 | * The method supports multiple platforms (Windows, Linux, Mac) and uses platform-specific 27 | * installation mechanisms. If the operating system is unsupported, the installation fails with 28 | * an appropriate message. 29 | * 30 | * @param appFile The application file to be installed. 31 | * @param onResult A callback that provides the result of the installation. 32 | * The first parameter indicates success or failure (Boolean). 33 | * The second parameter contains an optional error message (String?). 34 | */ 35 | override suspend fun installApp(appFile: File, onResult: (Boolean, String?) -> Unit) { 36 | logger.d { "Starting installation for file: ${appFile.absolutePath}" } 37 | val osDetected = getOperatingSystem() 38 | logger.d { "Detected OS: $osDetected" } 39 | 40 | when (osDetected) { 41 | OperatingSystem.WINDOWS -> installOnWindows(appFile, onResult) 42 | OperatingSystem.LINUX -> installOnLinux(appFile, onResult) 43 | OperatingSystem.MACOS -> installOnMac(appFile, onResult) 44 | else -> { 45 | val message = "Installation not supported for: ${getOperatingSystem()}" 46 | logger.d { message } 47 | onResult(false, message) 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * Installs a .deb package on a Linux system using `pkexec` and `dpkg`. 54 | * This method attempts to elevate privileges if required and executes the installation command. 55 | * 56 | * @param installerFile The .deb file to be installed. 57 | * @param onResult A callback to handle the result of the installation. 58 | * The first parameter is a Boolean indicating success or failure. 59 | * The second parameter is an optional String containing an error message or output. 60 | */ 61 | private fun installOnLinux(installerFile: File, onResult: (Boolean, String?) -> Unit) { 62 | logger.d { "Starting installation for .deb package." } 63 | 64 | if (!installerFile.exists()) { 65 | val msg = "DEB file not found: ${installerFile.absolutePath}" 66 | logger.d { msg } 67 | onResult(false, msg) 68 | return 69 | } 70 | 71 | logger.d { "Executing dpkg via pkexec, which will prompt for a password if needed." } 72 | 73 | val command = listOf("pkexec", "dpkg", "-i", installerFile.absolutePath) 74 | logger.d { "pkexec command: $command" } 75 | 76 | runCommand(command) { success, output -> 77 | logger.d { "pkexec + dpkg result: success=$success, output=$output" } 78 | 79 | if (!success) { 80 | logger.d { "dpkg via pkexec failed." } 81 | onResult(false, output) 82 | } else { 83 | logger.d { "DEB package installation succeeded!" } 84 | onResult(true, output) 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * Executes a system command using the provided list of command arguments 91 | * and returns the result asynchronously through a callback function. 92 | * 93 | * @param command A list of strings representing the command to execute and its arguments. 94 | * @param onResult A callback function to handle the result of the command execution. 95 | * The first parameter is a Boolean indicating success (true) or failure (false). 96 | * The second parameter is an optional String containing the output of the command 97 | * or an error message in case of failure. 98 | */ 99 | private fun runCommand(command: List, onResult: (Boolean, String?) -> Unit) { 100 | logger.d { "Executing command: $command" } 101 | try { 102 | val process = ProcessBuilder(command) 103 | .redirectErrorStream(true) 104 | .start() 105 | 106 | val output = process.inputStream.bufferedReader().readText() 107 | val exitCode = process.waitFor() 108 | 109 | logger.d { "Command completed (exitCode=$exitCode). Output: $output" } 110 | 111 | if (exitCode == 0) { 112 | onResult(true, "Success. Output: $output") 113 | } else { 114 | onResult(false, "Failure (code=$exitCode). Output: $output") 115 | } 116 | 117 | } catch (e: Exception) { 118 | logger.d { "Exception in runCommand(): ${e.message}" } 119 | e.printStackTrace() 120 | onResult(false, "Exception during execution: ${e.message}") 121 | } 122 | } 123 | 124 | /** 125 | * Installs a package on macOS using an installer script with administrator privileges. 126 | * 127 | * @param installerFile The installer file to be executed, typically a `.pkg` file. 128 | * @param onResult A callback to handle the result of the operation. The first parameter 129 | * indicates success or failure as a Boolean. The second parameter provides 130 | * an optional error message as a String. 131 | */ 132 | private fun installOnMac(installerFile: File, onResult: (Boolean, String?) -> Unit) { 133 | val script = """ 134 | do shell script "installer -pkg ${installerFile.absolutePath} -target /" with administrator privileges 135 | """.trimIndent() 136 | 137 | try { 138 | val process = ProcessBuilder("osascript", "-e", script).start() 139 | val exitCode = process.waitFor() 140 | 141 | if (exitCode == 0) { 142 | onResult(true, null) 143 | } else { 144 | val errorMessage = process.errorStream.bufferedReader().readText() 145 | onResult(false, errorMessage) 146 | } 147 | } catch (e: Exception) { 148 | onResult(false, e.message) 149 | } 150 | } 151 | 152 | 153 | override suspend fun uninstallApp(packageName: String, onResult: (success: Boolean, message: String?) -> Unit) { 154 | TODO("Not yet implemented") 155 | } 156 | 157 | override suspend fun uninstallApp(onResult: (success: Boolean, message: String?) -> Unit) { 158 | TODO("Not yet implemented") 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /platformtools/appmanager/src/jvmMain/kotlin/io/github/kdroidfilter/platformtools/appmanager/AppRestarter.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.appmanager 2 | 3 | import java.io.File 4 | import kotlin.system.exitProcess 5 | 6 | object AppManager { 7 | val applicationExecutablePath: String by lazy { 8 | try { 9 | File(ProcessHandle.current().info().command().get()).absolutePath 10 | } catch (e: Exception) { 11 | e.printStackTrace() 12 | throw RuntimeException("Failed to get application executable path") 13 | } 14 | } 15 | } 16 | 17 | actual fun restartApplication() { 18 | try { 19 | val processBuilder = ProcessBuilder(AppManager.applicationExecutablePath) 20 | processBuilder.start() 21 | exitProcess(0) 22 | } catch (e: Exception) { 23 | e.printStackTrace() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /platformtools/appmanager/src/jvmMain/kotlin/io/github/kdroidfilter/platformtools/appmanager/WindowsPrivilegeHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.appmanager 2 | 3 | import com.sun.jna.platform.win32.* 4 | import com.sun.jna.ptr.IntByReference 5 | import java.io.File 6 | import java.io.InputStreamReader 7 | 8 | /** 9 | * Object containing configuration settings specific to Windows platform. 10 | * 11 | * @property requireAdmin Indicates whether administrator privileges are required 12 | * for certain operations on the Windows platform. Defaults to `true`. 13 | */ 14 | object WindowsInstallerConfig { 15 | var requireAdmin: Boolean = true 16 | } 17 | 18 | 19 | /** 20 | * Utility object that offers functions to handle Windows-specific privilege and process management tasks. 21 | */ 22 | object WindowsPrivilegeHelper { 23 | 24 | /** 25 | * Checks if the process is already running with elevated privileges (admin). 26 | */ 27 | fun isProcessElevated(): Boolean { 28 | val hToken = WinNT.HANDLEByReference() 29 | try { 30 | // Open the token of the current process 31 | val success = Advapi32.INSTANCE.OpenProcessToken( 32 | Kernel32.INSTANCE.GetCurrentProcess(), 33 | WinNT.TOKEN_QUERY, 34 | hToken 35 | ) 36 | if (!success) return false 37 | 38 | val elevation = WinNT.TOKEN_ELEVATION() 39 | val size = IntByReference() 40 | val result = Advapi32.INSTANCE.GetTokenInformation( 41 | hToken.value, 42 | WinNT.TOKEN_INFORMATION_CLASS.TokenElevation, 43 | elevation, 44 | elevation.size(), 45 | size 46 | ) 47 | if (!result) return false 48 | 49 | // If TokenIsElevated != 0, then the process is in admin mode. 50 | return elevation.TokenIsElevated != 0 51 | } finally { 52 | Kernel32.INSTANCE.CloseHandle(hToken.value) 53 | } 54 | } 55 | 56 | /** 57 | * Requests elevation (UAC) to install an MSI via msiexec. 58 | * Waits for the msiexec process to finish and returns the result via the callback. 59 | */ 60 | private fun requestAdminPrivilegesForMsi(msiPath: String, onResult: (Boolean, String?) -> Unit) { 61 | val shellExecuteInfo = ShellAPI.SHELLEXECUTEINFO().apply { 62 | cbSize = size() 63 | lpVerb = "runas" // Requests elevation (UAC) 64 | lpFile = "msiexec" // Executable in the PATH 65 | lpParameters = "/i \"$msiPath\" /quiet /l*v \"${File(msiPath).parentFile?.absolutePath}\\installation_log.txt\"" 66 | nShow = WinUser.SW_SHOWNORMAL 67 | fMask = Shell32.SEE_MASK_NOCLOSEPROCESS 68 | } 69 | 70 | val success = Shell32.INSTANCE.ShellExecuteEx(shellExecuteInfo) 71 | if (!success) { 72 | val errorCode = Kernel32.INSTANCE.GetLastError() 73 | onResult(false, "ShellExecuteEx failed for the MSI. Error code: $errorCode") 74 | return 75 | } 76 | 77 | val hProcess = shellExecuteInfo.hProcess 78 | if (hProcess == null) { 79 | onResult(false, "Null process handle after ShellExecuteEx.") 80 | return 81 | } 82 | 83 | try { 84 | // Wait for the msiexec process to finish 85 | val waitResult = Kernel32.INSTANCE.WaitForSingleObject(hProcess, WinBase.INFINITE) 86 | if (waitResult != WinBase.WAIT_OBJECT_0) { 87 | onResult(false, "Failed to wait for the msiexec process.") 88 | return 89 | } 90 | 91 | // Get the exit code of msiexec 92 | val exitCode = IntByReference() 93 | val getExitSuccess = Kernel32.INSTANCE.GetExitCodeProcess(hProcess, exitCode) 94 | if (!getExitSuccess) { 95 | val errorCode = Kernel32.INSTANCE.GetLastError() 96 | onResult(false, "GetExitCodeProcess failed. Error code: $errorCode") 97 | return 98 | } 99 | 100 | if (exitCode.value == 0) { 101 | onResult(true, null) 102 | } else { 103 | onResult(false, "MSI installation failed with exit code: ${exitCode.value}") 104 | } 105 | 106 | } finally { 107 | Kernel32.INSTANCE.CloseHandle(hProcess) 108 | } 109 | } 110 | 111 | /** 112 | * Installs an MSI file on Windows. 113 | * @param installerFile The MSI file to install. 114 | * @param onResult Callback (success: Boolean, errorMessage: String?). 115 | * @param requireAdmin Indicates whether installation must be done in admin mode. 116 | */ 117 | fun installOnWindows( 118 | installerFile: File, 119 | onResult: (Boolean, String?) -> Unit 120 | ) { 121 | val requireAdmin = WindowsInstallerConfig.requireAdmin 122 | // 1. Check if admin privileges are explicitly required 123 | if (requireAdmin && !isProcessElevated()) { 124 | // If admin rights are required and we are not in admin mode, 125 | // request elevation and wait for the result. 126 | requestAdminPrivilegesForMsi(installerFile.absolutePath, onResult) 127 | return 128 | } 129 | 130 | // 2. Validate the file 131 | if (!installerFile.exists() || !installerFile.extension.equals("msi", ignoreCase = true)) { 132 | onResult(false, "File not found or incorrect extension (MSI expected).") 133 | return 134 | } 135 | 136 | // 3. Prepare the msiexec command 137 | val command = listOf( 138 | "msiexec", 139 | "/i", installerFile.absolutePath, 140 | "/quiet", 141 | "/l*v", "${installerFile.parentFile?.absolutePath}\\installation_log.txt" 142 | ) 143 | 144 | try { 145 | // 4. Build the ProcessBuilder 146 | val processBuilder = ProcessBuilder(command).apply { 147 | redirectErrorStream(true) 148 | } 149 | 150 | // 5. Start the msiexec process 151 | val process = processBuilder.start() 152 | val exitCode = process.waitFor() 153 | 154 | // 6. Read the output stream 155 | val output = InputStreamReader(process.inputStream).readText().trim() 156 | 157 | // 7. Check the exit code 158 | if (exitCode == 0) { 159 | onResult(true, null) 160 | } else { 161 | val errorMessage = """ 162 | Installation failed (return code: $exitCode). 163 | Output: 164 | $output 165 | """.trimIndent() 166 | onResult(false, errorMessage) 167 | } 168 | } catch (e: Exception) { 169 | onResult(false, "Exception during installation: ${e.message}") 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /platformtools/core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | 3 | plugins { 4 | alias(libs.plugins.multiplatform) 5 | alias(libs.plugins.android.library) 6 | alias(libs.plugins.vannitktech.maven.publish) 7 | } 8 | 9 | val libVersion : String by rootProject.extra 10 | 11 | group = "io.github.kdroidfilter.platformtools.core" 12 | version = libVersion 13 | 14 | kotlin { 15 | jvmToolchain(17) 16 | 17 | androidTarget { publishLibraryVariants("release") } 18 | jvm() 19 | js { browser() } 20 | wasmJs { browser() } 21 | iosX64() 22 | iosArm64() 23 | iosSimulatorArm64() 24 | macosX64() 25 | macosArm64() 26 | linuxX64() 27 | mingwX64() 28 | 29 | sourceSets { 30 | commonMain.dependencies { 31 | } 32 | 33 | commonTest.dependencies { 34 | implementation(kotlin("test")) 35 | } 36 | 37 | val androidJvmMain by creating { 38 | dependsOn(commonMain.get()) 39 | } 40 | 41 | jvmMain { 42 | dependsOn(androidJvmMain) 43 | } 44 | 45 | androidMain { 46 | dependsOn(androidJvmMain) 47 | dependencies { 48 | implementation(libs.androidcontextprovider) 49 | } 50 | } 51 | 52 | val ios = listOf( 53 | iosX64(), 54 | iosArm64(), 55 | iosSimulatorArm64() 56 | ) 57 | 58 | val macos = listOf( 59 | macosX64(), 60 | macosArm64() 61 | ) 62 | 63 | val iosMain by creating { 64 | dependsOn(commonMain.get()) 65 | } 66 | 67 | val macosMain by creating { 68 | dependsOn(commonMain.get()) 69 | } 70 | 71 | ios.forEach { 72 | it.compilations["main"].defaultSourceSet { 73 | dependsOn(iosMain) 74 | } 75 | } 76 | 77 | macos.forEach { 78 | it.compilations["main"].defaultSourceSet { 79 | dependsOn(macosMain) 80 | } 81 | } 82 | 83 | 84 | 85 | } 86 | 87 | //https://kotlinlang.org/docs/native-objc-interop.html#export-of-kdoc-comments-to-generated-objective-c-headers 88 | targets.withType { 89 | compilations["main"].compileTaskProvider.configure { 90 | compilerOptions { 91 | freeCompilerArgs.add("-Xexport-kdoc") 92 | } 93 | } 94 | } 95 | 96 | } 97 | 98 | android { 99 | namespace = "io.github.kdroidfilter.platformtools.core" 100 | compileSdk = 35 101 | 102 | defaultConfig { 103 | minSdk = 21 104 | } 105 | } 106 | 107 | mavenPublishing { 108 | coordinates( 109 | groupId = "io.github.kdroidfilter", 110 | artifactId = "platformtools.core", 111 | version = version.toString() 112 | ) 113 | 114 | pom { 115 | name.set("PlatformTools Core") 116 | description.set(" Core of PlatformTools, a Kotlin Multiplatform library to manage platform-specific utilities and tools.") 117 | inceptionYear.set("2025") // Change si la création du projet est plus ancienne. 118 | url.set("https://github.com/kdroidFilter/") 119 | 120 | licenses { 121 | license { 122 | name.set("MIT License") 123 | url.set("https://opensource.org/licenses/MIT") 124 | } 125 | } 126 | 127 | developers { 128 | developer { 129 | id.set("kdroidfilter") 130 | name.set("Elyahou Hadass") 131 | email.set("elyahou.hadass@gmail.com") 132 | } 133 | } 134 | 135 | scm { 136 | connection.set("scm:git:git://github.com/kdroidFilter/platformtools.git") 137 | developerConnection.set("scm:git:ssh://git@github.com:kdroidFilter/platformtools.git") 138 | url.set("https://github.com/kdroidFilter/platformtools") 139 | } 140 | } 141 | 142 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 143 | 144 | signAllPublications() 145 | } 146 | 147 | -------------------------------------------------------------------------------- /platformtools/core/src/androidJvmMain/kotlin/io/github/kdroidfilter/platformtools/CacheProvider.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | import java.io.File 4 | 5 | /** 6 | * Provides the directory used for storing cached data. 7 | * 8 | * @return A File object representing the cache directory. 9 | */ 10 | expect fun getCacheDir(): File 11 | -------------------------------------------------------------------------------- /platformtools/core/src/androidJvmMain/kotlin/io/github/kdroidfilter/platformtools/VersionProvider.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | /** 4 | * Retrieves the version of the application. 5 | * 6 | * @return A string representing the application version. 7 | */ 8 | expect fun getAppVersion(): String -------------------------------------------------------------------------------- /platformtools/core/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/CacheProvider.android.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | 4 | import com.kdroid.androidcontextprovider.ContextProvider 5 | import java.io.File 6 | 7 | actual fun getCacheDir(): File { 8 | val context = ContextProvider.getContext() 9 | return context.cacheDir 10 | } -------------------------------------------------------------------------------- /platformtools/core/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/OsProvider.android.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getOperatingSystem(): OperatingSystem = OperatingSystem.ANDROID -------------------------------------------------------------------------------- /platformtools/core/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/PlatformProvider.android.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getPlatform(): Platform = Platform.ANDROID -------------------------------------------------------------------------------- /platformtools/core/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/VersionProvider.android.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | import com.kdroid.androidcontextprovider.ContextProvider 4 | 5 | actual fun getAppVersion(): String { 6 | val context = ContextProvider.getContext() 7 | return try { 8 | val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) 9 | packageInfo.versionName ?: "Unknown" 10 | } catch (e: Exception) { 11 | "Error: ${e.message}" 12 | } 13 | } 14 | 15 | fun getAppVersion(packageName: String): String { 16 | val context = ContextProvider.getContext() 17 | return try { 18 | val packageInfo = context.packageManager.getPackageInfo(packageName, 0) 19 | packageInfo.versionName ?: "Unknown" 20 | } catch (e: Exception) { 21 | "Error: ${e.message}" 22 | } 23 | } -------------------------------------------------------------------------------- /platformtools/core/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/OsProvider.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | /** 4 | * Represents the operating systems a device or environment can run. 5 | * 6 | * This enum class is used to identify the underlying platform or operating system 7 | * being utilized by an application, and it is commonly utilized in platform-specific logic. 8 | * 9 | * Enum Constants: 10 | * - `WINDOWS`: Represents the Microsoft Windows operating system. 11 | * - `MACOS`: Represents the macOS operating system from Apple. 12 | * - `LINUX`: Represents Linux-based operating systems. 13 | * - `ANDROID`: Represents the Android operating system. 14 | * - `IOS`: Represents the iOS operating system from Apple. 15 | * - `UNKNOWN`: Represents an unrecognized or unsupported operating system. 16 | */ 17 | enum class OperatingSystem { 18 | WINDOWS, MACOS, LINUX, ANDROID, IOS, UNKNOWN 19 | } 20 | 21 | /** 22 | * Determines the operating system on which the application is currently running. 23 | * 24 | * @return An instance of [OperatingSystem] representing the current platform or operating system. 25 | */ 26 | expect fun getOperatingSystem(): OperatingSystem -------------------------------------------------------------------------------- /platformtools/core/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/PlatformProvider.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | /** 4 | * Represents the various platforms or environments where an application can run. 5 | * 6 | * This enum class is used to identify the target runtime platform, and it is 7 | * commonly utilized for platform-specific logic or feature implementation. 8 | * 9 | * Enum Constants: 10 | * - `ANDROID`: Represents the Android platform. 11 | * - `JVM`: Represents the Java Virtual Machine environment. 12 | * - `IOS_NATIVE`: Represents the iOS native platform. 13 | * - `JS`: Represents the JavaScript environment. 14 | * - `WASM_JS`: Represents the WebAssembly JavaScript platform. 15 | * - `LINUX_NATIVE`: Represents Linux native platforms. 16 | * - `MAC_OS_NATIVE`: Represents the macOS native platform. 17 | * - `WINDOWS_NATIVE`: Represents Windows native platforms. 18 | */ 19 | enum class Platform { 20 | ANDROID, JVM, IOS_NATIVE, JS, WASM_JS, LINUX_NATIVE, MAC_OS_NATIVE, WINDOWS_NATIVE 21 | } 22 | 23 | /** 24 | * Determines the platform on which the application is currently running. 25 | * 26 | * @return An instance of [Platform] representing the specific platform or environment, such as Android, JVM, Native, or JavaScript. 27 | */ 28 | expect fun getPlatform(): Platform -------------------------------------------------------------------------------- /platformtools/core/src/iosMain/kotlin/io/github/kdroidfilter/platformtools/OsProvider.ios.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getOperatingSystem(): OperatingSystem = OperatingSystem.IOS 4 | -------------------------------------------------------------------------------- /platformtools/core/src/iosMain/kotlin/io/github/kdroidfilter/platformtools/PlatformProvider.ios.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getPlatform(): Platform = Platform.IOS_NATIVE -------------------------------------------------------------------------------- /platformtools/core/src/jsMain/kotlin/io/github/kdroidfilter/platformtools/OsProvider.js.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getOperatingSystem(): OperatingSystem { 4 | val userAgent = js("navigator.userAgent") as String 5 | val platform = js("navigator.platform") as String 6 | 7 | return when { 8 | userAgent.contains("Windows", ignoreCase = true) -> OperatingSystem.WINDOWS 9 | userAgent.contains("Macintosh", ignoreCase = true) || platform.contains("Mac", ignoreCase = true) -> OperatingSystem.MACOS 10 | userAgent.contains("Linux", ignoreCase = true) -> OperatingSystem.LINUX 11 | userAgent.contains("Android", ignoreCase = true) -> OperatingSystem.ANDROID 12 | userAgent.contains("iPhone", ignoreCase = true) || 13 | userAgent.contains("iPad", ignoreCase = true) || 14 | userAgent.contains("iPod", ignoreCase = true) -> OperatingSystem.IOS 15 | 16 | else -> OperatingSystem.UNKNOWN 17 | } 18 | } -------------------------------------------------------------------------------- /platformtools/core/src/jsMain/kotlin/io/github/kdroidfilter/platformtools/PlatformProvider.js.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getPlatform(): Platform = Platform.JS -------------------------------------------------------------------------------- /platformtools/core/src/jvmMain/kotlin/io/github/kdroidfilter/platformtools/CacheProvider.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | 4 | import java.io.File 5 | 6 | actual fun getCacheDir(): File = File(System.getProperty("java.io.tmpdir")) 7 | -------------------------------------------------------------------------------- /platformtools/core/src/jvmMain/kotlin/io/github/kdroidfilter/platformtools/OsProvider.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getOperatingSystem(): OperatingSystem { 4 | val osName = System.getProperty("os.name").lowercase() 5 | return when { 6 | osName.contains("win") -> OperatingSystem.WINDOWS 7 | osName.contains("mac") -> OperatingSystem.MACOS 8 | osName.contains("nix") || osName.contains("nux") || osName.contains("aix") -> OperatingSystem.LINUX 9 | else -> OperatingSystem.UNKNOWN 10 | } 11 | } -------------------------------------------------------------------------------- /platformtools/core/src/jvmMain/kotlin/io/github/kdroidfilter/platformtools/PlatformProvider.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getPlatform(): Platform = Platform.JVM -------------------------------------------------------------------------------- /platformtools/core/src/jvmMain/kotlin/io/github/kdroidfilter/platformtools/VersionProvider.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getAppVersion(): String { 4 | return System.getProperty("jpackage.app-version") ?: "0.1.0" 5 | } -------------------------------------------------------------------------------- /platformtools/core/src/linuxX64Main/kotlin/io/github/kdroidfilter/platformtools/OsProvider.linuxX64.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getOperatingSystem(): OperatingSystem = OperatingSystem.LINUX -------------------------------------------------------------------------------- /platformtools/core/src/linuxX64Main/kotlin/io/github/kdroidfilter/platformtools/PlatformProvider.linuxX64.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getPlatform(): Platform = Platform.LINUX_NATIVE -------------------------------------------------------------------------------- /platformtools/core/src/macosMain/kotlin/io/github/kdroidfilter/platformtools/OsProvider.macos.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getOperatingSystem(): OperatingSystem = OperatingSystem.MACOS 4 | -------------------------------------------------------------------------------- /platformtools/core/src/macosMain/kotlin/io/github/kdroidfilter/platformtools/PlatformProvider.macos.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getPlatform(): Platform = Platform.MAC_OS_NATIVE -------------------------------------------------------------------------------- /platformtools/core/src/mingwX64Main/kotlin/io/github/kdroidfilter/platformtools/OsProvider.mingwX64.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getOperatingSystem(): OperatingSystem = OperatingSystem.WINDOWS -------------------------------------------------------------------------------- /platformtools/core/src/mingwX64Main/kotlin/io/github/kdroidfilter/platformtools/PlatformProvider.mingwX64.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getPlatform(): Platform = Platform.WINDOWS_NATIVE -------------------------------------------------------------------------------- /platformtools/core/src/wasmJsMain/kotlin/io/github/kdroidfilter/platformtools/OsProvider.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | fun getUserAgent(): String = 4 | js("window.navigator.userAgent") 5 | 6 | fun getOs(): String = 7 | js("window.navigator.platform") 8 | 9 | actual fun getOperatingSystem(): OperatingSystem { 10 | val userAgent = getUserAgent() 11 | val platform = getOs() 12 | 13 | return when { 14 | userAgent.contains("Windows", ignoreCase = true) -> OperatingSystem.WINDOWS 15 | userAgent.contains("Macintosh", ignoreCase = true) || platform.contains("Mac", ignoreCase = true) -> OperatingSystem.MACOS 16 | userAgent.contains("Linux", ignoreCase = true) -> OperatingSystem.LINUX 17 | userAgent.contains("Android", ignoreCase = true) -> OperatingSystem.ANDROID 18 | userAgent.contains("iPhone", ignoreCase = true) || 19 | userAgent.contains("iPad", ignoreCase = true) || 20 | userAgent.contains("iPod", ignoreCase = true) -> OperatingSystem.IOS 21 | 22 | else -> OperatingSystem.UNKNOWN 23 | } 24 | } -------------------------------------------------------------------------------- /platformtools/core/src/wasmJsMain/kotlin/io/github/kdroidfilter/platformtools/PlatformProvider.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools 2 | 3 | actual fun getPlatform(): Platform = Platform.WASM_JS -------------------------------------------------------------------------------- /platformtools/darkmodedetector/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | 3 | plugins { 4 | alias(libs.plugins.multiplatform) 5 | alias(libs.plugins.android.library) 6 | alias(libs.plugins.compose.compiler) 7 | alias(libs.plugins.compose) 8 | alias(libs.plugins.vannitktech.maven.publish) 9 | } 10 | 11 | val libVersion : String by rootProject.extra 12 | 13 | group = "io.github.kdroidfilter.platformtools.darkmodedetector" 14 | version = libVersion 15 | 16 | 17 | kotlin { 18 | jvmToolchain(17) 19 | 20 | androidTarget() 21 | jvm() 22 | js { browser() } 23 | wasmJs { browser() } 24 | iosX64() 25 | iosArm64() 26 | iosSimulatorArm64() 27 | macosX64() 28 | macosArm64() 29 | 30 | sourceSets { 31 | commonMain.dependencies { 32 | implementation(project(":platformtools:core")) 33 | implementation(compose.runtime) 34 | implementation(compose.foundation) 35 | } 36 | 37 | androidMain.dependencies { 38 | } 39 | 40 | jvmMain.dependencies { 41 | implementation(libs.jna) 42 | implementation(libs.jna.platform) 43 | implementation(libs.jfa) 44 | implementation(libs.kermit) 45 | } 46 | 47 | } 48 | } 49 | 50 | android { 51 | namespace = "io.github.kdroidfilter.platformtools.darkmodedetector" 52 | compileSdk = 35 53 | 54 | defaultConfig { 55 | minSdk = 21 56 | } 57 | } 58 | 59 | mavenPublishing { 60 | coordinates( 61 | groupId = "io.github.kdroidfilter", 62 | artifactId = "platformtools.darkmodedetector", 63 | version = version.toString() 64 | ) 65 | 66 | pom { 67 | name.set("PlatformTools Dark Mode") 68 | description.set("Dark Mode Detection module for PlatformTools, a Kotlin Multiplatform library for managing platform-specific utilities and tools.") 69 | inceptionYear.set("2025") 70 | url.set("https://github.com/kdroidFilter/") 71 | 72 | licenses { 73 | license { 74 | name.set("MIT License") 75 | url.set("https://opensource.org/licenses/MIT") 76 | } 77 | } 78 | 79 | developers { 80 | developer { 81 | id.set("kdroidfilter") 82 | name.set("Elyahou Hadass") 83 | email.set("elyahou.hadass@gmail.com") 84 | } 85 | } 86 | 87 | scm { 88 | connection.set("scm:git:git://github.com/kdroidFilter/platformtools.git") 89 | developerConnection.set("scm:git:ssh://git@github.com:kdroidFilter/platformtools.git") 90 | url.set("https://github.com/kdroidFilter/platformtools") 91 | } 92 | } 93 | 94 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 95 | 96 | signAllPublications() 97 | } -------------------------------------------------------------------------------- /platformtools/darkmodedetector/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/darkmodedetector/IsSystemInDarkMode.android.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.darkmodedetector 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | actual fun isSystemInDarkMode(): Boolean = isSystemInDarkTheme() -------------------------------------------------------------------------------- /platformtools/darkmodedetector/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/darkmodedetector/IsSystemInDarkMode.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.darkmodedetector 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | expect fun isSystemInDarkMode() : Boolean -------------------------------------------------------------------------------- /platformtools/darkmodedetector/src/jsMain/kotlin/io/github/kdroidfilter/platformtools/darkmodedetector/IsSystemInDarkMode.js.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.darkmodedetector 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | actual fun isSystemInDarkMode(): Boolean = isSystemInDarkTheme() -------------------------------------------------------------------------------- /platformtools/darkmodedetector/src/jvmMain/kotlin/io/github/kdroidfilter/platformtools/darkmodedetector/IsSystemInDarkMode.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.darkmodedetector 2 | 3 | import androidx.compose.runtime.Composable 4 | import io.github.kdroidfilter.platformtools.OperatingSystem 5 | import io.github.kdroidfilter.platformtools.darkmodedetector.linux.isLinuxInDarkMode 6 | import io.github.kdroidfilter.platformtools.darkmodedetector.mac.isMacOsInDarkMode 7 | import io.github.kdroidfilter.platformtools.darkmodedetector.windows.isWindowsInDarkMode 8 | import io.github.kdroidfilter.platformtools.getOperatingSystem 9 | 10 | 11 | /** 12 | * Composable function that returns whether the system is in dark mode. 13 | * It handles macOS, Windows, and Linux. For Windows and Linux, it returns false as a placeholder. 14 | */ 15 | @Composable 16 | actual fun isSystemInDarkMode(): Boolean { 17 | return when (getOperatingSystem()) { 18 | OperatingSystem.MACOS -> isMacOsInDarkMode() 19 | OperatingSystem.WINDOWS -> isWindowsInDarkMode() 20 | OperatingSystem.LINUX -> isLinuxInDarkMode() 21 | else -> false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /platformtools/darkmodedetector/src/jvmMain/kotlin/io/github/kdroidfilter/platformtools/darkmodedetector/mac/MacOSThemeDetector.kt: -------------------------------------------------------------------------------- 1 | // Inspired by the code from the jSystemThemeDetector project: 2 | // https://github.com/Dansoftowner/jSystemThemeDetector/blob/master/src/main/java/com/jthemedetecor/MacOSThemeDetector.java 3 | 4 | package io.github.kdroidfilter.platformtools.darkmodedetector.mac 5 | 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.DisposableEffect 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import co.touchlab.kermit.Logger 11 | import co.touchlab.kermit.Logger.Companion.setMinSeverity 12 | import co.touchlab.kermit.Severity 13 | import de.jangassen.jfa.foundation.Foundation 14 | import de.jangassen.jfa.foundation.ID 15 | import java.util.concurrent.ConcurrentHashMap 16 | import java.util.concurrent.Executors 17 | import java.util.function.Consumer 18 | import java.util.regex.Pattern 19 | 20 | // Initialize logger using kotlin-logging 21 | private val macLogger = Logger.withTag("MacOSThemeDetector").apply { setMinSeverity(Severity.Warn) } 22 | 23 | /** 24 | * MacOSThemeDetector registers an observer with NSDistributedNotificationCenter 25 | * to detect theme changes in macOS. It reads the system preference "AppleInterfaceStyle" 26 | * (which is "Dark" when in dark mode) from NSUserDefaults. 27 | */ 28 | internal object MacOSThemeDetector { 29 | 30 | // Set of listeners to notify when the theme changes (true = dark, false = light) 31 | private val listeners: MutableSet> = ConcurrentHashMap.newKeySet() 32 | 33 | // Pattern to match if the style string contains "dark" (case insensitive) 34 | private val darkPattern: Pattern = Pattern.compile(".*dark.*", Pattern.CASE_INSENSITIVE) 35 | 36 | // Executor to run callbacks in a dedicated thread 37 | private val callbackExecutor = Executors.newSingleThreadExecutor { r -> 38 | Thread(r, "MacOS Theme Detector Thread").apply { isDaemon = true } 39 | } 40 | 41 | /** 42 | * Callback invoked by the Objective-C runtime when the system posts 43 | * the "AppleInterfaceThemeChangedNotification" notification. 44 | * The expected Objective-C signature is "v@" (void return, no parameters). 45 | */ 46 | @JvmStatic 47 | private val themeChangedCallback = object : com.sun.jna.Callback { 48 | fun callback() { 49 | callbackExecutor.execute { 50 | val isDark = isDark() 51 | macLogger.d { "Theme change detected. Dark mode: $isDark" } 52 | notifyListeners(isDark) 53 | } 54 | } 55 | } 56 | 57 | // Initialize the observer on startup. 58 | init { 59 | initObserver() 60 | } 61 | 62 | /** 63 | * Initializes the Objective-C observer. 64 | * This method creates a custom Objective-C class ("NSColorChangesObserver") 65 | * that extends NSObject, adds a method "handleAppleThemeChanged:" that calls our callback, 66 | * and registers the observer with NSDistributedNotificationCenter. 67 | */ 68 | private fun initObserver() { 69 | macLogger.d { "Initializing macOS theme observer" } 70 | val pool = Foundation.NSAutoreleasePool() 71 | try { 72 | val delegateClass: ID = Foundation.allocateObjcClassPair( 73 | Foundation.getObjcClass("NSObject"), 74 | "NSColorChangesObserver" 75 | ) 76 | if (!ID.NIL.equals(delegateClass)) { 77 | val selector = Foundation.createSelector("handleAppleThemeChanged:") 78 | val added = Foundation.addMethod(delegateClass, selector, themeChangedCallback, "v@") 79 | if (!added) { 80 | macLogger.e { "Failed to add observer method to NSColorChangesObserver" } 81 | } 82 | Foundation.registerObjcClassPair(delegateClass) 83 | } 84 | val delegateObj = Foundation.invoke("NSColorChangesObserver", "new") 85 | Foundation.invoke( 86 | Foundation.invoke("NSDistributedNotificationCenter", "defaultCenter"), 87 | "addObserver:selector:name:object:", 88 | delegateObj, 89 | Foundation.createSelector("handleAppleThemeChanged:"), 90 | Foundation.nsString("AppleInterfaceThemeChangedNotification"), 91 | ID.NIL 92 | ) 93 | macLogger.d { "Observer successfully registered" } 94 | } finally { 95 | pool.drain() 96 | } 97 | } 98 | 99 | /** 100 | * Reads the system theme by checking the "AppleInterfaceStyle" preference. 101 | * Returns true if the system is in dark mode, false otherwise. 102 | */ 103 | fun isDark(): Boolean { 104 | val pool = Foundation.NSAutoreleasePool() 105 | return try { 106 | val userDefaults = Foundation.invoke("NSUserDefaults", "standardUserDefaults") 107 | val styleKey = Foundation.nsString("AppleInterfaceStyle") 108 | val result = Foundation.invoke(userDefaults, "objectForKey:", styleKey) 109 | val styleString = Foundation.toStringViaUTF8(result) 110 | darkPattern.matcher(styleString ?: "").matches() 111 | } catch (e: Exception) { 112 | macLogger.e(e) { "Error reading system theme" } 113 | false 114 | } finally { 115 | pool.drain() 116 | } 117 | } 118 | 119 | fun registerListener(listener: Consumer) { 120 | listeners.add(listener) 121 | } 122 | 123 | fun removeListener(listener: Consumer) { 124 | listeners.remove(listener) 125 | } 126 | 127 | private fun notifyListeners(isDark: Boolean) { 128 | listeners.forEach { it.accept(isDark) } 129 | } 130 | } 131 | 132 | 133 | /** 134 | * A helper composable function that returns the current macOS dark mode state, 135 | * updating automatically when the system theme changes. 136 | */ 137 | @Composable 138 | internal fun isMacOsInDarkMode(): Boolean { 139 | val darkModeState = remember { mutableStateOf(MacOSThemeDetector.isDark()) } 140 | DisposableEffect(Unit) { 141 | macLogger.d { "Registering macOS dark mode listener in Compose" } 142 | val listener = Consumer { newValue -> 143 | macLogger.d { "Compose macOS dark mode updated: $newValue" } 144 | darkModeState.value = newValue 145 | } 146 | MacOSThemeDetector.registerListener(listener) 147 | onDispose { 148 | macLogger.d { "Removing macOS dark mode listener in Compose" } 149 | MacOSThemeDetector.removeListener(listener) 150 | } 151 | } 152 | return darkModeState.value 153 | } -------------------------------------------------------------------------------- /platformtools/darkmodedetector/src/jvmMain/kotlin/io/github/kdroidfilter/platformtools/darkmodedetector/windows/Dwmapi.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.darkmodedetector.windows 2 | 3 | import com.sun.jna.Library 4 | import com.sun.jna.Native 5 | import com.sun.jna.Pointer 6 | import com.sun.jna.platform.win32.WinDef.HWND 7 | import com.sun.jna.win32.W32APIOptions 8 | 9 | internal interface DwmApi : Library { 10 | companion object { 11 | val INSTANCE: DwmApi = Native.load("dwmapi", DwmApi::class.java, W32APIOptions.DEFAULT_OPTIONS) 12 | const val DWMWA_USE_IMMERSIVE_DARK_MODE = 20 13 | } 14 | 15 | fun DwmSetWindowAttribute( 16 | hwnd: HWND, 17 | dwAttribute: Int, 18 | pvAttribute: Pointer, 19 | cbAttribute: Int 20 | ): Int 21 | } -------------------------------------------------------------------------------- /platformtools/darkmodedetector/src/nativeMain/kotlin/io/github/kdroidfilter/platformtools/darkmodedetector/IsSystemInDarkMode.native.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.darkmodedetector 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | actual fun isSystemInDarkMode(): Boolean = isSystemInDarkTheme() -------------------------------------------------------------------------------- /platformtools/darkmodedetector/src/wasmJsMain/kotlin/io/github/kdroidfilter/platformtools/darkmodedetector/IsSystemInDarkMode.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.darkmodedetector 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | actual fun isSystemInDarkMode(): Boolean = isSystemInDarkTheme() -------------------------------------------------------------------------------- /platformtools/permissionhandler/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | 3 | plugins { 4 | alias(libs.plugins.multiplatform) 5 | alias(libs.plugins.android.library) 6 | alias(libs.plugins.vannitktech.maven.publish) 7 | } 8 | 9 | val libVersion : String by rootProject.extra 10 | 11 | group = "io.github.kdroidfilter.platformtools.permissionhandler" 12 | version = libVersion 13 | 14 | kotlin { 15 | jvmToolchain(17) 16 | 17 | androidTarget { publishLibraryVariants("release") } 18 | jvm() 19 | js { browser() } 20 | wasmJs { 21 | browser() 22 | } 23 | iosX64() 24 | iosArm64() 25 | iosSimulatorArm64() 26 | 27 | 28 | sourceSets { 29 | commonMain.dependencies { 30 | implementation(libs.kotlinx.coroutines.core) 31 | } 32 | 33 | commonTest.dependencies { 34 | implementation(kotlin("test")) 35 | } 36 | androidMain.dependencies { 37 | implementation(libs.androidx.core) 38 | implementation(libs.androidx.activity.ktx) 39 | implementation(libs.androidcontextprovider) 40 | 41 | } 42 | wasmJsMain.dependencies { 43 | implementation(libs.kotlinx.browser.wasm.js) 44 | 45 | } 46 | 47 | } 48 | 49 | //https://kotlinlang.org/docs/native-objc-interop.html#export-of-kdoc-comments-to-generated-objective-c-headers 50 | targets.withType { 51 | compilations["main"].compileTaskProvider.configure { 52 | compilerOptions { 53 | freeCompilerArgs.add("-Xexport-kdoc") 54 | } 55 | } 56 | } 57 | 58 | } 59 | 60 | android { 61 | namespace = "io.github.kdroidfilter.platformtools.permissionhandler" 62 | compileSdk = 35 63 | 64 | defaultConfig { 65 | minSdk = 21 66 | } 67 | } 68 | 69 | mavenPublishing { 70 | coordinates( 71 | groupId = "io.github.kdroidfilter", 72 | artifactId = "platformtools.permissionhandler", 73 | version = version.toString() 74 | ) 75 | 76 | pom { 77 | name.set("PlatformTools Permission Handler") 78 | description.set("Simplify permission management in your App with this easy-to-use library.") 79 | inceptionYear.set("2025") 80 | url.set("https://github.com/kdroidFilter/") 81 | 82 | licenses { 83 | license { 84 | name.set("MIT License") 85 | url.set("https://opensource.org/licenses/MIT") 86 | } 87 | } 88 | 89 | developers { 90 | developer { 91 | id.set("kdroidfilter") 92 | name.set("Elyahou Hadass") 93 | email.set("elyahou.hadass@gmail.com") 94 | } 95 | } 96 | 97 | scm { 98 | connection.set("scm:git:git://github.com/kdroidFilter/platformtools.git") 99 | developerConnection.set("scm:git:ssh://git@github.com:kdroidFilter/platformtools.git") 100 | url.set("https://github.com/kdroidFilter/platformtools") 101 | } 102 | } 103 | 104 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 105 | 106 | signAllPublications() 107 | } 108 | 109 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/BackgroundLocation.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | import android.Manifest 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.util.Log 8 | import com.kdroid.androidcontextprovider.ContextProvider 9 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionActivity 10 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionCallbackManager 11 | 12 | /** 13 | * Checks if the app has the background location permission. 14 | * 15 | * @return true if the ACCESS_BACKGROUND_LOCATION permission is granted 16 | */ 17 | fun hasBackgroundLocationPermission(): Boolean { 18 | val context = ContextProvider.getContext() 19 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 20 | context.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED 21 | } else { 22 | // For versions < Q (29), ACCESS_BACKGROUND_LOCATION does not exist separately 23 | true 24 | } 25 | } 26 | 27 | /** 28 | * Requests the background location permission. 29 | * Before requesting ACCESS_BACKGROUND_LOCATION, ensure ACCESS_FINE_LOCATION is already granted, 30 | * use hasLocationPermission and requestLocationPermission() 31 | * 32 | * Note: Make sure to add the following permission in your AndroidManifest.xml: 33 | * 34 | * 35 | * @param onGranted Callback invoked when the permission is granted 36 | * @param onDenied Callback invoked when the permission is denied 37 | */ 38 | fun requestBackgroundLocationPermission( 39 | onGranted: () -> Unit, 40 | onDenied: () -> Unit 41 | ) { 42 | val context = ContextProvider.getContext() 43 | 44 | // Check if the permission is already granted 45 | if (hasBackgroundLocationPermission()) { 46 | onGranted() 47 | return 48 | } 49 | 50 | // Check if the OS version requires this permission 51 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { 52 | // ACCESS_BACKGROUND_LOCATION is not required below Q 53 | onGranted() 54 | return 55 | } 56 | 57 | val requestId = PermissionCallbackManager.registerCallbacks(onGranted, onDenied) 58 | 59 | try { 60 | val intent = Intent(context, PermissionActivity::class.java).apply { 61 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 62 | putExtra(PermissionActivity.EXTRA_REQUEST_ID, requestId) 63 | putExtra(PermissionActivity.EXTRA_REQUEST_TYPE, PermissionActivity.REQUEST_TYPE_BACKGROUND_LOCATION) 64 | } 65 | context.startActivity(intent) 66 | } catch (e: Exception) { 67 | Log.e("BackgroundLocationPerm", "Error launching PermissionActivity", e) 68 | PermissionCallbackManager.unregisterCallbacks(requestId) 69 | onDenied() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Bluetooth.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | import android.Manifest 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.util.Log 8 | import androidx.core.content.ContextCompat 9 | import com.kdroid.androidcontextprovider.ContextProvider 10 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionActivity 11 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionCallbackManager 12 | 13 | /** 14 | * Checks if the application has permission to use Bluetooth. 15 | * 16 | * This method verifies whether the app has been granted the `BLUETOOTH_CONNECT` permission. 17 | * Required for Android 12 (API level 31) and above. 18 | * 19 | * @return true if the Bluetooth permission is granted, false otherwise. 20 | */ 21 | fun hasBluetoothPermission(): Boolean { 22 | val context = ContextProvider.getContext() 23 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12+ 24 | ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED 25 | } else { 26 | // No explicit Bluetooth permissions required for devices below Android 12 27 | true 28 | } 29 | } 30 | 31 | /** 32 | * Requests Bluetooth permission for the application. 33 | * 34 | * This method initiates a permission request flow. If the permission is already granted, 35 | * it invokes the `onGranted` callback immediately. Otherwise, it starts the 36 | * `PermissionActivity` to request the permission from the user. 37 | * 38 | * Note: Ensure to add the following permissions in the app's manifest file: 39 | * 40 | * 41 | * 42 | * 43 | * @param onGranted Callback to be invoked if the permission is granted. 44 | * @param onDenied Callback to be invoked if the permission is denied. 45 | */ 46 | fun requestBluetoothPermission( 47 | onGranted: () -> Unit, 48 | onDenied: () -> Unit, 49 | ) { 50 | val context = ContextProvider.getContext() 51 | 52 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12+ 53 | when (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT)) { 54 | PackageManager.PERMISSION_GRANTED -> { 55 | onGranted() 56 | } 57 | else -> { 58 | val requestId = PermissionCallbackManager.registerCallbacks(onGranted, onDenied) 59 | 60 | try { 61 | val intent = Intent(context, PermissionActivity::class.java).apply { 62 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 63 | putExtra(PermissionActivity.EXTRA_REQUEST_ID, requestId) 64 | putExtra(PermissionActivity.EXTRA_REQUEST_TYPE, PermissionActivity.REQUEST_TYPE_BLUETOOTH) 65 | } 66 | 67 | context.startActivity(intent) 68 | } catch (e: Exception) { 69 | Log.e("BluetoothPermission", "Error while launching PermissionActivity", e) 70 | e.printStackTrace() 71 | PermissionCallbackManager.unregisterCallbacks(requestId) 72 | onDenied() 73 | } 74 | } 75 | } 76 | } else { 77 | Log.d("BluetoothPermission", "No Bluetooth permission required for this Android version.") 78 | onGranted() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Camera.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | import android.Manifest 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.util.Log 8 | import androidx.core.content.ContextCompat 9 | import com.kdroid.androidcontextprovider.ContextProvider 10 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionActivity 11 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionCallbackManager 12 | 13 | /** 14 | * Checks if the application has permission to access the camera. 15 | * 16 | * This method verifies whether the app has been granted the `CAMERA` permission. 17 | * 18 | * @return true if the camera permission is granted, false otherwise. 19 | */ 20 | fun hasCameraPermission(): Boolean { 21 | val context = ContextProvider.getContext() 22 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 23 | ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED 24 | } else { 25 | // Permissions are automatically granted on devices below Android M 26 | true 27 | } 28 | } 29 | 30 | /** 31 | * Requests camera permission for the application. 32 | * 33 | * This method initiates a permission request flow. If the permission is already granted, 34 | * it invokes the `onGranted` callback immediately. Otherwise, it starts the 35 | * `PermissionActivity` to request the permission from the user. 36 | * 37 | * Note: Ensure to add 38 | * 39 | * 40 | * in the app's manifest file. 41 | * 42 | * @param onGranted Callback to be invoked if the permission is granted. 43 | * @param onDenied Callback to be invoked if the permission is denied. 44 | */ 45 | fun requestCameraPermission( 46 | onGranted: () -> Unit, 47 | onDenied: () -> Unit, 48 | ) { 49 | val context = ContextProvider.getContext() 50 | 51 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 52 | when (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)) { 53 | PackageManager.PERMISSION_GRANTED -> { 54 | onGranted() 55 | } 56 | else -> { 57 | val requestId = PermissionCallbackManager.registerCallbacks(onGranted, onDenied) 58 | 59 | try { 60 | val intent = Intent(context, PermissionActivity::class.java).apply { 61 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 62 | putExtra(PermissionActivity.EXTRA_REQUEST_ID, requestId) 63 | putExtra(PermissionActivity.EXTRA_REQUEST_TYPE, PermissionActivity.REQUEST_TYPE_CAMERA) 64 | } 65 | 66 | context.startActivity(intent) 67 | } catch (e: Exception) { 68 | Log.e("CameraPermission", "Error while launching PermissionActivity", e) 69 | e.printStackTrace() 70 | PermissionCallbackManager.unregisterCallbacks(requestId) 71 | onDenied() 72 | } 73 | } 74 | } 75 | } else { 76 | Log.d("CameraPermission", "No camera permission required for this Android version.") 77 | onGranted() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Installation.android.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | import android.content.Intent 4 | import android.os.Build 5 | import android.util.Log 6 | import com.kdroid.androidcontextprovider.ContextProvider 7 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionActivity 8 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionCallbackManager 9 | 10 | /** 11 | * Checks if the application has permission to install APK files. 12 | * 13 | * On devices running Android Oreo (API level 26) or higher, this method verifies 14 | * whether the app has been granted the `REQUEST_INSTALL_PACKAGES` permission. On lower 15 | * versions, it returns true by default as this permission is not required. 16 | * 17 | * @return true if the install permission is granted, or if the platform version is lower than API level 26. 18 | */ 19 | actual fun hasInstallPermission(): Boolean { 20 | val context = ContextProvider.getContext() 21 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 22 | context.packageManager.canRequestPackageInstalls() 23 | } else { 24 | true 25 | } 26 | } 27 | 28 | 29 | /** 30 | * Requests the install permission required to allow the app to install APK files from unknown sources. 31 | * This method checks if the required permission has already been granted. If granted, the `onGranted` 32 | * callback is invoked. If not, the method triggers a permission request workflow via a dedicated 33 | * `PermissionActivity`, and the appropriate callback (`onGranted` or `onDenied`) is invoked based 34 | * on the user's action. 35 | * 36 | * Note: Ensure to add 37 | * to your AndroidManifest.xml file. * 38 | * @param onGranted A callback function that is invoked when the install permission is granted. 39 | * @param onDenied A callback function that is invoked when the install permission is denied. 40 | */ 41 | actual fun requestInstallPermission( 42 | onGranted: () -> Unit, 43 | onDenied: () -> Unit, 44 | ) { 45 | val context = ContextProvider.getContext() 46 | 47 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 48 | // Check if the permission is already granted 49 | if (hasInstallPermission()) { 50 | onGranted() 51 | } else { 52 | val requestId = PermissionCallbackManager.registerCallbacks(onGranted, onDenied) 53 | 54 | // Create and launch PermissionActivity 55 | try { 56 | val intent = Intent(context, PermissionActivity::class.java).apply { 57 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 58 | putExtra(PermissionActivity.EXTRA_REQUEST_ID, requestId) 59 | putExtra(PermissionActivity.EXTRA_REQUEST_TYPE, PermissionActivity.REQUEST_TYPE_INSTALL) 60 | } 61 | context.startActivity(intent) 62 | } catch (e: Exception) { 63 | Log.e("InstallPermission", "Error while launching PermissionActivity", e) 64 | PermissionCallbackManager.unregisterCallbacks(requestId) 65 | onDenied() 66 | } 67 | } 68 | } else { 69 | Log.d("InstallPermission", "Install permission not needed") 70 | onGranted() 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Location.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | import android.Manifest 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.util.Log 8 | import com.kdroid.androidcontextprovider.ContextProvider 9 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionActivity 10 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionCallbackManager 11 | 12 | /** 13 | * Checks if the app has the required location permissions. 14 | * 15 | * @param preciseLocation true to check for ACCESS_FINE_LOCATION, false to check only ACCESS_COARSE_LOCATION 16 | * @return true if the required permissions are granted 17 | */ 18 | fun hasLocationPermission(preciseLocation: Boolean = true): Boolean { 19 | val context = ContextProvider.getContext() 20 | val permission = if (preciseLocation) { 21 | Manifest.permission.ACCESS_FINE_LOCATION 22 | } else { 23 | Manifest.permission.ACCESS_COARSE_LOCATION 24 | } 25 | 26 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 27 | context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED 28 | } else { 29 | // For versions < M (23), permissions are granted at installation 30 | // if declared in the Manifest 31 | val packageInfo = context.packageManager.getPackageInfo( 32 | context.packageName, 33 | PackageManager.GET_PERMISSIONS 34 | ) 35 | 36 | packageInfo.requestedPermissions?.contains(permission) == true 37 | } 38 | } 39 | 40 | /** 41 | * Requests location permissions. 42 | * 43 | * This method first checks if the permissions are already granted. If yes, the onGranted 44 | * callback is invoked. Otherwise, it triggers a permission request via PermissionActivity, 45 | * and the appropriate callback (onGranted or onDenied) is invoked based on the user's action. 46 | * 47 | * Note: Ensure you add the following permissions in your AndroidManifest.xml: 48 | * 49 | * 50 | * 51 | * @param preciseLocation true to request ACCESS_FINE_LOCATION, false for ACCESS_COARSE_LOCATION only 52 | * @param onGranted Callback invoked when the permission is granted 53 | * @param onDenied Callback invoked when the permission is denied 54 | */ 55 | fun requestLocationPermission( 56 | preciseLocation: Boolean = true, 57 | onGranted: () -> Unit, 58 | onDenied: () -> Unit 59 | ) { 60 | val context = ContextProvider.getContext() 61 | 62 | // Check if the permission is already granted 63 | if (hasLocationPermission(preciseLocation)) { 64 | onGranted() 65 | return 66 | } 67 | 68 | val requestId = PermissionCallbackManager.registerCallbacks(onGranted, onDenied) 69 | 70 | try { 71 | val intent = Intent(context, PermissionActivity::class.java).apply { 72 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 73 | putExtra(PermissionActivity.EXTRA_REQUEST_ID, requestId) 74 | putExtra(PermissionActivity.EXTRA_REQUEST_TYPE, PermissionActivity.REQUEST_TYPE_LOCATION) 75 | putExtra(PermissionActivity.EXTRA_PRECISE_LOCATION, preciseLocation) 76 | } 77 | context.startActivity(intent) 78 | } catch (e: Exception) { 79 | Log.e("LocationPermission", "Error launching PermissionActivity", e) 80 | PermissionCallbackManager.unregisterCallbacks(requestId) 81 | onDenied() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Notification.android.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | import android.Manifest 4 | import android.app.NotificationManager 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import android.os.Build 9 | import android.util.Log 10 | import androidx.core.content.ContextCompat 11 | import com.kdroid.androidcontextprovider.ContextProvider 12 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionActivity 13 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionCallbackManager 14 | 15 | /** 16 | * Checks if the application has permission to post notifications. 17 | * 18 | * On devices running Android 13 (API level 33) or higher, this method 19 | * verifies whether the app has been granted the `POST_NOTIFICATIONS` 20 | * permission. On lower versions, it returns true by default as this 21 | * permission is not required. 22 | * 23 | * @return true if the notification permission is granted, or if the 24 | * platform version is lower than API level 33. 25 | */ 26 | actual fun hasNotificationPermission(): Boolean { 27 | val context = ContextProvider.getContext() 28 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 29 | val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 30 | notificationManager.areNotificationsEnabled() 31 | } else { 32 | true 33 | } 34 | } 35 | 36 | /** 37 | * Requests notification permission for the application. On Android 13 38 | * (API 33) and above, this method initiates an intent to the system 39 | * settings screen where the user can grant the notification permission. 40 | * 41 | * Note: Ensure to add 42 | * in the app's manifest file. 43 | * 44 | */ 45 | actual fun requestNotificationPermission( 46 | onGranted: () -> Unit, 47 | onDenied: () -> Unit, 48 | ) { 49 | val context = ContextProvider.getContext() 50 | 51 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 52 | when (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)) { 53 | PackageManager.PERMISSION_GRANTED -> { 54 | onGranted() 55 | } 56 | else -> { 57 | val requestId = PermissionCallbackManager.registerCallbacks(onGranted, onDenied) 58 | 59 | try { 60 | val intent = Intent(context, PermissionActivity::class.java).apply { 61 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 62 | putExtra(PermissionActivity.EXTRA_REQUEST_ID, requestId) 63 | putExtra(PermissionActivity.EXTRA_REQUEST_TYPE, PermissionActivity.REQUEST_TYPE_NOTIFICATION) 64 | } 65 | 66 | context.startActivity(intent) 67 | } catch (e: Exception) { 68 | Log.e("NotificationPermission", "Error while launching PermissionActivity", e) 69 | e.printStackTrace() 70 | PermissionCallbackManager.unregisterCallbacks(requestId) 71 | onDenied() 72 | } 73 | } 74 | } 75 | } else { 76 | Log.d("NotificationPermission", "No notification permission required for this Android version.") 77 | onGranted() 78 | } 79 | } -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Overlay.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | import android.content.Intent 4 | import android.os.Build 5 | import android.provider.Settings 6 | import android.util.Log 7 | import com.kdroid.androidcontextprovider.ContextProvider 8 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionActivity 9 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionCallbackManager 10 | 11 | /** 12 | * Checks if the application has permission to draw overlays on top of other apps. 13 | * 14 | * On devices running Android Marshmallow (API level 23) or higher, this method verifies 15 | * whether the app has been granted the `SYSTEM_ALERT_WINDOW` permission. On lower 16 | * versions, it returns true by default as this permission is not required. 17 | * 18 | * @return true if the overlay permission is granted, or if the platform version is lower than API level 23. 19 | */ 20 | fun hasOverlayPermission(): Boolean { 21 | val context = ContextProvider.getContext() 22 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 23 | Settings.canDrawOverlays(context) 24 | } else { 25 | true 26 | } 27 | } 28 | 29 | /** 30 | * Requests overlay permission for the application. On Android M (API 23) and above, this method 31 | * initiates an intent to the system settings screen where the user can grant the overlay permission. 32 | * 33 | * Note: Ensure to add the following permission in the app's manifest file: 34 | * 35 | * 36 | * @param onGranted Callback executed when the permission is granted. 37 | * @param onDenied Callback executed when the permission is denied. 38 | */ 39 | fun requestOverlayPermission( 40 | onGranted: () -> Unit, 41 | onDenied: () -> Unit, 42 | ) { 43 | val context = ContextProvider.getContext() 44 | 45 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 46 | // Check if the permission is already granted 47 | if (hasOverlayPermission()) { 48 | onGranted() 49 | } else { 50 | val requestId = PermissionCallbackManager.registerCallbacks(onGranted, onDenied) 51 | 52 | // Create and launch PermissionActivity 53 | try { 54 | val intent = Intent(context, PermissionActivity::class.java).apply { 55 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 56 | putExtra(PermissionActivity.EXTRA_REQUEST_ID, requestId) 57 | putExtra(PermissionActivity.EXTRA_REQUEST_TYPE, PermissionActivity.REQUEST_TYPE_OVERLAY) 58 | } 59 | context.startActivity(intent) 60 | } catch (e: Exception) { 61 | Log.e("OverlayPermission", "Error while launching PermissionActivity", e) 62 | PermissionCallbackManager.unregisterCallbacks(requestId) 63 | onDenied() 64 | } 65 | } 66 | } else { 67 | Log.d("OverlayPermission", "Overlay permission not needed") 68 | onGranted() 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/ReadContacts.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | import android.Manifest 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.util.Log 8 | import androidx.core.content.ContextCompat 9 | import com.kdroid.androidcontextprovider.ContextProvider 10 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionActivity 11 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionCallbackManager 12 | 13 | /** 14 | * Determines whether the app has been granted permission to read contacts. 15 | * 16 | * On devices running Android M (API level 23) or higher, this method checks 17 | * if the `READ_CONTACTS` permission has been granted. On older versions, the 18 | * permission is automatically granted. 19 | * 20 | * @return true if the `READ_CONTACTS` permission is granted, or if the platform 21 | * version is lower than API level 23. 22 | */ 23 | fun hasReadContactsPermission(): Boolean { 24 | val context = ContextProvider.getContext() 25 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 26 | ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == 27 | PackageManager.PERMISSION_GRANTED 28 | } else { 29 | true // On older versions of Android, permission is automatically granted. 30 | } 31 | } 32 | 33 | /** 34 | * Requests the `READ_CONTACTS` permission from the user. If the permission is already granted, 35 | * the `onGranted` callback is invoked. Otherwise, a request for the permission is initiated. 36 | * Note: Ensure to add the following permission in the app's manifest file: 37 | * 38 | * 39 | * @param onGranted A callback function executed when the `READ_CONTACTS` permission is granted. 40 | * @param onDenied A callback function executed when the `READ_CONTACTS` permission is denied. 41 | */ 42 | fun requestReadContactsPermission( 43 | onGranted: () -> Unit, 44 | onDenied: () -> Unit, 45 | ) { 46 | val context = ContextProvider.getContext() 47 | 48 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 49 | if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) 50 | == PackageManager.PERMISSION_GRANTED) { 51 | onGranted() 52 | } else { 53 | val requestId = PermissionCallbackManager.registerCallbacks(onGranted, onDenied) 54 | try { 55 | val intent = Intent(context, PermissionActivity::class.java).apply { 56 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 57 | putExtra(PermissionActivity.EXTRA_REQUEST_ID, requestId) 58 | putExtra(PermissionActivity.EXTRA_REQUEST_TYPE, PermissionActivity.REQUEST_TYPE_READ_CONTACTS) 59 | } 60 | context.startActivity(intent) 61 | } catch (e: Exception) { 62 | Log.e("ReadContacts", "Error while launching PermissionActivity", e) 63 | PermissionCallbackManager.unregisterCallbacks(requestId) 64 | onDenied() 65 | } 66 | } 67 | } else { 68 | onGranted() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/ReadExternalStorage.kt: -------------------------------------------------------------------------------- 1 | // ReadExternalStoragePermission.kt 2 | package io.github.kdroidfilter.platformtools.permissionhandler 3 | 4 | import android.Manifest 5 | import android.content.Intent 6 | import android.content.pm.PackageManager 7 | import android.os.Build 8 | import android.util.Log 9 | import androidx.core.content.ContextCompat 10 | import com.kdroid.androidcontextprovider.ContextProvider 11 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionActivity 12 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionCallbackManager 13 | 14 | enum class MediaType { 15 | IMAGES, 16 | VIDEO, 17 | AUDIO 18 | } 19 | 20 | /** 21 | * Checks if the application has permission to read external storage or specific media types. 22 | * 23 | * For Android versions below 13, it checks for `READ_EXTERNAL_STORAGE`. 24 | * For Android 13 and above, it checks for specific media permissions. 25 | * 26 | * @param mediaTypes The specific media types to check permissions for (only applicable for Android 13+). 27 | * @return true if all required permissions are granted, false otherwise. 28 | */ 29 | fun hasReadExternalStoragePermission(mediaTypes: Set = emptySet()): Boolean { 30 | val context = ContextProvider.getContext() 31 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 32 | // Android 13 and above: Check specific media permissions 33 | mediaTypes.all { mediaType -> 34 | when (mediaType) { 35 | MediaType.IMAGES -> ContextCompat.checkSelfPermission( 36 | context, 37 | Manifest.permission.READ_MEDIA_IMAGES 38 | ) == PackageManager.PERMISSION_GRANTED 39 | 40 | MediaType.VIDEO -> ContextCompat.checkSelfPermission( 41 | context, 42 | Manifest.permission.READ_MEDIA_VIDEO 43 | ) == PackageManager.PERMISSION_GRANTED 44 | 45 | MediaType.AUDIO -> ContextCompat.checkSelfPermission( 46 | context, 47 | Manifest.permission.READ_MEDIA_AUDIO 48 | ) == PackageManager.PERMISSION_GRANTED 49 | } 50 | } 51 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 52 | // Below Android 13 but Android M and above: Check READ_EXTERNAL_STORAGE 53 | ContextCompat.checkSelfPermission( 54 | context, 55 | Manifest.permission.READ_EXTERNAL_STORAGE 56 | ) == PackageManager.PERMISSION_GRANTED 57 | } else { 58 | // Permissions are automatically granted on devices below Android M 59 | true 60 | } 61 | } 62 | 63 | /** 64 | * Requests read external storage permission for the application. 65 | * 66 | * For Android versions below 13, it requests `READ_EXTERNAL_STORAGE`. 67 | * For Android 13 and above, it requests specific media permissions based on the provided media types. 68 | * 69 | * Note: Ensure to add the necessary permissions in the app's manifest file: 70 | * - For Android <13: 71 | * 72 | * - For Android 13+: 73 | * 74 | * 75 | * 76 | * 77 | * @param mediaTypes The specific media types to request permissions for (only applicable for Android 13+). 78 | * @param onGranted Callback to be invoked if all requested permissions are granted. 79 | * @param onDenied Callback to be invoked if any of the requested permissions are denied. 80 | */ 81 | fun requestReadExternalStoragePermission( 82 | mediaTypes: Set = emptySet(), 83 | onGranted: () -> Unit, 84 | onDenied: () -> Unit, 85 | ) { 86 | val context = ContextProvider.getContext() 87 | 88 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 89 | // Android 13 and above: Request specific media permissions 90 | if (mediaTypes.isEmpty()) { 91 | Log.e("ReadExternalStorage", "No media types specified for Android 13+") 92 | onDenied() 93 | return 94 | } 95 | 96 | val permissionsToRequest = mediaTypes.map { mediaType -> 97 | when (mediaType) { 98 | MediaType.IMAGES -> Manifest.permission.READ_MEDIA_IMAGES 99 | MediaType.VIDEO -> Manifest.permission.READ_MEDIA_VIDEO 100 | MediaType.AUDIO -> Manifest.permission.READ_MEDIA_AUDIO 101 | } 102 | }.toSet() 103 | 104 | val allGranted = permissionsToRequest.all { permission -> 105 | ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED 106 | } 107 | 108 | if (allGranted) { 109 | onGranted() 110 | return 111 | } 112 | 113 | val requestId = PermissionCallbackManager.registerCallbacks(onGranted, onDenied) 114 | 115 | try { 116 | val intent = Intent(context, PermissionActivity::class.java).apply { 117 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 118 | putExtra(PermissionActivity.EXTRA_REQUEST_ID, requestId) 119 | putExtra(PermissionActivity.EXTRA_REQUEST_TYPE, PermissionActivity.REQUEST_TYPE_READ_MEDIA) 120 | putExtra(PermissionActivity.EXTRA_MEDIA_TYPES, mediaTypes.map { it.name }.toTypedArray()) 121 | } 122 | 123 | context.startActivity(intent) 124 | } catch (e: Exception) { 125 | Log.e("ReadExternalStorage", "Error while launching PermissionActivity", e) 126 | e.printStackTrace() 127 | PermissionCallbackManager.unregisterCallbacks(requestId) 128 | onDenied() 129 | } 130 | 131 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 132 | // Below Android 13 but Android M and above: Request READ_EXTERNAL_STORAGE 133 | if (ContextCompat.checkSelfPermission( 134 | context, 135 | Manifest.permission.READ_EXTERNAL_STORAGE 136 | ) == PackageManager.PERMISSION_GRANTED 137 | ) { 138 | onGranted() 139 | return 140 | } 141 | 142 | val requestId = PermissionCallbackManager.registerCallbacks(onGranted, onDenied) 143 | 144 | try { 145 | val intent = Intent(context, PermissionActivity::class.java).apply { 146 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 147 | putExtra(PermissionActivity.EXTRA_REQUEST_ID, requestId) 148 | putExtra(PermissionActivity.EXTRA_REQUEST_TYPE, PermissionActivity.REQUEST_TYPE_READ_EXTERNAL_STORAGE) 149 | } 150 | 151 | context.startActivity(intent) 152 | } catch (e: Exception) { 153 | Log.e("ReadExternalStorage", "Error while launching PermissionActivity", e) 154 | e.printStackTrace() 155 | PermissionCallbackManager.unregisterCallbacks(requestId) 156 | onDenied() 157 | } 158 | 159 | } else { 160 | // Permissions are automatically granted on devices below Android M 161 | Log.d("ReadExternalStorage", "No read external storage permission required for this Android version.") 162 | onGranted() 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/RecordAudio.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | import android.Manifest 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.util.Log 8 | import androidx.core.content.ContextCompat 9 | import com.kdroid.androidcontextprovider.ContextProvider 10 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionActivity 11 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionCallbackManager 12 | 13 | /** 14 | * Checks if the application has permission to record audio. 15 | * 16 | * This method verifies whether the app has been granted the `RECORD_AUDIO` permission. 17 | * 18 | * @return true if the audio recording permission is granted, false otherwise. 19 | */ 20 | fun hasRecordAudioPermission(): Boolean { 21 | val context = ContextProvider.getContext() 22 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 23 | ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED 24 | } else { 25 | // Permissions are automatically granted on devices below Android M 26 | true 27 | } 28 | } 29 | 30 | /** 31 | * Requests record audio permission for the application. 32 | * 33 | * This method initiates a permission request flow. If the permission is already granted, 34 | * it invokes the `onGranted` callback immediately. Otherwise, it starts the 35 | * `PermissionActivity` to request the permission from the user. 36 | * 37 | * Note: Ensure to add 38 | * 39 | * in the app's manifest file. 40 | * 41 | * @param onGranted Callback to be invoked if the permission is granted. 42 | * @param onDenied Callback to be invoked if the permission is denied. 43 | */ 44 | fun requestRecordAudioPermission( 45 | onGranted: () -> Unit, 46 | onDenied: () -> Unit, 47 | ) { 48 | val context = ContextProvider.getContext() 49 | 50 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 51 | when (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)) { 52 | PackageManager.PERMISSION_GRANTED -> { 53 | onGranted() 54 | } 55 | else -> { 56 | val requestId = PermissionCallbackManager.registerCallbacks(onGranted, onDenied) 57 | 58 | try { 59 | val intent = Intent(context, PermissionActivity::class.java).apply { 60 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 61 | putExtra(PermissionActivity.EXTRA_REQUEST_ID, requestId) 62 | putExtra(PermissionActivity.EXTRA_REQUEST_TYPE, PermissionActivity.REQUEST_TYPE_RECORD_AUDIO) 63 | } 64 | 65 | context.startActivity(intent) 66 | } catch (e: Exception) { 67 | Log.e("AudioPermission", "Error while launching PermissionActivity", e) 68 | e.printStackTrace() 69 | PermissionCallbackManager.unregisterCallbacks(requestId) 70 | onDenied() 71 | } 72 | } 73 | } 74 | } else { 75 | Log.d("AudioPermission", "No audio recording permission required for this Android version.") 76 | onGranted() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/WriteContacts.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | import android.Manifest 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.util.Log 8 | import androidx.core.content.ContextCompat 9 | import com.kdroid.androidcontextprovider.ContextProvider 10 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionActivity 11 | import io.github.kdroidfilter.platformtools.permissionhandler.manager.PermissionCallbackManager 12 | 13 | /** 14 | * Checks if the application has permission to write to contacts (WRITE_CONTACTS). 15 | * 16 | * @return true if permission is granted, false otherwise. 17 | */ 18 | fun hasWriteContactsPermission(): Boolean { 19 | val context = ContextProvider.getContext() 20 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 21 | ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) == 22 | PackageManager.PERMISSION_GRANTED 23 | } else { 24 | // Sur les anciennes versions d'Android, la permission est automatiquement accordée 25 | true 26 | } 27 | } 28 | 29 | /** 30 | * Requests permission to write to contacts (WRITE_CONTACTS). 31 | * 32 | * If permission is already granted, immediately invoke the [onGranted] callback. 33 | * Otherwise, launch the `PermissionActivity` to request permission from the user. 34 | * 35 | * Note: Ensure to add the following permission in the app's manifest file: 36 | * 37 | * 38 | * @param onGranted Callback invoked if permission is granted. 39 | * @param onDenied Callback invoked if permission is denied. 40 | */ 41 | fun requestWriteContactsPermission( 42 | onGranted: () -> Unit, 43 | onDenied: () -> Unit, 44 | ) { 45 | val context = ContextProvider.getContext() 46 | 47 | // Si la version d'Android >= M (6.0), on vérifie et/ou demande la permission 48 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 49 | if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) 50 | == PackageManager.PERMISSION_GRANTED 51 | ) { 52 | // Permission déjà accordée 53 | onGranted() 54 | } else { 55 | // On enregistre les callbacks (onGranted, onDenied) pour y accéder depuis PermissionActivity 56 | val requestId = PermissionCallbackManager.registerCallbacks(onGranted, onDenied) 57 | try { 58 | // On lance la PermissionActivity, qui gère la demande de permission 59 | val intent = Intent(context, PermissionActivity::class.java).apply { 60 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 61 | putExtra(PermissionActivity.EXTRA_REQUEST_ID, requestId) 62 | putExtra( 63 | PermissionActivity.EXTRA_REQUEST_TYPE, 64 | PermissionActivity.REQUEST_TYPE_WRITE_CONTACTS 65 | ) 66 | } 67 | context.startActivity(intent) 68 | } catch (e: Exception) { 69 | // En cas de problème (ex. Activity non trouvée), on nettoie et on invoque onDenied() 70 | Log.e("WriteContacts", "Error while launching PermissionActivity", e) 71 | PermissionCallbackManager.unregisterCallbacks(requestId) 72 | onDenied() 73 | } 74 | } 75 | } else { 76 | // Sur les anciennes versions, aucune vérification n'est nécessaire 77 | onGranted() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/manager/PermissionCallbackManager.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler.manager 2 | 3 | /** 4 | * Manages permission callbacks for asynchronous request handling. 5 | * 6 | * This object is responsible for storing and managing callbacks associated with permission 7 | * requests. Callbacks are registered with unique request IDs, allowing specific actions 8 | * to be executed depending on whether the permission request is granted or denied. 9 | */ 10 | internal object PermissionCallbackManager { 11 | private val callbackMap = mutableMapOf Unit, () -> Unit>>() 12 | private var currentRequestId = 0 13 | 14 | @Synchronized 15 | fun registerCallbacks(onGranted: () -> Unit, onDenied: () -> Unit): Int { 16 | val requestId = currentRequestId++ 17 | callbackMap[requestId] = Pair(onGranted, onDenied) 18 | return requestId 19 | } 20 | 21 | @Synchronized 22 | fun unregisterCallbacks(requestId: Int) { 23 | callbackMap.remove(requestId) 24 | } 25 | 26 | fun getCallbacks(requestId: Int): Pair<() -> Unit, () -> Unit>? { 27 | return callbackMap[requestId] 28 | } 29 | } -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/androidMain/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/appleMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Installation.apple.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | 4 | 5 | actual fun hasInstallPermission(): Boolean = false 6 | 7 | 8 | actual fun requestInstallPermission(onGranted: () -> Unit, onDenied: () -> Unit) { 9 | onDenied() 10 | } -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Installation.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | expect fun hasInstallPermission(): Boolean 4 | 5 | expect fun requestInstallPermission(onGranted: () -> Unit, onDenied: () -> Unit) 6 | 7 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Notification.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | 4 | /** 5 | * Checks if the app has notification permission. 6 | * 7 | * @return true if the app has notification permission, false otherwise. 8 | */ 9 | expect fun hasNotificationPermission(): Boolean 10 | 11 | /** 12 | * Requests notification permission on the platform. 13 | * 14 | * @param onGranted Callback invoked when the permission is granted. 15 | * @param onDenied Callback invoked when the permission is denied. 16 | */ 17 | expect fun requestNotificationPermission( 18 | onGranted: () -> Unit, 19 | onDenied: () -> Unit, 20 | ) -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/jsMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Installation.js.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | actual fun hasInstallPermission(): Boolean = false 4 | actual fun requestInstallPermission(onGranted: () -> Unit, onDenied: () -> Unit) { 5 | onDenied() 6 | } -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/jsMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Notification.js.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | import kotlin.js.Promise 4 | 5 | /** 6 | * Represents the different permission states for notifications. 7 | */ 8 | internal enum class NotificationPermission(val value: String) { 9 | GRANTED("granted"), 10 | DENIED("denied"), 11 | DEFAULT("default"); 12 | 13 | companion object { 14 | fun fromValue(value: String): NotificationPermission = 15 | entries.firstOrNull { it.value == value } ?: DEFAULT 16 | } 17 | } 18 | 19 | 20 | /** 21 | * Checks if the application has permission to display notifications. 22 | * 23 | * @return true if the application has permission, false otherwise. 24 | */ 25 | actual fun hasNotificationPermission(): Boolean = 26 | (js("Notification.permission") as String) == NotificationPermission.GRANTED.value 27 | 28 | /** 29 | * Requests permission to display notifications. 30 | * 31 | * @param onGranted Callback executed if the permission is granted 32 | * @param onDenied Callback executed if the permission is denied 33 | */ 34 | actual fun requestNotificationPermission( 35 | onGranted: () -> Unit, 36 | onDenied: () -> Unit 37 | ) { 38 | handleNotificationPermissionRequest( 39 | onGranted = onGranted, 40 | onDenied = onDenied, 41 | onDefault = onDenied // By default, treat as denied 42 | ) 43 | } 44 | 45 | /** 46 | * Extended version of the permission request handling the "default" case. 47 | * 48 | * @param onGranted Callback executed if the permission is granted 49 | * @param onDenied Callback executed if the permission is denied 50 | * @param onDefault Callback executed if the response is neither granted nor denied 51 | */ 52 | fun requestNotificationPermission( 53 | onGranted: () -> Unit, 54 | onDenied: () -> Unit, 55 | onDefault: () -> Unit 56 | ) { 57 | handleNotificationPermissionRequest(onGranted, onDenied, onDefault) 58 | } 59 | 60 | /** 61 | * Internal function to handle the permission request. 62 | */ 63 | private fun handleNotificationPermissionRequest( 64 | onGranted: () -> Unit, 65 | onDenied: () -> Unit, 66 | onDefault: () -> Unit 67 | ) { 68 | try { 69 | val requestPermission = js("Notification.requestPermission()") as Promise 70 | requestPermission 71 | .then { permission -> 72 | when (NotificationPermission.fromValue(permission)) { 73 | NotificationPermission.GRANTED -> onGranted() 74 | NotificationPermission.DENIED -> onDenied() 75 | NotificationPermission.DEFAULT -> onDefault() 76 | } 77 | } 78 | .catch { error -> 79 | console.error("Error during permission request", error) 80 | onDenied() 81 | } 82 | } catch (e: Exception) { 83 | console.error("Unexpected error during permission request", e) 84 | onDenied() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/jvmMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Installation.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | actual fun hasInstallPermission(): Boolean = true 4 | 5 | actual fun requestInstallPermission(onGranted: () -> Unit, onDenied: () -> Unit) { 6 | onGranted 7 | } -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/jvmMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Notification.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | actual fun hasNotificationPermission(): Boolean { 4 | return true 5 | } 6 | 7 | actual fun requestNotificationPermission(onGranted: () -> Unit, onDenied: () -> Unit) { 8 | //No need to request Permission 9 | onGranted() 10 | } -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/nativeMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Notification.native.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | import platform.UserNotifications.* 4 | import kotlin.concurrent.AtomicInt 5 | 6 | /** 7 | * Checks if the app has permission to display notifications on iOS. 8 | * 9 | * This function uses `UNUserNotificationCenter` to retrieve the current notification settings and determines 10 | * if the authorization status is either `authorized` or `provisional`. 11 | * 12 | * Note: This function actively waits for the asynchronous callback to complete, which may cause a block 13 | * in the current thread. Use with caution in a production environment. 14 | * 15 | * @return `true` if notification permissions are granted or provisionally granted, `false` otherwise. 16 | */ 17 | actual fun hasNotificationPermission(): Boolean { 18 | var isAuthorized = false 19 | val semaphore = AtomicInt(0) 20 | 21 | UNUserNotificationCenter.currentNotificationCenter().getNotificationSettingsWithCompletionHandler { settings -> 22 | isAuthorized = settings?.authorizationStatus == UNAuthorizationStatusAuthorized || 23 | settings?.authorizationStatus == UNAuthorizationStatusProvisional 24 | semaphore.value = 1 25 | } 26 | 27 | // Wait for the callback to be executed (synchronization needed because iOS is asynchronous) 28 | while (semaphore.value == 0) { 29 | // Active loop to wait for the result 30 | } 31 | 32 | return isAuthorized 33 | } 34 | 35 | 36 | /** 37 | * Requests notification permission from the user on iOS. 38 | * 39 | * This function uses `UNUserNotificationCenter` to request authorization for notifications with options 40 | * for alerts, sounds, and badges. 41 | * 42 | * @param onGranted Callback to be executed if the user grants notification permissions. 43 | * @param onDenied Callback to be executed if the user denies notification permissions. 44 | * 45 | * Required Configuration in `Info.plist`: 46 | * Add the following keys to your `Info.plist` file to provide a clear description to the user when the app 47 | * requests notification permissions: 48 | * 49 | * ```xml 50 | * NSLocationWhenInUseUsageDescription 51 | * We need access to notifications to keep you updated about important events. 52 | * ``` 53 | * 54 | * If you are using remote notifications, make sure to enable the "Push Notifications" capability 55 | * in your Xcode project. 56 | */ 57 | actual fun requestNotificationPermission(onGranted: () -> Unit, onDenied: () -> Unit) { 58 | val notificationCenter = UNUserNotificationCenter.currentNotificationCenter() 59 | val options: ULong = UNAuthorizationOptionAlert or UNAuthorizationOptionSound or UNAuthorizationOptionBadge 60 | 61 | notificationCenter.requestAuthorizationWithOptions(options) { granted, error -> 62 | if (granted) { 63 | onGranted() 64 | } else { 65 | onDenied() 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/wasmJsMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Installation.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | 4 | actual fun hasInstallPermission(): Boolean = false 5 | actual fun requestInstallPermission(onGranted: () -> Unit, onDenied: () -> Unit) { 6 | onDenied() 7 | } -------------------------------------------------------------------------------- /platformtools/permissionhandler/src/wasmJsMain/kotlin/io/github/kdroidfilter/platformtools/permissionhandler/Notification.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.permissionhandler 2 | 3 | import io.github.kdroidfilter.platformtools.permissionhandler.NotificationPermission.entries 4 | import kotlin.js.Promise 5 | 6 | 7 | /** 8 | * Exposes Notification.permission in Kotlin/Wasm. 9 | */ 10 | @JsFun("() => Notification.permission") 11 | private external fun getNotificationPermissionValue(): String 12 | 13 | /** 14 | * Exposes Notification.requestPermission() in Kotlin/Wasm. 15 | */ 16 | @JsFun("() => Notification.requestPermission()") 17 | private external fun requestNotificationPermissionJs(): Promise 18 | 19 | internal enum class NotificationPermission(val value: String) { 20 | GRANTED("granted"), 21 | DENIED("denied"), 22 | DEFAULT("default"); 23 | 24 | companion object { 25 | fun fromValue(value: String): NotificationPermission = 26 | entries.firstOrNull { it.value == value } ?: DEFAULT 27 | } 28 | } 29 | 30 | /** 31 | * Checks if the application has permission to display notifications. 32 | * 33 | * @return true if the application has permission, false otherwise. 34 | */ 35 | actual fun hasNotificationPermission(): Boolean { 36 | // Call the external function instead of js("Notification.permission") 37 | val currentPermission = getNotificationPermissionValue() 38 | return currentPermission == NotificationPermission.GRANTED.value 39 | } 40 | 41 | /** 42 | * Requests permission to display notifications. 43 | * 44 | * @param onGranted Callback executed if the permission is granted 45 | * @param onDenied Callback executed if the permission is denied 46 | */ 47 | actual fun requestNotificationPermission( 48 | onGranted: () -> Unit, 49 | onDenied: () -> Unit 50 | ) { 51 | // We handle "default" as denied in the simpler function 52 | handleNotificationPermissionRequest( 53 | onGranted = onGranted, 54 | onDenied = onDenied, 55 | onDefault = onDenied 56 | ) 57 | } 58 | 59 | /** 60 | * Extended version of the permission request handling the "default" case. 61 | * 62 | * @param onGranted Callback executed if the permission is granted 63 | * @param onDenied Callback executed if the permission is denied 64 | * @param onDefault Callback executed if the response is neither granted nor denied 65 | */ 66 | fun requestNotificationPermission( 67 | onGranted: () -> Unit, 68 | onDenied: () -> Unit, 69 | onDefault: () -> Unit 70 | ) { 71 | handleNotificationPermissionRequest(onGranted, onDenied, onDefault) 72 | } 73 | 74 | /** 75 | * Internal function to handle the permission request. 76 | */ 77 | private fun handleNotificationPermissionRequest( 78 | onGranted: () -> Unit, 79 | onDenied: () -> Unit, 80 | onDefault: () -> Unit 81 | ) { 82 | try { 83 | val requestPermission = requestNotificationPermissionJs() 84 | requestPermission.then { permission -> 85 | // Safely cast JsAny to String 86 | val permString = permission as String 87 | 88 | when (NotificationPermission.fromValue(permString)) { 89 | NotificationPermission.GRANTED -> onGranted() 90 | NotificationPermission.DENIED -> onDenied() 91 | NotificationPermission.DEFAULT -> onDefault() 92 | } 93 | null // Explicitly return null to satisfy the lambda 94 | } 95 | } catch (e: Exception) { 96 | println("Unexpected error during permission request $e") 97 | onDenied() 98 | } 99 | } 100 | 101 | -------------------------------------------------------------------------------- /platformtools/releasefetcher/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | import org.jetbrains.dokka.gradle.DokkaTask 3 | 4 | plugins { 5 | alias(libs.plugins.multiplatform) 6 | alias(libs.plugins.android.library) 7 | alias(libs.plugins.vannitktech.maven.publish) 8 | alias(libs.plugins.kotlinx.serialization) 9 | } 10 | 11 | val libVersion : String by rootProject.extra 12 | 13 | group = "io.github.kdroidfilter.platformtools.releasefetcher" 14 | version = libVersion 15 | 16 | kotlin { 17 | jvmToolchain(17) 18 | 19 | androidTarget { publishLibraryVariants("release") } 20 | jvm() 21 | 22 | 23 | sourceSets { 24 | commonMain.dependencies { 25 | implementation(project(":platformtools:core")) 26 | implementation(libs.kotlinx.serialization.json) 27 | implementation(libs.kotlinx.coroutines.core) 28 | implementation(libs.ktor.client.core) 29 | implementation(libs.ktor.client.content.negotiation) 30 | implementation(libs.ktor.client.serialization) 31 | implementation(libs.ktor.client.logging) 32 | implementation(libs.ktor.client.cio) 33 | api(libs.semver) 34 | implementation(libs.slf4j.simple) 35 | implementation(libs.kermit) 36 | 37 | } 38 | 39 | commonTest.dependencies { 40 | implementation(kotlin("test")) 41 | } 42 | 43 | 44 | jvmMain { 45 | dependencies { 46 | implementation(libs.androidcontextprovider) 47 | 48 | } 49 | } 50 | 51 | androidMain { 52 | dependencies { 53 | implementation(libs.androidcontextprovider) 54 | } 55 | } 56 | 57 | 58 | } 59 | 60 | //https://kotlinlang.org/docs/native-objc-interop.html#export-of-kdoc-comments-to-generated-objective-c-headers 61 | targets.withType { 62 | compilations["main"].compileTaskProvider.configure { 63 | compilerOptions { 64 | freeCompilerArgs.add("-Xexport-kdoc") 65 | } 66 | } 67 | } 68 | 69 | } 70 | 71 | android { 72 | namespace = "io.github.kdroidfilter.platformtools.releasefetcher" 73 | compileSdk = 35 74 | 75 | defaultConfig { 76 | minSdk = 21 77 | } 78 | } 79 | 80 | mavenPublishing { 81 | coordinates( 82 | groupId = "io.github.kdroidfilter", 83 | artifactId = "platformtools.releasefetcher", 84 | version = version.toString() 85 | ) 86 | 87 | pom { 88 | name.set("PlatformTools ReleaseFetcher") 89 | description.set("A module for Platform Tools library to manage and fetch releases from many sources (Only Github for now).") 90 | inceptionYear.set("2025") 91 | url.set("https://github.com/kdroidFilter/") 92 | 93 | licenses { 94 | license { 95 | name.set("MIT License") 96 | url.set("https://opensource.org/licenses/MIT") 97 | } 98 | } 99 | 100 | developers { 101 | developer { 102 | id.set("kdroidfilter") 103 | name.set("Elyahou Hadass") 104 | email.set("elyahou.hadass@gmail.com") 105 | } 106 | } 107 | 108 | scm { 109 | connection.set("scm:git:git://github.com/kdroidFilter/platformtools.git") 110 | developerConnection.set("scm:git:ssh://git@github.com:kdroidFilter/platformtools.git") 111 | url.set("https://github.com/kdroidFilter/platformtools") 112 | } 113 | } 114 | 115 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 116 | 117 | signAllPublications() 118 | } 119 | 120 | tasks.withType().configureEach { 121 | moduleName.set("Platforms Tools") 122 | offlineMode.set(true) 123 | } -------------------------------------------------------------------------------- /platformtools/releasefetcher/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/releasefetcher/config/Client.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.releasefetcher.config 2 | 3 | import io.github.kdroidfilter.platformtools.releasefetcher.downloader.ReleaseFetcherConfig 4 | import io.ktor.client.* 5 | import io.ktor.client.engine.cio.* 6 | import io.ktor.client.plugins.* 7 | 8 | // Global Ktor client configuration 9 | internal val client = HttpClient(CIO) { 10 | followRedirects = true 11 | 12 | install(HttpTimeout) { 13 | requestTimeoutMillis = ReleaseFetcherConfig.clientTimeOut 14 | } 15 | } -------------------------------------------------------------------------------- /platformtools/releasefetcher/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/releasefetcher/downloader/Downloader.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.releasefetcher.downloader 2 | 3 | import co.touchlab.kermit.Logger 4 | import co.touchlab.kermit.Logger.Companion.setMinSeverity 5 | import co.touchlab.kermit.Severity 6 | import io.github.kdroidfilter.platformtools.getCacheDir 7 | import io.github.kdroidfilter.platformtools.releasefetcher.config.client 8 | import io.ktor.client.call.* 9 | import io.ktor.client.plugins.* 10 | import io.ktor.client.request.* 11 | import io.ktor.http.* 12 | import io.ktor.utils.io.* 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.withContext 15 | import java.io.File 16 | 17 | private val logger = Logger.withTag("Downloader").apply { setMinSeverity(Severity.Warn) } 18 | 19 | 20 | /** 21 | * Downloader is responsible for handling file downloads from a given URL. 22 | * This class provides functionality to asynchronously download files, report 23 | * download progress, and handle errors that may occur during the download process. 24 | */ 25 | class Downloader { 26 | 27 | /** 28 | * Downloads an application file from the provided URL and tracks the download progress. 29 | * 30 | * @param downloadUrl The URL from which the application will be downloaded. 31 | * @param onProgress A callback function reporting download progress percentage and the downloaded file (if available). 32 | * The percentage is a `Double` ranging from 0.0 to 100.0, or -1.0 in case of errors. 33 | * The `file` parameter is the local file being downloaded, or `null` during download progress updates. 34 | * @return A `Boolean` indicating whether the download was successful. 35 | * Returns `true` if the download completes successfully, otherwise `false`. 36 | */ 37 | suspend fun downloadApp( 38 | downloadUrl: String, 39 | onProgress: (percentage: Double, file: File?) -> Unit 40 | ): Boolean { 41 | val bufferSize = ReleaseFetcherConfig.downloaderBufferSize 42 | logger.d { "Starting download from URL: $downloadUrl" } 43 | 44 | val fileName = downloadUrl.substringAfterLast('/').substringBefore('?') 45 | val cacheDir = getCacheDir() 46 | val destinationFile = File(cacheDir, fileName) 47 | 48 | onProgress(0.0, null) 49 | logger.d { "Download initialized: 0%" } 50 | 51 | return try { 52 | val response = client.get(downloadUrl) { 53 | onDownload { bytesSentTotal, contentLength -> 54 | val progress = if (contentLength != null && contentLength > 0) { 55 | (bytesSentTotal * 100.0 / contentLength) 56 | } else 0.0 57 | logger.d { "Progress: $bytesSentTotal / $contentLength bytes" } 58 | onProgress(progress, null) 59 | } 60 | } 61 | 62 | if (response.status.isSuccess()) { 63 | val channel: ByteReadChannel = response.body() 64 | val contentLength = response.contentLength() ?: -1L 65 | logger.d { "Content length: $contentLength bytes" } 66 | 67 | withContext(Dispatchers.IO) { 68 | destinationFile.outputStream().buffered(bufferSize).use { output -> 69 | val buffer = ByteArray(bufferSize) 70 | while (!channel.isClosedForRead) { 71 | val bytesRead = channel.readAvailable(buffer) 72 | if (bytesRead == -1) break 73 | output.write(buffer, 0, bytesRead) 74 | } 75 | } 76 | } 77 | 78 | if (destinationFile.exists()) { 79 | logger.d { "Download completed. Size: ${destinationFile.length()} bytes" } 80 | onProgress(100.0, destinationFile) 81 | true 82 | } else { 83 | logger.e { "Error: File not created" } 84 | onProgress(-1.0, null) // Keep -1.0 for errors 85 | false 86 | } 87 | } else { 88 | logger.e { "Download failed: ${response.status}" } 89 | onProgress(-1.0, null) // Keep -1.0 for errors 90 | false 91 | } 92 | } catch (e: Exception) { 93 | logger.e(e) { "Download error: ${e.message}" } 94 | onProgress(-1.0, null) // Keep -1.0 for errors 95 | false 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /platformtools/releasefetcher/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/releasefetcher/downloader/ReleaseFetcherConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.releasefetcher.downloader 2 | 3 | import io.ktor.client.plugins.HttpTimeoutConfig 4 | 5 | /** 6 | * Configuration object for managing buffer settings used during operations like file downloads. 7 | * 8 | * This object contains parameters that control the behavior of the buffer, such as its size. The buffer size 9 | * is critical for managing resource usage and optimizing performance during data transfers. 10 | */ 11 | object ReleaseFetcherConfig { 12 | var downloaderBufferSize: Int = 2 * 1024 * 1024 13 | var clientTimeOut: Long = HttpTimeoutConfig.INFINITE_TIMEOUT_MS 14 | } -------------------------------------------------------------------------------- /platformtools/releasefetcher/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/releasefetcher/github/GitHubReleaseFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.releasefetcher.github 2 | 3 | import io.github.kdroidfilter.platformtools.OperatingSystem 4 | import io.github.kdroidfilter.platformtools.getAppVersion 5 | import io.github.kdroidfilter.platformtools.getOperatingSystem 6 | import io.github.kdroidfilter.platformtools.releasefetcher.config.client 7 | import io.github.kdroidfilter.platformtools.releasefetcher.github.model.Release 8 | import io.github.z4kn4fein.semver.toVersion 9 | import io.ktor.client.call.* 10 | import io.ktor.client.request.* 11 | import io.ktor.client.statement.* 12 | import io.ktor.http.* 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.withContext 15 | import kotlinx.serialization.json.Json 16 | 17 | class GitHubReleaseFetcher( 18 | private val owner: String, 19 | private val repo: String, 20 | ) { 21 | 22 | private val json = Json { ignoreUnknownKeys = true } 23 | 24 | /** 25 | * Fetches the latest release from the GitHub API using Ktor. 26 | */ 27 | suspend fun getLatestRelease(): Release? = withContext(Dispatchers.IO) { 28 | try { 29 | val response: HttpResponse = client.get("https://api.github.com/repos/$owner/$repo/releases/latest") 30 | 31 | if (response.status == HttpStatusCode.OK) { 32 | val responseBody: String = response.body() 33 | json.decodeFromString(responseBody) 34 | } else { 35 | // Handle different response codes as needed 36 | null 37 | } 38 | } catch (e: Exception) { 39 | // Log or handle the error as necessary 40 | e.printStackTrace() 41 | null 42 | } 43 | } 44 | 45 | /** 46 | * Checks for an update. If an update is available, executes [onUpdateNeeded] with the new version and changelog. 47 | */ 48 | suspend fun checkForUpdate( 49 | onUpdateNeeded: (latestVersion: String, changelog: String) -> Unit, 50 | ) { 51 | val latestRelease = getLatestRelease() 52 | if (latestRelease != null) { 53 | val currentVersion = getAppVersion().toVersion(strict = false) 54 | val latestVersion = latestRelease.tag_name.toVersion(strict = false) 55 | 56 | if (latestVersion > currentVersion) { 57 | onUpdateNeeded(latestVersion.toString(), latestRelease.body) 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * Returns the download link suitable for the current platform. 64 | */ 65 | fun getDownloadLinkForPlatform(release: Release): String? { 66 | val operatingSystemFileTypes = mapOf( 67 | OperatingSystem.ANDROID to ".apk", 68 | OperatingSystem.WINDOWS to ".msi", 69 | OperatingSystem.LINUX to ".deb", 70 | OperatingSystem.MACOS to ".dmg" 71 | ) 72 | 73 | val fileType = operatingSystemFileTypes[getOperatingSystem()] ?: return null 74 | 75 | // Find the corresponding asset 76 | val asset = release.assets.firstOrNull { it.name.endsWith(fileType, ignoreCase = true) } 77 | return asset?.browser_download_url 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /platformtools/releasefetcher/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/releasefetcher/github/model/Asset.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.releasefetcher.github.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Asset( 7 | val url: String, 8 | val id: Int, 9 | val node_id: String, 10 | val name: String, 11 | val label: String, 12 | val uploader: Uploader, 13 | val content_type: String, 14 | val state: String, 15 | val size: Int, 16 | val download_count: Int, 17 | val created_at: String, 18 | val updated_at: String, 19 | val browser_download_url: String 20 | ) -------------------------------------------------------------------------------- /platformtools/releasefetcher/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/releasefetcher/github/model/Author.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.releasefetcher.github.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Author( 7 | val login: String, 8 | val id: Int, 9 | val node_id: String, 10 | val avatar_url: String, 11 | val gravatar_id: String, 12 | val url: String, 13 | val html_url: String, 14 | val followers_url: String, 15 | val following_url: String, 16 | val gists_url: String, 17 | val starred_url: String, 18 | val subscriptions_url: String, 19 | val organizations_url: String, 20 | val repos_url: String, 21 | val events_url: String, 22 | val received_events_url: String, 23 | val type: String, 24 | val user_view_type: String, 25 | val site_admin: Boolean 26 | ) -------------------------------------------------------------------------------- /platformtools/releasefetcher/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/releasefetcher/github/model/Release.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.releasefetcher.github.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Release( 7 | val url: String, 8 | val assets_url: String, 9 | val upload_url: String, 10 | val html_url: String, 11 | val id: Int, 12 | val author: Author, 13 | val node_id: String, 14 | val tag_name: String, 15 | val target_commitish: String, 16 | val name: String, 17 | val draft: Boolean, 18 | val prerelease: Boolean, 19 | val created_at: String, 20 | val published_at: String, 21 | val assets: List, 22 | val tarball_url: String, 23 | val zipball_url: String, 24 | val body: String, 25 | val mentions_count: Int? = null 26 | ) -------------------------------------------------------------------------------- /platformtools/releasefetcher/src/commonMain/kotlin/io/github/kdroidfilter/platformtools/releasefetcher/github/model/Uploader.kt: -------------------------------------------------------------------------------- 1 | package io.github.kdroidfilter.platformtools.releasefetcher.github.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Uploader( 7 | val login: String, 8 | val id: Int, 9 | val node_id: String, 10 | val avatar_url: String, 11 | val gravatar_id: String, 12 | val url: String, 13 | val html_url: String, 14 | val followers_url: String, 15 | val following_url: String, 16 | val gists_url: String, 17 | val starred_url: String, 18 | val subscriptions_url: String, 19 | val organizations_url: String, 20 | val repos_url: String, 21 | val events_url: String, 22 | val received_events_url: String, 23 | val type: String, 24 | val user_view_type: String, 25 | val site_admin: Boolean 26 | ) 27 | 28 | -------------------------------------------------------------------------------- /sample/composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | 3 | plugins { 4 | alias(libs.plugins.multiplatform) 5 | alias(libs.plugins.compose.compiler) 6 | alias(libs.plugins.compose) 7 | alias(libs.plugins.android.application) 8 | } 9 | 10 | kotlin { 11 | jvmToolchain(17) 12 | 13 | androidTarget() 14 | jvm() 15 | 16 | 17 | sourceSets { 18 | commonMain.dependencies { 19 | implementation(compose.runtime) 20 | implementation(compose.foundation) 21 | implementation(compose.material3) 22 | implementation(compose.ui) 23 | implementation(compose.components.resources) 24 | implementation(compose.components.uiToolingPreview) 25 | implementation(libs.navigation.compose) 26 | implementation(project(":platformtools:core")) 27 | implementation(project(":platformtools:appmanager")) 28 | implementation(project(":platformtools:releasefetcher")) 29 | implementation(project(":platformtools:permissionhandler")) 30 | implementation(project(":platformtools:darkmodedetector")) 31 | } 32 | 33 | androidMain.dependencies { 34 | implementation(libs.androidx.activityCompose) 35 | } 36 | 37 | jvmMain.dependencies { 38 | implementation(compose.desktop.currentOs) 39 | } 40 | 41 | } 42 | } 43 | 44 | android { 45 | namespace = "sample.app" 46 | compileSdk = 35 47 | 48 | defaultConfig { 49 | minSdk = 21 50 | targetSdk = 35 51 | 52 | applicationId = "sample.app.androidApp" 53 | versionCode = 1 54 | versionName = "1.0.1" 55 | } 56 | } 57 | 58 | compose.desktop { 59 | application { 60 | mainClass = "MainKt" 61 | 62 | nativeDistributions { 63 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 64 | packageName = "sample" 65 | packageVersion = "1.0.1" 66 | macOS { 67 | jvmArgs( 68 | "-Dapple.awt.application.appearance=system" 69 | ) 70 | } 71 | jvmArgs( 72 | "-Dorg.slf4j.simpleLogger.defaultLogLevel=debug" 73 | ) 74 | } 75 | 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /sample/composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /sample/composeApp/src/androidMain/kotlin/sample/app/main.kt: -------------------------------------------------------------------------------- 1 | package sample.app 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import sample.app.permissionHandler.NotificationPermissionSample 7 | import sample.app.permissions.* 8 | 9 | class AppActivity : ComponentActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | setContent { 13 | val permissionScreenList = listOf( 14 | PermissionScreen("Notification", { NotificationPermissionSample() }), 15 | PermissionScreen("Installation", { InstallPermissionSample() }), 16 | PermissionScreen("Overlay", { OverlayPermissionSample() }), 17 | PermissionScreen("Location", { LocationPermissionSample() }), 18 | PermissionScreen("Background Location", { BackgroundLocationPermissionSample() }), 19 | PermissionScreen("Camera", { CameraPermissionSample() }), 20 | PermissionScreen("Contacts", { ContactsPermissionSample() }), 21 | PermissionScreen("Record Audio", { RecordAudioPermissionSample() }), 22 | PermissionScreen("Read External Storage", { ReadExternalStoragePermissionSample() }), 23 | PermissionScreen("Bluetooth", { BluetoothPermissionSample() }), 24 | 25 | ) 26 | App(permissionScreenList) 27 | } 28 | } 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /sample/composeApp/src/androidMain/kotlin/sample/app/permissions/BackgroundLocationPermissionSample.kt: -------------------------------------------------------------------------------- 1 | package sample.app.permissions 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.unit.dp 11 | import io.github.kdroidfilter.platformtools.permissionhandler.hasBackgroundLocationPermission 12 | import io.github.kdroidfilter.platformtools.permissionhandler.requestBackgroundLocationPermission 13 | 14 | @OptIn(ExperimentalMaterial3Api::class) 15 | @Composable 16 | fun BackgroundLocationPermissionSample() { 17 | val context = LocalContext.current 18 | var backgroundLocationGranted by remember { mutableStateOf(hasBackgroundLocationPermission()) } 19 | 20 | Scaffold( 21 | topBar = { 22 | TopAppBar(title = { Text("Background Location Permission") }) 23 | }, 24 | content = { padding -> 25 | Box( 26 | modifier = Modifier 27 | .fillMaxSize() 28 | .padding(padding), 29 | contentAlignment = Alignment.Center 30 | ) { 31 | Column( 32 | horizontalAlignment = Alignment.CenterHorizontally, 33 | verticalArrangement = Arrangement.spacedBy(16.dp) 34 | ) { 35 | // Background Location Status 36 | Column( 37 | horizontalAlignment = Alignment.CenterHorizontally 38 | ) { 39 | Text( 40 | text = if (backgroundLocationGranted) 41 | "Background location granted ✅" 42 | else 43 | "Background location required 🚫", 44 | style = MaterialTheme.typography.bodyLarge 45 | ) 46 | 47 | if (!backgroundLocationGranted) { 48 | Spacer(modifier = Modifier.height(8.dp)) 49 | Button( 50 | onClick = { 51 | requestBackgroundLocationPermission( 52 | onGranted = { 53 | backgroundLocationGranted = true 54 | Toast.makeText( 55 | context, 56 | "Background location granted", 57 | Toast.LENGTH_SHORT 58 | ).show() 59 | }, 60 | onDenied = { 61 | backgroundLocationGranted = false 62 | Toast.makeText( 63 | context, 64 | "Background location denied", 65 | Toast.LENGTH_SHORT 66 | ).show() 67 | } 68 | ) 69 | } 70 | ) { 71 | Text("Request Background Location") 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /sample/composeApp/src/androidMain/kotlin/sample/app/permissions/BluetoothPermissionSample.kt: -------------------------------------------------------------------------------- 1 | package sample.app.permissions 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.unit.dp 11 | import io.github.kdroidfilter.platformtools.permissionhandler.hasBluetoothPermission 12 | import io.github.kdroidfilter.platformtools.permissionhandler.requestBluetoothPermission 13 | 14 | @OptIn(ExperimentalMaterial3Api::class) 15 | @Composable 16 | fun BluetoothPermissionSample() { 17 | val context = LocalContext.current 18 | var permissionGranted by remember { mutableStateOf(hasBluetoothPermission()) } 19 | 20 | Scaffold( 21 | topBar = { 22 | TopAppBar(title = { Text("Bluetooth Permissions") }) 23 | }, 24 | content = { padding -> 25 | Box( 26 | modifier = Modifier 27 | .fillMaxSize() 28 | .padding(padding), 29 | contentAlignment = Alignment.Center 30 | ) { 31 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 32 | Text( 33 | text = if (permissionGranted) "Bluetooth permission granted ✅" else "Bluetooth permission required 🚫", 34 | style = MaterialTheme.typography.bodyLarge 35 | ) 36 | Spacer(modifier = Modifier.height(16.dp)) 37 | if (!permissionGranted) { 38 | Button( 39 | onClick = { 40 | requestBluetoothPermission( 41 | onGranted = { 42 | permissionGranted = true 43 | Toast.makeText(context, "Permission granted", Toast.LENGTH_SHORT).show() 44 | }, 45 | onDenied = { 46 | permissionGranted = false 47 | Toast.makeText(context, "Permission denied", Toast.LENGTH_SHORT).show() 48 | } 49 | ) 50 | } 51 | ) { 52 | Text("Request Permission") 53 | } 54 | } 55 | } 56 | } 57 | } 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /sample/composeApp/src/androidMain/kotlin/sample/app/permissions/CameraPermissionSample.kt: -------------------------------------------------------------------------------- 1 | package sample.app.permissions 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.unit.dp 11 | import io.github.kdroidfilter.platformtools.permissionhandler.hasCameraPermission 12 | import io.github.kdroidfilter.platformtools.permissionhandler.requestCameraPermission 13 | 14 | @OptIn(ExperimentalMaterial3Api::class) 15 | @Composable 16 | fun CameraPermissionSample() { 17 | val context = LocalContext.current 18 | var permissionGranted by remember { mutableStateOf(hasCameraPermission()) } 19 | 20 | Scaffold( 21 | topBar = { 22 | TopAppBar(title = { Text("Camera Permissions") }) 23 | }, 24 | content = { padding -> 25 | Box( 26 | modifier = Modifier 27 | .fillMaxSize() 28 | .padding(padding), 29 | contentAlignment = Alignment.Center 30 | ) { 31 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 32 | Text( 33 | text = if (permissionGranted) "Camera permission granted ✅" else "Camera permission required 🚫", 34 | style = MaterialTheme.typography.bodyLarge 35 | ) 36 | Spacer(modifier = Modifier.height(16.dp)) 37 | if (!permissionGranted) { 38 | Button( 39 | onClick = { 40 | requestCameraPermission( 41 | onGranted = { 42 | permissionGranted = true 43 | Toast.makeText(context, "Permission granted", Toast.LENGTH_SHORT).show() 44 | }, 45 | onDenied = { 46 | permissionGranted = false 47 | Toast.makeText(context, "Permission denied", Toast.LENGTH_SHORT).show() 48 | } 49 | ) 50 | } 51 | ) { 52 | Text("Request Permission") 53 | } 54 | } 55 | } 56 | } 57 | } 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /sample/composeApp/src/androidMain/kotlin/sample/app/permissions/ContactsPermissionSample.kt: -------------------------------------------------------------------------------- 1 | package sample.app.permissions 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.unit.dp 11 | import io.github.kdroidfilter.platformtools.permissionhandler.hasReadContactsPermission 12 | import io.github.kdroidfilter.platformtools.permissionhandler.requestReadContactsPermission 13 | import io.github.kdroidfilter.platformtools.permissionhandler.hasWriteContactsPermission 14 | import io.github.kdroidfilter.platformtools.permissionhandler.requestWriteContactsPermission 15 | 16 | @OptIn(ExperimentalMaterial3Api::class) 17 | @Composable 18 | fun ContactsPermissionSample() { 19 | val context = LocalContext.current 20 | 21 | var readContactsGranted by remember { 22 | mutableStateOf(hasReadContactsPermission()) 23 | } 24 | var writeContactsGranted by remember { 25 | mutableStateOf(hasWriteContactsPermission()) 26 | } 27 | 28 | Scaffold( 29 | topBar = { 30 | TopAppBar(title = { Text("Contacts Permissions") }) 31 | }, 32 | content = { padding -> 33 | Box( 34 | modifier = Modifier 35 | .fillMaxSize() 36 | .padding(padding), 37 | contentAlignment = Alignment.Center 38 | ) { 39 | Column( 40 | horizontalAlignment = Alignment.CenterHorizontally, 41 | verticalArrangement = Arrangement.spacedBy(16.dp) 42 | ) { 43 | 44 | // ---- READ CONTACTS ---- 45 | Column( 46 | horizontalAlignment = Alignment.CenterHorizontally, 47 | modifier = Modifier.padding(bottom = 24.dp) 48 | ) { 49 | Text( 50 | text = if (readContactsGranted) 51 | "Read contacts granted ✅" 52 | else 53 | "Read contacts required 🚫", 54 | style = MaterialTheme.typography.bodyLarge 55 | ) 56 | 57 | if (!readContactsGranted) { 58 | Spacer(modifier = Modifier.height(8.dp)) 59 | Button( 60 | onClick = { 61 | requestReadContactsPermission( 62 | onGranted = { 63 | readContactsGranted = true 64 | Toast.makeText( 65 | context, 66 | "Read contacts granted", 67 | Toast.LENGTH_SHORT 68 | ).show() 69 | }, 70 | onDenied = { 71 | readContactsGranted = false 72 | Toast.makeText( 73 | context, 74 | "Read contacts denied", 75 | Toast.LENGTH_SHORT 76 | ).show() 77 | } 78 | ) 79 | } 80 | ) { 81 | Text("Request Read Contacts Permission") 82 | } 83 | } 84 | } 85 | 86 | // ---- WRITE CONTACTS ---- 87 | Column( 88 | horizontalAlignment = Alignment.CenterHorizontally 89 | ) { 90 | Text( 91 | text = if (writeContactsGranted) 92 | "Write contacts granted ✅" 93 | else 94 | "Write contacts required 🚫", 95 | style = MaterialTheme.typography.bodyLarge 96 | ) 97 | 98 | if (!writeContactsGranted) { 99 | Spacer(modifier = Modifier.height(8.dp)) 100 | Button( 101 | onClick = { 102 | requestWriteContactsPermission( 103 | onGranted = { 104 | writeContactsGranted = true 105 | Toast.makeText( 106 | context, 107 | "Write contacts granted", 108 | Toast.LENGTH_SHORT 109 | ).show() 110 | }, 111 | onDenied = { 112 | writeContactsGranted = false 113 | Toast.makeText( 114 | context, 115 | "Write contacts denied", 116 | Toast.LENGTH_SHORT 117 | ).show() 118 | } 119 | ) 120 | } 121 | ) { 122 | Text("Request Write Contacts Permission") 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | ) 130 | } 131 | -------------------------------------------------------------------------------- /sample/composeApp/src/androidMain/kotlin/sample/app/permissions/InstallPermissionSample.kt: -------------------------------------------------------------------------------- 1 | package sample.app.permissions 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.unit.dp 11 | import io.github.kdroidfilter.platformtools.permissionhandler.hasInstallPermission 12 | import io.github.kdroidfilter.platformtools.permissionhandler.requestInstallPermission 13 | 14 | @OptIn(ExperimentalMaterial3Api::class) 15 | @Composable 16 | fun InstallPermissionSample() { 17 | val context = LocalContext.current 18 | var permissionGranted by remember { mutableStateOf(hasInstallPermission()) } 19 | 20 | Scaffold( 21 | topBar = { 22 | TopAppBar(title = { Text("Install Permissions") }) 23 | }, 24 | content = { padding -> 25 | Box( 26 | modifier = Modifier 27 | .fillMaxSize() 28 | .padding(padding), 29 | contentAlignment = Alignment.Center 30 | ) { 31 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 32 | Text( 33 | text = if (permissionGranted) "Install permission granted ✅" else "Install permission required 🚫", 34 | style = MaterialTheme.typography.bodyLarge 35 | ) 36 | Spacer(modifier = Modifier.height(16.dp)) 37 | if (!permissionGranted) { 38 | Button( 39 | onClick = { 40 | requestInstallPermission( 41 | onGranted = { 42 | permissionGranted = true 43 | Toast.makeText(context, "Permission granted", Toast.LENGTH_SHORT).show() 44 | }, 45 | onDenied = { 46 | permissionGranted = false 47 | Toast.makeText(context, "Permission denied", Toast.LENGTH_SHORT).show() 48 | } 49 | ) 50 | } 51 | ) { 52 | Text("Request Permission") 53 | } 54 | } 55 | } 56 | } 57 | } 58 | ) 59 | } 60 | 61 | -------------------------------------------------------------------------------- /sample/composeApp/src/androidMain/kotlin/sample/app/permissions/LocationPermissionSample.kt: -------------------------------------------------------------------------------- 1 | package sample.app.permissions 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.unit.dp 11 | import io.github.kdroidfilter.platformtools.permissionhandler.hasLocationPermission 12 | import io.github.kdroidfilter.platformtools.permissionhandler.requestLocationPermission 13 | 14 | @OptIn(ExperimentalMaterial3Api::class) 15 | @Composable 16 | fun LocationPermissionSample() { 17 | val context = LocalContext.current 18 | var preciseLocationGranted by remember { mutableStateOf(hasLocationPermission(preciseLocation = true)) } 19 | var approximateLocationGranted by remember { mutableStateOf(hasLocationPermission(preciseLocation = false)) } 20 | 21 | Scaffold( 22 | topBar = { 23 | TopAppBar(title = { Text("Location Permissions") }) 24 | }, 25 | content = { padding -> 26 | Box( 27 | modifier = Modifier 28 | .fillMaxSize() 29 | .padding(padding), 30 | contentAlignment = Alignment.Center 31 | ) { 32 | Column( 33 | horizontalAlignment = Alignment.CenterHorizontally, 34 | verticalArrangement = Arrangement.spacedBy(16.dp) 35 | ) { 36 | // Precise Location Status 37 | Column( 38 | horizontalAlignment = Alignment.CenterHorizontally, 39 | modifier = Modifier.padding(bottom = 24.dp) 40 | ) { 41 | Text( 42 | text = if (preciseLocationGranted) 43 | "Precise location granted ✅" 44 | else 45 | "Precise location required 🚫", 46 | style = MaterialTheme.typography.bodyLarge 47 | ) 48 | 49 | if (!preciseLocationGranted) { 50 | Spacer(modifier = Modifier.height(8.dp)) 51 | Button( 52 | onClick = { 53 | requestLocationPermission( 54 | preciseLocation = true, 55 | onGranted = { 56 | preciseLocationGranted = true 57 | Toast.makeText( 58 | context, 59 | "Precise location granted", 60 | Toast.LENGTH_SHORT 61 | ).show() 62 | }, 63 | onDenied = { 64 | preciseLocationGranted = false 65 | Toast.makeText( 66 | context, 67 | "Precise location denied", 68 | Toast.LENGTH_SHORT 69 | ).show() 70 | } 71 | ) 72 | } 73 | ) { 74 | Text("Request Precise Location") 75 | } 76 | } 77 | } 78 | 79 | // Approximate Location Status 80 | Column( 81 | horizontalAlignment = Alignment.CenterHorizontally 82 | ) { 83 | Text( 84 | text = if (approximateLocationGranted) 85 | "Approximate location granted ✅" 86 | else 87 | "Approximate location required 🚫", 88 | style = MaterialTheme.typography.bodyLarge 89 | ) 90 | 91 | if (!approximateLocationGranted) { 92 | Spacer(modifier = Modifier.height(8.dp)) 93 | Button( 94 | onClick = { 95 | requestLocationPermission( 96 | preciseLocation = false, 97 | onGranted = { 98 | approximateLocationGranted = true 99 | Toast.makeText( 100 | context, 101 | "Approximate location granted", 102 | Toast.LENGTH_SHORT 103 | ).show() 104 | }, 105 | onDenied = { 106 | approximateLocationGranted = false 107 | Toast.makeText( 108 | context, 109 | "Approximate location denied", 110 | Toast.LENGTH_SHORT 111 | ).show() 112 | } 113 | ) 114 | } 115 | ) { 116 | Text("Request Approximate Location") 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | ) 124 | } -------------------------------------------------------------------------------- /sample/composeApp/src/androidMain/kotlin/sample/app/permissions/OverlayPermissionSample.kt: -------------------------------------------------------------------------------- 1 | package sample.app.permissions 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material3.Button 11 | import androidx.compose.material3.ExperimentalMaterial3Api 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Scaffold 14 | import androidx.compose.material3.Text 15 | import androidx.compose.material3.TopAppBar 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.platform.LocalContext 24 | import androidx.compose.ui.unit.dp 25 | import io.github.kdroidfilter.platformtools.permissionhandler.hasOverlayPermission 26 | import io.github.kdroidfilter.platformtools.permissionhandler.requestOverlayPermission 27 | 28 | @OptIn(ExperimentalMaterial3Api::class) 29 | @Composable 30 | fun OverlayPermissionSample() { 31 | val context = LocalContext.current 32 | var permissionGranted by remember { mutableStateOf(hasOverlayPermission()) } 33 | 34 | Scaffold( 35 | topBar = { 36 | TopAppBar(title = { Text("Overlay Permissions") }) 37 | }, 38 | content = { padding -> 39 | Box( 40 | modifier = Modifier 41 | .fillMaxSize() 42 | .padding(padding), 43 | contentAlignment = Alignment.Center 44 | ) { 45 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 46 | Text( 47 | text = if (permissionGranted) "Overlay permission granted ✅" else "Overlay permission required 🚫", 48 | style = MaterialTheme.typography.bodyLarge 49 | ) 50 | Spacer(modifier = Modifier.height(16.dp)) 51 | if (!permissionGranted) { 52 | Button( 53 | onClick = { 54 | requestOverlayPermission( 55 | onGranted = { 56 | permissionGranted = true 57 | Toast.makeText(context, "Overlay permission granted", Toast.LENGTH_SHORT).show() 58 | }, 59 | onDenied = { 60 | permissionGranted = false 61 | Toast.makeText(context, "Overlay permission denied", Toast.LENGTH_SHORT).show() 62 | } 63 | ) 64 | } 65 | ) { 66 | Text("Request Permission") 67 | } 68 | } 69 | } 70 | } 71 | } 72 | ) 73 | } 74 | 75 | -------------------------------------------------------------------------------- /sample/composeApp/src/androidMain/kotlin/sample/app/permissions/RecordAudioPermissionSample.kt: -------------------------------------------------------------------------------- 1 | package sample.app.permissions 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.unit.dp 11 | import io.github.kdroidfilter.platformtools.permissionhandler.hasRecordAudioPermission 12 | import io.github.kdroidfilter.platformtools.permissionhandler.requestRecordAudioPermission 13 | 14 | @OptIn(ExperimentalMaterial3Api::class) 15 | @Composable 16 | fun RecordAudioPermissionSample() { 17 | val context = LocalContext.current 18 | var permissionGranted by remember { mutableStateOf(hasRecordAudioPermission()) } 19 | 20 | Scaffold( 21 | topBar = { 22 | TopAppBar(title = { Text("Record Audio Permissions") }) 23 | }, 24 | content = { padding -> 25 | Box( 26 | modifier = Modifier 27 | .fillMaxSize() 28 | .padding(padding), 29 | contentAlignment = Alignment.Center 30 | ) { 31 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 32 | Text( 33 | text = if (permissionGranted) "Record Audio permission granted ✅" else "Record Audio permission required 🚫", 34 | style = MaterialTheme.typography.bodyLarge 35 | ) 36 | Spacer(modifier = Modifier.height(16.dp)) 37 | if (!permissionGranted) { 38 | Button( 39 | onClick = { 40 | requestRecordAudioPermission( 41 | onGranted = { 42 | permissionGranted = true 43 | Toast.makeText(context, "Permission granted", Toast.LENGTH_SHORT).show() 44 | }, 45 | onDenied = { 46 | permissionGranted = false 47 | Toast.makeText(context, "Permission denied", Toast.LENGTH_SHORT).show() 48 | } 49 | ) 50 | } 51 | ) { 52 | Text("Request Permission") 53 | } 54 | } 55 | } 56 | } 57 | } 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /sample/composeApp/src/commonMain/kotlin/sample/app/App.kt: -------------------------------------------------------------------------------- 1 | package sample.app 2 | 3 | 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.Menu 9 | import androidx.compose.material3.* 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.rememberCoroutineScope 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.unit.sp 15 | import androidx.navigation.NavHostController 16 | import androidx.navigation.compose.NavHost 17 | import androidx.navigation.compose.composable 18 | import androidx.navigation.compose.rememberNavController 19 | import io.github.kdroidfilter.platformtools.darkmodedetector.isSystemInDarkMode 20 | import kotlinx.coroutines.CoroutineScope 21 | import kotlinx.coroutines.launch 22 | import sample.app.permissionHandler.NotificationPermissionSample 23 | 24 | data class Route( 25 | val title: String, 26 | val destination: String, 27 | val content : @Composable () -> Unit 28 | ) 29 | 30 | val permissionScreensList = listOf( 31 | PermissionScreen("Notification", { NotificationPermissionSample() }) 32 | ) 33 | 34 | @OptIn(ExperimentalMaterial3Api::class) 35 | @Composable 36 | fun App(permissionScreensList: List = sample.app.permissionScreensList) { 37 | val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) 38 | val navController = rememberNavController() 39 | val scope = rememberCoroutineScope() 40 | 41 | val routes = listOf( 42 | Route("Core", "core", { CoreDemo() }), 43 | Route("App Manager", "appmanager", { AppManagerDemo() }), 44 | Route("Release Fetcher", "releasefetcher", { ReleaseFetcherDemo() }), 45 | Route("Permission Handler", "permissionhandler", {PermissionHandlerDemo(permissionScreensList)}) 46 | ) 47 | 48 | MaterialTheme( 49 | colorScheme = if (isSystemInDarkMode()) darkColorScheme() else lightColorScheme(), 50 | typography = Typography(), 51 | content = { 52 | ModalNavigationDrawer( 53 | drawerContent = { 54 | DrawerContent(navController, scope, drawerState, routes) 55 | }, 56 | drawerState = drawerState, 57 | ) { 58 | Scaffold( 59 | topBar = { 60 | TopAppBar( 61 | title = { Text("Platform Tools Demo") }, 62 | navigationIcon = { 63 | IconButton(onClick = { 64 | scope.launch { 65 | drawerState.open() 66 | } 67 | }) { 68 | Icon(Icons.Default.Menu, contentDescription = "Open Drawer") 69 | } 70 | } 71 | ) 72 | } 73 | ) { innerPadding -> 74 | Box(modifier = Modifier.padding(innerPadding)) { 75 | NavHost(navController = navController, startDestination = routes.first().destination) { 76 | routes.forEach { route -> 77 | composable(route.destination) { 78 | route.content() 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | ) 87 | } 88 | 89 | @Composable 90 | private fun DrawerContent( 91 | navController: NavHostController, 92 | scope: CoroutineScope, 93 | drawerState: DrawerState, 94 | routes: List 95 | ) { 96 | ModalDrawerSheet { 97 | Column(modifier = Modifier.padding(16.dp)) { 98 | Text(text = "Modules", fontSize = 24.sp, modifier = Modifier.padding(bottom = 16.dp)) 99 | HorizontalDivider() 100 | 101 | routes.forEach { route -> 102 | NavigationDrawerItem( 103 | label = { Text(route.title, fontSize = 18.sp) }, 104 | selected = false, 105 | onClick = { 106 | scope.launch { 107 | drawerState.close() 108 | } 109 | navController.navigate(route.destination) { 110 | launchSingleTop = true 111 | } 112 | }, 113 | modifier = Modifier.padding(vertical = 8.dp) 114 | ) 115 | } 116 | } 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /sample/composeApp/src/commonMain/kotlin/sample/app/AppManagerDemo.kt: -------------------------------------------------------------------------------- 1 | package sample.app 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.Button 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import io.github.kdroidfilter.platformtools.appmanager.hasAppVersionChanged 15 | import io.github.kdroidfilter.platformtools.appmanager.isFirstInstallation 16 | import io.github.kdroidfilter.platformtools.appmanager.restartApplication 17 | 18 | @OptIn(ExperimentalMaterial3Api::class) 19 | @Composable 20 | fun AppManagerDemo() { 21 | Column( 22 | modifier = Modifier 23 | .fillMaxSize() 24 | .padding(16.dp), 25 | verticalArrangement = Arrangement.spacedBy(16.dp) 26 | ) { 27 | Text(if (isFirstInstallation()) "This is the first Installation !" else "This is not the first Installation !", style = MaterialTheme.typography.bodyLarge) 28 | 29 | Text(if (hasAppVersionChanged()) "The app was updated !" else "The app was not updated !", style = MaterialTheme.typography.bodyLarge) 30 | Button(onClick = { restartApplication() }) { 31 | Text("Restart Application") 32 | } 33 | Text("For detailed usage of this module, refer to https://github.com/kdroidFilter/AppwithAutoUpdater", style = MaterialTheme.typography.bodyLarge) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /sample/composeApp/src/commonMain/kotlin/sample/app/CoreDemo.kt: -------------------------------------------------------------------------------- 1 | package sample.app 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import io.github.kdroidfilter.platformtools.getAppVersion 12 | import io.github.kdroidfilter.platformtools.getCacheDir 13 | import io.github.kdroidfilter.platformtools.getOperatingSystem 14 | import io.github.kdroidfilter.platformtools.getPlatform 15 | 16 | @OptIn(ExperimentalMaterial3Api::class) 17 | @Composable 18 | fun CoreDemo() { 19 | var operatingSystem = getOperatingSystem() 20 | var platform = getPlatform() 21 | var cacheDir = getCacheDir() 22 | var appVersion = getAppVersion() 23 | 24 | Column( 25 | modifier = Modifier 26 | .fillMaxSize() 27 | .padding(16.dp), 28 | verticalArrangement = Arrangement.spacedBy(16.dp) 29 | ) { 30 | Text("Operating System: $operatingSystem", style = MaterialTheme.typography.bodyLarge) 31 | Text("Platform: $platform", style = MaterialTheme.typography.bodyLarge) 32 | Text("Cache Directory: ${cacheDir.absolutePath}", style = MaterialTheme.typography.bodyLarge) 33 | Text("App Version: $appVersion", style = MaterialTheme.typography.bodyLarge) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sample/composeApp/src/commonMain/kotlin/sample/app/PermissionHandlerDemo.kt: -------------------------------------------------------------------------------- 1 | package sample.app 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material3.* 5 | import androidx.compose.runtime.* 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.unit.dp 8 | 9 | 10 | @OptIn(ExperimentalMaterial3Api::class) 11 | @Composable 12 | fun PermissionHandlerDemo(screens: List ) { 13 | 14 | var currentScreenIndex by remember { mutableStateOf(0) } 15 | 16 | Scaffold( 17 | topBar = { 18 | TopAppBar( 19 | title = { Text("Permission Examples") }, 20 | ) 21 | } 22 | ) { paddingValues -> 23 | Column( 24 | modifier = Modifier 25 | .fillMaxSize() 26 | .padding(paddingValues) 27 | ) { 28 | // Navigation Tabs 29 | ScrollableTabRow( 30 | selectedTabIndex = currentScreenIndex, 31 | modifier = Modifier.fillMaxWidth() 32 | ) { 33 | screens.forEachIndexed { index, screen -> 34 | Tab( 35 | selected = currentScreenIndex == index, 36 | onClick = { currentScreenIndex = index }, 37 | text = { Text(screen.title) } 38 | ) 39 | } 40 | } 41 | 42 | // Content 43 | Box( 44 | modifier = Modifier 45 | .fillMaxSize() 46 | .padding(16.dp) 47 | ) { 48 | screens[currentScreenIndex].content() 49 | } 50 | } 51 | } 52 | } 53 | 54 | data class PermissionScreen( 55 | val title: String, 56 | val content: @Composable () -> Unit 57 | ) 58 | 59 | -------------------------------------------------------------------------------- /sample/composeApp/src/commonMain/kotlin/sample/app/ReleaseFetcherDemo.kt: -------------------------------------------------------------------------------- 1 | package sample.app 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.lazy.LazyColumn 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.unit.dp 9 | import io.github.kdroidfilter.platformtools.releasefetcher.github.GitHubReleaseFetcher 10 | import io.github.kdroidfilter.platformtools.releasefetcher.github.model.Release 11 | import io.github.kdroidfilter.platformtools.releasefetcher.downloader.Downloader 12 | import kotlinx.coroutines.launch 13 | import java.io.File 14 | 15 | @OptIn(ExperimentalMaterial3Api::class) 16 | @Composable 17 | fun ReleaseFetcherDemo() { 18 | val scope = rememberCoroutineScope() 19 | val fetcher = remember { GitHubReleaseFetcher("kdroidFilter", "KmpRealTimeLogger") } 20 | val downloader = remember { Downloader() } 21 | 22 | var release by remember { mutableStateOf(null) } 23 | var progress by remember { mutableStateOf(0.0) } 24 | var downloadStatus by remember { mutableStateOf("") } 25 | 26 | LazyColumn( 27 | modifier = Modifier 28 | .fillMaxSize() 29 | .padding(16.dp), 30 | verticalArrangement = Arrangement.spacedBy(16.dp) 31 | ) { 32 | item { 33 | Button(onClick = { 34 | scope.launch { 35 | release = fetcher.getLatestRelease() 36 | } 37 | }) { 38 | Text("Fetch Latest Release") 39 | } 40 | } 41 | item { 42 | release?.let { 43 | Text("Latest Version: ${it.tag_name}", style = MaterialTheme.typography.bodyLarge) 44 | Text("Changelog: ${it.body}", style = MaterialTheme.typography.bodyLarge) 45 | 46 | Button(onClick = { 47 | scope.launch { 48 | val downloadLink = fetcher.getDownloadLinkForPlatform(it) 49 | if (downloadLink != null) { 50 | downloadStatus = "Downloading..." 51 | downloader.downloadApp(downloadLink) { percentage, file -> 52 | progress = percentage 53 | if (file != null && percentage == 100.0) { 54 | downloadStatus = "Download complete: ${file.absolutePath}" 55 | } else if (percentage == -1.0) { 56 | downloadStatus = "Download failed." 57 | } 58 | } 59 | } else { 60 | downloadStatus = "No suitable download link for platform." 61 | } 62 | } 63 | }) { 64 | Text("Download Release") 65 | } 66 | 67 | } 68 | } 69 | item { 70 | Text("Progress: $progress %", style = MaterialTheme.typography.bodyLarge) 71 | Text(downloadStatus, style = MaterialTheme.typography.bodyLarge) 72 | } 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /sample/composeApp/src/commonMain/kotlin/sample/app/permissionHandler/NotificationPermissionSample.kt: -------------------------------------------------------------------------------- 1 | package sample.app.permissionHandler 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material3.Button 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Scaffold 13 | import androidx.compose.material3.SnackbarHost 14 | import androidx.compose.material3.SnackbarHostState 15 | import androidx.compose.material3.Text 16 | import androidx.compose.material3.TopAppBar 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.LaunchedEffect 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.unit.dp 26 | import io.github.kdroidfilter.platformtools.permissionhandler.hasNotificationPermission 27 | import io.github.kdroidfilter.platformtools.permissionhandler.requestNotificationPermission 28 | 29 | @OptIn(ExperimentalMaterial3Api::class) 30 | @Composable 31 | fun NotificationPermissionSample() { 32 | var permissionGranted by remember { mutableStateOf(hasNotificationPermission()) } 33 | var showSnackbar by remember { mutableStateOf(false) } 34 | var snackbarMessage by remember { mutableStateOf("") } 35 | 36 | val snackbarHostState = remember { SnackbarHostState() } 37 | 38 | Scaffold( 39 | topBar = { 40 | TopAppBar(title = { Text("Notification Permissions") }) 41 | }, 42 | snackbarHost = { 43 | SnackbarHost(hostState = snackbarHostState) 44 | }, 45 | content = { padding -> 46 | Box( 47 | modifier = Modifier 48 | .fillMaxSize() 49 | .padding(padding), 50 | contentAlignment = Alignment.Center 51 | ) { 52 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 53 | Text( 54 | text = if (permissionGranted) "Notification permission granted ✅" else "Notification permission required 🚫", 55 | style = MaterialTheme.typography.bodyLarge 56 | ) 57 | Spacer(modifier = Modifier.height(16.dp)) 58 | if (!permissionGranted) { 59 | Button( 60 | onClick = { 61 | requestNotificationPermission( 62 | onGranted = { 63 | permissionGranted = true 64 | snackbarMessage = "Permission granted ✅" 65 | showSnackbar = true 66 | }, 67 | onDenied = { 68 | permissionGranted = false 69 | snackbarMessage = "Permission denied 🚫" 70 | showSnackbar = true 71 | } 72 | ) 73 | } 74 | ) { 75 | Text("Request Permission") 76 | } 77 | } 78 | } 79 | } 80 | } 81 | ) 82 | 83 | if (showSnackbar) { 84 | LaunchedEffect(snackbarMessage) { 85 | snackbarHostState.showSnackbar(snackbarMessage) 86 | showSnackbar = false 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /sample/composeApp/src/jvmMain/kotlin/sample/app/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.unit.dp 2 | import androidx.compose.ui.window.Window 3 | import androidx.compose.ui.window.application 4 | import androidx.compose.ui.window.rememberWindowState 5 | import io.github.kdroidfilter.platformtools.darkmodedetector.windows.setWindowsAdaptiveTitleBar 6 | 7 | import sample.app.App 8 | import java.awt.Dimension 9 | 10 | fun main() = application { 11 | 12 | Window( 13 | title = "sample", 14 | state = rememberWindowState(width = 800.dp, height = 600.dp), 15 | onCloseRequest = ::exitApplication, 16 | ) { 17 | window.minimumSize = Dimension(350, 600) 18 | window.setWindowsAdaptiveTitleBar() 19 | App() 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /sample/terminalApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.multiplatform) 3 | } 4 | 5 | kotlin { 6 | listOf( 7 | macosX64(), 8 | macosArm64(), 9 | linuxX64(), 10 | mingwX64(), 11 | ).forEach { 12 | it.binaries.executable { 13 | entryPoint = "main" 14 | } 15 | } 16 | 17 | sourceSets { 18 | commonMain.dependencies { 19 | implementation(project(":platformtools:core")) 20 | } 21 | } 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/terminalApp/src/commonMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import io.github.kdroidfilter.platformtools.getOperatingSystem 2 | 3 | fun main() { 4 | println("The Operating System is " + getOperatingSystem().name.lowercase().replaceFirstChar { it.uppercase()}) 5 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Platform-Tools" 2 | 3 | pluginManagement { 4 | repositories { 5 | google { 6 | content { 7 | includeGroupByRegex("com\\.android.*") 8 | includeGroupByRegex("com\\.google.*") 9 | includeGroupByRegex("androidx.*") 10 | includeGroupByRegex("android.*") 11 | } 12 | } 13 | gradlePluginPortal() 14 | mavenCentral() 15 | } 16 | } 17 | 18 | dependencyResolutionManagement { 19 | repositories { 20 | google { 21 | content { 22 | includeGroupByRegex("com\\.android.*") 23 | includeGroupByRegex("com\\.google.*") 24 | includeGroupByRegex("androidx.*") 25 | includeGroupByRegex("android.*") 26 | } 27 | } 28 | mavenCentral() 29 | } 30 | } 31 | include(":platformtools:core") 32 | include(":platformtools:appmanager") 33 | include(":platformtools:releasefetcher") 34 | include(":platformtools:permissionhandler") 35 | include(":platformtools:darkmodedetector") 36 | include(":sample:composeApp") 37 | include(":sample:terminalApp") 38 | 39 | 40 | --------------------------------------------------------------------------------