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..d14b126 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help improve this app +title: '[bug] ' +labels: bug +assignees: '' + +--- + +# Describe the bug + + + +# To Reproduce + + + +# Expected behavior + + + +# Screenshots + + +# Smartphone (please complete the following information) + - Device: + - Android Version: + - µLauncher Version: + +# Additional info + + 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/workflows/android.yml b/.github/workflows/android.yml index ba9a709..912995a 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -29,4 +29,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: launcher-debug-${{ github.sha }}.apk - path: app/build/outputs/apk/default/debug/app-default-debug.apk + path: app/build/outputs/apk/debug/app-debug.apk 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/docs/build.md b/BUILD.md similarity index 50% rename from docs/build.md rename to BUILD.md index 75921f9..ac1f11d 100644 --- a/docs/build.md +++ b/BUILD.md @@ -8,25 +8,10 @@ Make sure that `JAVA_HOME` and `ANDROID_HOME` are set correctly. ``` git clone https://github.com/jrpie/Launcher cd Launcher - -./gradlew assembleDefaultRelease +./gradlew build ``` -This will create an apk file at `app/build/outputs/apk/default/release/app-default-release-unsigned.apk`. - -Note that you need to sign it: -``` -apksigner sign --ks "$YOUR_KEYSTORE" \ - --ks-key-alias "$YOUR_ALIAS" \ - --ks-pass="pass:$YOUR_PASSWORD" \ - --key-pass="pass:$YOUR_PASSWORD" \ - --alignment-preserved \ - --v1-signing-enabled=true \ - --v2-signing-enabled=true \ - --v3-signing-enabled=true \ - --v4-signing-enabled=true \ - app-default-release-unsigned.apk -``` +Note that you need to sign the apk. See [this guide](https://developer.android.com/build/building-cmdline) 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..b6c1f9d 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,22 @@ µ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*. +Your home screen 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. -Get it on F-Droid -Get it on Accrescent -Get it on Obtainium -Get it on GitHub +This is a fork of [finnmglas's app Launcher][original-repo]. + + + Get it on F-Droid + + Get it on Obtainium + + +or download the latest APK from the [releases section](https://github.com/jrpie/Launcher/releases/latest). 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. -µ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: @@ -93,14 +63,36 @@ There are several ways to contribute to this app: * 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. + - Create a new branch named `feature/` of `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. +See [BUILD.md](BUILD.md) for instructions how to build this project. +## Notable changes compared to [Finn's Launcher][original-repo]: + +* Edge gestures: There is a setting to allow distinguishing swiping at the edges of the screen from swiping in the center. +* Compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used. +* The home button now works as expected. + +### 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. + +### 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 diff --git a/app/build.gradle b/app/build.gradle index 1a0a6fb..d851ef5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,9 +1,9 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' -apply plugin: 'kotlinx-serialization' android { + dataBinding { enabled = true } @@ -23,8 +23,8 @@ android { minSdkVersion 21 targetSdkVersion 35 compileSdk 35 - versionCode 44 - versionName "0.1.4" + versionCode 34 + versionName "j-0.0.18" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -43,37 +43,10 @@ android { buildTypes { release { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - debug { - applicationIdSuffix = ".debug" - versionNameSuffix = "-debug" + // minifyEnabled true + // proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - - 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 @@ -85,26 +58,24 @@ android { // Disables dependency metadata when building Android App Bundles. includeInBundle = false } - lint { + + lintOptions { 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.gridlayout:gridlayout:1.0.0' implementation 'androidx.palette:palette-ktx:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.4.0' + implementation 'androidx.recyclerview:recyclerview:1.3.2' 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" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 9e3e326..329090c 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. # @@ -27,4 +25,3 @@ # 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/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..466aefc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,98 +3,82 @@ xmlns:tools="http://schemas.android.com/tools"> - + - - - - - - + tools:ignore="UnusedAttribute" > + + + android:theme="@style/launcherHomeTheme" + android:launchMode="singleTask" > - - - - - - - - + android:name="de.jrpie.android.launcher.ui.tutorial.TutorialActivity" + android:configChanges="orientation|screenSize" > + android:name="de.jrpie.android.launcher.ui.list.ListActivity" + android:configChanges="orientation|screenSize" + android:windowSoftInputMode="adjustResize" > + + + + + + + - - + - + android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" + android:label="@string/accessibility_service_name"> - - diff --git a/app/src/main/java/de/jrpie/android/launcher/Application.kt b/app/src/main/java/de/jrpie/android/launcher/Application.kt index e6cce23..25e3e07 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Application.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt @@ -1,95 +1,19 @@ 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.apps.AppInfo 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 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() } } @@ -106,55 +30,12 @@ class Application : android.app.Application() { 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 { + 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..cc1b982 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -3,129 +3,147 @@ 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.content.Context import android.content.Intent import android.content.pm.LauncherApps -import android.content.pm.LauncherApps.ShortcutQuery import android.content.pm.PackageManager -import android.content.pm.ShortcutInfo +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Rect +import android.net.Uri import android.os.Build import android.os.Bundle import android.os.UserHandle import android.os.UserManager import android.provider.Settings import android.util.Log -import android.widget.Toast -import androidx.annotation.RequiresApi +import android.view.View +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import android.view.inputmethod.InputMethodManager +import android.widget.ImageView 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.list.apps.AppsRecyclerAdapter import de.jrpie.android.launcher.ui.tutorial.TutorialActivity -import androidx.core.net.toUri -const val LOG_TAG = "Launcher" +/* Objects used by multiple activities */ +val appsList: MutableList = ArrayList() + +/* REQUEST CODES */ + +const val REQUEST_CHOOSE_APP = 1 +const val REQUEST_UNINSTALL = 2 const val REQUEST_SET_DEFAULT_HOME = 42 -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) - } else { - val testIntent = Intent(Intent.ACTION_MAIN) - testIntent.addCategory(Intent.CATEGORY_HOME) - val defaultHome = testIntent.resolveActivity(context.packageManager)?.packageName - return defaultHome == context.packageName - } +const val LOG_TAG = "Launcher" + +/* 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 setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) { - val isDefault = isDefaultHomeScreen(context) - if (checkDefault && isDefault) { - // Launcher is already the default home app - return - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + if (checkDefault + && 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 - ) + if (!roleManager.isRoleHeld(RoleManager.ROLE_HOME)) { + context.startActivityForResult( + roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME), + REQUEST_SET_DEFAULT_HOME + ) + } return } + if (checkDefault) { + val testIntent = Intent(Intent.ACTION_MAIN) + testIntent.addCategory(Intent.CATEGORY_HOME) + val defaultHome = testIntent.resolveActivity(context.packageManager)?.packageName + if (defaultHome == context.packageName) { + // Launcher is already the default home app + return + } + } val intent = Intent(Settings.ACTION_HOME_SETTINGS) context.startActivity(intent) } -fun getUserFromId(userId: Int?, context: Context): UserHandle { + +fun getIntent(packageName: String, context: Context): Intent? { + val intent: Intent? = context.packageManager.getLaunchIntentForPackage(packageName) + intent?.addCategory(Intent.CATEGORY_LAUNCHER) + return intent +} +/* --- */ + +fun getUserFromId(user: 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] + return profiles.firstOrNull { it.hashCode() == user } ?: profiles[0] } -@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 - } + +fun uninstallApp(appInfo: AppInfo, activity: Activity) { + val packageName = appInfo.packageName.toString() + val user = appInfo.user + + Log.i(LOG_TAG, "uninstalling $appInfo") + val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE) + intent.data = Uri.parse("package:$packageName") + getUserFromId(user, activity).let { user -> + intent.putExtra(Intent.EXTRA_USER, user) } - 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) { } + intent.putExtra(Intent.EXTRA_RETURN_RESULT, true) + activity.startActivityForResult( + intent, + REQUEST_UNINSTALL + ) } -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 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) +} + +fun openAppSettings( + appInfo: AppInfo, + context: Context, + sourceBounds: Rect? = null, + opts: Bundle? = null +) { + val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps + appInfo.getLauncherActivityInfo(context)?.let { app -> + launcherApps.startAppDetailsActivity(app.componentName, app.user, sourceBounds, opts) } } @@ -135,43 +153,24 @@ fun openTutorial(context: Context) { /** - * 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, context: Context) { + 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)) + loadList.add(DetailedAppInfo(it)) } } + // fallback option if (loadList.isEmpty()) { Log.w(LOG_TAG, "using fallback option to load packages") @@ -179,48 +178,40 @@ fun getApps( 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 app = AppInfo(ri.activityInfo.packageName, null, AppInfo.INVALID_USER) val detailedAppInfo = DetailedAppInfo( app, ri.loadLabel(packageManager), - ri.activityInfo.loadIcon(packageManager), - false + ri.activityInfo.loadIcon(packageManager) ) 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 + loadList.sortBy { it.getCustomLabel(context).toString() } + 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() + +// Used in Tutorial and Settings `ActivityOnResult` +fun saveListActivityChoice(data: Intent?) { + val forGesture = data?.getStringExtra("forGesture") ?: return + Gesture.byId(forGesture)?.let { Action.setActionForGesture(it, Action.fromIntent(data)) } } -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 +// 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) +} + +// 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/actions/Action.kt b/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt index 9a2dc62..0912207 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt @@ -2,61 +2,84 @@ package de.jrpie.android.launcher.actions import android.app.Activity import android.content.Context +import android.content.Intent 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.apps.AppInfo +import de.jrpie.android.launcher.apps.AppInfo.Companion.INVALID_USER +import de.jrpie.android.launcher.apps.DetailedAppInfo 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 { +interface Action { fun invoke(context: Context, rect: Rect? = null): Boolean + fun bindToGesture(prefEditor: Editor, id: String) 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)) - } + fun writeToIntent(intent: Intent) companion object { + /** + * Get an action for a specific id. + * An id is of the form: + * - "launcher:${launcher_action_name}", see [LauncherAction] + * - "${package_name}", see [AppAction] + * - "${package_name}:${activity_name}", see [AppAction] + * + * @param id + * @param user a user id, ignored if the action is a [LauncherAction]. + * @param context used to complete [AppInfo] if possible + */ + private fun fromId(id: String, user: Int?, context: Context? = null): Action? { + if (id.isEmpty()) { + return null + } + if (LauncherAction.isOtherAction(id)) { + return LauncherAction.byId(id) + } + + val values = id.split(";") + + var info = AppInfo(values[0], values.getOrNull(1), user ?: INVALID_USER) + + // try to complete an incomplete AppInfo if a context is provided + if (context != null && (info.user == INVALID_USER || info.activityName == null)) { + info = DetailedAppInfo.fromAppInfo(info, context)?.app?:info + } + + return AppAction(info) + } fun forGesture(gesture: Gesture): Action? { val id = gesture.id val preferences = LauncherPreferences.getSharedPreferences() - val json = preferences.getString(id, "null")!! - return Json.decodeFromString(json) + val actionId = preferences.getString("$id.app", "")!! + var u: Int? = preferences.getInt("$id.user", INVALID_USER) + u = if (u == INVALID_USER) null else u + + return fromId(actionId, u) } 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) - } - } + val editor = LauncherPreferences.getSharedPreferences().edit() + val boundActions = HashSet() + Gesture.entries.forEach { gesture -> + context.resources + .getStringArray(gesture.defaultsResource) + .filterNot { boundActions.contains(it) } + .map { Pair(it, fromId(it, null, context)) } + .firstOrNull { it.second?.isAvailable(context) ?: false } + ?.apply { + boundActions.add(first) + second?.bindToGesture(editor, gesture.id) + } } + editor.apply() } fun setActionForGesture(gesture: Gesture, action: Action?) { @@ -64,15 +87,16 @@ sealed interface Action { clearActionForGesture(gesture) return } - LauncherPreferences.getSharedPreferences().edit { - action.bindToGesture(this, gesture.id) - } + val editor = LauncherPreferences.getSharedPreferences().edit() + action.bindToGesture(editor, gesture.id) + editor.apply() } fun clearActionForGesture(gesture: Gesture) { - LauncherPreferences.getSharedPreferences().edit { - remove(gesture.id) - } + LauncherPreferences.getSharedPreferences().edit() + .putString(gesture.id + ".app", "") + .putInt(gesture.id + ".user", INVALID_USER) + .apply() } fun launch( @@ -83,9 +107,6 @@ sealed interface Action { ) { 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 { @@ -96,5 +117,11 @@ sealed interface Action { ).show() } } + + fun fromIntent(data: Intent): Action? { + val value = data.getStringExtra("action_id") ?: return null + val user = data.getIntExtra("user", INVALID_USER) + return fromId(value, user) + } } } \ 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 index 1446b13..741e19b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/AppAction.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/AppAction.kt @@ -2,47 +2,42 @@ 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.SharedPreferences 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.AppInfo.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 +import de.jrpie.android.launcher.getIntent +import de.jrpie.android.launcher.openAppSettings -@Serializable -@SerialName("action:app") -class AppAction(val app: AppInfo) : Action { +class AppAction(val appInfo: AppInfo) : Action { override fun invoke(context: Context, rect: Rect?): Boolean { - val packageName = app.packageName - if (app.user != INVALID_USER) { + val packageName = appInfo.packageName.toString() + if (appInfo.user != INVALID_USER) { val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps - app.getLauncherActivityInfo(context)?.let { app -> - Log.i("Launcher", "Starting ${this.app}") + appInfo.getLauncherActivityInfo(context)?.let { app -> + Log.i("Launcher", "Starting $appInfo") 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 - } + val intent = getIntent(packageName, context) + + if (intent != null) { + context.startActivity(intent) return true } + /* check if app is installed */ if (isAvailable(context)) { AlertDialog.Builder( @@ -52,7 +47,7 @@ class AppAction(val app: AppInfo) : Action { .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) + openAppSettings(appInfo, context) } .setNegativeButton(android.R.string.cancel, null) .setIcon(android.R.drawable.ic_dialog_info) @@ -63,19 +58,33 @@ class AppAction(val app: AppInfo) : Action { } override fun label(context: Context): String { - return DetailedAppInfo.fromAppInfo(app, context)?.getCustomLabel(context).toString() + return DetailedAppInfo.fromAppInfo(appInfo, context)?.getCustomLabel(context).toString() } override fun getIcon(context: Context): Drawable? { - return DetailedAppInfo.fromAppInfo(app, context)?.getIcon(context) + return DetailedAppInfo.fromAppInfo(appInfo, context)?.icon } override fun isAvailable(context: Context): Boolean { // check if app is installed - return DetailedAppInfo.fromAppInfo(app, context) != null + return DetailedAppInfo.fromAppInfo(appInfo, context) != null } - override fun canReachSettings(): Boolean { - return false + override fun bindToGesture(editor: SharedPreferences.Editor, id: String) { + val u = appInfo.user + + // TODO: replace this by AppInfo#serialize (breaking change to SharedPreferences!) + var app = appInfo.packageName.toString() + if (appInfo.activityName != null) { + app += ";${appInfo.activityName}" + } + editor + .putString("$id.app", app) + .putInt("$id.user", u) + } + + override fun writeToIntent(intent: Intent) { + intent.putExtra("action_id", "${appInfo.packageName};${appInfo.activityName}") + intent.putExtra("user", appInfo.user) } } \ 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 index a2434e1..e7358ba 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt @@ -1,7 +1,6 @@ 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 @@ -79,13 +78,6 @@ enum class Gesture( 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, @@ -114,13 +106,6 @@ enum class Gesture( 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, @@ -149,13 +134,6 @@ enum class Gesture( 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, @@ -184,73 +162,12 @@ enum class Gesture( 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 { @@ -307,17 +224,6 @@ enum class Gesture( } } - 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, @@ -355,14 +261,13 @@ enum class Gesture( } 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 } + return Gesture.values().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 index 6ba467e..6c23bf0 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt @@ -2,6 +2,7 @@ package de.jrpie.android.launcher.actions import android.content.Context import android.content.Intent +import android.content.SharedPreferences.Editor import android.graphics.Rect import android.graphics.drawable.Drawable import android.media.AudioManager @@ -9,158 +10,83 @@ 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.apps.AppInfo.Companion.INVALID_USER 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 }, + val launch: (Context) -> Unit ) : Action { SETTINGS( - "settings", + "launcher:settings", R.string.list_other_settings, R.drawable.baseline_settings_24, - ::openSettings, - true + ::openSettings ), CHOOSE( - "choose", + "launcher:choose", R.string.list_other_list, R.drawable.baseline_menu_24, - ::openAppsList, - true + ::openAppsList ), CHOOSE_FROM_FAVORITES( - "choose_from_favorites", + "launcher:chooseFromFavorites", 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() } + { context -> openAppsList(context, true) } ), VOLUME_UP( - "volume_up", + "launcher:volumeUp", R.string.list_other_volume_up, - R.drawable.baseline_volume_up_24, - { context -> audioVolumeAdjust(context, AudioManager.ADJUST_RAISE) } + R.drawable.baseline_volume_up_24, ::audioVolumeUp ), VOLUME_DOWN( - "volume_down", + "launcher:volumeDown", 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) } + R.drawable.baseline_volume_down_24, ::audioVolumeDown ), TRACK_NEXT( - "next_track", + "launcher:nextTrack", R.string.list_other_track_next, - R.drawable.baseline_skip_next_24, - { context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_NEXT) } + R.drawable.baseline_skip_next_24, ::audioNextTrack ), TRACK_PREV( - "previous_track", + "launcher:previousTrack", R.string.list_other_track_previous, - R.drawable.baseline_skip_previous_24, - { context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PREVIOUS) } + R.drawable.baseline_skip_previous_24, ::audioPreviousTrack ), EXPAND_NOTIFICATIONS_PANEL( - "expand_notifications_panel", + "launcher:expandNotificationsPanel", R.string.list_other_expand_notifications_panel, R.drawable.baseline_notifications_24, ::expandNotificationsPanel ), EXPAND_SETTINGS_PANEL( - "expand_settings_panel", + "launcher:expandSettingsPanel", 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", + "launcher:lockScreen", R.string.list_other_lock_screen, - R.drawable.baseline_lock_24, + R.drawable.baseline_lock_24px, { c -> LauncherPreferences.actions().lockMethod().lockOrEnable(c) } ), TORCH( - "toggle_torch", + "launcher:toggleTorch", R.string.list_other_torch, R.drawable.baseline_flashlight_on_24, - ::toggleTorch, + ::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, {}); + NOP("launcher:nop", R.string.list_other_nop, R.drawable.baseline_not_interested_24, {}); override fun invoke(context: Context, rect: Rect?): Boolean { launch(context) @@ -172,48 +98,86 @@ enum class LauncherAction( } override fun getIcon(context: Context): Drawable? { - return AppCompatResources.getDrawable(context, icon) + return context.getDrawable(icon) + } + + override fun bindToGesture(editor: Editor, id: String) { + editor + .putString("$id.app", this.id) + .putInt("$id.user", INVALID_USER) + } + + override fun writeToIntent(intent: Intent) { + intent.putExtra("action_id", id) } override fun isAvailable(context: Context): Boolean { - return this.available(context) - } - - override fun canReachSettings(): Boolean { - return this.canReachSettings + return true } companion object { fun byId(id: String): LauncherAction? { return entries.singleOrNull { it.id == id } } + + fun isOtherAction(id: String): Boolean { + return id.startsWith("launcher") + } } } /* 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 audioNextTrack(context: Context) { + + val mAudioManager = context.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) } -private fun audioVolumeAdjust(context: Context, direction: Int) { +private fun audioPreviousTrack(context: Context) { + val mAudioManager = context.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) +} + +private fun audioVolumeUp(context: Context) { val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager.adjustStreamVolume( AudioManager.STREAM_MUSIC, - direction, + AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI ) } +private fun audioVolumeDown(context: Context) { + val audioManager = + context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + audioManager.adjustStreamVolume( + AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_LOWER, + AudioManager.FLAG_SHOW_UI + ) +} /* End media player actions */ private fun toggleTorch(context: Context) { @@ -246,7 +210,6 @@ private fun expandNotificationsPanel(context: Context) { } } - private fun expandSettingsPanel(context: Context) { /* https://stackoverflow.com/a/31898506 */ try { @@ -264,25 +227,11 @@ private fun expandSettingsPanel(context: Context) { } } -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 -) { +fun openAppsList(context: Context, favorite: Boolean = false, hidden: Boolean = false) { val intent = Intent(context, ListActivity::class.java) intent.putExtra("intention", ListActivity.ListActivityIntention.VIEW.toString()) intent.putExtra( @@ -301,41 +250,6 @@ fun openAppsList( 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/lock/LauncherAccessibilityService.kt b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherAccessibilityService.kt index 7cb32d9..9e018d1 100644 --- 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 @@ -22,44 +22,26 @@ class LauncherAccessibilityService : AccessibilityService() { 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) { + fun lockScreen(context: Context) { try { context.startService( Intent( context, LauncherAccessibilityService::class.java ).apply { - this.action = action + action = ACTION_LOCK_SCREEN }) - } catch (_: Exception) { + } catch (e: Exception) { Toast.makeText( context, - context.getString(failureMessageRes), + context.getString(R.string.alert_lock_screen_failed), 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, @@ -76,9 +58,9 @@ class LauncherAccessibilityService : AccessibilityService() { 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) + lockScreen(context) } - setNegativeButton(R.string.dialog_cancel) { _, _ -> } + setNegativeButton(R.string.dialog_consent_accessibility_cancel) { _, _ -> } }.create().also { it.show() }.apply { val buttonOk = getButton(AlertDialog.BUTTON_POSITIVE) val checkboxes = listOf( @@ -112,9 +94,7 @@ class LauncherAccessibilityService : AccessibilityService() { } when (action) { - ACTION_REQUEST_ENABLE -> {} // do nothing ACTION_LOCK_SCREEN -> handleLockScreen() - ACTION_RECENT_APPS -> performGlobalAction(GLOBAL_ACTION_RECENTS) } } return super.onStartCommand(intent, flags, startId) 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 index 93b4cbf..16572e5 100644 --- 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 @@ -2,14 +2,13 @@ package de.jrpie.android.launcher.actions.lock import android.content.Context import android.os.Build -import android.widget.Button +import android.view.LayoutInflater 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 +@Suppress("unused") enum class LockMethod( private val lock: (Context) -> Unit, private val isEnabled: (Context) -> Boolean, @@ -37,28 +36,32 @@ enum class LockMethod( companion object { fun chooseMethod(context: Context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || - ! BuildConfig.USE_ACCESSIBILITY_SERVICE) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { // 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