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..207fe34 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,19 @@ µ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. + +This is a fork of [finnmglas's app Launcher][original-repo]. + Get it on F-Droid -Get it on Accrescent Get it on Obtainium Get it on GitHub + +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 +60,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..d4e22e6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { minSdkVersion 21 targetSdkVersion 35 compileSdk 35 - versionCode 44 - versionName "0.1.4" + versionCode 35 + versionName "j-0.0.19" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -43,37 +43,14 @@ android { buildTypes { release { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + // 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 @@ -85,23 +62,22 @@ 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") 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/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..e3e5f7c 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Application.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt @@ -1,40 +1,23 @@ 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.AsyncTask 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.apps.DetailedAppInfo 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() - } - } + val apps = MutableLiveData>() // TODO: only update specific apps private val launcherAppsCallback = object : LauncherApps.Callback() { @@ -84,12 +67,10 @@ class Application : android.app.Application() { } 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() } } @@ -110,10 +91,12 @@ class Application : android.app.Application() { // 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 + // First time opening the app: set defaults and start tutorial if (!LauncherPreferences.internal().started()) { resetPreferences(this) + + LauncherPreferences.internal().started(true) + openTutorial(this) } @@ -124,37 +107,15 @@ class Application : android.app.Application() { 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)) - } + AsyncTask.execute { 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..e8d3851 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -3,74 +3,58 @@ 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.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 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 -const val LOG_TAG = "Launcher" +/* 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) { +const val LOG_TAG = "Launcher" + +fun setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) { + + if (checkDefault + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + && context is Activity + ) { val roleManager = context.getSystemService(RoleManager::class.java) - return roleManager.isRoleHeld(RoleManager.ROLE_HOME) - } else { + 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 - return defaultHome == context.packageName + if (defaultHome == context.packageName) { + // Launcher is already the default home app + return + } } -} - -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 - && 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 - } - val intent = Intent(Settings.ACTION_HOME_SETTINGS) context.startActivity(intent) } @@ -84,91 +68,34 @@ fun getUserFromId(userId: Int?, context: Context): UserHandle { return profiles.firstOrNull { it.hashCode() == userId } ?: 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 - } - } - - 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 openInBrowser(url: String, context: Context) { - val intent = Intent(Intent.ACTION_VIEW, url.toUri()) + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) 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() - } + context.startActivity(intent) } fun openTutorial(context: Context) { - context.startActivity(Intent(context, TutorialActivity::class.java)) + context.startActivity(Intent(context, TutorialActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }) } /** * Load all apps. */ -fun getApps( - packageManager: PackageManager, - context: Context -): MutableList { - var start = System.currentTimeMillis() - val loadList = mutableListOf() +fun getApps(packageManager: PackageManager, context: Context): MutableList { + val start = System.currentTimeMillis() + 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)) } } @@ -179,48 +106,27 @@ 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) } + loadList.sortBy { it.getCustomLabel(context).toString() } - var end = System.currentTimeMillis() + val 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 } -// 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 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..2d03061 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,6 +2,7 @@ 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 @@ -11,7 +12,6 @@ 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 @@ -21,14 +21,15 @@ sealed interface Action { 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) { + intent.putExtra("action", Json.encodeToString(this)) + } + companion object { fun forGesture(gesture: Gesture): Action? { @@ -40,23 +41,20 @@ sealed interface Action { } 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, Json.decodeFromString(it)) } + .firstOrNull { it.second.isAvailable(context) } + ?.apply { + boundActions.add(first) + second.bindToGesture(editor, gesture.id) + } } + editor.apply() } fun setActionForGesture(gesture: Gesture, action: Action?) { @@ -64,15 +62,15 @@ 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() + .remove(gesture.id) + .apply() } fun launch( @@ -83,9 +81,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 +91,10 @@ sealed interface Action { ).show() } } + + fun fromIntent(data: Intent): Action? { + val json = data.getStringExtra("action") ?: return null + return Json.decodeFromString(json) + } } } \ 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..2bb3780 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,7 +2,6 @@ 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 @@ -11,7 +10,7 @@ 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 @@ -35,11 +34,7 @@ class AppAction(val app: AppInfo) : Action { context.packageManager.getLaunchIntentForPackage(packageName)?.let { it.addCategory(Intent.CATEGORY_LAUNCHER) - try { - context.startActivity(it) - } catch (_: ActivityNotFoundException) { - return false - } + context.startActivity(it) return true } @@ -67,15 +62,11 @@ class AppAction(val app: AppInfo) : Action { } override fun getIcon(context: Context): Drawable? { - return DetailedAppInfo.fromAppInfo(app, context)?.getIcon(context) + return DetailedAppInfo.fromAppInfo(app, context)?.icon } 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 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..3c641ff 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 @@ -9,15 +9,9 @@ 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 @@ -39,88 +33,45 @@ 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", R.string.list_other_settings, R.drawable.baseline_settings_24, - ::openSettings, - true + ::openSettings ), CHOOSE( "choose", R.string.list_other_list, R.drawable.baseline_menu_24, - ::openAppsList, - true + ::openAppsList ), 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() } + { context -> openAppsList(context, true) } ), VOLUME_UP( "volume_up", 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", 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", 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", 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", @@ -134,31 +85,17 @@ enum class LauncherAction( 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, + R.drawable.baseline_lock_24px, { 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 + ::toggleTorch ), NOP("nop", R.string.list_other_nop, R.drawable.baseline_not_interested_24, {}); @@ -172,15 +109,11 @@ enum class LauncherAction( } override fun getIcon(context: Context): Drawable? { - return AppCompatResources.getDrawable(context, icon) + return context.getDrawable(icon) } override fun isAvailable(context: Context): Boolean { - return this.available(context) - } - - override fun canReachSettings(): Boolean { - return this.canReachSettings + return true } companion object { @@ -192,28 +125,56 @@ enum class LauncherAction( /* 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 +207,6 @@ private fun expandNotificationsPanel(context: Context) { } } - private fun expandSettingsPanel(context: Context) { /* https://stackoverflow.com/a/31898506 */ try { @@ -264,25 +224,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,16 +247,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) } @@ -324,7 +260,6 @@ private class LauncherActionSerializer : KSerializer { ) { element("value", String.serializer().descriptor) } - override fun deserialize(decoder: Decoder): LauncherAction { val s = decoder.decodeStructure(descriptor) { decodeElementIndex(descriptor) @@ -338,4 +273,5 @@ private class LauncherActionSerializer : KSerializer { 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..a8ef6f2 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,7 +58,7 @@ 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) { _, _ -> } }.create().also { it.show() }.apply { @@ -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..5198288 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 @@ -4,12 +4,11 @@ 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 +@Suppress("unused") enum class LockMethod( private val lock: (Context) -> Unit, private val isEnabled: (Context) -> Boolean, @@ -37,8 +36,7 @@ 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 diff --git a/app/src/main/java/de/jrpie/android/launcher/apps/AbstractAppInfo.kt b/app/src/main/java/de/jrpie/android/launcher/apps/AbstractAppInfo.kt deleted file mode 100644 index dd60752..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/apps/AbstractAppInfo.kt +++ /dev/null @@ -1,22 +0,0 @@ -package de.jrpie.android.launcher.apps - -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -/** - * This interface is implemented by [AppInfo] and [PinnedShortcutInfo]. - */ -@Serializable -sealed interface AbstractAppInfo { - fun serialize(): String { - return Json.encodeToString(this) - } - companion object { - const val INVALID_USER = -1 - - fun deserialize(serialized: String): AbstractAppInfo { - return Json.decodeFromString(serialized) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/apps/AbstractDetailedAppInfo.kt b/app/src/main/java/de/jrpie/android/launcher/apps/AbstractDetailedAppInfo.kt deleted file mode 100644 index 9c7413d..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/apps/AbstractDetailedAppInfo.kt +++ /dev/null @@ -1,42 +0,0 @@ -package de.jrpie.android.launcher.apps - -import android.content.Context -import android.graphics.drawable.Drawable -import android.os.UserHandle -import android.util.Log -import de.jrpie.android.launcher.Application -import de.jrpie.android.launcher.actions.Action -import de.jrpie.android.launcher.preferences.LauncherPreferences - -/** - * This interface is implemented by [DetailedAppInfo] and [DetailedPinnedShortcutInfo] - */ -sealed interface AbstractDetailedAppInfo { - fun getRawInfo(): AbstractAppInfo - fun getLabel(): String - fun getIcon(context: Context): Drawable - fun getUser(context: Context): UserHandle - fun isPrivate(): Boolean - fun isRemovable(): Boolean - fun getAction(): Action - - - fun getCustomLabel(context: Context): String { - val map = (context.applicationContext as? Application)?.getCustomAppNames() - return map?.get(getRawInfo()) ?: getLabel() - } - - - fun setCustomLabel(label: CharSequence?) { - Log.i("Launcher", "Setting custom label for ${this.getRawInfo()} to ${label}.") - val map = LauncherPreferences.apps().customNames() ?: HashMap() - - if (label.isNullOrEmpty()) { - map.remove(getRawInfo()) - } else { - map[getRawInfo()] = label.toString() - } - LauncherPreferences.apps().customNames(map) - } - -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt b/app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt index ca387c0..948249f 100644 --- a/app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt +++ b/app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt @@ -1,53 +1,41 @@ package de.jrpie.android.launcher.apps import android.content.Context -import android.icu.text.Normalizer2 -import android.os.Build import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.AppAction import de.jrpie.android.launcher.actions.Gesture -import de.jrpie.android.launcher.actions.ShortcutAction import de.jrpie.android.launcher.preferences.LauncherPreferences import java.util.Locale import kotlin.text.Regex.Companion.escape class AppFilter( var context: Context, - var query: String, + var search: String, var favoritesVisibility: AppSetVisibility = AppSetVisibility.VISIBLE, var hiddenVisibility: AppSetVisibility = AppSetVisibility.HIDDEN, - var privateSpaceVisibility: AppSetVisibility = AppSetVisibility.VISIBLE ) { - - operator fun invoke(apps: List): List { + operator fun invoke(apps: List): List { var apps = - apps.sortedBy { app -> app.getCustomLabel(context).lowercase(Locale.ROOT) } + apps.sortedBy { app -> app.getCustomLabel(context).toString().lowercase(Locale.ROOT) } val hidden = LauncherPreferences.apps().hidden() ?: setOf() val favorites = LauncherPreferences.apps().favorites() ?: setOf() - val private = apps.filter { it.isPrivate() } - .map { it.getRawInfo() }.toSet() apps = apps.filter { info -> favoritesVisibility.predicate(favorites, info) && hiddenVisibility.predicate(hidden, info) - && privateSpaceVisibility.predicate(private, info) } if (LauncherPreferences.apps().hideBoundApps()) { val boundApps = Gesture.entries .filter(Gesture::isEnabled) - .mapNotNull { g -> Action.forGesture(g) } - .mapNotNull { - (it as? AppAction)?.app - ?: (it as? ShortcutAction)?.shortcut - } + .mapNotNull { g -> (Action.forGesture(g) as? AppAction)?.app } .toSet() - apps = apps.filterNot { info -> boundApps.contains(info.getRawInfo()) } + apps = apps.filterNot { info -> boundApps.contains(info.app) } } // normalize text for search - val allowedSpecialCharacters = unicodeNormalize(query) + val allowedSpecialCharacters = search .lowercase(Locale.ROOT) .toCharArray() .distinct() @@ -57,21 +45,20 @@ class AppFilter( val disallowedCharsRegex = "[^\\p{L}$allowedSpecialCharacters]".toRegex() fun normalize(text: String): String { - return unicodeNormalize(text).replace(disallowedCharsRegex, "") + return text.lowercase(Locale.ROOT).replace(disallowedCharsRegex, "") } - - if (query.isEmpty()) { + if (search.isEmpty()) { return apps } else { - val r: MutableList = ArrayList() - val appsSecondary: MutableList = ArrayList() - val normalizedQuery: String = normalize(query) + val r: MutableList = ArrayList() + val appsSecondary: MutableList = ArrayList() + val normalizedText: String = normalize(search) for (item in apps) { - val itemLabel: String = normalize(item.getCustomLabel(context)) + val itemLabel: String = normalize(item.getCustomLabel(context).toString()) - if (itemLabel.startsWith(normalizedQuery)) { + if (itemLabel.startsWith(normalizedText)) { r.add(item) - } else if (itemLabel.contains(normalizedQuery)) { + } else if (itemLabel.contains(normalizedText)) { appsSecondary.add(item) } } @@ -83,20 +70,12 @@ class AppFilter( companion object { enum class AppSetVisibility( - val predicate: (set: Set, AbstractDetailedAppInfo) -> Boolean + val predicate: (set: Set, DetailedAppInfo) -> Boolean ) { VISIBLE({ _, _ -> true }), - HIDDEN({ set, appInfo -> !set.contains(appInfo.getRawInfo()) }), - EXCLUSIVE({ set, appInfo -> set.contains(appInfo.getRawInfo()) }), + HIDDEN({ set, appInfo -> !set.contains(appInfo.app) }), + EXCLUSIVE({ set, appInfo -> set.contains(appInfo.app) }), ; } - - private fun unicodeNormalize(s: String): String { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - val normalizer = Normalizer2.getNFKDInstance() - return normalizer.normalize(s.lowercase(Locale.ROOT)) - } - return s.lowercase(Locale.ROOT) - } } } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/apps/AppInfo.kt b/app/src/main/java/de/jrpie/android/launcher/apps/AppInfo.kt index 9534431..21614f8 100644 --- a/app/src/main/java/de/jrpie/android/launcher/apps/AppInfo.kt +++ b/app/src/main/java/de/jrpie/android/launcher/apps/AppInfo.kt @@ -4,18 +4,33 @@ import android.app.Service import android.content.Context import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherApps -import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER import de.jrpie.android.launcher.getUserFromId -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json /** * Represents an app installed on the users device. * Contains the minimal amount of data required to identify the app. */ @Serializable -@SerialName("app") -data class AppInfo(val packageName: String, val activityName: String?, val user: Int = INVALID_USER): AbstractAppInfo { +class AppInfo(val packageName: String, val activityName: String?, val user: Int = INVALID_USER) { + + fun serialize(): String { + return Json.encodeToString(this) + } + + override fun equals(other: Any?): Boolean { + if(other is AppInfo) { + return other.user == user && other.packageName == packageName + && other.activityName == activityName + } + return super.equals(other) + } + + override fun hashCode(): Int { + return packageName.hashCode() + } fun getLauncherActivityInfo( context: Context @@ -26,4 +41,17 @@ data class AppInfo(val packageName: String, val activityName: String?, val user: return activityList.firstOrNull { app -> app.name == activityName } ?: activityList.firstOrNull() } + + + override fun toString(): String { + return "AppInfo {package=$packageName, activity=$activityName, user=$user}" + } + + companion object { + const val INVALID_USER = -1 + + fun deserialize(serialized: String): AppInfo { + return Json.decodeFromString(serialized) + } + } } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/apps/DetailedAppInfo.kt b/app/src/main/java/de/jrpie/android/launcher/apps/DetailedAppInfo.kt index 76f7fbb..519798d 100644 --- a/app/src/main/java/de/jrpie/android/launcher/apps/DetailedAppInfo.kt +++ b/app/src/main/java/de/jrpie/android/launcher/apps/DetailedAppInfo.kt @@ -4,23 +4,21 @@ import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.LauncherActivityInfo import android.graphics.drawable.Drawable -import android.os.UserHandle -import de.jrpie.android.launcher.actions.Action -import de.jrpie.android.launcher.actions.AppAction -import de.jrpie.android.launcher.getUserFromId +import android.util.Log +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.preferences.LauncherPreferences /** - * Stores information used to create [de.jrpie.android.launcher.ui.list.apps.AppsRecyclerAdapter] rows. + * Stores information used to create [AppsRecyclerAdapter] rows. */ class DetailedAppInfo( - private val app: AppInfo, - private val label: CharSequence, - private val icon: Drawable, - private val privateSpace: Boolean, - private val removable: Boolean = true, -): AbstractDetailedAppInfo { + val app: AppInfo, + val label: CharSequence, + val icon: Drawable, + val isSystemApp: Boolean = false, +) { - constructor(activityInfo: LauncherActivityInfo, private: Boolean) : this( + constructor(activityInfo: LauncherActivityInfo) : this( AppInfo( activityInfo.applicationInfo.packageName, activityInfo.name, @@ -28,47 +26,32 @@ class DetailedAppInfo( ), activityInfo.label, activityInfo.getBadgedIcon(0), - private, - // App can be uninstalled iff it is not a system app - activityInfo.applicationInfo.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 + activityInfo.applicationInfo.flags.and(ApplicationInfo.FLAG_SYSTEM) != 0 ) + fun getCustomLabel(context: Context): CharSequence { + val map = (context.applicationContext as? Application)?.getCustomAppNames() ?: return label - - override fun getLabel(): String { - return label.toString() + return map[app] ?: label } - override fun getIcon(context: Context): Drawable { - return icon - } + fun setCustomLabel(label: CharSequence?) { - override fun getRawInfo(): AppInfo { - return app - } + Log.i("Launcher", "Setting custom label for ${this.app} to ${label}.") + val map = LauncherPreferences.apps().customNames() ?: HashMap() - override fun getUser(context: Context): UserHandle { - return getUserFromId(app.user, context) - } + if (label.isNullOrEmpty()) { + map.remove(app) + } else { + map[app] = label.toString() + } - override fun isPrivate(): Boolean { - return privateSpace + LauncherPreferences.apps().customNames(map) } - override fun isRemovable(): Boolean { - return removable - } - - override fun getAction(): Action { - return AppAction(app) - } - - companion object { fun fromAppInfo(appInfo: AppInfo, context: Context): DetailedAppInfo? { - return appInfo.getLauncherActivityInfo(context)?.let { - DetailedAppInfo(it, it.user == getPrivateSpaceUser(context)) - } + return appInfo.getLauncherActivityInfo(context)?.let { DetailedAppInfo(it) } } } } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/apps/DetailedPinnedShortcutInfo.kt b/app/src/main/java/de/jrpie/android/launcher/apps/DetailedPinnedShortcutInfo.kt deleted file mode 100644 index f66034d..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/apps/DetailedPinnedShortcutInfo.kt +++ /dev/null @@ -1,66 +0,0 @@ -package de.jrpie.android.launcher.apps - -import android.app.Service -import android.content.Context -import android.content.pm.LauncherApps -import android.content.pm.ShortcutInfo -import android.graphics.drawable.Drawable -import android.os.Build -import android.os.UserHandle -import androidx.annotation.RequiresApi -import de.jrpie.android.launcher.actions.Action -import de.jrpie.android.launcher.actions.ShortcutAction -import de.jrpie.android.launcher.getUserFromId - -@RequiresApi(Build.VERSION_CODES.N_MR1) -class DetailedPinnedShortcutInfo( - private val shortcutInfo: PinnedShortcutInfo, - private val label: String, - private val icon: Drawable, - private val privateSpace: Boolean -) : AbstractDetailedAppInfo { - - constructor(context: Context, shortcut: ShortcutInfo) : this( - PinnedShortcutInfo(shortcut), - shortcut.longLabel.toString(), - (context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps) - .getShortcutBadgedIconDrawable(shortcut, 0), - shortcut.userHandle == getPrivateSpaceUser(context) - ) - - override fun getRawInfo(): AbstractAppInfo { - return shortcutInfo - } - - override fun getLabel(): String { - return label - } - - override fun getIcon(context: Context): Drawable { - return icon - } - - override fun getUser(context: Context): UserHandle { - return getUserFromId(shortcutInfo.user, context) - } - - override fun isPrivate(): Boolean { - return privateSpace - } - - override fun isRemovable(): Boolean { - return true - } - - override fun getAction(): Action { - return ShortcutAction(shortcutInfo) - } - - companion object { - fun fromPinnedShortcutInfo(shortcutInfo: PinnedShortcutInfo, context: Context): DetailedPinnedShortcutInfo? { - return shortcutInfo.getShortcutInfo(context)?.let { - DetailedPinnedShortcutInfo(context, it) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/apps/PinnedShortcutInfo.kt b/app/src/main/java/de/jrpie/android/launcher/apps/PinnedShortcutInfo.kt deleted file mode 100644 index 54230ae..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/apps/PinnedShortcutInfo.kt +++ /dev/null @@ -1,46 +0,0 @@ -package de.jrpie.android.launcher.apps - -import android.app.Service -import android.content.ComponentName -import android.content.Context -import android.content.pm.LauncherApps -import android.content.pm.LauncherApps.ShortcutQuery -import android.content.pm.ShortcutInfo -import android.os.Build -import androidx.annotation.RequiresApi -import de.jrpie.android.launcher.getUserFromId -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - - -@RequiresApi(Build.VERSION_CODES.N_MR1) -@Serializable -@SerialName("shortcut") -data class PinnedShortcutInfo( - val id: String, - val packageName: String, - val activityName: String, - val user: Int -): AbstractAppInfo { - - constructor(info: ShortcutInfo) : this(info.id, info.`package`, info.activity?.className ?: "", info.userHandle.hashCode()) - - fun getShortcutInfo(context: Context): ShortcutInfo? { - val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps - - return try { - launcherApps.getShortcuts( - ShortcutQuery().apply { - setQueryFlags(ShortcutQuery.FLAG_MATCH_PINNED) - setPackage(packageName) - setActivity(ComponentName(packageName, activityName)) - setShortcutIds(listOf(id)) - }, - getUserFromId(user, context) - )?.firstOrNull() - } catch(_: Exception) { - // can throw SecurityException or IllegalStateException when profile is locked - null - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/apps/PrivateSpace.kt b/app/src/main/java/de/jrpie/android/launcher/apps/PrivateSpace.kt deleted file mode 100644 index 24665d7..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/apps/PrivateSpace.kt +++ /dev/null @@ -1,140 +0,0 @@ -package de.jrpie.android.launcher.apps - -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent -import android.content.pm.LauncherApps -import android.os.Build -import android.os.UserHandle -import android.os.UserManager -import android.provider.Settings -import android.widget.Toast -import de.jrpie.android.launcher.R -import de.jrpie.android.launcher.isDefaultHomeScreen -import de.jrpie.android.launcher.setDefaultHomeScreen - - -/* - * Checks whether the device supports private space. - */ -fun isPrivateSpaceSupported(): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM -} - -fun getPrivateSpaceUser(context: Context): UserHandle? { - if (!isPrivateSpaceSupported()) { - return null - } - val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager - val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - return userManager.userProfiles.firstOrNull { u -> - launcherApps.getLauncherUserInfo(u)?.userType == UserManager.USER_TYPE_PROFILE_PRIVATE - } -} - -/** - * Check whether the user has created a private space and whether µLauncher can access it. - */ -fun isPrivateSpaceSetUp( - context: Context, - showToast: Boolean = false, - launchSettings: Boolean = false -): Boolean { - if (!isPrivateSpaceSupported()) { - if (showToast) { - Toast.makeText( - context, - context.getString(R.string.alert_requires_android_v), - Toast.LENGTH_LONG - ).show() - } - return false - } - val privateSpaceUser = getPrivateSpaceUser(context) - if (privateSpaceUser != null) { - return true - } - if (!isDefaultHomeScreen(context)) { - if (showToast) { - Toast.makeText( - context, - context.getString(R.string.toast_private_space_default_home_screen), - Toast.LENGTH_LONG - ).show() - } - if (launchSettings) { - setDefaultHomeScreen(context) - } - } else { - if (showToast) { - Toast.makeText( - context, - context.getString(R.string.toast_private_space_not_available), - Toast.LENGTH_LONG - ).show() - } - if (launchSettings) { - try { - context.startActivity(Intent(Settings.ACTION_PRIVACY_SETTINGS)) - } catch (_: ActivityNotFoundException) { - } - } - } - return false -} - -fun isPrivateSpaceLocked(context: Context): Boolean { - if (!isPrivateSpaceSupported()) { - return false - } - val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager - val privateSpaceUser = getPrivateSpaceUser(context) ?: return false - return userManager.isQuietModeEnabled(privateSpaceUser) -} - -fun lockPrivateSpace(context: Context, lock: Boolean) { - if (!isPrivateSpaceSupported()) { - return - } - - // silently return when trying to unlock but hide when locked is set - if (!lock && hidePrivateSpaceWhenLocked(context)) { - return - } - - val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager - val privateSpaceUser = getPrivateSpaceUser(context) ?: return - userManager.requestQuietModeEnabled(lock, privateSpaceUser) -} - -fun togglePrivateSpaceLock(context: Context) { - if (!isPrivateSpaceSetUp(context, showToast = true, launchSettings = true)) { - return - } - - val lock = isPrivateSpaceLocked(context) - lockPrivateSpace(context, !lock) - if (!lock) { - Toast.makeText( - context, - context.getString(R.string.toast_private_space_locked), - Toast.LENGTH_LONG - ).show() - } -} - -@Suppress("SameReturnValue") -fun hidePrivateSpaceWhenLocked(context: Context): Boolean { - // Trying to access the setting as a 3rd party launcher raises a security exception. - // This is an Android bug: https://issuetracker.google.com/issues/352276244#comment5 - // The logic for this is implemented. - // TODO: replace this once the Android bug is fixed - return false - - // TODO: perhaps this should be cached - // https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Launcher3/src/com/android/launcher3/util/SettingsCache.java;l=61;drc=56bf7ad33bc9d5ed3c18e7abefeec5c177ec75d7 - - // val key = "hide_privatespace_entry_point" - // return Settings.Secure.getInt(context.contentResolver, key, 0) == 1 -} - diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/ColorPreference.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/ColorPreference.kt index 0f95efd..a21d458 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/ColorPreference.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/ColorPreference.kt @@ -17,7 +17,6 @@ import androidx.core.graphics.green import androidx.core.graphics.red import androidx.preference.Preference import de.jrpie.android.launcher.R -import androidx.core.graphics.toColorInt class ColorPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) { @@ -53,7 +52,7 @@ class ColorPreference(context: Context, attrs: AttributeSet?) : AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { setView(R.layout.dialog_choose_color) setTitle(R.string.dialog_choose_color_title) - setPositiveButton(android.R.string.ok) { _, _ -> + setPositiveButton(R.string.dialog_select_color_ok) { _, _ -> persistInt(currentColor) summary = currentColor.getHex() } @@ -84,10 +83,10 @@ class ColorPreference(context: Context, attrs: AttributeSet?) : override fun onTextChanged(text: CharSequence?, p1: Int, p2: Int, p3: Int) {} override fun afterTextChanged(editable: Editable?) { preview.hasFocus() || return - val newText = editable?.toString() ?: return - newText.isBlank() && return + val newText = editable?.toString() + newText.isNullOrBlank() && return try { - val newColor = newText.toColorInt() + val newColor = Color.parseColor(newText.toString()) currentColor = newColor updateColor(false) } catch (_: IllegalArgumentException) { diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java index 85979fe..640e6ce 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java @@ -5,9 +5,8 @@ import java.util.Set; import de.jrpie.android.launcher.R; import de.jrpie.android.launcher.actions.lock.LockMethod; -import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer; -import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer; -import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer; +import de.jrpie.android.launcher.preferences.serialization.SetAppInfoPreferenceSerializer; +import de.jrpie.android.launcher.preferences.serialization.MapAppInfoStringPreferenceSerializer; import de.jrpie.android.launcher.preferences.theme.Background; import de.jrpie.android.launcher.preferences.theme.ColorTheme; import de.jrpie.android.launcher.preferences.theme.Font; @@ -21,24 +20,18 @@ import eu.jonahbauer.android.preference.annotations.Preferences; r = R.class, value = { @PreferenceGroup(name = "internal", prefix = "settings_internal_", suffix = "_key", value = { - // set after the user finished the tutorial @Preference(name = "started", type = boolean.class, defaultValue = "false"), @Preference(name = "started_time", type = long.class), - // see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt @Preference(name = "version_code", type = int.class, defaultValue = "-1"), }), @PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = { - @Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class), - @Preference(name = "hidden", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class), - @Preference(name = "pinned_shortcuts", type = Set.class, serializer = SetPinnedShortcutInfoPreferenceSerializer.class), - @Preference(name = "custom_names", type = HashMap.class, serializer = MapAbstractAppInfoStringPreferenceSerializer.class), + @Preference(name = "favorites", type = Set.class, serializer = SetAppInfoPreferenceSerializer.class), + @Preference(name = "hidden", type = Set.class, serializer = SetAppInfoPreferenceSerializer.class), + @Preference(name = "custom_names", type = HashMap.class, serializer = MapAppInfoStringPreferenceSerializer.class), @Preference(name = "hide_bound_apps", type = boolean.class, defaultValue = "false"), - @Preference(name = "hide_paused_apps", type = boolean.class, defaultValue = "false"), - @Preference(name = "hide_private_space_apps", type = boolean.class, defaultValue = "false"), }), @PreferenceGroup(name = "list", prefix = "settings_list_", suffix = "_key", value = { - @Preference(name = "layout", type = ListLayout.class, defaultValue = "DEFAULT"), - @Preference(name = "reverse_layout", type = boolean.class, defaultValue = "false") + @Preference(name = "layout", type = ListLayout.class, defaultValue = "DEFAULT") }), @PreferenceGroup(name = "gestures", prefix = "settings_gesture_", suffix = "_key", value = { }), @@ -64,8 +57,7 @@ import eu.jonahbauer.android.preference.annotations.Preferences; }), @PreferenceGroup(name = "display", prefix = "settings_display_", suffix = "_key", value = { @Preference(name = "screen_timeout_disabled", type = boolean.class, defaultValue = "false"), - @Preference(name = "hide_status_bar", type = boolean.class, defaultValue = "true"), - @Preference(name = "hide_navigation_bar", type = boolean.class, defaultValue = "false"), + @Preference(name = "full_screen", type = boolean.class, defaultValue = "true"), @Preference(name = "rotate_screen", type = boolean.class, defaultValue = "true"), }), @PreferenceGroup(name = "functionality", prefix = "settings_functionality_", suffix = "_key", value = { diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/ListLayout.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/ListLayout.kt index 5f7b9d6..e20945a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/ListLayout.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/ListLayout.kt @@ -1,7 +1,6 @@ package de.jrpie.android.launcher.preferences import android.content.Context -import android.util.TypedValue import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -28,10 +27,8 @@ enum class ListLayout( GRID( { c -> val displayMetrics = c.resources.displayMetrics - val widthColumnPx = - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 90f, displayMetrics) - val numColumns = (displayMetrics.widthPixels / widthColumnPx).toInt() - GridLayoutManager(c, numColumns) + val widthSp = displayMetrics.widthPixels / displayMetrics.scaledDensity + GridLayoutManager(c, (widthSp / 90).toInt()) }, R.layout.list_apps_row_variant_grid, false diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt index 59ecc7a..bb59948 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt @@ -5,12 +5,8 @@ import android.util.Log import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.apps.AppInfo -import de.jrpie.android.launcher.apps.AbstractAppInfo -import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER import de.jrpie.android.launcher.apps.DetailedAppInfo import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion1 -import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion2 -import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown import de.jrpie.android.launcher.ui.HomeActivity @@ -18,7 +14,7 @@ import de.jrpie.android.launcher.ui.HomeActivity * Increase when breaking changes are introduced and write an appropriate case in * `migratePreferencesToNewVersion` */ -const val PREFERENCE_VERSION = 4 +const val PREFERENCE_VERSION = 2 const val UNKNOWN_PREFERENCE_VERSION = -1 private const val TAG = "Launcher - Preferences" @@ -36,20 +32,13 @@ fun migratePreferencesToNewVersion(context: Context) { UNKNOWN_PREFERENCE_VERSION -> { /* still using the old preferences file */ migratePreferencesFromVersionUnknown(context) - Log.i(TAG, "migration of preferences complete (${UNKNOWN_PREFERENCE_VERSION} -> ${PREFERENCE_VERSION}).") + + Log.i(TAG, "migration of preferences complete.") } 1 -> { migratePreferencesFromVersion1() - Log.i(TAG, "migration of preferences complete (1 -> ${PREFERENCE_VERSION}).") - } - 2 -> { - migratePreferencesFromVersion2() - Log.i(TAG, "migration of preferences complete (2 -> ${PREFERENCE_VERSION}).") - } - 3 -> { - migratePreferencesFromVersion3() - Log.i(TAG, "migration of preferences complete (3 -> ${PREFERENCE_VERSION}).") + Log.i(TAG, "migration of preferences complete.") } else -> { @@ -73,16 +62,16 @@ fun resetPreferences(context: Context) { LauncherPreferences.internal().versionCode(PREFERENCE_VERSION) - val hidden: MutableSet = mutableSetOf() + val hidden: MutableSet = mutableSetOf() val launcher = DetailedAppInfo.fromAppInfo( AppInfo( BuildConfig.APPLICATION_ID, HomeActivity::class.java.name, - INVALID_USER + AppInfo.INVALID_USER ), context ) - launcher?.getRawInfo()?.let { hidden.add(it) } - Log.i(TAG,"Hiding ${launcher?.getRawInfo()}") + launcher?.app?.let { hidden.add(it) } + Log.i(TAG,"Hiding ${launcher?.app}") LauncherPreferences.apps().hidden(hidden) Action.resetToDefaultActions(context) diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt index a1cb022..66723ad 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt @@ -5,27 +5,14 @@ import de.jrpie.android.launcher.actions.AppAction import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.LauncherAction 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.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION -import kotlinx.serialization.Serializable +import de.jrpie.android.launcher.preferences.serialization.MapAppInfoStringPreferenceSerializer import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.json.JSONException import org.json.JSONObject -import androidx.core.content.edit - - -@Serializable -@Suppress("unused") -private class LegacyMapEntry(val key: AppInfo, val value: String) - -private fun serializeMapAppInfo(value: Map?): Set? { - return value?.map { (key, value) -> - Json.encodeToString(LegacyMapEntry(key, value)) - }?.toSet() -} - val oldLauncherActionIds: Map = mapOf( @@ -81,28 +68,28 @@ private fun Action.Companion.legacyFromPreference(id: String): Action? { val actionId = preferences.getString("$id.app", "")!! var u: Int? = preferences.getInt( "$id.user", - INVALID_USER + AppInfo.INVALID_USER ) - u = if (u == INVALID_USER) null else u + u = if (u == AppInfo.INVALID_USER) null else u return Action.legacyFromId(actionId, u) } private fun migrateAppInfoStringMap(key: String) { val preferences = LauncherPreferences.getSharedPreferences() - serializeMapAppInfo( + MapAppInfoStringPreferenceSerializer().serialize( preferences.getStringSet(key, setOf())?.mapNotNull { entry -> try { - val obj = JSONObject(entry) + val obj = JSONObject(entry); val info = AppInfo.legacyDeserialize(obj.getString("key")) - val value = obj.getString("value") + val value = obj.getString("value"); Pair(info, value) } catch (_: JSONException) { null } }?.toMap(HashMap()) )?.let { - preferences.edit { putStringSet(key, it) } + preferences.edit().putStringSet(key, it as Set).apply() } } @@ -111,16 +98,16 @@ private fun migrateAppInfoSet(key: String) { .map(AppInfo.Companion::legacyDeserialize) .map(AppInfo::serialize) .toSet() - .let { LauncherPreferences.getSharedPreferences().edit { putStringSet(key, it) } } + .let { LauncherPreferences.getSharedPreferences().edit().putStringSet(key, it).apply() } } private fun migrateAction(key: String) { Action.legacyFromPreference(key)?.let { action -> - LauncherPreferences.getSharedPreferences().edit { - putString(key, Json.encodeToString(action)) - .remove("$key.app") - .remove("$key.user") - } + LauncherPreferences.getSharedPreferences().edit() + .putString(key, Json.encodeToString(action)) + .remove("$key.app") + .remove("$key.user") + .apply() } } @@ -130,12 +117,11 @@ private fun migrateAction(key: String) { * (see [PREFERENCE_VERSION]) */ fun migratePreferencesFromVersion1() { + assert(PREFERENCE_VERSION == 2) assert(LauncherPreferences.internal().versionCode() == 1) Gesture.entries.forEach { g -> migrateAction(g.id) } migrateAppInfoSet(LauncherPreferences.apps().keys().hidden()) migrateAppInfoSet(LauncherPreferences.apps().keys().favorites()) migrateAppInfoStringMap(LauncherPreferences.apps().keys().customNames()) LauncherPreferences.internal().versionCode(2) - - migratePreferencesFromVersion2() } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt deleted file mode 100644 index 4e6eae1..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt +++ /dev/null @@ -1,20 +0,0 @@ -package de.jrpie.android.launcher.preferences.legacy - -import de.jrpie.android.launcher.actions.Action -import de.jrpie.android.launcher.actions.Gesture -import de.jrpie.android.launcher.actions.LauncherAction -import de.jrpie.android.launcher.preferences.LauncherPreferences -import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION - - -/** - * Migrate preferences from version 2 (used until version 0.0.21) to the current format - * (see [PREFERENCE_VERSION]) - */ -fun migratePreferencesFromVersion2() { - assert(LauncherPreferences.internal().versionCode() == 2) - // previously there was no setting for this - Action.setActionForGesture(Gesture.BACK, LauncherAction.CHOOSE) - LauncherPreferences.internal().versionCode(3) - migratePreferencesFromVersion3() -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt deleted file mode 100644 index 4a9241f..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt +++ /dev/null @@ -1,85 +0,0 @@ -package de.jrpie.android.launcher.preferences.legacy - -import android.content.SharedPreferences -import android.content.SharedPreferences.Editor -import de.jrpie.android.launcher.apps.AppInfo -import de.jrpie.android.launcher.apps.AbstractAppInfo -import de.jrpie.android.launcher.preferences.LauncherPreferences -import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION -import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer -import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import java.util.HashSet -import androidx.core.content.edit - -/** - * Migrate preferences from version 3 (used until version 0.0.23) to the current format - * (see [PREFERENCE_VERSION]) - */ - - -fun deserializeSet(value: Set?): Set? { - return value?.map { - Json.decodeFromString(it) - }?.toHashSet() -} - -fun deserializeMap(value: Set?): HashMap? { - return value?.associateTo(HashMap()) { - val entry = Json.decodeFromString(it) - Pair(entry.key, entry.value) - } -} - -@Serializable -private class MapEntry(val key: AppInfo, val value: String) - -private fun migrateSetAppInfo(key: String, preferences: SharedPreferences, editor: Editor) { - try { - val serializer = SetAbstractAppInfoPreferenceSerializer() - val set = HashSet() - - deserializeSet(preferences.getStringSet(key, null))?.let { - set.addAll(it) - } - @Suppress("UNCHECKED_CAST") - editor.putStringSet( - key, - serializer.serialize(set as java.util.Set) as Set? - ) - } catch (e: Exception) { - e.printStackTrace() - editor.putStringSet(key, null) - } - -} -private fun migrateMapAppInfoString(key: String, preferences: SharedPreferences, editor: Editor ) { - try { - val serializer = MapAbstractAppInfoStringPreferenceSerializer() - val map = HashMap() - - deserializeMap(preferences.getStringSet(key, null))?.let { - map.putAll(it) - } - @Suppress("UNCHECKED_CAST") - editor.putStringSet(key, serializer.serialize(map) as Set?) - } catch (e: Exception) { - e.printStackTrace() - editor.putStringSet(key, null) - } -} - -fun migratePreferencesFromVersion3() { - assert(PREFERENCE_VERSION == 4) - assert(LauncherPreferences.internal().versionCode() == 3) - - val preferences = LauncherPreferences.getSharedPreferences() - preferences.edit { - migrateSetAppInfo(LauncherPreferences.apps().keys().favorites(), preferences, this) - migrateSetAppInfo(LauncherPreferences.apps().keys().hidden(), preferences, this) - migrateMapAppInfoString(LauncherPreferences.apps().keys().customNames(), preferences, this) - } - - LauncherPreferences.internal().versionCode(4) -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt index 2d1152d..1ecbd74 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt @@ -4,9 +4,10 @@ import android.content.Context import android.content.SharedPreferences import android.util.Log import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION import de.jrpie.android.launcher.preferences.theme.Background import de.jrpie.android.launcher.preferences.theme.ColorTheme -import androidx.core.content.edit + private fun migrateStringPreference( @@ -49,6 +50,7 @@ private const val TAG = "Preferences ? -> 1" * and a different file was used. */ fun migratePreferencesFromVersionUnknown(context: Context) { + assert(PREFERENCE_VERSION == 2) Log.i( TAG, @@ -64,317 +66,318 @@ fun migratePreferencesFromVersionUnknown(context: Context) { return } - LauncherPreferences.getSharedPreferences().edit { + val newPrefs = LauncherPreferences.getSharedPreferences().edit() - migrateBooleanPreference( - oldPrefs, - this, - "startedBefore", - "internal.started_before", - false - ) + migrateBooleanPreference( + oldPrefs, + newPrefs, + "startedBefore", + "internal.started_before", + false + ) - migrateStringPreference( - oldPrefs, - this, - "action_volumeUpApp", - "action.volume_up.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_volumeUpApp_user", - "action.volume_up.user", - -1 - ) - migrateStringPreference( - oldPrefs, - this, - "action_volumeDownApp", - "action.volume_down.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_volumeDownApp_user", - "action.volume_down.user", - -1 - ) - migrateStringPreference(oldPrefs, this, "action_timeApp", "action.time.app", "") - migrateIntPreference(oldPrefs, this, "action_timeApp_user", "action.time.user", -1) - migrateStringPreference(oldPrefs, this, "action_dateApp", "action.date.app", "") - migrateIntPreference(oldPrefs, this, "action_dateApp_user", "action.date.user", -1) - migrateStringPreference( - oldPrefs, - this, - "action_longClickApp", - "action.long_click.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_longClickApp_user", - "action.long_click.user", - -1 - ) - migrateStringPreference( - oldPrefs, - this, - "action_doubleClickApp", - "action.double_click.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_doubleClickApp_user", - "action.double_click.user", - -1 - ) - migrateStringPreference(oldPrefs, this, "action_upApp", "action.up.app", "") - migrateIntPreference(oldPrefs, this, "action_upApp_user", "action.up.user", -1) - migrateStringPreference( - oldPrefs, - this, - "action_up_leftApp", - "action.up_left.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_up_leftApp_user", - "action.up_left.user", - -1 - ) - migrateStringPreference( - oldPrefs, - this, - "action_up_rightApp", - "action.up_right.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_up_rightApp_user", - "action.up_right.user", - -1 - ) - migrateStringPreference( - oldPrefs, - this, - "action_doubleUpApp", - "action.double_up.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_doubleUpApp_user", - "action.double_up.user", - -1 - ) - migrateStringPreference(oldPrefs, this, "action_downApp", "action.down.app", "") - migrateIntPreference(oldPrefs, this, "action_downApp_user", "action.down.user", -1) - migrateStringPreference( - oldPrefs, - this, - "action_down_leftApp", - "action.down_left.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_down_leftApp_user", - "action.down_left.user", - -1 - ) - migrateStringPreference( - oldPrefs, - this, - "action_down_rightApp", - "action.down_right.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_down_rightApp_user", - "action.down_right.user", - -1 - ) - migrateStringPreference( - oldPrefs, - this, - "action_doubleDownApp", - "action.double_down.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_doubleDownApp_user", - "action.double_down.user", - -1 - ) - migrateStringPreference(oldPrefs, this, "action_leftApp", "action.left.app", "") - migrateIntPreference(oldPrefs, this, "action_leftApp_user", "action.left.user", -1) - migrateStringPreference( - oldPrefs, - this, - "action_left_topApp", - "action.left_top.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_left_topApp_user", - "action.left_top.user", - -1 - ) - migrateStringPreference( - oldPrefs, - this, - "action_left_bottomApp", - "action.left_bottom.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_left_bottomApp_user", - "action.left_bottom.user", - -1 - ) - migrateStringPreference( - oldPrefs, - this, - "action_doubleLeftApp", - "action.double_left.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_doubleLeftApp_user", - "action.double_left.user", - -1 - ) - migrateStringPreference(oldPrefs, this, "action_rightApp", "action.right.app", "") - migrateIntPreference( - oldPrefs, - this, - "action_rightApp_user", - "action.right.user", - -1 - ) - migrateStringPreference( - oldPrefs, - this, - "action_right_topApp", - "action.right_top.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_right_topApp_user", - "action.right_top.user", - -1 - ) - migrateStringPreference( - oldPrefs, - this, - "action_right_bottomApp", - "action.right_bottom.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_right_bottomApp_user", - "action.right_bottom.user", - -1 - ) - migrateStringPreference( - oldPrefs, - this, - "action_doubleRightApp", - "action.double_right.app", - "" - ) - migrateIntPreference( - oldPrefs, - this, - "action_doubleRightApp_user", - "action.double_right.user", - -1 - ) - migrateBooleanPreference(oldPrefs, this, "timeVisible", "clock.time_visible", true) - migrateBooleanPreference(oldPrefs, this, "dateVisible", "clock.date_visible", true) - migrateBooleanPreference( - oldPrefs, - this, - "dateLocalized", - "clock.date_localized", - false - ) - migrateBooleanPreference( - oldPrefs, - this, - "dateTimeFlip", - "clock.date_time_flip", - false - ) - migrateBooleanPreference( - oldPrefs, - this, - "disableTimeout", - "display.disable_timeout", - false - ) - migrateBooleanPreference( - oldPrefs, - this, - "useFullScreen", - "display.use_full_screen", - true - ) - migrateBooleanPreference( - oldPrefs, - this, - "enableDoubleActions", - "enabled_gestures.double_actions", - true - ) - migrateBooleanPreference( - oldPrefs, - this, - "enableEdgeActions", - "enabled_gestures.edge_actions", - true - ) - migrateBooleanPreference( - oldPrefs, - this, - "searchAutoLaunch", - "functionality.search_auto_launch", - true - ) - migrateBooleanPreference( - oldPrefs, - this, - "searchAutoKeyboard", - "functionality.search_auto_keyboard", - true - ) - } + migrateStringPreference( + oldPrefs, + newPrefs, + "action_volumeUpApp", + "action.volume_up.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_volumeUpApp_user", + "action.volume_up.user", + -1 + ) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_volumeDownApp", + "action.volume_down.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_volumeDownApp_user", + "action.volume_down.user", + -1 + ) + migrateStringPreference(oldPrefs, newPrefs, "action_timeApp", "action.time.app", "") + migrateIntPreference(oldPrefs, newPrefs, "action_timeApp_user", "action.time.user", -1) + migrateStringPreference(oldPrefs, newPrefs, "action_dateApp", "action.date.app", "") + migrateIntPreference(oldPrefs, newPrefs, "action_dateApp_user", "action.date.user", -1) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_longClickApp", + "action.long_click.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_longClickApp_user", + "action.long_click.user", + -1 + ) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_doubleClickApp", + "action.double_click.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_doubleClickApp_user", + "action.double_click.user", + -1 + ) + migrateStringPreference(oldPrefs, newPrefs, "action_upApp", "action.up.app", "") + migrateIntPreference(oldPrefs, newPrefs, "action_upApp_user", "action.up.user", -1) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_up_leftApp", + "action.up_left.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_up_leftApp_user", + "action.up_left.user", + -1 + ) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_up_rightApp", + "action.up_right.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_up_rightApp_user", + "action.up_right.user", + -1 + ) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_doubleUpApp", + "action.double_up.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_doubleUpApp_user", + "action.double_up.user", + -1 + ) + migrateStringPreference(oldPrefs, newPrefs, "action_downApp", "action.down.app", "") + migrateIntPreference(oldPrefs, newPrefs, "action_downApp_user", "action.down.user", -1) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_down_leftApp", + "action.down_left.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_down_leftApp_user", + "action.down_left.user", + -1 + ) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_down_rightApp", + "action.down_right.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_down_rightApp_user", + "action.down_right.user", + -1 + ) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_doubleDownApp", + "action.double_down.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_doubleDownApp_user", + "action.double_down.user", + -1 + ) + migrateStringPreference(oldPrefs, newPrefs, "action_leftApp", "action.left.app", "") + migrateIntPreference(oldPrefs, newPrefs, "action_leftApp_user", "action.left.user", -1) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_left_topApp", + "action.left_top.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_left_topApp_user", + "action.left_top.user", + -1 + ) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_left_bottomApp", + "action.left_bottom.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_left_bottomApp_user", + "action.left_bottom.user", + -1 + ) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_doubleLeftApp", + "action.double_left.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_doubleLeftApp_user", + "action.double_left.user", + -1 + ) + migrateStringPreference(oldPrefs, newPrefs, "action_rightApp", "action.right.app", "") + migrateIntPreference( + oldPrefs, + newPrefs, + "action_rightApp_user", + "action.right.user", + -1 + ) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_right_topApp", + "action.right_top.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_right_topApp_user", + "action.right_top.user", + -1 + ) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_right_bottomApp", + "action.right_bottom.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_right_bottomApp_user", + "action.right_bottom.user", + -1 + ) + migrateStringPreference( + oldPrefs, + newPrefs, + "action_doubleRightApp", + "action.double_right.app", + "" + ) + migrateIntPreference( + oldPrefs, + newPrefs, + "action_doubleRightApp_user", + "action.double_right.user", + -1 + ) + migrateBooleanPreference(oldPrefs, newPrefs, "timeVisible", "clock.time_visible", true) + migrateBooleanPreference(oldPrefs, newPrefs, "dateVisible", "clock.date_visible", true) + migrateBooleanPreference( + oldPrefs, + newPrefs, + "dateLocalized", + "clock.date_localized", + false + ) + migrateBooleanPreference( + oldPrefs, + newPrefs, + "dateTimeFlip", + "clock.date_time_flip", + false + ) + migrateBooleanPreference( + oldPrefs, + newPrefs, + "disableTimeout", + "display.disable_timeout", + false + ) + migrateBooleanPreference( + oldPrefs, + newPrefs, + "useFullScreen", + "display.use_full_screen", + true + ) + migrateBooleanPreference( + oldPrefs, + newPrefs, + "enableDoubleActions", + "enabled_gestures.double_actions", + true + ) + migrateBooleanPreference( + oldPrefs, + newPrefs, + "enableEdgeActions", + "enabled_gestures.edge_actions", + true + ) + migrateBooleanPreference( + oldPrefs, + newPrefs, + "searchAutoLaunch", + "functionality.search_auto_launch", + true + ) + migrateBooleanPreference( + oldPrefs, + newPrefs, + "searchAutoKeyboard", + "functionality.search_auto_keyboard", + true + ) + + newPrefs.apply() when (oldPrefs.getString("theme", "finn")) { "finn" -> { diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt index 3e19daf..4a745a2 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt @@ -2,8 +2,7 @@ package de.jrpie.android.launcher.preferences.serialization -import de.jrpie.android.launcher.apps.AbstractAppInfo -import de.jrpie.android.launcher.apps.PinnedShortcutInfo +import de.jrpie.android.launcher.apps.AppInfo import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer import kotlinx.serialization.Serializable @@ -11,61 +10,40 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json - +// Serializers for [LauncherPreference$Config] @Suppress("UNCHECKED_CAST") -class SetAbstractAppInfoPreferenceSerializer : - PreferenceSerializer?, java.util.Set?> { +class SetAppInfoPreferenceSerializer : + PreferenceSerializer?, java.util.Set?> { @Throws(PreferenceSerializationException::class) - override fun serialize(value: java.util.Set?): java.util.Set { - return value?.map(AbstractAppInfo::serialize) - ?.toHashSet() as java.util.Set + override fun serialize(value: java.util.Set?): java.util.Set? { + return value?.map(AppInfo::serialize)?.toHashSet() as java.util.Set } @Throws(PreferenceSerializationException::class) - override fun deserialize(value: java.util.Set?): java.util.Set? { - return value?.map(java.lang.String::toString)?.map(AbstractAppInfo::deserialize) - ?.toHashSet() as? java.util.Set + override fun deserialize(value: java.util.Set?): java.util.Set? { + return value?.map (java.lang.String::toString)?.map(AppInfo::deserialize)?.toHashSet() as? java.util.Set } } @Suppress("UNCHECKED_CAST") -class SetPinnedShortcutInfoPreferenceSerializer : - PreferenceSerializer?, java.util.Set?> { - @Throws(PreferenceSerializationException::class) - override fun serialize(value: java.util.Set?): java.util.Set { - return value?.map { Json.encodeToString(it) } - ?.toHashSet() as java.util.Set - } +class MapAppInfoStringPreferenceSerializer : + PreferenceSerializer?, java.util.Set?> { + + @Serializable() + private class MapEntry(val key: AppInfo, val value: String) @Throws(PreferenceSerializationException::class) - override fun deserialize(value: java.util.Set?): java.util.Set? { - return value?.map(java.lang.String::toString) - ?.map { Json.decodeFromString(it) } - ?.toHashSet() as? java.util.Set - } -} - - -@Suppress("UNCHECKED_CAST") -class MapAbstractAppInfoStringPreferenceSerializer : - PreferenceSerializer?, java.util.Set?> { - - @Serializable - private class MapEntry(val key: AbstractAppInfo, val value: String) - - @Throws(PreferenceSerializationException::class) - override fun serialize(value: java.util.HashMap?): java.util.Set? { + override fun serialize(value: java.util.HashMap?): java.util.Set? { return value?.map { (key, value) -> Json.encodeToString(MapEntry(key, value)) }?.toHashSet() as? java.util.Set } @Throws(PreferenceSerializationException::class) - override fun deserialize(value: java.util.Set?): java.util.HashMap? { + override fun deserialize(value: java.util.Set?): java.util.HashMap? { return value?.associateTo(HashMap()) { val entry = Json.decodeFromString(it.toString()) Pair(entry.key, entry.value) } } } - diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/theme/Background.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/theme/Background.kt index ca385af..da301c0 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/theme/Background.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/theme/Background.kt @@ -16,13 +16,7 @@ enum class Background(val id: Int, val dim: Boolean = false, val blur: Boolean = ; fun applyToTheme(theme: Resources.Theme) { - var background = this - - // force a solid background when using the light theme - if (LauncherPreferences.theme().colorTheme() == ColorTheme.LIGHT) { - background = SOLID - } - theme.applyStyle(background.id, true) + theme.applyStyle(id, true) } fun applyToWindow(window: Window) { diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/theme/ColorTheme.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/theme/ColorTheme.kt index d3088f4..1e57f9f 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/theme/ColorTheme.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/theme/ColorTheme.kt @@ -2,9 +2,10 @@ package de.jrpie.android.launcher.preferences.theme import android.content.Context import android.content.res.Resources -import com.google.android.material.color.DynamicColors import de.jrpie.android.launcher.R +import com.google.android.material.color.DynamicColors +@Suppress("unused") enum class ColorTheme( private val id: Int, private val labelResource: Int, @@ -25,7 +26,7 @@ enum class ColorTheme( R.style.colorThemeLight, R.string.settings_theme_color_theme_item_light, R.style.textShadowLight, - { true }), + { false }), DYNAMIC( R.style.colorThemeDynamic, R.string.settings_theme_color_theme_item_dynamic, diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt b/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt index 1ca4d2b..b8fc82e 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt @@ -27,14 +27,10 @@ fun View.blink( } // Taken from: https://stackoverflow.com/a/30340794/12787264 -fun ImageView.transformGrayscale(grayscale: Boolean) { - this.colorFilter = if (grayscale) { - ColorMatrixColorFilter(ColorMatrix().apply { - setSaturation(0f) - }) - } else { - null - } +fun ImageView.transformGrayscale() { + this.colorFilter = ColorMatrixColorFilter(ColorMatrix().apply { + setSaturation(0f) + }) } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 2ab5d9f..5633113 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -2,15 +2,18 @@ package de.jrpie.android.launcher.ui import android.annotation.SuppressLint import android.content.SharedPreferences -import android.content.res.Configuration import android.content.res.Resources +import android.os.AsyncTask import android.os.Build import android.os.Bundle +import android.util.DisplayMetrics +import android.view.GestureDetector import android.view.KeyEvent import android.view.MotionEvent -import android.view.View +import android.view.ViewConfiguration import android.window.OnBackInvokedDispatcher import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.GestureDetectorCompat import androidx.core.view.isVisible import de.jrpie.android.launcher.R import de.jrpie.android.launcher.actions.Action @@ -19,8 +22,19 @@ import de.jrpie.android.launcher.actions.LauncherAction import de.jrpie.android.launcher.databinding.HomeBinding import de.jrpie.android.launcher.openTutorial import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion +import de.jrpie.android.launcher.preferences.resetPreferences import de.jrpie.android.launcher.ui.tutorial.TutorialActivity +import java.text.SimpleDateFormat +import java.util.Date import java.util.Locale +import java.util.Timer +import kotlin.concurrent.fixedRateTimer +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.tan + /** * [HomeActivity] is the actual application Launcher, @@ -34,10 +48,10 @@ import java.util.Locale * - Setting global variables (preferences etc.) * - Opening the [TutorialActivity] on new installations */ -class HomeActivity : UIObject, AppCompatActivity() { +class HomeActivity : UIObject, AppCompatActivity(), + GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener { private lateinit var binding: HomeBinding - private var touchGestureDetector: TouchGestureDetector? = null private var sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> @@ -46,20 +60,24 @@ class HomeActivity : UIObject, AppCompatActivity() { ) { recreate() } - - if (prefKey?.startsWith("action.") == true) { - updateSettingsFallbackButtonVisibility() - } } + private var edgeWidth = 0.15f + + 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) super.onCreate() - // Initialise layout binding = HomeBinding.inflate(layoutInflater) - setContentView(binding.root) // Handle back key / gesture on Android 13+, cf. onKeyDown() @@ -70,72 +88,46 @@ class HomeActivity : UIObject, AppCompatActivity() { handleBack() } } - binding.buttonFallbackSettings.setOnClickListener { - LauncherAction.SETTINGS.invoke(this) - } - } - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - touchGestureDetector?.updateScreenSize(windowManager) } override fun onStart() { super.onStart() - super.onStart() + mDetector = GestureDetectorCompat(this, this) + mDetector.setOnDoubleTapListener(this) - // If the tutorial was not finished, start it - if (!LauncherPreferences.internal().started()) { - openTutorial(this) - } + super.onStart() LauncherPreferences.getSharedPreferences() .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) } - override fun onWindowFocusChanged(hasFocus: Boolean) { - super.onWindowFocusChanged(hasFocus) - - if (hasFocus && LauncherPreferences.display().hideNavigationBar()) { - hideNavigationBar() - } - } - - - private fun updateSettingsFallbackButtonVisibility() { - // If µLauncher settings can not be reached from any action bound to an enabled gesture, - // show the fallback button. - binding.buttonFallbackSettings.visibility = if ( - !Gesture.entries.any { g -> - g.isEnabled() && Action.forGesture(g)?.canReachSettings() == true - } - ) { - View.VISIBLE - } else { - View.GONE - } - } - - private fun initClock() { + private fun updateClock() { + clockTimer.cancel() val locale = Locale.getDefault() val dateVisible = LauncherPreferences.clock().dateVisible() val timeVisible = LauncherPreferences.clock().timeVisible() var dateFMT = "yyyy-MM-dd" var timeFMT = "HH:mm" + val period = 100L if (LauncherPreferences.clock().showSeconds()) { timeFMT += ":ss" } - + /* + I thought about adding an option to show microseconds as well ( timeFMT += ".SSS" ). + However setting period ot 1L (or even 10L) causes high CPU load, + so that doesn't seem to be a good idea. + */ if (LauncherPreferences.clock().localized()) { dateFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, dateFMT) timeFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, timeFMT) } - var upperFormat = dateFMT - var lowerFormat = timeFMT + var upperFormat = SimpleDateFormat(dateFMT, locale) + var lowerFormat = SimpleDateFormat(timeFMT, locale) var upperVisible = dateVisible var lowerVisible = timeVisible @@ -150,10 +142,21 @@ class HomeActivity : UIObject, AppCompatActivity() { binding.homeUpperView.setTextColor(LauncherPreferences.clock().color()) binding.homeLowerView.setTextColor(LauncherPreferences.clock().color()) - binding.homeLowerView.format24Hour = lowerFormat - binding.homeUpperView.format24Hour = upperFormat - binding.homeLowerView.format12Hour = lowerFormat - binding.homeUpperView.format12Hour = upperFormat + + clockTimer = fixedRateTimer("clockTimer", true, 0L, period) { + this@HomeActivity.runOnUiThread { + if (lowerVisible) { + val t = lowerFormat.format(Date()) + if (binding.homeLowerView.text != t) + binding.homeLowerView.text = t + } + if (upperVisible) { + val d = upperFormat.format(Date()) + if (binding.homeUpperView.text != d) + binding.homeUpperView.text = d + } + } + } } override fun getTheme(): Resources.Theme { @@ -171,31 +174,14 @@ class HomeActivity : UIObject, AppCompatActivity() { override fun onResume() { super.onResume() - /* This should be initialized in onCreate() - However on some devices there seems to be a bug where the touchGestureDetector - is not working properly after resuming the app. - Reinitializing the touchGestureDetector every time the app is resumed might help to fix that. - (see issue #138) - */ - touchGestureDetector = TouchGestureDetector( - this, 0, 0, - LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f - ).also { - it.updateScreenSize(windowManager) - } + edgeWidth = LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - binding.root.setOnApplyWindowInsetsListener { _, windowInsets -> - @Suppress("deprecation") // required to support API 29 - val insets = windowInsets.systemGestureInsets - touchGestureDetector?.setSystemGestureInsets(insets) + updateClock() + } - windowInsets - } - } - - initClock() - updateSettingsFallbackButtonVisibility() + override fun onPause() { + super.onPause() + clockTimer.cancel() } override fun onDestroy() { @@ -211,7 +197,6 @@ class HomeActivity : UIObject, AppCompatActivity() { // Only used pre Android 13, cf. onBackInvokedDispatcher handleBack() } - KeyEvent.KEYCODE_VOLUME_UP -> { if (Action.forGesture(Gesture.VOLUME_UP) == LauncherAction.VOLUME_UP) { // Let the OS handle the key event. This works better with some custom ROMs @@ -232,11 +217,97 @@ class HomeActivity : UIObject, AppCompatActivity() { return true } - override fun onTouchEvent(event: MotionEvent): Boolean { - touchGestureDetector?.onTouchEvent(event) + override fun onFling(e1: MotionEvent?, e2: MotionEvent, dX: Float, dY: Float): Boolean { + + if (e1 == null) return false + + + val displayMetrics: DisplayMetrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(displayMetrics) + + val width = displayMetrics.widthPixels + val height = displayMetrics.heightPixels + + val diffX = e1.x - e2.x + val diffY = e1.y - e2.y + + val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe() + val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe() + + val threshold = ViewConfiguration.get(this).scaledTouchSlop + val angularThreshold = tan(Math.PI / 6) + + var gesture = if (angularThreshold * abs(diffX) > abs(diffY)) { // horizontal swipe + if (diffX > threshold) + Gesture.SWIPE_LEFT + else if (diffX < -threshold) + Gesture.SWIPE_RIGHT + else null + } else if (angularThreshold * abs(diffY) > abs(diffX)) { // vertical swipe + // Only open if the swipe was not from the phones top edge + // TODO: replace 100px by sensible dp value (e.g. twice the height of the status bar) + if (diffY < -threshold && e1.y > 100) + Gesture.SWIPE_DOWN + else if (diffY > threshold) + Gesture.SWIPE_UP + else null + } else null + + if (doubleActions && bufferedPointerCount > 1) { + gesture = gesture?.let(Gesture::getDoubleVariant) + } + + if (edgeActions) { + if (max(e1.x, e2.x) < edgeWidth * width) { + gesture = gesture?.getEdgeVariant(Gesture.Edge.LEFT) + } else if (min(e1.x, e2.x) > (1 - edgeWidth) * width) { + gesture = gesture?.getEdgeVariant(Gesture.Edge.RIGHT) + } + + if (max(e1.y, e2.y) < edgeWidth * height) { + gesture = gesture?.getEdgeVariant(Gesture.Edge.TOP) + } else if (min(e1.y, e2.y) > (1 - edgeWidth) * height) { + gesture = gesture?.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() { binding.homeUpperView.setOnClickListener { @@ -258,10 +329,22 @@ class HomeActivity : UIObject, AppCompatActivity() { private fun handleBack() { - Gesture.BACK(this) + LauncherAction.CHOOSE.launch(this) } override fun isHomeScreen(): Boolean { return true } + + + /* 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/ui/LegalInfoActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/LegalInfoActivity.kt deleted file mode 100644 index bd2fa23..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/ui/LegalInfoActivity.kt +++ /dev/null @@ -1,46 +0,0 @@ -package de.jrpie.android.launcher.ui - -import android.content.res.Resources -import android.os.Bundle -import android.view.MenuItem -import androidx.appcompat.app.AppCompatActivity -import de.jrpie.android.launcher.R -import de.jrpie.android.launcher.databinding.LegalInfoBinding - -class LegalInfoActivity : AppCompatActivity(), UIObject { - private lateinit var binding: LegalInfoBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - super.onCreate() - - // Initialise layout - binding = LegalInfoBinding.inflate(layoutInflater) - setContentView(binding.root) - - setTitle(R.string.legal_info_title) - setSupportActionBar(binding.legalInfoAppbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - override fun onStart() { - super.onStart() - super.onStart() - } - - override fun getTheme(): Resources.Theme { - return modifyTheme(super.getTheme()) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - finish() - return true - } - else -> { - return super.onOptionsItemSelected(item) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt deleted file mode 100644 index 71908ba..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt +++ /dev/null @@ -1,154 +0,0 @@ -package de.jrpie.android.launcher.ui - -import android.app.AlertDialog -import android.app.Service -import android.content.Context -import android.content.pm.LauncherApps -import android.content.pm.LauncherApps.PinItemRequest -import android.content.res.Resources -import android.os.Build -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import de.jrpie.android.launcher.R -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.PinnedShortcutInfo -import de.jrpie.android.launcher.databinding.ActivityPinShortcutBinding -import de.jrpie.android.launcher.preferences.LauncherPreferences -import androidx.core.content.edit - -class PinShortcutActivity : AppCompatActivity(), UIObject { - private lateinit var binding: ActivityPinShortcutBinding - - private var isBound = false - private var request: PinItemRequest? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - super.onCreate() - enableEdgeToEdge() - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - finish() - return - } - - binding = ActivityPinShortcutBinding.inflate(layoutInflater) - setContentView(binding.root) - - val launcherApps = getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps - - val request = launcherApps.getPinItemRequest(intent) - this.request = request - if (request == null || request.requestType != PinItemRequest.REQUEST_TYPE_SHORTCUT) { - finish() - return - } - - binding.pinShortcutLabel.text = request.shortcutInfo!!.shortLabel ?: "?" - binding.pinShortcutLabel.setCompoundDrawables( - launcherApps.getShortcutBadgedIconDrawable(request.shortcutInfo, 0).also { - val size = (40 * resources.displayMetrics.density).toInt() - it.setBounds(0,0, size, size) - }, null, null, null) - - binding.pinShortcutButtonBind.setOnClickListener { - AlertDialog.Builder(this, R.style.AlertDialogCustom) - .setTitle(getString(R.string.pin_shortcut_button_bind)) - .setView(R.layout.dialog_select_gesture) - .setNegativeButton(android.R.string.cancel, null) - .create().also { it.show() }.let { dialog -> - val viewManager = LinearLayoutManager(dialog.context) - val viewAdapter = GestureRecyclerAdapter (dialog.context) { gesture -> - if (!isBound) { - isBound = true - request.accept() - } - LauncherPreferences.getSharedPreferences().edit { - ShortcutAction(PinnedShortcutInfo(request.shortcutInfo!!)).bindToGesture( - this, - gesture.id - ) - } - dialog.dismiss() - } - dialog.findViewById(R.id.dialog_select_gesture_recycler).apply { - setHasFixedSize(true) - layoutManager = viewManager - adapter = viewAdapter - } - } - } - - binding.pinShortcutClose.setOnClickListener { finish() } - binding.pinShortcutButtonOk.setOnClickListener { finish() } - } - - override fun onStart() { - super.onStart() - super.onStart() - } - - override fun onDestroy() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - super.onDestroy() - return - } - if(binding.pinShortcutSwitchVisible.isChecked) { - if(!isBound) { - request?.accept() - } - request?.shortcutInfo?.let { - val set = LauncherPreferences.apps().pinnedShortcuts() ?: mutableSetOf() - set.add(PinnedShortcutInfo(it)) - LauncherPreferences.apps().pinnedShortcuts(set) - } - } - super.onDestroy() - } - - override fun getTheme(): Resources.Theme { - return modifyTheme(super.getTheme()) - } - - inner class GestureRecyclerAdapter(val context: Context, val onClick: (Gesture) -> Unit): RecyclerView.Adapter() { - private val gestures = Gesture.entries.filter { it.isEnabled() }.toList() - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val label: TextView = itemView.findViewById(R.id.dialog_select_gesture_row_name) - val description: TextView = itemView.findViewById(R.id.dialog_select_gesture_row_description) - val icon: ImageView = itemView.findViewById(R.id.dialog_select_gesture_row_icon) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(parent.context) - val view: View = inflater.inflate(R.layout.dialog_select_gesture_row, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val gesture = gestures[position] - holder.label.text = gesture.getLabel(context) - holder.description.text = gesture.getDescription(context) - holder.icon.setImageDrawable( - Action.forGesture(gesture)?.getIcon(context) - ) - holder.itemView.setOnClickListener { - onClick(gesture) - } - } - - override fun getItemCount(): Int { - return gestures.size - } - - } -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt deleted file mode 100644 index 8e8ed4e..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt +++ /dev/null @@ -1,339 +0,0 @@ -package de.jrpie.android.launcher.ui - -import android.content.Context -import android.graphics.Insets -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.util.DisplayMetrics -import android.view.MotionEvent -import android.view.ViewConfiguration -import android.view.WindowManager -import androidx.annotation.RequiresApi -import de.jrpie.android.launcher.actions.Gesture -import de.jrpie.android.launcher.preferences.LauncherPreferences -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min -import kotlin.math.tan - -class TouchGestureDetector( - private val context: Context, - var width: Int, - var height: Int, - var edgeWidth: Float -) { - private val ANGULAR_THRESHOLD = tan(Math.PI / 6) - private val TOUCH_SLOP: Int - private val TOUCH_SLOP_SQUARE: Int - private val DOUBLE_TAP_SLOP: Int - private val DOUBLE_TAP_SLOP_SQUARE: Int - private val LONG_PRESS_TIMEOUT: Int - private val TAP_TIMEOUT: Int - private val DOUBLE_TAP_TIMEOUT: Int - - private val MIN_TRIANGLE_HEIGHT = 250 - - private val longPressHandler = Handler(Looper.getMainLooper()) - - private var systemGestureInsetTop = 100 - private var systemGestureInsetBottom = 0 - private var systemGestureInsetLeft = 0 - private var systemGestureInsetRight = 0 - - - data class Vector(val x: Float, val y: Float) { - fun absSquared(): Float { - return this.x * this.x + this.y * this.y - } - - fun plus(vector: Vector): Vector { - return Vector(this.x + vector.x, this.y + vector.y) - } - - fun max(other: Vector): Vector { - return Vector(max(this.x, other.x), max(this.y, other.y)) - } - - fun min(other: Vector): Vector { - return Vector(min(this.x, other.x), min(this.y, other.y)) - } - - operator fun minus(vector: Vector): Vector { - return Vector(this.x - vector.x, this.y - vector.y) - } - } - - - class PointerPath( - val number: Int, - val start: Vector, - var last: Vector = start - ) { - var min = Vector(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY) - var max = Vector(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY) - fun sizeSquared(): Float { - return (max - min).absSquared() - } - - fun getDirection(): Vector { - return last - start - } - - fun update(vector: Vector) { - min = min.min(vector) - max = max.max(vector) - last = vector - } - } - - private fun PointerPath.startIntersectsSystemGestureInsets(): Boolean { - // ignore x, since this makes edge swipes very hard to execute - return start.y < systemGestureInsetTop - || start.y > height - systemGestureInsetBottom - } - - private fun PointerPath.intersectsSystemGestureInsets(): Boolean { - return min.x < systemGestureInsetLeft - || min.y < systemGestureInsetTop - || max.x > width - systemGestureInsetRight - || max.y > height - systemGestureInsetBottom - } - - private fun PointerPath.isTap(): Boolean { - if (intersectsSystemGestureInsets()) { - return false - } - return sizeSquared() < TOUCH_SLOP_SQUARE - } - - init { - val configuration = ViewConfiguration.get(context) - TOUCH_SLOP = configuration.scaledTouchSlop - TOUCH_SLOP_SQUARE = TOUCH_SLOP * TOUCH_SLOP - DOUBLE_TAP_SLOP = configuration.scaledDoubleTapSlop - DOUBLE_TAP_SLOP_SQUARE = DOUBLE_TAP_SLOP * DOUBLE_TAP_SLOP - - LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout() - TAP_TIMEOUT = ViewConfiguration.getTapTimeout() - DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout() - } - - private var paths = HashMap() - - /* Set when - * - the longPressHandler has detected this gesture as a long press - * - the gesture was cancelled by MotionEvent.ACTION_CANCEL - * In any case, the current gesture should be ignored by further detection logic. - */ - private var cancelled = false - - private var lastTappedTime = 0L - private var lastTappedLocation: Vector? = null - - fun onTouchEvent(event: MotionEvent) { - - if (event.actionMasked == MotionEvent.ACTION_CANCEL) { - synchronized(this@TouchGestureDetector) { - cancelled = true - } - } - - val pointerIdToIndex = - (0.. abs(direction.y)) { // horizontal swipe - if (direction.x > TOUCH_SLOP) - Gesture.SWIPE_RIGHT - else if (direction.x < -TOUCH_SLOP) - Gesture.SWIPE_LEFT - else null - } else if (ANGULAR_THRESHOLD * abs(direction.y) > abs(direction.x)) { // vertical swipe - if (direction.y < -TOUCH_SLOP) - Gesture.SWIPE_UP - else if (direction.y > TOUCH_SLOP) - Gesture.SWIPE_DOWN - else null - } else null - } - - private fun classifyPaths(paths: Map, timeStart: Long, timeEnd: Long) { - val duration = timeEnd - timeStart - val pointerCount = paths.entries.size - if (paths.entries.isEmpty()) { - return - } - - val mainPointerPath = paths.entries.firstOrNull { it.value.number == 0 }?.value ?: return - - // Ignore swipes starting at the very top and the very bottom - if (paths.entries.any { it.value.startIntersectsSystemGestureInsets() }) { - return - } - - if (pointerCount == 1 && mainPointerPath.isTap()) { - // detect taps - - if (duration in 0..TAP_TIMEOUT) { - if (timeStart - lastTappedTime < DOUBLE_TAP_TIMEOUT && - lastTappedLocation?.let { - (mainPointerPath.last - it).absSquared() < DOUBLE_TAP_SLOP_SQUARE - } == true - ) { - Gesture.DOUBLE_CLICK.invoke(context) - } else { - lastTappedTime = timeEnd - lastTappedLocation = mainPointerPath.last - } - } - } else { - // detect swipes - - val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe() - val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe() - - var gesture = getGestureForDirection(mainPointerPath.getDirection()) - - if (doubleActions && pointerCount > 1) { - if (paths.entries.any { getGestureForDirection(it.value.getDirection()) != gesture }) { - // the directions of the pointers don't match - return - } - gesture = gesture?.let(Gesture::getDoubleVariant) - } - - // detect triangles - val startEndMin = mainPointerPath.start.min(mainPointerPath.last) - val startEndMax = mainPointerPath.start.max(mainPointerPath.last) - when (gesture) { - Gesture.SWIPE_DOWN -> { - if (startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) { - gesture = Gesture.SWIPE_LARGER - } else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) { - gesture = Gesture.SWIPE_SMALLER - } - } - - Gesture.SWIPE_UP -> { - if (startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) { - gesture = Gesture.SWIPE_LARGER_REVERSE - } else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) { - gesture = Gesture.SWIPE_SMALLER_REVERSE - } - } - - Gesture.SWIPE_RIGHT -> { - if (startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) { - gesture = Gesture.SWIPE_V - } else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) { - gesture = Gesture.SWIPE_LAMBDA - } - } - - Gesture.SWIPE_LEFT -> { - if (startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) { - gesture = Gesture.SWIPE_V_REVERSE - } else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) { - gesture = Gesture.SWIPE_LAMBDA_REVERSE - } - } - - else -> {} - } - - if (edgeActions) { - if (mainPointerPath.max.x < edgeWidth * width) { - gesture = gesture?.getEdgeVariant(Gesture.Edge.LEFT) - } else if (mainPointerPath.min.x > (1 - edgeWidth) * width) { - gesture = gesture?.getEdgeVariant(Gesture.Edge.RIGHT) - } - - if (mainPointerPath.max.y < edgeWidth * height) { - gesture = gesture?.getEdgeVariant(Gesture.Edge.TOP) - } else if (mainPointerPath.min.y > (1 - edgeWidth) * height) { - gesture = gesture?.getEdgeVariant(Gesture.Edge.BOTTOM) - } - } - - if (timeStart - lastTappedTime < 2 * DOUBLE_TAP_TIMEOUT) { - gesture = gesture?.getTapComboVariant() - } - gesture?.invoke(context) - } - } - - fun updateScreenSize(windowManager: WindowManager) { - val displayMetrics = DisplayMetrics() - @Suppress("deprecation") // required to support API < 30 - windowManager.defaultDisplay.getMetrics(displayMetrics) - width = displayMetrics.widthPixels - height = displayMetrics.heightPixels - } - - @RequiresApi(Build.VERSION_CODES.Q) - fun setSystemGestureInsets(insets: Insets) { - systemGestureInsetTop = insets.top - systemGestureInsetBottom = insets.bottom - systemGestureInsetLeft = insets.left - systemGestureInsetRight = insets.right - } -} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/UIObject.kt b/app/src/main/java/de/jrpie/android/launcher/ui/UIObject.kt index 51324f4..3702bb2 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/UIObject.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/UIObject.kt @@ -3,11 +3,7 @@ package de.jrpie.android.launcher.ui import android.app.Activity import android.content.pm.ActivityInfo import android.content.res.Resources -import android.os.Build -import android.view.View import android.view.Window -import android.view.WindowInsets -import android.view.WindowInsetsController import android.view.WindowManager import de.jrpie.android.launcher.preferences.LauncherPreferences @@ -15,12 +11,10 @@ import de.jrpie.android.launcher.preferences.LauncherPreferences * 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. */ -@Suppress("deprecation") // FLAG_FULLSCREEN is required to support API level < 30 fun setWindowFlags(window: Window, homeScreen: Boolean) { window.setFlags(0, 0) // clear flags - // Display notification bar - if (LauncherPreferences.display().hideStatusBar()) + if (LauncherPreferences.display().fullScreen()) window.setFlags( WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN @@ -42,19 +36,17 @@ fun setWindowFlags(window: Window, homeScreen: Boolean) { } - interface UIObject { fun onCreate() { - if (this !is Activity) { - return - } - setWindowFlags(window, isHomeScreen()) + if (this is Activity) { + setWindowFlags(window, isHomeScreen()) + + if (!LauncherPreferences.display().rotateScreen()) { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR + } - if (!LauncherPreferences.display().rotateScreen()) { - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR } } - fun onStart() { setOnClicks() adjustLayout() @@ -78,26 +70,4 @@ interface UIObject { fun isHomeScreen(): Boolean { return false } - - - @Suppress("DEPRECATION") - fun hideNavigationBar() { - if (this !is Activity) { - return - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - window.insetsController?.apply { - hide(WindowInsets.Type.navigationBars()) - systemBarsBehavior = - WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } - } else { - // Try to hide the navigation bar but do not hide the status bar - window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - or View.SYSTEM_UI_FLAG_IMMERSIVE - or View.SYSTEM_UI_FLAG_LAYOUT_STABLE) - } - } } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/ListActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/ListActivity.kt index fe27f0f..1fe5afb 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/ListActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/ListActivity.kt @@ -1,23 +1,25 @@ package de.jrpie.android.launcher.ui.list +import android.app.Activity +import android.content.Context +import android.content.Intent import android.content.res.Resources import android.graphics.Rect import android.os.Build import android.os.Bundle import android.view.View +import android.widget.Toast import android.window.OnBackInvokedDispatcher import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter -import de.jrpie.android.launcher.Application +import androidx.viewpager.widget.ViewPager +import com.google.android.material.tabs.TabLayout import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.REQUEST_UNINSTALL import de.jrpie.android.launcher.actions.LauncherAction import de.jrpie.android.launcher.apps.AppFilter -import de.jrpie.android.launcher.apps.hidePrivateSpaceWhenLocked -import de.jrpie.android.launcher.apps.isPrivateSpaceLocked -import de.jrpie.android.launcher.apps.isPrivateSpaceSetUp -import de.jrpie.android.launcher.apps.togglePrivateSpaceLock import de.jrpie.android.launcher.databinding.ListBinding import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.UIObject @@ -25,6 +27,12 @@ import de.jrpie.android.launcher.ui.list.apps.ListFragmentApps import de.jrpie.android.launcher.ui.list.other.ListFragmentOther +// TODO: Better solution for this intercommunication functionality (used in list-fragments) +var intention = ListActivity.ListActivityIntention.VIEW +var favoritesVisibility: AppFilter.Companion.AppSetVisibility = AppFilter.Companion.AppSetVisibility.VISIBLE +var hiddenVisibility: AppFilter.Companion.AppSetVisibility = AppFilter.Companion.AppSetVisibility.HIDDEN +var forGesture: String? = null + /** * The [ListActivity] is the most general purpose activity in Launcher: * - used to view all apps and edit their settings @@ -34,54 +42,6 @@ import de.jrpie.android.launcher.ui.list.other.ListFragmentOther */ class ListActivity : AppCompatActivity(), UIObject { private lateinit var binding: ListBinding - var intention = ListActivityIntention.VIEW - var favoritesVisibility: AppFilter.Companion.AppSetVisibility = - AppFilter.Companion.AppSetVisibility.VISIBLE - var privateSpaceVisibility: AppFilter.Companion.AppSetVisibility = - AppFilter.Companion.AppSetVisibility.VISIBLE - var hiddenVisibility: AppFilter.Companion.AppSetVisibility = - AppFilter.Companion.AppSetVisibility.HIDDEN - var forGesture: String? = null - - - private fun updateLockIcon(locked: Boolean) { - if ( - // only show lock for VIEW intention - (intention != ListActivityIntention.VIEW) - // hide lock when private space does not exist - || !isPrivateSpaceSetUp(this) - // hide lock when private space apps are hidden from the main list and we are not in the private space list - || (LauncherPreferences.apps().hidePrivateSpaceApps() - && privateSpaceVisibility != AppFilter.Companion.AppSetVisibility.EXCLUSIVE) - // hide lock when private space is locked and the hidden when locked setting is set - || (locked && hidePrivateSpaceWhenLocked(this)) - ) { - binding.listLock.visibility = View.GONE - return - } - - binding.listLock.visibility = View.VISIBLE - - binding.listLock.setImageDrawable( - AppCompatResources.getDrawable( - this, - if (locked) { - R.drawable.baseline_lock_24 - } else { - R.drawable.baseline_lock_open_24 - } - ) - ) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - binding.listLock.tooltipText = getString( - if (locked) { - R.string.tooltip_unlock_private_space - } else { - R.string.tooltip_lock_private_space - } - ) - } - } enum class ListActivityIntention(val titleResource: Int) { @@ -108,15 +68,10 @@ class ListActivity : AppCompatActivity(), UIObject { ?.let { ListActivityIntention.valueOf(it) } ?: ListActivityIntention.VIEW - @Suppress("deprecation") // required to support API level < 33 favoritesVisibility = bundle.getSerializable("favoritesVisibility") as? AppFilter.Companion.AppSetVisibility ?: favoritesVisibility - @Suppress("deprecation") // required to support API level < 33 - privateSpaceVisibility = bundle.getSerializable("privateSpaceVisibility") - as? AppFilter.Companion.AppSetVisibility ?: privateSpaceVisibility - @Suppress("deprecation") // required to support API level < 33 hiddenVisibility = bundle.getSerializable("hiddenVisibility") - as? AppFilter.Companion.AppSetVisibility ?: hiddenVisibility + as? AppFilter.Companion.AppSetVisibility ?: favoritesVisibility if (intention != ListActivityIntention.VIEW) forGesture = bundle.getString("forGesture") @@ -132,17 +87,6 @@ class ListActivity : AppCompatActivity(), UIObject { } - if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { - isPrivateSpaceSetUp(this, showToast = true, launchSettings = true) - if (isPrivateSpaceLocked(this)) { - togglePrivateSpaceLock(this) - } - } - updateLockIcon(isPrivateSpaceLocked(this)) - - val privateSpaceLocked = (this.applicationContext as Application).privateSpaceLocked - privateSpaceLocked.observe(this) { updateLockIcon(it) } - // android:windowSoftInputMode="adjustResize" doesn't work in full screen. // workaround from https://stackoverflow.com/a/57623505 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { @@ -153,7 +97,7 @@ class ListActivity : AppCompatActivity(), UIObject { binding.listContainer.context.resources.displayMetrics.heightPixels val diff = height - r.bottom if (diff != 0 && - LauncherPreferences.display().hideStatusBar() + LauncherPreferences.display().fullScreen() ) { if (binding.listContainer.paddingBottom != diff) { binding.listContainer.setPadding(0, 0, 0, diff) @@ -181,19 +125,30 @@ class ListActivity : AppCompatActivity(), UIObject { finish() } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_UNINSTALL) { + if (resultCode == Activity.RESULT_OK) { + Toast.makeText(this, getString(R.string.list_removed), Toast.LENGTH_LONG).show() + finish() + } else if (resultCode == Activity.RESULT_FIRST_USER) { + Toast.makeText(this, getString(R.string.list_not_removed), Toast.LENGTH_LONG).show() + finish() + } + } + } + + fun updateTitle() { var titleResource = intention.titleResource if (intention == ListActivityIntention.VIEW) { - titleResource = - if (hiddenVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { - R.string.list_title_hidden - } else if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { - R.string.list_title_private_space - } else if (favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { - R.string.list_title_favorite - } else { - R.string.list_title_view - } + titleResource = if (hiddenVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { + R.string.list_title_hidden + } else if (favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { + R.string.list_title_favorite + } else { + R.string.list_title_view + } } binding.listHeading.text = getString(titleResource) @@ -206,12 +161,6 @@ class ListActivity : AppCompatActivity(), UIObject { override fun setOnClicks() { binding.listClose.setOnClickListener { finish() } - binding.listLock.setOnClickListener { - togglePrivateSpaceLock(this) - if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { - finish() - } - } } override fun adjustLayout() { @@ -223,11 +172,11 @@ class ListActivity : AppCompatActivity(), UIObject { updateTitle() - val sectionsPagerAdapter = ListSectionsPagerAdapter(this) - binding.listViewpager.let { - it.adapter = sectionsPagerAdapter - binding.listTabs.setupWithViewPager(it) - } + val sectionsPagerAdapter = ListSectionsPagerAdapter(this, supportFragmentManager) + val viewPager: ViewPager = findViewById(R.id.list_viewpager) + viewPager.adapter = sectionsPagerAdapter + val tabs: TabLayout = findViewById(R.id.list_tabs) + tabs.setupWithViewPager(viewPager) } } @@ -239,15 +188,9 @@ private val TAB_TITLES = arrayOf( /** * The [ListSectionsPagerAdapter] returns the fragment, * which corresponds to the selected tab in [ListActivity]. - * - * This should eventually be replaced by a [FragmentStateAdapter] - * However this keyboard does not open when using [ViewPager2] - * so currently [ViewPager] is used here. - * https://github.com/jrpie/launcher/issues/130 */ -@Suppress("deprecation") -class ListSectionsPagerAdapter(private val activity: ListActivity) : - FragmentPagerAdapter(activity.supportFragmentManager) { +class ListSectionsPagerAdapter(private val context: Context, fm: FragmentManager) : + FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { override fun getItem(position: Int): Fragment { return when (position) { @@ -258,11 +201,11 @@ class ListSectionsPagerAdapter(private val activity: ListActivity) : } override fun getPageTitle(position: Int): CharSequence { - return activity.resources.getString(TAB_TITLES[position]) + return context.resources.getString(TAB_TITLES[position]) } override fun getCount(): Int { - return when (activity.intention) { + return when (intention) { ListActivity.ListActivityIntention.VIEW -> 1 else -> 2 } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/AppsRecyclerAdapter.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/AppsRecyclerAdapter.kt index 65278ce..60f8019 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/AppsRecyclerAdapter.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/AppsRecyclerAdapter.kt @@ -2,6 +2,7 @@ package de.jrpie.android.launcher.ui.list.apps import android.annotation.SuppressLint import android.app.Activity +import android.content.Intent import android.graphics.Rect import android.view.LayoutInflater import android.view.View @@ -14,12 +15,12 @@ import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.R -import de.jrpie.android.launcher.actions.Action -import de.jrpie.android.launcher.actions.Gesture -import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo +import de.jrpie.android.launcher.REQUEST_CHOOSE_APP +import de.jrpie.android.launcher.actions.AppAction import de.jrpie.android.launcher.apps.AppFilter import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.apps.DetailedAppInfo +import de.jrpie.android.launcher.getUserFromId import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.ListLayout import de.jrpie.android.launcher.ui.list.ListActivity @@ -46,11 +47,7 @@ class AppsRecyclerAdapter( RecyclerView.Adapter() { private val apps = (activity.applicationContext as Application).apps - private val appsListDisplayed: MutableList = mutableListOf() - private val grayscale = LauncherPreferences.theme().monochromeIcons() - - // temporarily disable auto launch - var disableAutoLaunch: Boolean = false + private val appsListDisplayed: MutableList = mutableListOf() init { apps.observe(this.activity as AppCompatActivity) { @@ -68,7 +65,7 @@ class AppsRecyclerAdapter( override fun onClick(v: View) { val rect = Rect() img.getGlobalVisibleRect(rect) - selectItem(bindingAdapterPosition, rect) + selectItem(adapterPosition, rect) } init { @@ -80,19 +77,20 @@ class AppsRecyclerAdapter( override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { var appLabel = appsListDisplayed[i].getCustomLabel(activity) - val appIcon = appsListDisplayed[i].getIcon(activity) - - viewHolder.img.transformGrayscale(grayscale) - viewHolder.img.setImageDrawable(appIcon.constantState?.newDrawable() ?: appIcon) - if (layout.useBadgedText) { appLabel = activity.packageManager.getUserBadgedLabel( appLabel, - appsListDisplayed[i].getUser(activity) + getUserFromId(appsListDisplayed[i].app.user, activity) ).toString() } - viewHolder.textView.text = appLabel + val appIcon = appsListDisplayed[i].icon + + viewHolder.textView.text = appLabel + viewHolder.img.setImageDrawable(appIcon) + + if (LauncherPreferences.theme().monochromeIcons()) + viewHolder.img.transformGrayscale() // decide when to show the options popup menu about if (intention == ListActivity.ListActivityIntention.VIEW) { @@ -117,26 +115,22 @@ class AppsRecyclerAdapter( @Suppress("SameReturnValue") private fun showOptionsPopup( viewHolder: ViewHolder, - appInfo: AbstractDetailedAppInfo + appInfo: DetailedAppInfo ): Boolean { //create the popup menu val popup = PopupMenu(activity, viewHolder.img) popup.inflate(R.menu.menu_app) - if (!appInfo.isRemovable()) { + if (appInfo.isSystemApp) { popup.menu.findItem(R.id.app_menu_delete).setVisible(false) } - if (appInfo !is DetailedAppInfo) { - popup.menu.findItem(R.id.app_menu_info).setVisible(false) - } - - if (LauncherPreferences.apps().hidden()?.contains(appInfo.getRawInfo()) == true) { + if (LauncherPreferences.apps().hidden()?.contains(appInfo.app) == true) { popup.menu.findItem(R.id.app_menu_hidden).setTitle(R.string.list_app_hidden_remove) } - if (LauncherPreferences.apps().favorites()?.contains(appInfo.getRawInfo()) == true) { + if (LauncherPreferences.apps().favorites()?.contains(appInfo.app) == true) { popup.menu.findItem(R.id.app_menu_favorite).setTitle(R.string.list_app_favorite_remove) } @@ -144,19 +138,19 @@ class AppsRecyclerAdapter( popup.setOnMenuItemClickListener { when (it.itemId) { R.id.app_menu_delete -> { - appInfo.getRawInfo().uninstall(activity); true + appInfo.app.uninstall(activity); true } R.id.app_menu_info -> { - (appInfo.getRawInfo() as? AppInfo)?.openSettings(activity); true + appInfo.app.openSettings(activity); true } R.id.app_menu_favorite -> { - appInfo.getRawInfo().toggleFavorite(); true + appInfo.app.toggleFavorite(); true } R.id.app_menu_hidden -> { - appInfo.getRawInfo().toggleHidden(root); true + appInfo.app.toggleHidden(root); true } R.id.app_menu_rename -> { @@ -191,14 +185,15 @@ class AppsRecyclerAdapter( val appInfo = appsListDisplayed[pos] when (intention) { ListActivity.ListActivityIntention.VIEW -> { - appInfo.getAction().invoke(activity, rect) + AppAction(appInfo.app).invoke(activity, rect) } ListActivity.ListActivityIntention.PICK -> { + val returnIntent = Intent() + AppAction(appInfo.app).writeToIntent(returnIntent) + returnIntent.putExtra("forGesture", forGesture) + activity.setResult(REQUEST_CHOOSE_APP, returnIntent) activity.finish() - forGesture ?: return - val gesture = Gesture.byId(forGesture) ?: return - Action.setActionForGesture(gesture, appInfo.getAction()) } } } @@ -210,11 +205,10 @@ class AppsRecyclerAdapter( if (triggerAutoLaunch && appsListDisplayed.size == 1 && intention == ListActivity.ListActivityIntention.VIEW - && !disableAutoLaunch && LauncherPreferences.functionality().searchAutoLaunch() ) { - val app = appsListDisplayed[0] - app.getAction().invoke(activity) + val info = appsListDisplayed[0] + AppAction(info.app).invoke(activity) val inputMethodManager = activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager @@ -228,7 +222,7 @@ class AppsRecyclerAdapter( * The function [setSearchString] is used to search elements within this [RecyclerView]. */ fun setSearchString(search: String) { - appFilter.query = search + appFilter.search = search updateAppsList(true) } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ContextMenuActions.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ContextMenuActions.kt index 22dff02..e09111e 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ContextMenuActions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ContextMenuActions.kt @@ -1,11 +1,11 @@ package de.jrpie.android.launcher.ui.list.apps -import android.app.Activity import android.app.Service import android.content.Context import android.content.Intent import android.content.pm.LauncherApps import android.graphics.Rect +import android.net.Uri import android.os.Bundle import android.util.Log import android.view.View @@ -13,13 +13,11 @@ import android.widget.EditText import androidx.appcompat.app.AlertDialog import com.google.android.material.snackbar.Snackbar import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.REQUEST_UNINSTALL import de.jrpie.android.launcher.apps.AppInfo -import de.jrpie.android.launcher.apps.AbstractAppInfo -import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo -import de.jrpie.android.launcher.apps.PinnedShortcutInfo +import de.jrpie.android.launcher.apps.DetailedAppInfo import de.jrpie.android.launcher.getUserFromId import de.jrpie.android.launcher.preferences.LauncherPreferences -import androidx.core.net.toUri private const val LOG_TAG = "AppContextMenu" @@ -34,29 +32,27 @@ fun AppInfo.openSettings( } } -fun AbstractAppInfo.uninstall(activity: Activity) { - if (this is AppInfo) { - val packageName = this.packageName - val userId = this.user +fun AppInfo.uninstall(activity: android.app.Activity) { + val packageName = this.packageName.toString() + val userId = this.user - Log.i(LOG_TAG, "uninstalling $this") + Log.i(LOG_TAG, "uninstalling $this") - val intent = Intent(Intent.ACTION_DELETE) - intent.data = "package:$packageName".toUri() - getUserFromId(userId, activity).let { user -> - intent.putExtra(Intent.EXTRA_USER, user) - } - activity.startActivity(intent) - - } else if(this is PinnedShortcutInfo) { - val pinned = LauncherPreferences.apps().pinnedShortcuts() ?: mutableSetOf() - pinned.remove(this) - LauncherPreferences.apps().pinnedShortcuts(pinned) + val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE) + intent.data = Uri.parse("package:$packageName") + getUserFromId(userId, activity).let { user -> + intent.putExtra(Intent.EXTRA_USER, user) } + + intent.putExtra(Intent.EXTRA_RETURN_RESULT, true) + activity.startActivityForResult( + intent, + REQUEST_UNINSTALL + ) } -fun AbstractAppInfo.toggleFavorite() { - val favorites: MutableSet = +fun AppInfo.toggleFavorite() { + val favorites: MutableSet = LauncherPreferences.apps().favorites() ?: mutableSetOf() if (favorites.contains(this)) { @@ -73,8 +69,8 @@ fun AbstractAppInfo.toggleFavorite() { /** * @param view: used to show a snackbar letting the user undo the action */ -fun AbstractAppInfo.toggleHidden(view: View) { - val hidden: MutableSet = +fun AppInfo.toggleHidden(view: View) { + val hidden: MutableSet = LauncherPreferences.apps().hidden() ?: mutableSetOf() if (hidden.contains(this)) { hidden.remove(this) @@ -91,12 +87,12 @@ fun AbstractAppInfo.toggleHidden(view: View) { LauncherPreferences.apps().hidden(hidden) } -fun AbstractDetailedAppInfo.showRenameDialog(context: Context) { +fun DetailedAppInfo.showRenameDialog(context: Context) { AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { - setTitle(context.getString(R.string.dialog_rename_title, getLabel())) + setTitle(context.getString(R.string.dialog_rename_title, label)) setView(R.layout.dialog_rename_app) - setNegativeButton(android.R.string.cancel) { d, _ -> d.cancel() } - setPositiveButton(android.R.string.ok) { d, _ -> + setNegativeButton(R.string.dialog_cancel) { d, _ -> d.cancel() } + setPositiveButton(R.string.dialog_rename_ok) { d, _ -> setCustomLabel( (d as? AlertDialog) ?.findViewById(R.id.dialog_rename_app_edit_text) @@ -106,7 +102,7 @@ fun AbstractDetailedAppInfo.showRenameDialog(context: Context) { }.create().also { it.show() }.apply { val input = findViewById(R.id.dialog_rename_app_edit_text) input?.setText(getCustomLabel(context)) - input?.hint = getLabel() + input?.hint = label } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt index 1a55bbb..d870211 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt @@ -1,22 +1,21 @@ package de.jrpie.android.launcher.ui.list.apps -import android.content.ActivityNotFoundException import android.content.Intent import android.content.SharedPreferences import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import de.jrpie.android.launcher.R import de.jrpie.android.launcher.apps.AppFilter import de.jrpie.android.launcher.databinding.ListAppsBinding import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.list.ListActivity +import de.jrpie.android.launcher.ui.list.favoritesVisibility +import de.jrpie.android.launcher.ui.list.forGesture +import de.jrpie.android.launcher.ui.list.hiddenVisibility +import de.jrpie.android.launcher.ui.list.intention import de.jrpie.android.launcher.ui.openSoftKeyboard @@ -27,11 +26,11 @@ import de.jrpie.android.launcher.ui.openSoftKeyboard */ class ListFragmentApps : Fragment(), UIObject { private lateinit var binding: ListAppsBinding - private lateinit var appsRecyclerAdapter: AppsRecyclerAdapter + private lateinit var appsRViewAdapter: AppsRecyclerAdapter private var sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> - appsRecyclerAdapter.updateAppsList() + appsRViewAdapter.updateAppsList() } override fun onCreateView( @@ -49,7 +48,7 @@ class ListFragmentApps : Fragment(), UIObject { .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) binding.listAppsCheckBoxFavorites.isChecked = - ((activity as? ListActivity)?.favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) + (favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) } override fun onStop() { @@ -62,93 +61,60 @@ class ListFragmentApps : Fragment(), UIObject { override fun setOnClicks() {} override fun adjustLayout() { - val listActivity = activity as? ListActivity ?: return - appsRecyclerAdapter = + appsRViewAdapter = AppsRecyclerAdapter( - listActivity, binding.root, listActivity.intention, listActivity.forGesture, + requireActivity(), binding.root, intention, forGesture, appFilter = AppFilter( requireContext(), "", - favoritesVisibility = listActivity.favoritesVisibility, - privateSpaceVisibility = listActivity.privateSpaceVisibility, - hiddenVisibility = listActivity.hiddenVisibility + favoritesVisibility = favoritesVisibility, + hiddenVisibility = hiddenVisibility ), layout = LauncherPreferences.list().layout() ) - // set up the list / recycler binding.listAppsRview.apply { // improve performance (since content changes don't change the layout size) setHasFixedSize(true) layoutManager = LauncherPreferences.list().layout().layoutManager(context) - .also { - if (LauncherPreferences.list().reverseLayout()) { - (it as? LinearLayoutManager)?.reverseLayout = true - (it as? GridLayoutManager)?.reverseLayout = true - } - } - adapter = appsRecyclerAdapter + adapter = appsRViewAdapter } binding.listAppsSearchview.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { - appsRecyclerAdapter.setSearchString(query) + appsRViewAdapter.setSearchString(query) if (LauncherPreferences.functionality().searchWeb()) { val i = Intent(Intent.ACTION_WEB_SEARCH).putExtra("query", query) - try { - activity?.startActivity(i) - } catch (_: ActivityNotFoundException) { - Toast.makeText( - requireContext(), - R.string.toast_activity_not_found_search_web, - Toast.LENGTH_LONG - ).show() - } - + activity?.startActivity(i) } else { - appsRecyclerAdapter.selectItem(0) + appsRViewAdapter.selectItem(0) } return true } override fun onQueryTextChange(newText: String): Boolean { - - if (newText == " " && - !appsRecyclerAdapter.disableAutoLaunch && - (activity as? ListActivity)?.intention - == ListActivity.ListActivityIntention.VIEW && - LauncherPreferences.functionality().searchAutoLaunch() - ) { - appsRecyclerAdapter.disableAutoLaunch = true - binding.listAppsSearchview.apply { - queryHint = context.getString(R.string.list_apps_search_hint_no_auto_launch) - setQuery("", false) - } - return false - } - - appsRecyclerAdapter.setSearchString(newText) + appsRViewAdapter.setSearchString(newText) return false } }) binding.listAppsCheckBoxFavorites.setOnClickListener { - listActivity.favoritesVisibility = + favoritesVisibility = if (binding.listAppsCheckBoxFavorites.isChecked) { AppFilter.Companion.AppSetVisibility.EXCLUSIVE } else { AppFilter.Companion.AppSetVisibility.VISIBLE } - appsRecyclerAdapter.setFavoritesVisibility(listActivity.favoritesVisibility) + appsRViewAdapter.setFavoritesVisibility(favoritesVisibility) (activity as? ListActivity)?.updateTitle() } - if (listActivity.intention == ListActivity.ListActivityIntention.VIEW + if (intention == ListActivity.ListActivityIntention.VIEW && LauncherPreferences.functionality().searchAutoOpenKeyboard() ) { binding.listAppsSearchview.openSoftKeyboard(requireContext()) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt index f176469..ddfcfd1 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt @@ -1,6 +1,7 @@ package de.jrpie.android.launcher.ui.list.other import android.app.Activity +import android.content.Intent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -8,10 +9,9 @@ import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import de.jrpie.android.launcher.R -import de.jrpie.android.launcher.actions.Action -import de.jrpie.android.launcher.actions.Gesture +import de.jrpie.android.launcher.REQUEST_CHOOSE_APP import de.jrpie.android.launcher.actions.LauncherAction -import de.jrpie.android.launcher.ui.list.ListActivity +import de.jrpie.android.launcher.ui.list.forGesture /** * The [OtherRecyclerAdapter] will only be displayed in the ListActivity, @@ -23,8 +23,7 @@ import de.jrpie.android.launcher.ui.list.ListActivity class OtherRecyclerAdapter(val activity: Activity) : RecyclerView.Adapter() { - private val othersList: Array = - LauncherAction.entries.filter { it.isAvailable(activity) }.toTypedArray() + private val othersList: Array = LauncherAction.values() inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { @@ -33,13 +32,10 @@ class OtherRecyclerAdapter(val activity: Activity) : override fun onClick(v: View) { - val pos = bindingAdapterPosition + val pos = adapterPosition val content = othersList[pos] - activity.finish() - val gestureId = (activity as? ListActivity)?.forGesture ?: return - val gesture = Gesture.byId(gestureId) ?: return - Action.setActionForGesture(gesture, content) + forGesture?.let { returnChoiceIntent(it, content) } } init { @@ -64,4 +60,12 @@ class OtherRecyclerAdapter(val activity: Activity) : val view: View = inflater.inflate(R.layout.list_other_row, parent, false) return ViewHolder(view) } + + private fun returnChoiceIntent(forGesture: String, action: LauncherAction) { + val returnIntent = Intent() + returnIntent.putExtra("forGesture", forGesture) + action.writeToIntent(returnIntent) + activity.setResult(REQUEST_CHOOSE_APP, returnIntent) + activity.finish() + } } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt index cd59726..e46a956 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt @@ -1,5 +1,6 @@ package de.jrpie.android.launcher.ui.settings +import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.res.Resources @@ -7,14 +8,15 @@ import android.os.Bundle import android.provider.Settings import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.viewpager2.adapter.FragmentStateAdapter -import com.google.android.material.tabs.TabLayoutMediator +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter +import androidx.viewpager.widget.ViewPager +import com.google.android.material.tabs.TabLayout import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.REQUEST_CHOOSE_APP import de.jrpie.android.launcher.databinding.SettingsBinding import de.jrpie.android.launcher.preferences.LauncherPreferences -import de.jrpie.android.launcher.preferences.theme.Background -import de.jrpie.android.launcher.preferences.theme.ColorTheme +import de.jrpie.android.launcher.saveListActivityChoice import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.settings.actions.SettingsFragmentActions import de.jrpie.android.launcher.ui.settings.launcher.SettingsFragmentLauncher @@ -31,29 +33,13 @@ import de.jrpie.android.launcher.ui.settings.meta.SettingsFragmentMeta */ class SettingsActivity : AppCompatActivity(), UIObject { - private val solidBackground = LauncherPreferences.theme().background() == Background.SOLID - || LauncherPreferences.theme().colorTheme() == ColorTheme.LIGHT - - private val sharedPreferencesListener = + private var sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> - if (solidBackground && - (prefKey == LauncherPreferences.theme().keys().background() || - prefKey == LauncherPreferences.theme().keys().colorTheme()) + if (prefKey?.startsWith("theme.") == true || + prefKey?.startsWith("display.") == true ) { - // Switching from solid background to a transparent background using `recreate()` - // causes a very ugly glitch, making the settings unreadable. - // This ugly workaround causes a jump to the top of the list, but at least - // the text stays readable. - val i = Intent(this, SettingsActivity::class.java) - .also { it.putExtra(EXTRA_TAB, 1) } - finish() - startActivity(i) - } else - if (prefKey?.startsWith("theme.") == true || - prefKey?.startsWith("display.") == true - ) { - recreate() - } + recreate() + } } private lateinit var binding: SettingsBinding @@ -67,14 +53,12 @@ class SettingsActivity : AppCompatActivity(), UIObject { setContentView(binding.root) // set up tabs and swiping in settings - val sectionsPagerAdapter = SettingsSectionsPagerAdapter(this) - binding.settingsViewpager.apply { - adapter = sectionsPagerAdapter - setCurrentItem(intent.getIntExtra(EXTRA_TAB, 0), false) - } - TabLayoutMediator(binding.settingsTabs, binding.settingsViewpager) { tab, position -> - tab.text = sectionsPagerAdapter.getPageTitle(position) - }.attach() + val sectionsPagerAdapter = SettingsSectionsPagerAdapter(this, supportFragmentManager) + val viewPager: ViewPager = findViewById(R.id.settings_viewpager) + viewPager.adapter = sectionsPagerAdapter + + val tabs: TabLayout = findViewById(R.id.settings_tabs) + tabs.setupWithViewPager(viewPager) } override fun onStart() { @@ -103,21 +87,24 @@ class SettingsActivity : AppCompatActivity(), UIObject { } } - companion object { - private const val EXTRA_TAB = "tab" + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + REQUEST_CHOOSE_APP -> saveListActivityChoice(data) + else -> super.onActivityResult(requestCode, resultCode, data) + } } } private val TAB_TITLES = arrayOf( - R.string.settings_tab_actions, + R.string.settings_tab_app, R.string.settings_tab_launcher, R.string.settings_tab_meta ) -class SettingsSectionsPagerAdapter(private val activity: FragmentActivity) : - FragmentStateAdapter(activity) { +class SettingsSectionsPagerAdapter(private val context: Context, fm: FragmentManager) : + FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - override fun createFragment(position: Int): Fragment { + override fun getItem(position: Int): Fragment { return when (position) { 0 -> SettingsFragmentActions() 1 -> SettingsFragmentLauncher() @@ -126,11 +113,11 @@ class SettingsSectionsPagerAdapter(private val activity: FragmentActivity) : } } - fun getPageTitle(position: Int): CharSequence { - return activity.resources.getString(TAB_TITLES[position]) + override fun getPageTitle(position: Int): CharSequence { + return context.resources.getString(TAB_TITLES[position]) } - override fun getItemCount(): Int { + override fun getCount(): Int { return 3 } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/actions/SettingsFragmentActionsRecycler.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/actions/SettingsFragmentActionsRecycler.kt index ae47ce2..d7862fa 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/actions/SettingsFragmentActionsRecycler.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/actions/SettingsFragmentActionsRecycler.kt @@ -11,11 +11,11 @@ import android.view.ViewGroup import android.widget.Button import android.widget.ImageView import android.widget.TextView -import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.REQUEST_CHOOSE_APP import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.apps.AppFilter @@ -94,8 +94,6 @@ class SettingsFragmentActionsRecycler : Fragment(), UIObject { class ActionsRecyclerAdapter(val activity: Activity) : RecyclerView.Adapter() { - private val drawableUnknown = AppCompatResources.getDrawable(activity, R.drawable.baseline_question_mark_24) - private val gesturesList: ArrayList = Gesture.entries.filter(Gesture::isEnabled) as ArrayList @@ -117,18 +115,15 @@ class ActionsRecyclerAdapter(val activity: Activity) : private fun updateViewHolder(gesture: Gesture, viewHolder: ViewHolder) { val action = Action.forGesture(gesture) + val drawable = action?.getIcon(activity) - if (action == null) { + if (action == null || drawable == null) { viewHolder.img.visibility = View.INVISIBLE viewHolder.removeAction.visibility = View.GONE viewHolder.chooseButton.visibility = View.VISIBLE return } - // Use the unknown icon if there is an action, but we can't find its icon. - // Probably an app was uninstalled. - val drawable = action.getIcon(activity) ?: drawableUnknown - viewHolder.img.visibility = View.VISIBLE viewHolder.removeAction.visibility = View.VISIBLE viewHolder.chooseButton.visibility = View.INVISIBLE @@ -142,7 +137,9 @@ class ActionsRecyclerAdapter(val activity: Activity) : val description = gesture.getDescription(activity) viewHolder.descriptionTextView.text = description - viewHolder.img.transformGrayscale(LauncherPreferences.theme().monochromeIcons()) + + if (LauncherPreferences.theme().monochromeIcons()) + viewHolder.img.transformGrayscale() updateViewHolder(gesture, viewHolder) viewHolder.img.setOnClickListener { chooseApp(gesture) } @@ -178,6 +175,9 @@ class ActionsRecyclerAdapter(val activity: Activity) : intent.putExtra("intention", ListActivity.ListActivityIntention.PICK.toString()) intent.putExtra("hiddenVisibility", AppFilter.Companion.AppSetVisibility.VISIBLE) intent.putExtra("forGesture", gesture.id) // for which action we choose the app - activity.startActivity(intent) + activity.startActivityForResult( + intent, + REQUEST_CHOOSE_APP + ) } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt index a8efb43..0ae08c2 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt @@ -34,17 +34,6 @@ class SettingsFragmentLauncher : PreferenceFragmentCompat() { ) val timeVisible = LauncherPreferences.clock().timeVisible() showSeconds?.isVisible = timeVisible - - val background = findPreference( - LauncherPreferences.theme().keys().background() - ) - val lightTheme = LauncherPreferences.theme().colorTheme() == ColorTheme.LIGHT - background?.isVisible = !lightTheme - - val hidePausedApps = findPreference( - LauncherPreferences.apps().keys().hidePausedApps() - ) - hidePausedApps?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N } override fun onStart() { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt index dea0bcf..e11c45d 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt @@ -2,23 +2,18 @@ package de.jrpie.android.launcher.ui.settings.meta import android.app.AlertDialog import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Button -import android.widget.TextView import androidx.fragment.app.Fragment -import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.R -import de.jrpie.android.launcher.copyToClipboard import de.jrpie.android.launcher.databinding.SettingsMetaBinding -import de.jrpie.android.launcher.getDeviceInfo import de.jrpie.android.launcher.openInBrowser -import de.jrpie.android.launcher.openTutorial import de.jrpie.android.launcher.preferences.resetPreferences -import de.jrpie.android.launcher.ui.LegalInfoActivity import de.jrpie.android.launcher.ui.UIObject +import de.jrpie.android.launcher.ui.tutorial.TutorialActivity /** * The [SettingsFragmentMeta] is a used as a tab in the SettingsActivity. @@ -44,19 +39,25 @@ class SettingsFragmentMeta : Fragment(), UIObject { super.onStart() } + // Rate App + // Just copied code from https://stackoverflow.com/q/10816757/12787264 + // that is how we write good software ^^ + + private fun rateIntentForUrl(url: String): Intent { + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse(String.format("%s?id=%s", url, this.requireContext().packageName)) + ) + var flags = Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_MULTIPLE_TASK + flags = flags or Intent.FLAG_ACTIVITY_NEW_DOCUMENT + intent.addFlags(flags) + return intent + } + override fun setOnClicks() { - fun bindURL(view: View, urlRes: Int) { - view.setOnClickListener { - openInBrowser( - getString(urlRes), - requireContext() - ) - } - } - binding.settingsMetaButtonViewTutorial.setOnClickListener { - openTutorial(requireContext()) + startActivity(Intent(this.context, TutorialActivity::class.java)) } // prompting for settings-reset confirmation @@ -77,62 +78,55 @@ class SettingsFragmentMeta : Fragment(), UIObject { // view code - bindURL(binding.settingsMetaButtonViewCode, R.string.settings_meta_link_github) + binding.settingsMetaButtonViewCode.setOnClickListener { + openInBrowser( + getString(R.string.settings_meta_link_github), + requireContext() + ) + } // report a bug binding.settingsMetaButtonReportBug.setOnClickListener { - val deviceInfo = getDeviceInfo() - AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { - setView(R.layout.dialog_report_bug) - setTitle(R.string.dialog_report_bug_title) - setPositiveButton(R.string.dialog_report_bug_create_report) { _, _ -> - openInBrowser( - getString(R.string.settings_meta_report_bug_link), - requireContext() - ) - } - setNegativeButton(R.string.dialog_cancel) { _, _ -> } - }.create().also { it.show() }.apply { - val info = findViewById(R.id.dialog_report_bug_device_info) - val buttonClipboard = findViewById