diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 209b346..9a671f0 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,3 @@ -# How you can support jrpie/Launcher +# How you can support finnmglas/Launcher -custom: https://s.jrpie.de/launcher-donate +custom: sponsor.finnmglas.com diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..f580315 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help improve this app +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Smartphone (please complete the following information):** + - Device: [e.g. Samsung A7] + - Android Version: [e.g. Marshmallow, 6.0 or API 23] + +**Additional info** +Add any other info or comments about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml deleted file mode 100644 index fa112ae..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: Bug report -description: Create a report to help improve this app -title: '[bug] ' -labels: bug -body: - - type: markdown - attributes: - value: | - Thank you for helping to improve µLauncher! - - type: textarea - id: bug - attributes: - label: Describe the Bug - description: What happened? - placeholder: A clear and concise description of what the bug is. - render: markdown - validations: - required: true - - type: textarea - id: expected - attributes: - label: Expected Behavior - description: What did you expect to happen instead? - render: markdown - validations: - required: false - - type: textarea - id: reproduce - attributes: - label: To Reproduce - description: What steps are required to reproduce the bug? - render: markdown - placeholder: | - Steps to reproduce the behavior: - 1. Go to '...' - 2. Click on '....' - 3. Scroll down to '....' - 4. See error - validations: - required: false - - type: textarea - id: device - attributes: - label: Your Device - description: | - What device are you using? Adding this information helps to reproduce the bug. - You can copy this from µLauncher > Settings > Meta > Report Bug. - render: markdown - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 1a4f00c..e2b3f9d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,24 +1,20 @@ --- name: Feature request about: Suggest an idea for this project -title: '[feature] ' +title: '' labels: enhancement assignees: '' --- -# Please describe the problem to be solved +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - +**Describe the solution you'd like** +A clear and concise description of what you want to happen. -# Describe the solution you would suggest +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. - - -# Describe alternative solutions you've considered - - - -# Additional info - - +**Additional info** +Add any other info, comments or screenshots about the feature request here. diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml deleted file mode 100644 index ba9a709..0000000 --- a/.github/workflows/android.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Android CI - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build with Gradle - run: ./gradlew build - - - name: upload apk - uses: actions/upload-artifact@v4 - with: - name: launcher-debug-${{ github.sha }}.apk - path: app/build/outputs/apk/default/debug/app-default-debug.apk diff --git a/.gitignore b/.gitignore index c21100e..56cc642 100644 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,7 @@ bin/ gen/ out/ # Uncomment the following line in case you need and you don't have the release build type files in your app -release/ -app/release/ -.kotlin/ +# release/ # Gradle files .gradle/ @@ -40,15 +38,12 @@ captures/ # IntelliJ *.iml -.idea/* .idea/workspace.xml .idea/tasks.xml -.idea/other.xml .idea/gradle.xml .idea/assetWizardSettings.xml .idea/dictionaries .idea/libraries -.idea/deploymentTargetSelector.xml # Android Studio 3 in .gitignore file. .idea/caches .idea/modules.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..3cc336b --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..7e340a7 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..97021b9 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 0000000..0d3a1fb --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,263 @@ + + + + + + \ No newline at end of file diff --git a/.scripts/release.sh b/.scripts/release.sh deleted file mode 100755 index f207c87..0000000 --- a/.scripts/release.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash -export JAVA_HOME="/usr/lib/jvm/java-21-openjdk/" -OUTPUT_DIR="$HOME/launcher-release" -BUILD_TOOLS_DIR="$HOME/Android/Sdk/build-tools/35.0.0" -KEYSTORE="$HOME/data/keys/launcher_jrpie.jks" -KEYSTORE_ACCRESCENT="$HOME/data/keys/launcher_jrpie_accrescent.jks" -KEYSTORE_PASS=$(keepassxc-password "android_keys/launcher") -KEYSTORE_ACCRESCENT_PASS=$(keepassxc-password "android_keys/launcher-accrescent") - -if [[ $(git status --porcelain) ]]; then - echo "There are uncommitted changes." - - read -p "Continue anyway? (y/n) " -n 1 -r - echo # (optional) move to a new line - if ! [[ $REPLY =~ ^[Yy]$ ]] - then - exit 1 - fi - -fi - -rm -rf "$OUTPUT_DIR" -mkdir "$OUTPUT_DIR" - - -echo -echo "=======================" -echo " Default Release (apk) " -echo "=======================" - -./gradlew clean -./gradlew assembleDefaultRelease -mv app/build/outputs/apk/default/release/app-default-release-unsigned.apk "$OUTPUT_DIR/app-release.apk" -"$BUILD_TOOLS_DIR/apksigner" sign --ks "$KEYSTORE" \ - --ks-key-alias key0 \ - --ks-pass="pass:$KEYSTORE_PASS" \ - --key-pass="pass:$KEYSTORE_PASS" \ - --alignment-preserved \ - --v1-signing-enabled=true \ - --v2-signing-enabled=true \ - --v3-signing-enabled=true \ - --v4-signing-enabled=true \ - "$OUTPUT_DIR/app-release.apk" - -echo -echo "=======================" -echo " Default Release (aab) " -echo "=======================" - -./gradlew clean -./gradlew bundleDefaultRelease -mv app/build/outputs/bundle/defaultRelease/app-default-release.aab "$OUTPUT_DIR/app-release.aab" -"$BUILD_TOOLS_DIR/apksigner" sign --ks "$KEYSTORE" \ - --ks-key-alias key0 \ - --ks-pass="pass:$KEYSTORE_PASS" \ - --key-pass="pass:$KEYSTORE_PASS" \ - --v1-signing-enabled=true --v2-signing-enabled=true --v3-signing-enabled=true --v4-signing-enabled=true \ - --min-sdk-version=21 \ - "$OUTPUT_DIR/app-release.aab" - -echo -echo "=======================" -echo " Accrescent (apks) " -echo "=======================" - -./gradlew clean -./gradlew bundleAccrescentRelease -mv app/build/outputs/bundle/accrescentRelease/app-accrescent-release.aab "$OUTPUT_DIR/app-accrescent-release.aab" - -# build apks using bundletool from https://github.com/google/bundletool/releases -"$JAVA_HOME/bin/java" -jar /opt/android/bundletool.jar build-apks \ - --bundle="$OUTPUT_DIR/app-accrescent-release.aab" --output="$OUTPUT_DIR/launcher-accrescent.apks" \ - --ks="$KEYSTORE_ACCRESCENT" \ - --ks-pass="pass:$KEYSTORE_ACCRESCENT_PASS" \ - --ks-key-alias="key0" \ - --key-pass="pass:$KEYSTORE_ACCRESCENT_PASS" diff --git a/LICENSE b/LICENSE index 7435f65..a0bb980 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License Original Code Copyright (c) 2020 Finn Glas (https://github.com/finnmglas/Launcher) -Modifications Copyright (c) 2025 Josia Pietsch +Modifications Copyright (c) 2023 Josia Pietsch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ce1d0d0..2e25e3e 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,38 @@ -[![][shield-release]][latest-release] -[![Android CI](https://github.com/jrpie/Launcher/actions/workflows/android.yml/badge.svg)](https://github.com/jrpie/Launcher/actions/workflows/android.yml) + [![][shield-license]][license] -[![Chat on Matrix](https://matrix.to/img/matrix-badge.svg)][matrix] -[![Chat on Discord](https://img.shields.io/badge/discord-join%20chat-007ec6.svg?style=flat)][discord] + + + +# Launcher + +This is a fork of [finnmglas's app Launcher][original-repo]. + +## Notable changes: + +* Edge gestures: There is a setting to allow distinguishing swiping at the edges of the screen from swiping in the center. + +### Visual +* This app uses the system wallpaper instead of a custom solution. +* The font has been changed to [Hack][hack-font]. +* Font Awesome Icons were replaced by Material icons. +* The gear button on the home screen was removed. Instead pressing back opens the list of applications and the app settings are accessible from there. +### Search +* The search algorithm was modified to prefer matches at the beginning of the app name, i.e. when searching for `"te"`, `"termux"` is sorted before `"notes"`. +* The search bar was moved to the bottom of the screen. - -# μLauncher - - -µLauncher is an Android home screen that lets you launch apps using swipe gestures and button presses. -It is *minimal, efficient and free of distraction*. - - -Get it on F-Droid -Get it on Accrescent -Get it on Obtainium -Get it on GitHub - -You can also [get it on Google Play](https://play.google.com/store/apps/details?id=de.jrpie.android.launcher), but I don't recommend that. - -screenshot - screenshot - screenshot - screenshot - screenshot - screenshot - screenshot - - -µLauncher is a fork of [finnmglas's app Launcher][original-repo]. -An incomplete list of changes can be found [here](docs/launcher.md). - -## Features - -µLauncher only displays the date, time and a wallpaper. -Pressing back or swiping up (this can be configured) opens a list -of all installed apps, which can be searched efficiently. - -The following gestures are available: - - volume up / down, - - swipe up / down / left / right, - - swipe with two fingers, - - swipe on the left / right resp. top / bottom edge, - - tap, then swipe up / down / left / right, - - draw < / > / V / Λ - - click on date / time, - - double click, - - long click, - - back button. - -To every gesture you can bind one of the following actions: - - launch an app, - - open a list of all / favorite / private apps, - - open µLauncher settings, - - toggle private space lock, - - lock the screen, - - toggle the torch, - - volume up / down, - - go to previous / next audio track. - - - -µLauncher is compatible with [work profile](https://www.android.com/enterprise/work-profile/), -so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used. - -By default the font is set to [Hack][hack-font], but other fonts can be selected. - - - -## Contributing - -There are several ways to contribute to this app: -* You can add or improve [translations][toolate]. -
translation status -* If you find a bug or have an idea for a new feature you can [join the chat][chat] or open an [issue][issues]. Please note that I work on this project in my free time. Thus I might not respond immediately and not all ideas will be implemented. -* You can implement a new feature yourself: - - Create a fork of this repository: [![][shield-gh-fork]][fork] - - Create a new branch named `feature/` or `fix/` and commit your changes. - - Open a new pull request. - - -See [build.md](docs/build.md) for instructions how to build this project. -The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds. -Note that those are not signed. - +### Technical +* Small improvements to the gesture detection. +* Different apps set as default. +* Package name was changed to `de.jrpie.android.launcher` to avoid clashing with the original app. +* Dropped support for API < 21 (i.e. pre Lollypop) +* Some refactoring +--- --- [hack-font]: https://sourcefoundry.org/hack/ [original-repo]: https://github.com/finnmglas/Launcher - [toolate]: https://toolate.othing.xyz/projects/jrpie-launcher/ - [issues]: https://github.com/jrpie/Launcher/issues/ - [fork]: https://github.com/jrpie/Launcher/fork/ @@ -118,16 +44,14 @@ Note that those are not signed. [shield-release]: https://img.shields.io/github/v/release/jrpie/Launcher?style=flat - [latest-release]: https://github.com/jrpie/Launcher/releases/latest [shield-contribute]: https://img.shields.io/badge/contributions-welcome-007ec6.svg?style=flat [shield-license]: https://img.shields.io/badge/license-MIT-007ec6?style=flat [shield-gh-watch]: https://img.shields.io/github/watchers/jrpie/Launcher?label=Watch&style=social [shield-gh-star]: https://img.shields.io/github/stars/jrpie/Launcher?label=Star&style=social [shield-gh-fork]: https://img.shields.io/github/forks/jrpie/Launcher?label=Fork&style=social - [matrix]: https://s.jrpie.de/launcher-matrix - [discord]: https://s.jrpie.de/launcher-discord - [chat]: https://s.jrpie.de/launcher-chat + + diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 7e3e763..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,5 +0,0 @@ -# Reporting Security Issues - -For security related issues, please use the contact information -from the [security.txt](https://jrpie.de/.well-known/security.txt) on my website -or [report a vulnerability](https://github.com/jrpie/Launcher/security/advisories/new) on github. diff --git a/app/build.gradle b/app/build.gradle index 1a0a6fb..5f7bdc6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,115 +1,45 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' -apply plugin: 'kotlinx-serialization' +apply plugin: 'kotlin-android-extensions' android { - dataBinding { - enabled = true - } - - packaging { - resources.excludes.addAll( - [ - "META-INF/LICENSE.md", - "META-INF/NOTICE.md", - "META-INF/LICENSE-notice.md" - ] - ) - } + compileSdkVersion 34 + buildToolsVersion "34.0.0" defaultConfig { applicationId "de.jrpie.android.launcher" minSdkVersion 21 targetSdkVersion 35 - compileSdk 35 - versionCode 44 - versionName "0.1.4" + versionCode 16 + versionName "j-0.0.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = '17' - } - - buildFeatures { - viewBinding true - } - buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } - debug { - applicationIdSuffix = ".debug" - versionNameSuffix = "-debug" - } } - - flavorDimensions += "distribution" - - productFlavors { - create("default") { - dimension = "distribution" - getIsDefault().set(true) - buildConfigField "boolean", "USE_ACCESSIBILITY_SERVICE", "true" - } - create("accrescent") { - dimension = "distribution" - applicationIdSuffix = ".accrescent" - versionNameSuffix = "+accrescent" - buildConfigField "boolean", "USE_ACCESSIBILITY_SERVICE", "false" - } - } - - sourceSets { - accrescent { - manifest.srcFile 'src/accrescent/AndroidManifest.xml' - } - } - namespace 'de.jrpie.android.launcher' buildFeatures { buildConfig true } - dependenciesInfo { - // Disables dependency metadata when building APKs. - includeInApk = false - // Disables dependency metadata when building Android App Bundles. - includeInBundle = false - } - lint { - abortOnError false - } - - } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.activity:activity-ktx:1.8.0' - implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'androidx.core:core-ktx:1.15.0' - implementation 'androidx.constraintlayout:constraintlayout:2.2.0' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.2.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.palette:palette-ktx:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.4.0' - implementation 'androidx.preference:preference-ktx:1.2.1' - implementation 'com.google.android.material:material:1.12.0' - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") - implementation "eu.jonahbauer:android-preference-annotations:1.1.2" - annotationProcessor "eu.jonahbauer:android-preference-annotations:1.1.2" - annotationProcessor "com.android.databinding:compiler:$android_plugin_version" - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.2.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' } - diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 9e3e326..f1b4245 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,6 +1,4 @@ # Add project specific ProGuard rules here. --dontobfuscate --dontoptimize # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # @@ -21,10 +19,3 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile - -# see app/build/outputs/mapping/release/missing_rules.txt -# Please add these rules to your existing keep rules in order to suppress warnings. -# This is generated automatically by the Android Gradle plugin. --dontwarn javax.annotation.processing.AbstractProcessor --dontwarn javax.annotation.processing.SupportedAnnotationTypes --dontwarn javax.annotation.processing.SupportedSourceVersion diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..7f5bd2d --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,21 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "de.jrpie.android.launcher", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 16, + "versionName": "j-0.0.4", + "outputFile": "app-release.apk" + } + ], + "elementType": "File", + "minSdkVersionForDexing": 21 +} \ No newline at end of file diff --git a/app/src/accrescent/AndroidManifest.xml b/app/src/accrescent/AndroidManifest.xml deleted file mode 100644 index 16ea383..0000000 --- a/app/src/accrescent/AndroidManifest.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/app/src/debug/res/values/donottranslate.xml b/app/src/debug/res/values/donottranslate.xml deleted file mode 100644 index bf4f4e4..0000000 --- a/app/src/debug/res/values/donottranslate.xml +++ /dev/null @@ -1,3 +0,0 @@ - - μLauncher [debug] - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a5f8831..30baf2c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,98 +3,43 @@ xmlns:tools="http://schemas.android.com/tools"> - + - - + - - - - - - - + + + android:screenOrientation="portrait" + tools:ignore="LockedOrientationActivity"> - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index a5bfb80..f3bf62c 100644 Binary files a/app/src/main/ic_launcher-playstore.png and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/de/jrpie/android/launcher/Application.kt b/app/src/main/java/de/jrpie/android/launcher/Application.kt deleted file mode 100644 index e6cce23..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/Application.kt +++ /dev/null @@ -1,160 +0,0 @@ -package de.jrpie.android.launcher - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.content.SharedPreferences -import android.content.pm.LauncherApps -import android.content.pm.ShortcutInfo -import android.os.Build -import android.os.Build.VERSION_CODES -import android.os.UserHandle -import androidx.core.content.ContextCompat -import androidx.lifecycle.MutableLiveData -import androidx.preference.PreferenceManager -import de.jrpie.android.launcher.actions.TorchManager -import de.jrpie.android.launcher.apps.AbstractAppInfo -import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo -import de.jrpie.android.launcher.apps.isPrivateSpaceLocked -import de.jrpie.android.launcher.preferences.LauncherPreferences -import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion -import de.jrpie.android.launcher.preferences.resetPreferences -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class Application : android.app.Application() { - val apps = MutableLiveData>() - val privateSpaceLocked = MutableLiveData() - - private val profileAvailabilityBroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - // TODO: only update specific apps - // use Intent.EXTRA_USER - loadApps() - } - } - - // TODO: only update specific apps - private val launcherAppsCallback = object : LauncherApps.Callback() { - override fun onPackageRemoved(p0: String?, p1: UserHandle?) { - loadApps() - } - - override fun onPackageAdded(p0: String?, p1: UserHandle?) { - loadApps() - } - - override fun onPackageChanged(p0: String?, p1: UserHandle?) { - loadApps() - } - - override fun onPackagesAvailable(p0: Array?, p1: UserHandle?, p2: Boolean) { - // TODO - } - - override fun onPackagesSuspended(packageNames: Array?, user: UserHandle?) { - // TODO - } - - override fun onPackagesUnsuspended(packageNames: Array?, user: UserHandle?) { - // TODO - } - - override fun onPackagesUnavailable(p0: Array?, p1: UserHandle?, p2: Boolean) { - // TODO - } - - override fun onPackageLoadingProgressChanged( - packageName: String, - user: UserHandle, - progress: Float - ) { - // TODO - } - - override fun onShortcutsChanged( - packageName: String, - shortcuts: MutableList, - user: UserHandle - ) { - // TODO - } - } - - var torchManager: TorchManager? = null - private var customAppNames: HashMap? = null - private val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, pref -> - if (pref == getString(R.string.settings_apps_custom_names_key)) { - customAppNames = LauncherPreferences.apps().customNames() - } else if (pref == LauncherPreferences.apps().keys().pinnedShortcuts()) { - loadApps() - } - } - - override fun onCreate() { - super.onCreate() - // TODO Error: Invalid resource ID 0x00000000. - // DynamicColors.applyToActivitiesIfAvailable(this) - - - if (Build.VERSION.SDK_INT >= VERSION_CODES.M) { - torchManager = TorchManager(this) - } - - val preferences = PreferenceManager.getDefaultSharedPreferences(this) - LauncherPreferences.init(preferences, this.resources) - - - // Try to restore old preferences - migratePreferencesToNewVersion(this) - - // First time opening the app: set defaults - // The tutorial is started from HomeActivity#onStart, as starting it here is blocked by android - if (!LauncherPreferences.internal().started()) { - resetPreferences(this) - } - - - LauncherPreferences.getSharedPreferences() - .registerOnSharedPreferenceChangeListener(listener) - - - val launcherApps = getSystemService(LAUNCHER_APPS_SERVICE) as LauncherApps - launcherApps.registerCallback(launcherAppsCallback) - - if (Build.VERSION.SDK_INT >= VERSION_CODES.N) { - val filter = IntentFilter().also { - if (Build.VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM) { - it.addAction(Intent.ACTION_PROFILE_AVAILABLE) - it.addAction(Intent.ACTION_PROFILE_UNAVAILABLE) - } else { - it.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) - it.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) - } - } - ContextCompat.registerReceiver( - this, profileAvailabilityBroadcastReceiver, filter, - ContextCompat.RECEIVER_EXPORTED - ) - } - - if (Build.VERSION.SDK_INT >= VERSION_CODES.N_MR1) { - removeUnusedShortcuts(this) - } - loadApps() - } - - fun getCustomAppNames(): HashMap { - return (customAppNames ?: LauncherPreferences.apps().customNames() ?: HashMap()) - .also { customAppNames = it } - } - - private fun loadApps() { - privateSpaceLocked.postValue(isPrivateSpaceLocked(this)) - CoroutineScope(Dispatchers.Default).launch { - apps.postValue(getApps(packageManager, applicationContext)) - } - } -} diff --git a/app/src/main/java/de/jrpie/android/launcher/Functions.kt b/app/src/main/java/de/jrpie/android/launcher/Functions.kt index afc2c31..72f2075 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -1,226 +1,418 @@ package de.jrpie.android.launcher import android.app.Activity -import android.app.Service -import android.app.role.RoleManager -import android.content.ActivityNotFoundException -import android.content.ClipData -import android.content.ClipboardManager +import android.app.AlertDialog import android.content.Context import android.content.Intent -import android.content.pm.LauncherApps -import android.content.pm.LauncherApps.ShortcutQuery +import android.content.SharedPreferences import android.content.pm.PackageManager -import android.content.pm.ShortcutInfo +import android.graphics.BlendMode +import android.graphics.BlendModeColorFilter +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.media.AudioManager +import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.UserHandle -import android.os.UserManager +import android.os.SystemClock import android.provider.Settings -import android.util.Log +import android.util.DisplayMetrics +import android.view.KeyEvent +import android.view.View +import android.view.Window +import android.view.WindowManager +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import android.view.inputmethod.InputMethodManager +import android.widget.Button +import android.widget.ImageView +import android.widget.Switch import android.widget.Toast -import androidx.annotation.RequiresApi -import de.jrpie.android.launcher.actions.Action -import de.jrpie.android.launcher.actions.Gesture -import de.jrpie.android.launcher.actions.ShortcutAction -import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER -import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo -import de.jrpie.android.launcher.apps.AppInfo -import de.jrpie.android.launcher.apps.DetailedAppInfo -import de.jrpie.android.launcher.apps.DetailedPinnedShortcutInfo -import de.jrpie.android.launcher.apps.PinnedShortcutInfo -import de.jrpie.android.launcher.apps.getPrivateSpaceUser -import de.jrpie.android.launcher.apps.isPrivateSpaceSupported -import de.jrpie.android.launcher.preferences.LauncherPreferences -import de.jrpie.android.launcher.ui.tutorial.TutorialActivity -import androidx.core.net.toUri +import de.jrpie.android.launcher.list.ListActivity +import de.jrpie.android.launcher.list.apps.AppInfo +import de.jrpie.android.launcher.list.apps.AppsRecyclerAdapter +import de.jrpie.android.launcher.list.other.LauncherAction +import de.jrpie.android.launcher.settings.SettingsActivity +import de.jrpie.android.launcher.settings.intendedSettingsPause +import de.jrpie.android.launcher.tutorial.TutorialActivity -const val LOG_TAG = "Launcher" +/* Preference Key Constants */ -const val REQUEST_SET_DEFAULT_HOME = 42 +const val PREF_DOMINANT = "custom_dominant" +const val PREF_VIBRANT = "custom_vibrant" +const val PREF_THEME = "theme" -fun isDefaultHomeScreen(context: Context): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val roleManager = context.getSystemService(RoleManager::class.java) - return roleManager.isRoleHeld(RoleManager.ROLE_HOME) +const val PREF_SCREEN_TIMEOUT_DISABLED = "disableTimeout" +const val PREF_SCREEN_FULLSCREEN = "useFullScreen" +const val PREF_DATE_FORMAT = "dateFormat" + +const val PREF_DOUBLE_ACTIONS_ENABLED = "enableDoubleActions" +const val PREF_EDGE_ACTIONS_ENABLED = "enableEdgeActions" +const val PREF_SEARCH_AUTO_LAUNCH = "searchAutoLaunch" +const val PREF_SEARCH_AUTO_KEYBOARD = "searchAutoKeyboard" + +const val PREF_SLIDE_SENSITIVITY = "slideSensitivity" + +const val PREF_STARTED = "startedBefore" +const val PREF_STARTED_TIME = "firstStartup" + +const val PREF_VERSION = "version" + +/* Objects used by multiple activities */ +val appsList: MutableList = ArrayList() + +/* Variables containing settings */ +val displayMetrics = DisplayMetrics() + +var dominantColor = 0 +var vibrantColor = 0 + +/* REQUEST CODES */ + +const val REQUEST_CHOOSE_APP = 1 +const val REQUEST_UNINSTALL = 2 + +/* Animate */ + +// Taken from https://stackoverflow.com/questions/47293269 +fun View.blink( + times: Int = Animation.INFINITE, + duration: Long = 1000L, + offset: Long = 20L, + minAlpha: Float = 0.2f, + maxAlpha: Float = 1.0f, + repeatMode: Int = Animation.REVERSE +) { + startAnimation(AlphaAnimation(minAlpha, maxAlpha).also { + it.duration = duration + it.startOffset = offset + it.repeatMode = repeatMode + it.repeatCount = times + }) +} + +fun getPreferences(context: Context): SharedPreferences{ + return context.getSharedPreferences( + context.getString(R.string.preference_file_key), + Context.MODE_PRIVATE + ) +} + +/* Activity related */ + +fun isInstalled(uri: String, context: Context): Boolean { + if (uri.startsWith("launcher:")) return true // All internal actions + + try { + context.packageManager.getPackageInfo(uri, PackageManager.GET_ACTIVITIES) + return true + } catch (_: PackageManager.NameNotFoundException) { } + return false +} + +private fun getIntent(packageName: String, context: Context): Intent? { + val intent: Intent? = context.packageManager.getLaunchIntentForPackage(packageName) + intent?.addCategory(Intent.CATEGORY_LAUNCHER) + return intent +} + +fun launch( + data: String, activity: Activity, + animationIn: Int = android.R.anim.fade_in, animationOut: Int = android.R.anim.fade_out +) { + + if (LauncherAction.isOtherAction(data)) { // [type]:[info] + LauncherAction.byId(data)?.let {it.launch(activity) } + } + else launchApp(data, activity) // app + + activity.overridePendingTransition(animationIn, animationOut) +} + +/* Media player actions */ + +fun audioNextTrack(activity: Activity) { + + val mAudioManager = activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + val eventTime: Long = SystemClock.uptimeMillis() + + val downEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT, 0) + mAudioManager.dispatchMediaKeyEvent(downEvent) + + val upEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT, 0) + mAudioManager.dispatchMediaKeyEvent(upEvent) +} + +fun audioPreviousTrack(activity: Activity) { + val mAudioManager = activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + val eventTime: Long = SystemClock.uptimeMillis() + + val downEvent = + KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS, 0) + mAudioManager.dispatchMediaKeyEvent(downEvent) + + val upEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS, 0) + mAudioManager.dispatchMediaKeyEvent(upEvent) +} + +fun audioVolumeUp(activity: Activity) { + val audioManager = + activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + audioManager.adjustStreamVolume( + AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_RAISE, + AudioManager.FLAG_SHOW_UI + ) +} + +fun audioVolumeDown(activity: Activity) { + val audioManager = + activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + audioManager.adjustStreamVolume( + AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_LOWER, + AudioManager.FLAG_SHOW_UI + ) +} + +/* --- */ + +fun launchApp(packageName: String, context: Context) { + val intent = getIntent(packageName, context) + + if (intent != null) { + context.startActivity(intent) } else { - val testIntent = Intent(Intent.ACTION_MAIN) - testIntent.addCategory(Intent.CATEGORY_HOME) - val defaultHome = testIntent.resolveActivity(context.packageManager)?.packageName - return defaultHome == context.packageName + if (isInstalled(packageName, context)){ + + AlertDialog.Builder( + context, + R.style.AlertDialogCustom + ) + .setTitle(context.getString(R.string.alert_cant_open_title)) + .setMessage(context.getString(R.string.alert_cant_open_message)) + .setPositiveButton(android.R.string.ok + ) { _, _ -> + openAppSettings( + packageName, + context + ) + } + .setNegativeButton(android.R.string.cancel, null) + .setIcon(android.R.drawable.ic_dialog_info) + .show() + } else { + Toast.makeText( + context, + context.getString(R.string.toast_cant_open_message), + Toast.LENGTH_SHORT + ).show() + } } } -fun setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) { - val isDefault = isDefaultHomeScreen(context) - if (checkDefault && isDefault) { - // Launcher is already the default home app - return - } +fun openNewTabWindow(urls: String, context: Context) { + val uris = Uri.parse(urls) + val intents = Intent(Intent.ACTION_VIEW, uris) + val b = Bundle() + b.putBoolean("new_window", true) + intents.putExtras(b) + context.startActivity(intents) +} - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - && context is Activity - && checkDefault // using role manager only works when µLauncher is not already the default. - ) { - val roleManager = context.getSystemService(RoleManager::class.java) - context.startActivityForResult( - roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME), - REQUEST_SET_DEFAULT_HOME - ) - return - } +/* Settings related functions */ - val intent = Intent(Settings.ACTION_HOME_SETTINGS) +fun getSavedTheme(context: Context) : String { + return getPreferences(context).getString(PREF_THEME, "finn").toString() +} + +fun saveTheme(context: Context, themeName: String) : String { + getPreferences(context).edit() + .putString(PREF_THEME, themeName) + .apply() + + return themeName +} + +fun resetToDefaultTheme(activity: Activity) { + dominantColor = activity.resources.getColor(R.color.finnmglasTheme_background_color) + vibrantColor = activity.resources.getColor(R.color.finnmglasTheme_accent_color) + + getPreferences(activity).edit() + .putInt(PREF_DOMINANT, dominantColor) + .putInt(PREF_VIBRANT, vibrantColor) + .apply() + + saveTheme(activity,"finn") + loadSettings(activity) + + intendedSettingsPause = true + activity.recreate() +} + +fun resetToDarkTheme(activity: Activity) { + dominantColor = activity.resources.getColor(R.color.darkTheme_background_color) + vibrantColor = activity.resources.getColor(R.color.darkTheme_accent_color) + + getPreferences(activity).edit() + .putInt(PREF_DOMINANT, dominantColor) + .putInt(PREF_VIBRANT, vibrantColor) + .apply() + + saveTheme(activity,"dark") + + intendedSettingsPause = true + activity.recreate() +} + + +fun openAppSettings(pkg: String, context: Context) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + intent.data = Uri.parse("package:$pkg") context.startActivity(intent) } -fun getUserFromId(userId: Int?, context: Context): UserHandle { - /* TODO: this is an ugly hack. - Use userManager#getUserForSerialNumber instead (breaking change to SharedPreferences!) - */ - val userManager = context.getSystemService(Service.USER_SERVICE) as UserManager - val profiles = userManager.userProfiles - return profiles.firstOrNull { it.hashCode() == userId } ?: profiles[0] +fun openSettings(activity: Activity) { + activity.startActivity(Intent(activity, SettingsActivity::class.java)) } -@RequiresApi(Build.VERSION_CODES.N_MR1) -fun removeUnusedShortcuts(context: Context) { - val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps - fun getShortcuts(profile: UserHandle): List? { - return try { - launcherApps.getShortcuts( - ShortcutQuery().apply { - setQueryFlags(ShortcutQuery.FLAG_MATCH_PINNED) - }, - profile - ) - } catch (e: Exception) { - // https://github.com/jrpie/launcher/issues/116 - return null - } - } - - val userManager = context.getSystemService(Service.USER_SERVICE) as UserManager - val boundActions: MutableSet = - Gesture.entries.mapNotNull { Action.forGesture(it) as? ShortcutAction }.map { it.shortcut } - .toMutableSet() - LauncherPreferences.apps().pinnedShortcuts()?.let { boundActions.addAll(it) } - try { - userManager.userProfiles.filter { !userManager.isQuietModeEnabled(it) }.forEach { profile -> - getShortcuts(profile)?.groupBy { it.`package` }?.forEach { (p, shortcuts) -> - launcherApps.pinShortcuts(p, - shortcuts.filter { boundActions.contains(PinnedShortcutInfo(it)) } - .map { it.id }.toList(), - profile - ) - } - } - } catch (_: SecurityException) { } +fun openTutorial(activity: Activity){ + activity.startActivity(Intent(activity, TutorialActivity::class.java)) } -fun openInBrowser(url: String, context: Context) { - val intent = Intent(Intent.ACTION_VIEW, url.toUri()) - intent.putExtras(Bundle().apply { putBoolean("new_window", true) }) - try { - context.startActivity(intent) - } catch (_: ActivityNotFoundException) { - Toast.makeText(context, R.string.toast_activity_not_found_browser, Toast.LENGTH_LONG).show() - } +fun openAppsList(activity: Activity){ + val intent = Intent(activity, ListActivity::class.java) + intent.putExtra("intention", ListActivity.ListActivityIntention.VIEW.toString()) + intendedSettingsPause = true + activity.startActivity(intent) } -fun openTutorial(context: Context) { - context.startActivity(Intent(context, TutorialActivity::class.java)) -} - - /** - * Load all apps. + * [loadApps] is used to speed up the [AppsRecyclerAdapter] loading time, + * as it caches all the apps and allows for fast access to the data. */ -fun getApps( - packageManager: PackageManager, - context: Context -): MutableList { - var start = System.currentTimeMillis() - val loadList = mutableListOf() +fun loadApps(packageManager: PackageManager) { + val loadList = mutableListOf() - val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps - val userManager = context.getSystemService(Service.USER_SERVICE) as UserManager - - val privateSpaceUser = getPrivateSpaceUser(context) - - // TODO: shortcuts - launcherApps.getShortcuts() - val users = userManager.userProfiles - for (user in users) { - // don't load apps from a user profile that has quiet mode enabled - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (userManager.isQuietModeEnabled(user)) { - // hide paused apps - if (LauncherPreferences.apps().hidePausedApps()) { - continue - } - // hide apps from private space - if (isPrivateSpaceSupported() && - launcherApps.getLauncherUserInfo(user)?.userType == UserManager.USER_TYPE_PROFILE_PRIVATE - ) { - continue - } - } - } - launcherApps.getActivityList(null, user).forEach { - loadList.add(DetailedAppInfo(it, it.user == privateSpaceUser)) - } + val i = Intent(Intent.ACTION_MAIN, null) + i.addCategory(Intent.CATEGORY_LAUNCHER) + val allApps = packageManager.queryIntentActivities(i, 0) + for (ri in allApps) { + val app = AppInfo() + app.label = ri.loadLabel(packageManager) + app.packageName = ri.activityInfo.packageName + app.icon = ri.activityInfo.loadIcon(packageManager) + loadList.add(app) } + loadList.sortBy { it.label.toString() } - // fallback option - if (loadList.isEmpty()) { - Log.w(LOG_TAG, "using fallback option to load packages") - val i = Intent(Intent.ACTION_MAIN, null) - i.addCategory(Intent.CATEGORY_LAUNCHER) - val allApps = packageManager.queryIntentActivities(i, 0) - for (ri in allApps) { - val app = AppInfo(ri.activityInfo.packageName, null, INVALID_USER) - val detailedAppInfo = DetailedAppInfo( - app, - ri.loadLabel(packageManager), - ri.activityInfo.loadIcon(packageManager), - false - ) - loadList.add(detailedAppInfo) - } - } - loadList.sortBy { it.getCustomLabel(context) } - - var end = System.currentTimeMillis() - Log.i(LOG_TAG, "${loadList.size} apps loaded (${end - start}ms)") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - start = System.currentTimeMillis() - LauncherPreferences.apps().pinnedShortcuts() - ?.mapNotNull { DetailedPinnedShortcutInfo.fromPinnedShortcutInfo(it, context) } - ?.let { - end = System.currentTimeMillis() - Log.i(LOG_TAG, "${it.size} shortcuts loaded (${end - start}ms)") - loadList.addAll(it) - } - } - - return loadList + appsList.clear() + appsList.addAll(loadList) } -// used for the bug report button -fun getDeviceInfo(): String { - return """ - µLauncher version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) - Android version: ${Build.VERSION.RELEASE} (sdk ${Build.VERSION.SDK_INT}) - Model: ${Build.MODEL} - Device: ${Build.DEVICE} - Brand: ${Build.BRAND} - Manufacturer: ${Build.MANUFACTURER} - """.trimIndent() +fun loadSettings(context: Context) { + val preferences = getPreferences(context) + dominantColor = preferences.getInt(PREF_DOMINANT, 0) + vibrantColor = preferences.getInt(PREF_VIBRANT, 0) } -fun copyToClipboard(context: Context, text: String) { - val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clipData = ClipData.newPlainText("Debug Info", text) - clipboardManager.setPrimaryClip(clipData) -} \ No newline at end of file +fun resetSettings(context: Context) { + + val editor = getPreferences(context).edit() + + // set default theme + dominantColor = context.resources.getColor(R.color.finnmglasTheme_background_color) + vibrantColor = context.resources.getColor(R.color.finnmglasTheme_accent_color) + + editor + .putInt(PREF_DOMINANT, dominantColor) + .putInt(PREF_VIBRANT, vibrantColor) + .putString(PREF_THEME, "finn") + .putBoolean(PREF_SCREEN_TIMEOUT_DISABLED, false) + .putBoolean(PREF_SEARCH_AUTO_LAUNCH, false) + .putInt(PREF_DATE_FORMAT, 0) + .putBoolean(PREF_SCREEN_FULLSCREEN, true) + .putBoolean(PREF_DOUBLE_ACTIONS_ENABLED, false) + .putInt(PREF_SLIDE_SENSITIVITY, 50) + + Gesture.values().forEach { editor.putString(it.id, it.pickDefaultApp(context)) } + + editor.apply() +} + +fun setWindowFlags(window: Window) { + window.setFlags(0, 0) // clear flags + + val preferences = getPreferences(window.context) + // Display notification bar + if (preferences.getBoolean(PREF_SCREEN_FULLSCREEN, true)) + window.setFlags( + WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN + ) + else window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) + + // Screen Timeout + if (preferences.getBoolean(PREF_SCREEN_TIMEOUT_DISABLED, false)) + window.setFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + ) + else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) +} + +// Used in Tutorial and Settings `ActivityOnResult` +fun saveListActivityChoice(context: Context, data: Intent?) { + val value = data?.getStringExtra("value") + val forGesture = data?.getStringExtra("forGesture") ?: return + + Gesture.byId(forGesture)?.setApp(context, value.toString()) + + loadSettings(context) +} + +// Taken from https://stackoverflow.com/a/50743764/12787264 +fun openSoftKeyboard(context: Context, view: View) { + view.requestFocus() + // open the soft keyboard + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) +} + +/* Bitmaps */ + +fun setButtonColor(btn: Button, color: Int) { + if (Build.VERSION.SDK_INT >= 29) + btn.background.colorFilter = BlendModeColorFilter(color, BlendMode.MULTIPLY) + else { + // tested with API 17 (Android 4.4.2 on S4 mini) -> fails + // tested with API 28 (Android 9 on S8) -> necessary + btn.background.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) + } + // not setting it in any other case (yet), unable to find a good solution +} + +fun setSwitchColor(sw: Switch, trackColor: Int) { + if (Build.VERSION.SDK_INT >= 29) { + sw.trackDrawable.colorFilter = BlendModeColorFilter(trackColor, BlendMode.MULTIPLY) + } + else { + sw.trackDrawable.colorFilter = PorterDuffColorFilter(trackColor, PorterDuff.Mode.SRC_ATOP) + } +} + +// Taken from: https://stackoverflow.com/a/30340794/12787264 +fun transformGrayscale(imageView: ImageView){ + val matrix = ColorMatrix() + matrix.setSaturation(0f) + + val filter = ColorMatrixColorFilter(matrix) + imageView.colorFilter = filter +} diff --git a/app/src/main/java/de/jrpie/android/launcher/Gesture.kt b/app/src/main/java/de/jrpie/android/launcher/Gesture.kt new file mode 100644 index 0000000..8d70340 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/Gesture.kt @@ -0,0 +1,144 @@ +package de.jrpie.android.launcher + +import android.app.Activity +import android.content.Context + +/** + * @param id internal id to serialize the action. Used as a key in shared preferences. + * @param defaultsResource res id of array of default actions for the gesture. + * @param labelResource res id of the name of the gesture. + * @param animationIn res id of transition animation (in) when using the gesture to launch an app. + * @param animationOut res id of transition animation (out) when using the gesture to launch an app. + */ +enum class Gesture (val id: String, private val labelResource: Int, + private val defaultsResource: Int, + private val animationIn: Int = android.R.anim.fade_in, + private val animationOut: Int = android.R.anim.fade_out){ + VOLUME_UP("action_volumeUpApp", R.string.settings_gesture_vol_up, R.array.default_volume_up, 0,0), + VOLUME_DOWN("action_volumeDownApp", R.string.settings_gesture_vol_down, R.array.default_volume_down,0,0), + TIME("action_timeApp", R.string.settings_gesture_time, R.array.default_time), + DATE("action_dateApp", R.string.settings_gesture_date, R.array.default_date), + LONG_CLICK("action_longClickApp", R.string.settings_gesture_long_click, R.array.default_long_click, 0,0), + DOUBLE_CLICK("action_doubleClickApp", R.string.settings_gesture_double_click, R.array.default_double_click,0,0), + SWIPE_UP("action_upApp", R.string.settings_gesture_up, R.array.default_up, R.anim.bottom_up), + SWIPE_UP_LEFT_EDGE("action_up_leftApp", R.string.settings_gesture_up_left_edge, R.array.default_up_left, R.anim.bottom_up), + SWIPE_UP_RIGHT_EDGE("action_up_rightApp", R.string.settings_gesture_up_right_edge, R.array.default_up_right, R.anim.bottom_up), + SWIPE_UP_DOUBLE( "action_doubleUpApp", R.string.settings_gesture_double_up, R.array.default_double_up, R.anim.bottom_up), + SWIPE_DOWN("action_downApp", R.string.settings_gesture_down, R.array.default_down, R.anim.top_down), + SWIPE_DOWN_LEFT_EDGE("action_down_leftApp", R.string.settings_gesture_down_left_edge, R.array.default_down_left, R.anim.top_down), + SWIPE_DOWN_RIGHT_EDGE("action_down_rightApp", R.string.settings_gesture_down_right_edge, R.array.default_down_right, R.anim.top_down), + SWIPE_DOWN_DOUBLE("action_doubleDownApp", R.string.settings_gesture_double_down, R.array.default_double_down, R.anim.top_down), + SWIPE_LEFT("action_leftApp", R.string.settings_gesture_left, R.array.default_left, R.anim.right_left), + SWIPE_LEFT_TOP_EDGE("action_left_topApp", R.string.settings_gesture_left_top_edge, R.array.default_left_top, R.anim.right_left), + SWIPE_LEFT_BOTTOM_EDGE("action_left_bottomApp", R.string.settings_gesture_left_bottom_edge, R.array.default_left_bottom, R.anim.right_left), + SWIPE_LEFT_DOUBLE("action_doubleLeftApp", R.string.settings_gesture_double_left, R.array.default_double_left, R.anim.right_left), + SWIPE_RIGHT("action_rightApp", R.string.settings_gesture_right, R.array.default_right, R.anim.left_right), + SWIPE_RIGHT_TOP_EDGE("action_right_topApp", R.string.settings_gesture_right_top_edge, R.array.default_right_top, R.anim.left_right), + SWIPE_RIGHT_BOTTOM_EDGE("action_right_bottomApp", R.string.settings_gesture_right_bottom_edge, R.array.default_right_bottom, R.anim.left_right), + SWIPE_RIGHT_DOUBLE("action_doubleRightApp", R.string.settings_gesture_double_right, R.array.default_double_right, R.anim.left_right); + + enum class Edge{ + TOP, BOTTOM, LEFT, RIGHT + } + + fun getApp(context: Context): String { + return getPreferences(context).getString(this.id, "")!! + } + + fun removeApp(context: Context) { + getPreferences(context).edit() + .putString(this.id, "") // clear it + .apply() + } + + fun setApp(context: Context, app: String) { + getPreferences(context).edit() + .putString(this.id, app) + .apply() + } + + fun getLabel(context: Context): String { + return context.resources.getString(this.labelResource) + } + + fun pickDefaultApp(context: Context) : String { + return context.resources + .getStringArray(this.defaultsResource) + .firstOrNull { isInstalled(it, context) } + ?: "" + } + + fun getDoubleVariant(): Gesture { + return when(this) { + SWIPE_UP -> SWIPE_UP_DOUBLE + SWIPE_DOWN -> SWIPE_DOWN_DOUBLE + SWIPE_LEFT -> SWIPE_LEFT_DOUBLE + SWIPE_RIGHT -> SWIPE_RIGHT_DOUBLE + else -> this + } + } + + fun getEdgeVariant(edge: Edge): Gesture { + return when(edge) { + Edge.TOP -> + when(this) { + SWIPE_LEFT -> SWIPE_LEFT_TOP_EDGE + SWIPE_RIGHT -> SWIPE_RIGHT_TOP_EDGE + else -> this + } + Edge.BOTTOM -> + when(this) { + SWIPE_LEFT -> SWIPE_LEFT_BOTTOM_EDGE + SWIPE_RIGHT -> SWIPE_RIGHT_BOTTOM_EDGE + else -> this + } + Edge.LEFT -> + when(this) { + SWIPE_UP -> SWIPE_UP_LEFT_EDGE + SWIPE_DOWN -> SWIPE_DOWN_LEFT_EDGE + else -> this + } + Edge.RIGHT -> + when(this) { + SWIPE_UP -> SWIPE_UP_RIGHT_EDGE + SWIPE_DOWN -> SWIPE_DOWN_RIGHT_EDGE + else -> this + } + } + } + + fun isDoubleVariant(): Boolean { + return when(this){ + SWIPE_UP_DOUBLE, + SWIPE_DOWN_DOUBLE, + SWIPE_LEFT_DOUBLE, + SWIPE_RIGHT_DOUBLE -> true + else -> false + } + } + + fun isEdgeVariant(): Boolean { + return when(this){ + SWIPE_UP_RIGHT_EDGE, + SWIPE_UP_LEFT_EDGE, + SWIPE_DOWN_LEFT_EDGE, + SWIPE_DOWN_RIGHT_EDGE, + SWIPE_LEFT_TOP_EDGE, + SWIPE_LEFT_BOTTOM_EDGE, + SWIPE_RIGHT_TOP_EDGE, + SWIPE_RIGHT_BOTTOM_EDGE -> true + else -> false + } + } + + operator fun invoke(activity: Activity) { + launch(this.getApp(activity), activity, this.animationIn, this.animationOut) + } + + companion object { + fun byId(id: String): Gesture? { + return Gesture.values().firstOrNull {it.id == id } + } + } + +} diff --git a/app/src/main/java/de/jrpie/android/launcher/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/HomeActivity.kt new file mode 100644 index 0000000..3e7dd00 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/HomeActivity.kt @@ -0,0 +1,249 @@ +package de.jrpie.android.launcher + +import android.content.Intent +import android.os.AsyncTask +import android.os.Bundle +import android.view.GestureDetector +import android.view.KeyEvent +import android.view.MotionEvent +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.GestureDetectorCompat +import de.jrpie.android.launcher.BuildConfig.VERSION_NAME +import de.jrpie.android.launcher.list.other.LauncherAction +import de.jrpie.android.launcher.tutorial.TutorialActivity +import kotlinx.android.synthetic.main.home.* +import java.text.SimpleDateFormat +import java.util.* +import kotlin.concurrent.fixedRateTimer +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +/** + * [HomeActivity] is the actual application Launcher, + * what makes this application special / unique. + * + * In this activity we display the date and time, + * and we listen for actions like tapping, swiping or button presses. + * + * As it also is the first thing that is started when someone opens Launcher, + * it also contains some logic related to the overall application: + * - Setting global variables (preferences etc.) + * - Opening the [TutorialActivity] on new installations + */ +class HomeActivity: UIObject, AppCompatActivity(), + GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener { + + private var bufferedPointerCount = 1 // how many fingers on screen + private var pointerBufferTimer = Timer() + + private lateinit var mDetector: GestureDetectorCompat + + // timers + private var clockTimer = Timer() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val preferences = getPreferences(this) + windowManager.defaultDisplay.getMetrics(displayMetrics) + + loadSettings(this) + + // First time opening the app: show Tutorial, else: check versions + if (!preferences.getBoolean(PREF_STARTED, false)) + startActivity(Intent(this, TutorialActivity::class.java)) + else when (preferences.getString(PREF_VERSION, "")) { + // Check versions, make sure transitions between versions go well + + VERSION_NAME -> { /* the version installed and used previously are the same */ } + "" -> { /* The version used before was pre- v1.3.0, + as version tracking started then */ + + /* + * before, the dominant and vibrant color of the `finn` and `dark` theme + * were not stored anywhere. Now they have to be stored: + * -> we just reset them using newly implemented functions + */ + when (getSavedTheme(this)) { + "finn" -> resetToDefaultTheme(this) + "dark" -> resetToDarkTheme(this) + } + + preferences.edit() + .putString(PREF_VERSION, VERSION_NAME) // save new version + .apply() + + // show the new tutorial + startActivity(Intent(this, TutorialActivity::class.java)) + } + } + + // Preload apps to speed up the Apps Recycler + AsyncTask.execute { loadApps(packageManager) } + + // Initialise layout + setContentView(R.layout.home) + } + + override fun onStart(){ + super.onStart() + + mDetector = GestureDetectorCompat(this, this) + mDetector.setOnDoubleTapListener(this) + + // for if the settings changed + loadSettings(this) + super.onStart() + } + + override fun onResume() { + super.onResume() + + // Applying the date / time format (changeable in settings) + val dFormat = getPreferences(this).getInt(PREF_DATE_FORMAT, 0) + val upperFMT = resources.getStringArray(R.array.settings_launcher_time_formats_upper) + val lowerFMT = resources.getStringArray(R.array.settings_launcher_time_formats_lower) + + val dateFormat = SimpleDateFormat(upperFMT[dFormat], Locale.getDefault()) + val timeFormat = SimpleDateFormat(lowerFMT[dFormat], Locale.getDefault()) + + clockTimer = fixedRateTimer("clockTimer", true, 0L, 100) { + this@HomeActivity.runOnUiThread { + val t = timeFormat.format(Date()) + if (home_lower_view.text != t) + home_lower_view.text = t + + val d = dateFormat.format(Date()) + if (home_upper_view.text != d) + home_upper_view.text = d + } + } + } + + override fun onPause() { + super.onPause() + clockTimer.cancel() + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + when (keyCode) { + KeyEvent.KEYCODE_BACK -> LauncherAction.CHOOSE.launch(this) + KeyEvent.KEYCODE_VOLUME_UP -> Gesture.VOLUME_UP(this) + KeyEvent.KEYCODE_VOLUME_DOWN -> Gesture.VOLUME_DOWN(this) + } + return true + } + + override fun onFling(e1: MotionEvent?, e2: MotionEvent, dX: Float, dY: Float): Boolean { + + if (e1 == null) return false; + + val width = displayMetrics.widthPixels + val height = displayMetrics.heightPixels + + val diffX = e1.x - e2.x + val diffY = e1.y - e2.y + + val preferences = getPreferences(this) + + val doubleActions = preferences.getBoolean(PREF_DOUBLE_ACTIONS_ENABLED, false) + val edgeActions = preferences.getBoolean(PREF_EDGE_ACTIONS_ENABLED, false) + val edgeStrictness = 0.15 + // how distinguished the swipe has to be to launch something + // strictness = opposite of sensitivity. TODO - May have to be adjusted + val strictness = (4 / bufferedPointerCount) * ((100 - preferences.getInt(PREF_SLIDE_SENSITIVITY, 50)) / 50) + + var gesture = if(abs(diffX) > abs(diffY)) { // horizontal swipe + if (diffX > width / 4 && abs(diffX) > strictness * abs(diffY)) + Gesture.SWIPE_LEFT + else if (diffX < -width / 4 && abs(diffX) > strictness * abs(diffY)) + Gesture.SWIPE_RIGHT + else null + } else { // vertical swipe + // Only open if the swipe was not from the phones top edge + if (diffY < -height / 8 && abs(diffY) > strictness * abs(diffX) && e1.y > 100) + Gesture.SWIPE_DOWN + else if (diffY > height / 8 && abs(diffY) > strictness * abs(diffX)) + Gesture.SWIPE_UP + else null + } + + if (doubleActions && bufferedPointerCount > 1) { + gesture = gesture?.let(Gesture::getDoubleVariant) + } + + if (edgeActions) { + if(max(e1.x, e2.x) < edgeStrictness * width){ + gesture = gesture?.let{it.getEdgeVariant(Gesture.Edge.LEFT)}; + } else if (min(e1.x, e2.x) > (1-edgeStrictness) * width){ + gesture = gesture?.let{it.getEdgeVariant(Gesture.Edge.RIGHT)}; + } + + if(max(e1.y, e2.y) < edgeStrictness * height){ + gesture = gesture?.let{it.getEdgeVariant(Gesture.Edge.TOP)}; + } else if (min(e1.y, e2.y) > (1-edgeStrictness) * height){ + gesture = gesture?.let{it.getEdgeVariant(Gesture.Edge.BOTTOM)}; + } + } + gesture?.invoke(this) + + return true + } + + override fun onLongPress(event: MotionEvent) { + Gesture.LONG_CLICK(this) + } + + override fun onDoubleTap(event: MotionEvent): Boolean { + Gesture.DOUBLE_CLICK(this) + return false + } + + // Tooltip + override fun onSingleTapConfirmed(event: MotionEvent): Boolean { + + return false + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + + // Buffer / Debounce the pointer count + if (event.pointerCount > bufferedPointerCount) { + bufferedPointerCount = event.pointerCount + pointerBufferTimer = fixedRateTimer("pointerBufferTimer", true, 300, 1000) { + bufferedPointerCount = 1 + this.cancel() // a non-recurring timer + } + } + + return if (mDetector.onTouchEvent(event)) { false } else { super.onTouchEvent(event) } + } + + override fun setOnClicks() { + + val preferences = getPreferences(this) + home_upper_view.setOnClickListener { + when (preferences.getInt(PREF_DATE_FORMAT, 0)) { + 0 -> Gesture.DATE(this) + else -> Gesture.TIME(this) + } + } + + home_lower_view.setOnClickListener { + when (preferences.getInt(PREF_DATE_FORMAT, 0)) { + 0 -> Gesture.TIME(this) + else -> Gesture.DATE(this) + } + } + } + + /* TODO: Remove those. For now they are necessary + * because this inherits from GestureDetector.OnGestureListener */ + override fun onDoubleTapEvent(event: MotionEvent): Boolean { return false } + override fun onDown(event: MotionEvent): Boolean { return false } + override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dX: Float, dY: Float): Boolean { return false } + override fun onShowPress(event: MotionEvent) {} + override fun onSingleTapUp(event: MotionEvent): Boolean { return false } + +} diff --git a/app/src/main/java/de/jrpie/android/launcher/UIObject.kt b/app/src/main/java/de/jrpie/android/launcher/UIObject.kt new file mode 100644 index 0000000..894f948 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/UIObject.kt @@ -0,0 +1,22 @@ +package de.jrpie.android.launcher + +import android.app.Activity + +/** + * An interface implemented by every [Activity], Fragment etc. in Launcher. + * It handles themes and window flags - a useful abstraction as it is the same everywhere. + */ +interface UIObject { + fun onStart() { + if (this is Activity) setWindowFlags(window) + + applyTheme() + setOnClicks() + adjustLayout() + } + + // Don't use actual themes, rather create them on the fly for faster theme-switching + fun applyTheme() { } + fun setOnClicks() { } + fun adjustLayout() { } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt b/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt deleted file mode 100644 index 9a2dc62..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt +++ /dev/null @@ -1,100 +0,0 @@ -package de.jrpie.android.launcher.actions - -import android.app.Activity -import android.content.Context -import android.content.SharedPreferences.Editor -import android.graphics.Rect -import android.graphics.drawable.Drawable -import android.widget.Toast -import de.jrpie.android.launcher.R -import de.jrpie.android.launcher.preferences.LauncherPreferences -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import androidx.core.content.edit - - -@Serializable -sealed interface Action { - fun invoke(context: Context, rect: Rect? = null): Boolean - fun label(context: Context): String - fun getIcon(context: Context): Drawable? - fun isAvailable(context: Context): Boolean - - // Can the action be used to reach µLauncher settings? - fun canReachSettings(): Boolean - - - fun bindToGesture(prefEditor: Editor, id: String) { - prefEditor.putString(id, Json.encodeToString(this)) - } - - companion object { - - fun forGesture(gesture: Gesture): Action? { - val id = gesture.id - - val preferences = LauncherPreferences.getSharedPreferences() - val json = preferences.getString(id, "null")!! - return Json.decodeFromString(json) - } - - fun resetToDefaultActions(context: Context) { - LauncherPreferences.getSharedPreferences().edit { - val boundActions = HashSet() - Gesture.entries.forEach { gesture -> - context.resources - .getStringArray(gesture.defaultsResource) - .filterNot { boundActions.contains(it) } - .map { Pair(it, Json.decodeFromString(it)) } - .firstOrNull { it.second.isAvailable(context) } - ?.apply { - // allow to bind CHOOSE to multiple gestures - if (second != LauncherAction.CHOOSE) { - boundActions.add(first) - } - second.bindToGesture(this@edit, gesture.id) - } - } - } - } - - fun setActionForGesture(gesture: Gesture, action: Action?) { - if (action == null) { - clearActionForGesture(gesture) - return - } - LauncherPreferences.getSharedPreferences().edit { - action.bindToGesture(this, gesture.id) - } - } - - fun clearActionForGesture(gesture: Gesture) { - LauncherPreferences.getSharedPreferences().edit { - remove(gesture.id) - } - } - - fun launch( - action: Action?, - context: Context, - animationIn: Int = android.R.anim.fade_in, - animationOut: Int = android.R.anim.fade_out - ) { - if (action != null && action.invoke(context)) { - if (context is Activity) { - // There does not seem to be a good alternative to overridePendingTransition. - // Note that we can't use overrideActivityTransition here. - @Suppress("deprecation") - context.overridePendingTransition(animationIn, animationOut) - } - } else { - Toast.makeText( - context, - context.getString(R.string.toast_cant_open_message), - Toast.LENGTH_SHORT - ).show() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/AppAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/AppAction.kt deleted file mode 100644 index 1446b13..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/actions/AppAction.kt +++ /dev/null @@ -1,81 +0,0 @@ -package de.jrpie.android.launcher.actions - -import android.app.AlertDialog -import android.app.Service -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent -import android.content.pm.LauncherApps -import android.graphics.Rect -import android.graphics.drawable.Drawable -import android.util.Log -import de.jrpie.android.launcher.R -import de.jrpie.android.launcher.apps.AppInfo -import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER -import de.jrpie.android.launcher.apps.DetailedAppInfo -import de.jrpie.android.launcher.ui.list.apps.openSettings -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -@SerialName("action:app") -class AppAction(val app: AppInfo) : Action { - - override fun invoke(context: Context, rect: Rect?): Boolean { - val packageName = app.packageName - if (app.user != INVALID_USER) { - val launcherApps = - context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps - app.getLauncherActivityInfo(context)?.let { app -> - Log.i("Launcher", "Starting ${this.app}") - launcherApps.startMainActivity(app.componentName, app.user, rect, null) - return true - } - } - - context.packageManager.getLaunchIntentForPackage(packageName)?.let { - it.addCategory(Intent.CATEGORY_LAUNCHER) - try { - context.startActivity(it) - } catch (_: ActivityNotFoundException) { - return false - } - return true - } - - /* check if app is installed */ - if (isAvailable(context)) { - AlertDialog.Builder( - context, - R.style.AlertDialogCustom - ) - .setTitle(context.getString(R.string.alert_cant_open_title)) - .setMessage(context.getString(R.string.alert_cant_open_message)) - .setPositiveButton(android.R.string.ok) { _, _ -> - app.openSettings(context) - } - .setNegativeButton(android.R.string.cancel, null) - .setIcon(android.R.drawable.ic_dialog_info) - .show() - return true - } - return false - } - - override fun label(context: Context): String { - return DetailedAppInfo.fromAppInfo(app, context)?.getCustomLabel(context).toString() - } - - override fun getIcon(context: Context): Drawable? { - return DetailedAppInfo.fromAppInfo(app, context)?.getIcon(context) - } - - override fun isAvailable(context: Context): Boolean { - // check if app is installed - return DetailedAppInfo.fromAppInfo(app, context) != null - } - - override fun canReachSettings(): Boolean { - return false - } -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt b/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt deleted file mode 100644 index a2434e1..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt +++ /dev/null @@ -1,369 +0,0 @@ -package de.jrpie.android.launcher.actions - -import android.content.Context -import android.util.Log -import de.jrpie.android.launcher.R -import de.jrpie.android.launcher.preferences.LauncherPreferences - -/** - * @param id internal id to serialize the action. Used as a key in shared preferences. - * @param defaultsResource res id of array of default actions for the gesture. - * @param labelResource res id of the name of the gesture. - * @param animationIn res id of transition animation (in) when using the gesture to launch an app. - * @param animationOut res id of transition animation (out) when using the gesture to launch an app. - */ -enum class Gesture( - val id: String, - private val labelResource: Int, - private val descriptionResource: Int, - internal val defaultsResource: Int, - private val animationIn: Int = android.R.anim.fade_in, - private val animationOut: Int = android.R.anim.fade_out -) { - VOLUME_UP( - "action.volume_up", - R.string.settings_gesture_vol_up, - R.string.settings_gesture_description_vol_up, - R.array.default_volume_up, - 0, - 0 - ), - VOLUME_DOWN( - "action.volume_down", - R.string.settings_gesture_vol_down, - R.string.settings_gesture_description_vol_down, - R.array.default_volume_down, 0, 0 - ), - TIME( - "action.time", - R.string.settings_gesture_time, - R.string.settings_gesture_description_time, - R.array.default_time - ), - DATE( - "action.date", - R.string.settings_gesture_date, - R.string.settings_gesture_description_date, - R.array.default_date - ), - LONG_CLICK( - "action.long_click", - R.string.settings_gesture_long_click, - R.string.settings_gesture_description_long_click, - R.array.default_long_click, 0, 0 - ), - DOUBLE_CLICK( - "action.double_click", - R.string.settings_gesture_double_click, - R.string.settings_gesture_description_double_click, - R.array.default_double_click, 0, 0 - ), - SWIPE_UP( - "action.up", - R.string.settings_gesture_up, - R.string.settings_gesture_description_up, - R.array.default_up, - R.anim.bottom_up - ), - SWIPE_UP_LEFT_EDGE( - "action.up_left", - R.string.settings_gesture_up_left_edge, - R.string.settings_gesture_description_up_left_edge, - R.array.default_up_left, - R.anim.bottom_up - ), - SWIPE_UP_RIGHT_EDGE( - "action.up_right", - R.string.settings_gesture_up_right_edge, - R.string.settings_gesture_description_up_right_edge, - R.array.default_up_right, - R.anim.bottom_up - ), - TAP_AND_SWIPE_UP( - "action.tap_up", - R.string.settings_gesture_tap_up, - R.string.settings_gesture_description_tap_up, - R.array.default_up, - R.anim.bottom_up - ), - SWIPE_UP_DOUBLE( - "action.double_up", - R.string.settings_gesture_double_up, - R.string.settings_gesture_description_double_up, - R.array.default_double_up, - R.anim.bottom_up - ), - SWIPE_DOWN( - "action.down", - R.string.settings_gesture_down, - R.string.settings_gesture_description_down, - R.array.default_down, - R.anim.top_down - ), - SWIPE_DOWN_LEFT_EDGE( - "action.down_left", - R.string.settings_gesture_down_left_edge, - R.string.settings_gesture_description_down_left_edge, - R.array.default_down_left, - R.anim.top_down - ), - SWIPE_DOWN_RIGHT_EDGE( - "action.down_right", - R.string.settings_gesture_down_right_edge, - R.string.settings_gesture_description_down_right_edge, - R.array.default_down_right, - R.anim.top_down - ), - TAP_AND_SWIPE_DOWN( - "action.tap_down", - R.string.settings_gesture_tap_down, - R.string.settings_gesture_description_tap_down, - R.array.default_down, - R.anim.bottom_up - ), - SWIPE_DOWN_DOUBLE( - "action.double_down", - R.string.settings_gesture_double_down, - R.string.settings_gesture_description_double_down, - R.array.default_double_down, - R.anim.top_down - ), - SWIPE_LEFT( - "action.left", - R.string.settings_gesture_left, - R.string.settings_gesture_description_left, - R.array.default_messengers, - R.anim.right_left - ), - SWIPE_LEFT_TOP_EDGE( - "action.left_top", - R.string.settings_gesture_left_top_edge, - R.string.settings_gesture_description_left_top_edge, - R.array.default_messengers, - R.anim.right_left - ), - SWIPE_LEFT_BOTTOM_EDGE( - "action.left_bottom", - R.string.settings_gesture_left_bottom_edge, - R.string.settings_gesture_description_left_bottom_edge, - R.array.default_messengers, - R.anim.right_left - ), - TAP_AND_SWIPE_LEFT( - "action.tap_left", - R.string.settings_gesture_tap_left, - R.string.settings_gesture_description_tap_left, - R.array.default_messengers, - R.anim.right_left - ), - SWIPE_LEFT_DOUBLE( - "action.double_left", - R.string.settings_gesture_double_left, - R.string.settings_gesture_description_double_left, - R.array.default_messengers, - R.anim.right_left - ), - SWIPE_RIGHT( - "action.right", - R.string.settings_gesture_right, - R.string.settings_gesture_description_right, - R.array.default_right, - R.anim.left_right - ), - SWIPE_RIGHT_TOP_EDGE( - "action.right_top", - R.string.settings_gesture_right_top_edge, - R.string.settings_gesture_description_right_top_edge, - R.array.default_right_top, - R.anim.left_right - ), - SWIPE_RIGHT_BOTTOM_EDGE( - "action.right_bottom", - R.string.settings_gesture_right_bottom_edge, - R.string.settings_gesture_description_right_bottom_edge, - R.array.default_right_bottom, - R.anim.left_right - ), - TAP_AND_SWIPE_RIGHT( - "action.tap_right", - R.string.settings_gesture_tap_right, - R.string.settings_gesture_description_tap_right, - R.array.default_right, - R.anim.left_right - ), - SWIPE_RIGHT_DOUBLE( - "action.double_right", - R.string.settings_gesture_double_right, - R.string.settings_gesture_description_double_right, - R.array.default_double_right, - R.anim.left_right - ), - SWIPE_LARGER( - "action.larger", - R.string.settings_gesture_swipe_larger, - R.string.settings_gesture_description_swipe_larger, - R.array.no_default - ), - SWIPE_LARGER_REVERSE( - "action.larger_reverse", - R.string.settings_gesture_swipe_larger_reverse, - R.string.settings_gesture_description_swipe_larger_reverse, - R.array.no_default - ), - SWIPE_SMALLER( - "action.smaller", - R.string.settings_gesture_swipe_smaller, - R.string.settings_gesture_description_swipe_smaller, - R.array.no_default - ), - SWIPE_SMALLER_REVERSE( - "action.smaller_reverse", - R.string.settings_gesture_swipe_smaller_reverse, - R.string.settings_gesture_description_swipe_smaller_reverse, - R.array.no_default - ), - SWIPE_LAMBDA( - "action.lambda", - R.string.settings_gesture_swipe_lambda, - R.string.settings_gesture_description_swipe_lambda, - R.array.no_default - ), - SWIPE_LAMBDA_REVERSE( - "action.lambda_reverse", - R.string.settings_gesture_swipe_lambda_reverse, - R.string.settings_gesture_description_swipe_lambda_reverse, - R.array.no_default - ), - SWIPE_V( - "action.v", - R.string.settings_gesture_swipe_v, - R.string.settings_gesture_description_swipe_v, - R.array.no_default - ), - SWIPE_V_REVERSE( - "action.v_reverse", - R.string.settings_gesture_swipe_v_reverse, - R.string.settings_gesture_description_swipe_v_reverse, - R.array.no_default - ), - BACK( - "action.back", - R.string.settings_gesture_back, - R.string.settings_gesture_description_back, - R.array.default_back - ); - - enum class Edge { - TOP, BOTTOM, LEFT, RIGHT - } - - fun getLabel(context: Context): String { - return context.resources.getString(this.labelResource) - } - - fun getDescription(context: Context): String { - return context.resources.getString(this.descriptionResource) - } - - fun getDoubleVariant(): Gesture { - return when (this) { - SWIPE_UP -> SWIPE_UP_DOUBLE - SWIPE_DOWN -> SWIPE_DOWN_DOUBLE - SWIPE_LEFT -> SWIPE_LEFT_DOUBLE - SWIPE_RIGHT -> SWIPE_RIGHT_DOUBLE - else -> this - } - } - - fun getEdgeVariant(edge: Edge): Gesture { - return when (edge) { - Edge.TOP -> - when (this) { - SWIPE_LEFT -> SWIPE_LEFT_TOP_EDGE - SWIPE_RIGHT -> SWIPE_RIGHT_TOP_EDGE - else -> this - } - - Edge.BOTTOM -> - when (this) { - SWIPE_LEFT -> SWIPE_LEFT_BOTTOM_EDGE - SWIPE_RIGHT -> SWIPE_RIGHT_BOTTOM_EDGE - else -> this - } - - Edge.LEFT -> - when (this) { - SWIPE_UP -> SWIPE_UP_LEFT_EDGE - SWIPE_DOWN -> SWIPE_DOWN_LEFT_EDGE - else -> this - } - - Edge.RIGHT -> - when (this) { - SWIPE_UP -> SWIPE_UP_RIGHT_EDGE - SWIPE_DOWN -> SWIPE_DOWN_RIGHT_EDGE - else -> this - } - } - } - - fun getTapComboVariant(): Gesture { - return when (this) { - SWIPE_UP -> TAP_AND_SWIPE_UP - SWIPE_DOWN -> TAP_AND_SWIPE_DOWN - SWIPE_LEFT -> TAP_AND_SWIPE_LEFT - SWIPE_RIGHT -> TAP_AND_SWIPE_RIGHT - else -> this - } - - } - - fun isDoubleVariant(): Boolean { - return when (this) { - SWIPE_UP_DOUBLE, - SWIPE_DOWN_DOUBLE, - SWIPE_LEFT_DOUBLE, - SWIPE_RIGHT_DOUBLE -> true - - else -> false - } - } - - fun isEdgeVariant(): Boolean { - return when (this) { - SWIPE_UP_RIGHT_EDGE, - SWIPE_UP_LEFT_EDGE, - SWIPE_DOWN_LEFT_EDGE, - SWIPE_DOWN_RIGHT_EDGE, - SWIPE_LEFT_TOP_EDGE, - SWIPE_LEFT_BOTTOM_EDGE, - SWIPE_RIGHT_TOP_EDGE, - SWIPE_RIGHT_BOTTOM_EDGE -> true - - else -> false - } - } - - fun isEnabled(): Boolean { - if (isEdgeVariant()) { - return LauncherPreferences.enabled_gestures().edgeSwipe() - } - if (isDoubleVariant()) { - return LauncherPreferences.enabled_gestures().doubleSwipe() - } - return true - } - - operator fun invoke(context: Context) { - Log.i("Launcher", "Detected gesture: $this") - val action = Action.forGesture(this) - Action.launch(action, context, this.animationIn, this.animationOut) - } - - companion object { - fun byId(id: String): Gesture? { - return Gesture.entries.firstOrNull { it.id == id } - } - } - -} diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt deleted file mode 100644 index 6ba467e..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt +++ /dev/null @@ -1,341 +0,0 @@ -package de.jrpie.android.launcher.actions - -import android.content.Context -import android.content.Intent -import android.graphics.Rect -import android.graphics.drawable.Drawable -import android.media.AudioManager -import android.os.Build -import android.os.SystemClock -import android.view.KeyEvent -import android.widget.Toast -import androidx.appcompat.content.res.AppCompatResources -import de.jrpie.android.launcher.Application -import de.jrpie.android.launcher.BuildConfig -import de.jrpie.android.launcher.R -import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService -import de.jrpie.android.launcher.apps.AppFilter -import de.jrpie.android.launcher.apps.hidePrivateSpaceWhenLocked -import de.jrpie.android.launcher.apps.isPrivateSpaceSupported -import de.jrpie.android.launcher.apps.togglePrivateSpaceLock -import de.jrpie.android.launcher.preferences.LauncherPreferences -import de.jrpie.android.launcher.ui.list.ListActivity -import de.jrpie.android.launcher.ui.settings.SettingsActivity -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.encoding.decodeStructure -import kotlinx.serialization.encoding.encodeStructure - -@Serializable(with = LauncherActionSerializer::class) -@SerialName("action:launcher") -enum class LauncherAction( - val id: String, - val label: Int, - val icon: Int, - val launch: (Context) -> Unit, - private val canReachSettings: Boolean = false, - val available: (Context) -> Boolean = { true }, -) : Action { - SETTINGS( - "settings", - R.string.list_other_settings, - R.drawable.baseline_settings_24, - ::openSettings, - true - ), - CHOOSE( - "choose", - R.string.list_other_list, - R.drawable.baseline_menu_24, - ::openAppsList, - true - ), - CHOOSE_FROM_FAVORITES( - "choose_from_favorites", - R.string.list_other_list_favorites, - R.drawable.baseline_favorite_24, - { context -> openAppsList(context, favorite = true) }, - true - ), - CHOOSE_FROM_PRIVATE_SPACE( - "choose_from_private_space", - R.string.list_other_list_private_space, - R.drawable.baseline_security_24, - { context -> - if ((context.applicationContext as Application).privateSpaceLocked.value != true - || !hidePrivateSpaceWhenLocked(context) - ) { - openAppsList(context, private = true) - } - }, - available = { _ -> - isPrivateSpaceSupported() - } - ), - TOGGLE_PRIVATE_SPACE_LOCK( - "toggle_private_space_lock", - R.string.list_other_toggle_private_space_lock, - R.drawable.baseline_security_24, - ::togglePrivateSpaceLock, - available = { _ -> isPrivateSpaceSupported() } - ), - VOLUME_UP( - "volume_up", - R.string.list_other_volume_up, - R.drawable.baseline_volume_up_24, - { context -> audioVolumeAdjust(context, AudioManager.ADJUST_RAISE) } - ), - VOLUME_DOWN( - "volume_down", - R.string.list_other_volume_down, - R.drawable.baseline_volume_down_24, - { context -> audioVolumeAdjust(context, AudioManager.ADJUST_LOWER) } - ), - VOLUME_ADJUST( - "volume_adjust", - R.string.list_other_volume_adjust, - R.drawable.baseline_volume_adjust_24, - { context -> audioVolumeAdjust(context, AudioManager.ADJUST_SAME) } - ), - TRACK_PLAY_PAUSE( - "play_pause_track", - R.string.list_other_track_play_pause, - R.drawable.baseline_play_arrow_24, - { context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) } - ), - TRACK_NEXT( - "next_track", - R.string.list_other_track_next, - R.drawable.baseline_skip_next_24, - { context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_NEXT) } - ), - TRACK_PREV( - "previous_track", - R.string.list_other_track_previous, - R.drawable.baseline_skip_previous_24, - { context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PREVIOUS) } - ), - EXPAND_NOTIFICATIONS_PANEL( - "expand_notifications_panel", - R.string.list_other_expand_notifications_panel, - R.drawable.baseline_notifications_24, - ::expandNotificationsPanel - ), - EXPAND_SETTINGS_PANEL( - "expand_settings_panel", - R.string.list_other_expand_settings_panel, - R.drawable.baseline_settings_applications_24, - ::expandSettingsPanel - ), - RECENT_APPS( - "recent_apps", - R.string.list_other_recent_apps, - R.drawable.baseline_apps_24, - LauncherAccessibilityService::openRecentApps, - false, - { _ -> BuildConfig.USE_ACCESSIBILITY_SERVICE } - ), - LOCK_SCREEN( - "lock_screen", - R.string.list_other_lock_screen, - R.drawable.baseline_lock_24, - { c -> LauncherPreferences.actions().lockMethod().lockOrEnable(c) } - ), - TORCH( - "toggle_torch", - R.string.list_other_torch, - R.drawable.baseline_flashlight_on_24, - ::toggleTorch, - ), - LAUNCH_OTHER_LAUNCHER( - "launcher_other_launcher", - R.string.list_other_launch_other_launcher, - R.drawable.baseline_home_24, - ::launchOtherLauncher - ), - NOP("nop", R.string.list_other_nop, R.drawable.baseline_not_interested_24, {}); - - override fun invoke(context: Context, rect: Rect?): Boolean { - launch(context) - return true - } - - override fun label(context: Context): String { - return context.getString(label) - } - - override fun getIcon(context: Context): Drawable? { - return AppCompatResources.getDrawable(context, icon) - } - - override fun isAvailable(context: Context): Boolean { - return this.available(context) - } - - override fun canReachSettings(): Boolean { - return this.canReachSettings - } - - companion object { - fun byId(id: String): LauncherAction? { - return entries.singleOrNull { it.id == id } - } - } -} - - -/* Media player actions */ -private fun audioManagerPressKey(context: Context, key: Int) { - val mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - val eventTime: Long = SystemClock.uptimeMillis() - val downEvent = - KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, key, 0) - mAudioManager.dispatchMediaKeyEvent(downEvent) - val upEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, key, 0) - mAudioManager.dispatchMediaKeyEvent(upEvent) - -} - -private fun audioVolumeAdjust(context: Context, direction: Int) { - val audioManager = - context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - - audioManager.adjustStreamVolume( - AudioManager.STREAM_MUSIC, - direction, - AudioManager.FLAG_SHOW_UI - ) -} - -/* End media player actions */ - -private fun toggleTorch(context: Context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - Toast.makeText( - context, - context.getString(R.string.alert_requires_android_m), - Toast.LENGTH_LONG - ).show() - return - } - - (context.applicationContext as Application).torchManager?.toggleTorch(context) -} - -private fun expandNotificationsPanel(context: Context) { - /* https://stackoverflow.com/a/15582509 */ - try { - @Suppress("SpellCheckingInspection") - val statusBarService: Any? = context.getSystemService("statusbar") - val statusBarManager = Class.forName("android.app.StatusBarManager") - val showStatusBar = statusBarManager.getMethod("expandNotificationsPanel") - showStatusBar.invoke(statusBarService) - } catch (e: Exception) { - Toast.makeText( - context, - context.getString(R.string.alert_cant_expand_status_bar_panel), - Toast.LENGTH_LONG - ).show() - } -} - - -private fun expandSettingsPanel(context: Context) { - /* https://stackoverflow.com/a/31898506 */ - try { - @Suppress("SpellCheckingInspection") - val statusBarService: Any? = context.getSystemService("statusbar") - val statusBarManager = Class.forName("android.app.StatusBarManager") - val showStatusBar = statusBarManager.getMethod("expandSettingsPanel") - showStatusBar.invoke(statusBarService) - } catch (e: Exception) { - Toast.makeText( - context, - context.getString(R.string.alert_cant_expand_status_bar_panel), - Toast.LENGTH_LONG - ).show() - } -} - -private fun launchOtherLauncher(context: Context) { - context.startActivity( - Intent.createChooser( - Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) }, - context.getString(R.string.list_other_launch_other_launcher) - ) - ) -} - -private fun openSettings(context: Context) { - context.startActivity(Intent(context, SettingsActivity::class.java)) -} - -fun openAppsList( - context: Context, - favorite: Boolean = false, - hidden: Boolean = false, - private: Boolean = false -) { - val intent = Intent(context, ListActivity::class.java) - intent.putExtra("intention", ListActivity.ListActivityIntention.VIEW.toString()) - intent.putExtra( - "favoritesVisibility", - if (favorite) { - AppFilter.Companion.AppSetVisibility.EXCLUSIVE - } else { - AppFilter.Companion.AppSetVisibility.VISIBLE - } - ) - intent.putExtra( - "hiddenVisibility", - if (hidden) { - AppFilter.Companion.AppSetVisibility.EXCLUSIVE - } else { - AppFilter.Companion.AppSetVisibility.HIDDEN - } - ) - intent.putExtra( - "privateSpaceVisibility", - if (private) { - AppFilter.Companion.AppSetVisibility.EXCLUSIVE - } else if (!hidden && LauncherPreferences.apps().hidePrivateSpaceApps()) { - AppFilter.Companion.AppSetVisibility.HIDDEN - } else { - AppFilter.Companion.AppSetVisibility.VISIBLE - } - ) - - context.startActivity(intent) -} - -/* A custom serializer is required to store type information, - see https://github.com/Kotlin/kotlinx.serialization/issues/1486 - */ -private class LauncherActionSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor( - "action:launcher", - ) { - element("value", String.serializer().descriptor) - } - - override fun deserialize(decoder: Decoder): LauncherAction { - val s = decoder.decodeStructure(descriptor) { - decodeElementIndex(descriptor) - decodeSerializableElement(descriptor, 0, String.serializer()) - } - return LauncherAction.byId(s) ?: throw SerializationException() - } - - override fun serialize(encoder: Encoder, value: LauncherAction) { - encoder.encodeStructure(descriptor) { - encodeSerializableElement(descriptor, 0, String.serializer(), value.id) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/ShortcutAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/ShortcutAction.kt deleted file mode 100644 index a89f9e2..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/actions/ShortcutAction.kt +++ /dev/null @@ -1,57 +0,0 @@ -package de.jrpie.android.launcher.actions - -import android.app.Service -import android.content.Context -import android.content.pm.LauncherApps -import android.graphics.Rect -import android.graphics.drawable.Drawable -import android.os.Build -import de.jrpie.android.launcher.apps.PinnedShortcutInfo -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -@SerialName("action:shortcut") -class ShortcutAction(val shortcut: PinnedShortcutInfo) : Action { - - override fun invoke(context: Context, rect: Rect?): Boolean { - val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { - // TODO - return false - } - shortcut.getShortcutInfo(context)?.let { - launcherApps.startShortcut(it, rect, null) - } - - // TODO: handle null - return true - } - - override fun label(context: Context): String { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { - return "?" - } - - return shortcut.getShortcutInfo(context)?.longLabel?.toString() ?: "?" - } - - override fun getIcon(context: Context): Drawable? { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { - return null - } - val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps - return shortcut.getShortcutInfo(context)?.let { launcherApps.getShortcutBadgedIconDrawable(it, 0) } - } - - override fun isAvailable(context: Context): Boolean { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { - return false - } - return shortcut.getShortcutInfo(context) != null - } - - override fun canReachSettings(): Boolean { - return false - } -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/TorchManager.kt b/app/src/main/java/de/jrpie/android/launcher/actions/TorchManager.kt deleted file mode 100644 index 7e694c6..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/actions/TorchManager.kt +++ /dev/null @@ -1,90 +0,0 @@ -package de.jrpie.android.launcher.actions - -import android.content.Context -import android.hardware.camera2.CameraAccessException -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraManager -import android.os.Build -import android.os.Build.VERSION_CODES -import android.os.Handler -import android.os.Looper -import android.widget.Toast -import androidx.annotation.RequiresApi -import de.jrpie.android.launcher.R - -@RequiresApi(VERSION_CODES.M) -class TorchManager(context: Context) { - - private val camera = getCameraId(context) - private var torchEnabled = false - - private val torchCallback = object : CameraManager.TorchCallback() { - override fun onTorchModeChanged(cameraId: String, enabled: Boolean) { - synchronized(this@TorchManager) { - if (cameraId == camera) { - torchEnabled = enabled - } - } - } - } - - init { - registerCallback(context) - } - - private fun getCameraId(context: Context): String? { - val cameraManager = - context.getSystemService(Context.CAMERA_SERVICE) as CameraManager - - return cameraManager.cameraIdList.firstOrNull { c -> - cameraManager - .getCameraCharacteristics(c) - .get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false - } - } - - private fun registerCallback(context: Context) { - val cameraManager = - context.getSystemService(Context.CAMERA_SERVICE) as CameraManager - - cameraManager.registerTorchCallback( - torchCallback, - Handler(Looper.getMainLooper()) - ) - } - - fun toggleTorch(context: Context) { - synchronized(this) { - val cameraManager = - context.getSystemService(Context.CAMERA_SERVICE) as CameraManager - - if (camera == null) { - Toast.makeText( - context, - context.getString(R.string.alert_no_torch_found), - Toast.LENGTH_LONG - ).show() - return - } - - try { - if (!torchEnabled && Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { - cameraManager.turnOnTorchWithStrengthLevel( - camera, - cameraManager.getCameraCharacteristics(camera) - .get(CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL) ?: 1 - ) - } else { - cameraManager.setTorchMode(camera, !torchEnabled) - } - - } catch (e: CameraAccessException) { - Toast.makeText( - context, - context.getString(R.string.alert_torch_access_exception), - Toast.LENGTH_LONG - ).show() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherAccessibilityService.kt b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherAccessibilityService.kt deleted file mode 100644 index 7cb32d9..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherAccessibilityService.kt +++ /dev/null @@ -1,151 +0,0 @@ -package de.jrpie.android.launcher.actions.lock - -import android.accessibilityservice.AccessibilityService -import android.app.AlertDialog -import android.content.Context -import android.content.Intent -import android.os.Build -import android.provider.Settings -import android.util.Log -import android.view.accessibility.AccessibilityEvent -import android.widget.CheckBox -import android.widget.Toast -import de.jrpie.android.launcher.R - -class LauncherAccessibilityService : AccessibilityService() { - override fun onInterrupt() {} - - override fun onAccessibilityEvent(event: AccessibilityEvent?) { - // Intentionally left blank, we are not interested in any AccessibilityEvents. - // DO NOT ADD ANY CODE HERE! - } - - companion object { - private const val TAG = "Launcher Accessibility" - private const val ACTION_REQUEST_ENABLE = "ACTION_REQUEST_ENABLE" - const val ACTION_LOCK_SCREEN = "ACTION_LOCK_SCREEN" - const val ACTION_RECENT_APPS = "ACTION_RECENT_APPS" - - private fun invoke(context: Context, action: String, failureMessageRes: Int) { - try { - context.startService( - Intent( - context, - LauncherAccessibilityService::class.java - ).apply { - this.action = action - }) - } catch (_: Exception) { - Toast.makeText( - context, - context.getString(failureMessageRes), - Toast.LENGTH_LONG - ).show() - } - } - - fun lockScreen(context: Context) { - if (!isEnabled(context)) { - showEnableDialog(context) - } else { - invoke(context, ACTION_LOCK_SCREEN, R.string.alert_lock_screen_failed) - } - } - - fun openRecentApps(context: Context) { - if (!isEnabled(context)) { - showEnableDialog(context) - } else { - invoke(context, ACTION_RECENT_APPS, R.string.alert_recent_apps_failed) - } - } - - fun isEnabled(context: Context): Boolean { - val enabledServices = Settings.Secure.getString( - context.contentResolver, - Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES - ) ?: return false - - return enabledServices.split(":") - .contains("${context.packageName}/${LauncherAccessibilityService::class.java.name}") - .also { Log.d(TAG, "Accessibility Service enabled: $it") } - } - - fun showEnableDialog(context: Context) { - AlertDialog.Builder(context, R.style.AlertDialogDanger).apply { - setView(R.layout.dialog_consent_accessibility) - setTitle(R.string.dialog_consent_accessibility_title) - setPositiveButton(R.string.dialog_consent_accessibility_ok) { _, _ -> - invoke(context, ACTION_REQUEST_ENABLE, R.string.alert_enable_accessibility_failed) - } - setNegativeButton(R.string.dialog_cancel) { _, _ -> } - }.create().also { it.show() }.apply { - val buttonOk = getButton(AlertDialog.BUTTON_POSITIVE) - val checkboxes = listOf( - findViewById(R.id.dialog_consent_accessibility_checkbox_1), - findViewById(R.id.dialog_consent_accessibility_checkbox_2), - findViewById(R.id.dialog_consent_accessibility_checkbox_3), - findViewById(R.id.dialog_consent_accessibility_checkbox_4), - ) - val update = { - buttonOk.isEnabled = checkboxes.map { b -> b?.isChecked == true }.all { it } - } - update() - checkboxes.forEach { c -> - c?.setOnClickListener { _ -> update() } - } - } - } - } - - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - intent?.action?.let { action -> - if (!isEnabled(this)) { - Toast.makeText( - this, - getString(R.string.toast_accessibility_service_not_enabled), - Toast.LENGTH_LONG - ).show() - requestEnable() - return START_NOT_STICKY - } - - when (action) { - ACTION_REQUEST_ENABLE -> {} // do nothing - ACTION_LOCK_SCREEN -> handleLockScreen() - ACTION_RECENT_APPS -> performGlobalAction(GLOBAL_ACTION_RECENTS) - } - } - return super.onStartCommand(intent, flags, startId) - } - - private fun requestEnable() { - startActivity( - Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).addFlags( - Intent.FLAG_ACTIVITY_NEW_TASK - ) - ) - } - - private fun handleLockScreen() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - Toast.makeText( - this, - getText(R.string.toast_lock_screen_not_supported), - Toast.LENGTH_SHORT - ).show() - return - } - - val success = performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN) - if (!success) { - Toast.makeText( - this, - getText(R.string.alert_lock_screen_failed), - Toast.LENGTH_LONG - ).show() - requestEnable() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherDeviceAdmin.kt b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherDeviceAdmin.kt deleted file mode 100644 index 47c57d2..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherDeviceAdmin.kt +++ /dev/null @@ -1,56 +0,0 @@ -package de.jrpie.android.launcher.actions.lock - -import android.app.admin.DeviceAdminReceiver -import android.app.admin.DevicePolicyManager -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.widget.Toast -import de.jrpie.android.launcher.R - -class LauncherDeviceAdmin : DeviceAdminReceiver() { - companion object { - private fun getComponentName(context: Context): ComponentName { - return ComponentName(context, LauncherDeviceAdmin::class.java) - } - - private fun requestDeviceAdmin(context: Context) { - - val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN).apply { - putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, getComponentName(context)) - putExtra( - DevicePolicyManager.EXTRA_ADD_EXPLANATION, - context.getString(R.string.device_admin_explanation) - ) - } - context.startActivity(intent) - } - - fun isDeviceAdmin(context: Context): Boolean { - val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - return dpm.isAdminActive(getComponentName(context)) - } - - private fun assertDeviceAdmin(context: Context): Boolean { - val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - if (!dpm.isAdminActive(getComponentName(context))) { - Toast.makeText( - context, - context.getString(R.string.toast_device_admin_not_enabled), - Toast.LENGTH_LONG - ).show() - requestDeviceAdmin(context) - return false - } - return true - } - - fun lockScreen(context: Context) { - - assertDeviceAdmin(context) || return - - val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - dpm.lockNow() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt deleted file mode 100644 index 93b4cbf..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt +++ /dev/null @@ -1,71 +0,0 @@ -package de.jrpie.android.launcher.actions.lock - -import android.content.Context -import android.os.Build -import android.widget.Button -import androidx.appcompat.app.AlertDialog -import de.jrpie.android.launcher.BuildConfig -import de.jrpie.android.launcher.R -import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService -import de.jrpie.android.launcher.preferences.LauncherPreferences - - -enum class LockMethod( - private val lock: (Context) -> Unit, - private val isEnabled: (Context) -> Boolean, - private val enable: (Context) -> Unit -) { - DEVICE_ADMIN( - LauncherDeviceAdmin::lockScreen, - LauncherDeviceAdmin::isDeviceAdmin, - LauncherDeviceAdmin::lockScreen - ), - ACCESSIBILITY_SERVICE( - LauncherAccessibilityService::lockScreen, - LauncherAccessibilityService::isEnabled, - LauncherAccessibilityService::showEnableDialog - ), - ; - - fun lockOrEnable(context: Context) { - if (!this.isEnabled(context)) { - chooseMethod(context) - return - } - this.lock(context) - } - - companion object { - fun chooseMethod(context: Context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || - ! BuildConfig.USE_ACCESSIBILITY_SERVICE) { - // only device admin is available - setMethod(context, DEVICE_ADMIN) - return - } - AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { - setNegativeButton(R.string.dialog_cancel) { _, _ -> } - setView(R.layout.dialog_select_lock_method) - // setTitle() - }.create().also { it.show() }.apply { - findViewById