Compare commits

..

No commits in common. "master" and "j-0.0.20" have entirely different histories.

172 changed files with 1765 additions and 5070 deletions

4
.github/FUNDING.yml vendored
View file

@ -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

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help improve this app
title: '[bug] '
labels: bug
assignees: ''
---
# Describe the bug
<!-- A clear and concise description of what the bug is. -->
# To Reproduce
<!--
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
-->
# Expected behavior
<!-- A clear and concise description of what you expected to happen. -->
# Screenshots
<!-- If applicable, add screenshots to help explain your problem. -->
# Smartphone (please complete the following information)
- Device: <!-- e.g. Fairphone 5 -->
- Android Version: <!-- e.g. Marshmallow, 6.0 or API 23 -->
- µLauncher Version: <!-- e.g. j-0.0.11 or version code 25 -->
# Additional info
<!-- Add any other info or comments about the problem here. -->

View file

@ -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

View file

@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
export JAVA_HOME="/usr/lib/jvm/java-21-openjdk/" export JAVA_HOME="/usr/lib/jvm/java-23-openjdk/"
OUTPUT_DIR="$HOME/launcher-release" OUTPUT_DIR="$HOME/launcher-release"
BUILD_TOOLS_DIR="$HOME/Android/Sdk/build-tools/35.0.0" BUILD_TOOLS_DIR="$HOME/Android/Sdk/build-tools/35.0.0"
KEYSTORE="$HOME/data/keys/launcher_jrpie.jks" KEYSTORE="$HOME/data/keys/launcher_jrpie.jks"
@ -9,14 +9,7 @@ KEYSTORE_ACCRESCENT_PASS=$(keepassxc-password "android_keys/launcher-accrescent"
if [[ $(git status --porcelain) ]]; then if [[ $(git status --porcelain) ]]; then
echo "There are uncommitted changes." echo "There are uncommitted changes."
exit 1
read -p "Continue anyway? (y/n) " -n 1 -r
echo # (optional) move to a new line
if ! [[ $REPLY =~ ^[Yy]$ ]]
then
exit 1
fi
fi fi
rm -rf "$OUTPUT_DIR" rm -rf "$OUTPUT_DIR"
@ -30,17 +23,13 @@ echo "======================="
./gradlew clean ./gradlew clean
./gradlew assembleDefaultRelease ./gradlew assembleDefaultRelease
mv app/build/outputs/apk/default/release/app-default-release-unsigned.apk "$OUTPUT_DIR/app-release.apk" mv app/build/outputs/apk/default/release/app-default-release-unsigned.apk "$OUTPUT_DIR/unsigned.apk"
"$BUILD_TOOLS_DIR/apksigner" sign --ks "$KEYSTORE" \ $BUILD_TOOLS_DIR/apksigner sign --ks "$KEYSTORE" \
--ks-key-alias key0 \ --ks-key-alias key0 \
--ks-pass="pass:$KEYSTORE_PASS" \ --ks-pass="pass:$KEYSTORE_PASS" \
--key-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 \
--v1-signing-enabled=true \ "$OUTPUT_DIR/unsigned.apk"
--v2-signing-enabled=true \
--v3-signing-enabled=true \
--v4-signing-enabled=true \
"$OUTPUT_DIR/app-release.apk"
echo echo
echo "=======================" echo "======================="
@ -49,15 +38,17 @@ echo "======================="
./gradlew clean ./gradlew clean
./gradlew bundleDefaultRelease ./gradlew bundleDefaultRelease
mv app/build/outputs/bundle/defaultRelease/app-default-release.aab "$OUTPUT_DIR/app-release.aab" mv app/build/outputs/bundle/defaultRelease/app-default-release.aab $OUTPUT_DIR/app-release.aab
"$BUILD_TOOLS_DIR/apksigner" sign --ks "$KEYSTORE" \ $BUILD_TOOLS_DIR/apksigner sign --ks "$KEYSTORE" \
--ks-key-alias key0 \ --ks-key-alias key0 \
--ks-pass="pass:$KEYSTORE_PASS" \ --ks-pass="pass:$KEYSTORE_PASS" \
--key-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 \ --v1-signing-enabled=true --v2-signing-enabled=true --v3-signing-enabled=true --v4-signing-enabled=true \
--min-sdk-version=21 \ --min-sdk-version=21 \
--target-sdk-version=35 \
"$OUTPUT_DIR/app-release.aab" "$OUTPUT_DIR/app-release.aab"
echo echo
echo "=======================" echo "======================="
echo " Accrescent (apks) " echo " Accrescent (apks) "
@ -65,10 +56,8 @@ echo "======================="
./gradlew clean ./gradlew clean
./gradlew bundleAccrescentRelease ./gradlew bundleAccrescentRelease
mv app/build/outputs/bundle/accrescentRelease/app-accrescent-release.aab "$OUTPUT_DIR/app-accrescent-release.aab" mv app/build/outputs/bundle/accrescentRelease/app-accrescent-release.aab $OUTPUT_DIR/app-accrescent-release.aab
$JAVA_HOME/bin/java -jar /opt/android/bundletool.jar build-apks \
# 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" \ --bundle="$OUTPUT_DIR/app-accrescent-release.aab" --output="$OUTPUT_DIR/launcher-accrescent.apks" \
--ks="$KEYSTORE_ACCRESCENT" \ --ks="$KEYSTORE_ACCRESCENT" \
--ks-pass="pass:$KEYSTORE_ACCRESCENT_PASS" \ --ks-pass="pass:$KEYSTORE_ACCRESCENT_PASS" \

View file

@ -8,25 +8,10 @@ Make sure that `JAVA_HOME` and `ANDROID_HOME` are set correctly.
``` ```
git clone https://github.com/jrpie/Launcher git clone https://github.com/jrpie/Launcher
cd Launcher cd Launcher
./gradlew build
./gradlew assembleDefaultRelease
``` ```
This will create an apk file at `app/build/outputs/apk/default/release/app-default-release-unsigned.apk`. Note that you need to sign the 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
```
See [this guide](https://developer.android.com/build/building-cmdline) See [this guide](https://developer.android.com/build/building-cmdline)

View file

@ -14,12 +14,19 @@
µLauncher is an Android home screen that lets you launch apps using swipe gestures and button presses. µ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*. 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].
<a href="https://f-droid.org/packages/de.jrpie.android.launcher/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80"></a> <a href="https://f-droid.org/packages/de.jrpie.android.launcher/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80"></a>
<a href="https://accrescent.app/app/de.jrpie.android.launcher.accrescent"><img alt="Get it on Accrescent" src="https://accrescent.app/badges/get-it-on.png" height="80"></a>
<a href="https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/{%22id%22:%22de.jrpie.android.launcher%22,%22url%22:%22https://github.com/jrpie/Launcher%22,%22author%22:%22jrpie%22,%22name%22:%22%c2%b5Launcher%22,%22additionalSettings%22:%22{\%22apkFilterRegEx\%22:\%22release\%22,\%22invertAPKFilter\%22:false,\%22about\%22:\%22%c2%b5Launcher%20is%20a%20minimal%20home%20screen.\%22}%22}"><img src="https://raw.githubusercontent.com/ImranR98/Obtainium/b1c8ac6f2ab08497189721a788a5763e28ff64cd/assets/graphics/badge_obtainium.png" alt="Get it on Obtainium" height="80"></a> <a href="https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/{%22id%22:%22de.jrpie.android.launcher%22,%22url%22:%22https://github.com/jrpie/Launcher%22,%22author%22:%22jrpie%22,%22name%22:%22%c2%b5Launcher%22,%22additionalSettings%22:%22{\%22apkFilterRegEx\%22:\%22release\%22,\%22invertAPKFilter\%22:false,\%22about\%22:\%22%c2%b5Launcher%20is%20a%20minimal%20home%20screen.\%22}%22}"><img src="https://raw.githubusercontent.com/ImranR98/Obtainium/b1c8ac6f2ab08497189721a788a5763e28ff64cd/assets/graphics/badge_obtainium.png" alt="Get it on Obtainium" height="80"></a>
<a href="https://github.com/jrpie/launcher/releases/latest"><img src="https://raw.githubusercontent.com/NeoApplications/Neo-Backup/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub" height="80"></a> <a href="https://github.com/jrpie/launcher/releases/latest"><img src="https://raw.githubusercontent.com/NeoApplications/Neo-Backup/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub" height="80"></a>
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. 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.
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg" <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg"
@ -45,46 +52,6 @@ You can also [get it on Google Play](https://play.google.com/store/apps/details?
height="400"> height="400">
µ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 ## Contributing
There are several ways to contribute to this app: 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. * 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: * You can implement a new feature yourself:
- Create a fork of this repository: [![][shield-gh-fork]][fork] - Create a fork of this repository: [![][shield-gh-fork]][fork]
- Create a new branch named `feature/<your feature>` or `fix/<your fix>` and commit your changes. - Create a new branch named `feature/<your feature>` of `fix/<your fix>` and commit your changes.
- Open a new pull request. - Open a new pull request.
See [build.md](docs/build.md) for instructions how to build this project. See [BUILD.md](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.
## 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/ [hack-font]: https://sourcefoundry.org/hack/
[original-repo]: https://github.com/finnmglas/Launcher [original-repo]: https://github.com/finnmglas/Launcher

View file

@ -23,8 +23,8 @@ android {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 35 targetSdkVersion 35
compileSdk 35 compileSdk 35
versionCode 44 versionCode 36
versionName "0.1.4" versionName "j-0.0.20"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -43,8 +43,8 @@ android {
buildTypes { buildTypes {
release { release {
minifyEnabled true // minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' // proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
debug { debug {
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
@ -85,23 +85,22 @@ android {
// Disables dependency metadata when building Android App Bundles. // Disables dependency metadata when building Android App Bundles.
includeInBundle = false includeInBundle = false
} }
lint {
lintOptions {
abortOnError false abortOnError false
} }
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 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.appcompat:appcompat:1.7.0'
implementation 'androidx.core:core-ktx:1.15.0' implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.palette:palette-ktx: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 'androidx.preference:preference-ktx:1.2.1'
implementation 'com.google.android.material:material:1.12.0' implementation 'com.google.android.material:material:1.12.0'
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")

View file

@ -1,6 +1,4 @@
# Add project specific ProGuard rules here. # Add project specific ProGuard rules here.
-dontobfuscate
-dontoptimize
# You can control the set of applied configuration files using the # You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle. # proguardFiles setting in build.gradle.
# #
@ -27,4 +25,3 @@
# This is generated automatically by the Android Gradle plugin. # This is generated automatically by the Android Gradle plugin.
-dontwarn javax.annotation.processing.AbstractProcessor -dontwarn javax.annotation.processing.AbstractProcessor
-dontwarn javax.annotation.processing.SupportedAnnotationTypes -dontwarn javax.annotation.processing.SupportedAnnotationTypes
-dontwarn javax.annotation.processing.SupportedSourceVersion

View file

@ -19,16 +19,6 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/launcherBaseTheme" android:theme="@style/launcherBaseTheme"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<activity
android:name=".ui.PinShortcutActivity"
android:autoRemoveFromRecents="true"
android:excludeFromRecents="true"
android:exported="false">
<intent-filter>
<action android:name="android.content.pm.action.CONFIRM_PIN_SHORTCUT" />
<action android:name="android.content.pm.action.CONFIRM_PIN_APPWIDGET" />
</intent-filter>
</activity>
<activity <activity
android:name=".ui.HomeActivity" android:name=".ui.HomeActivity"
android:clearTaskOnLaunch="true" android:clearTaskOnLaunch="true"
@ -85,7 +75,7 @@
<service <service
android:name=".actions.lock.LauncherAccessibilityService" android:name=".actions.lock.LauncherAccessibilityService"
android:exported="true" android:exported="true"
android:label="@string/app_name" android:label="@string/accessibility_service_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter> <intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" /> <action android:name="android.accessibilityservice.AccessibilityService" />

View file

@ -1,40 +1,23 @@
package de.jrpie.android.launcher 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.SharedPreferences
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo import android.content.pm.ShortcutInfo
import android.os.AsyncTask
import android.os.Build import android.os.Build
import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES
import android.os.UserHandle import android.os.UserHandle
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import de.jrpie.android.launcher.actions.TorchManager import de.jrpie.android.launcher.actions.TorchManager
import de.jrpie.android.launcher.apps.AbstractAppInfo import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo import de.jrpie.android.launcher.apps.DetailedAppInfo
import de.jrpie.android.launcher.apps.isPrivateSpaceLocked
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion
import de.jrpie.android.launcher.preferences.resetPreferences import de.jrpie.android.launcher.preferences.resetPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class Application : android.app.Application() { class Application : android.app.Application() {
val apps = MutableLiveData<List<AbstractDetailedAppInfo>>() val apps = MutableLiveData<List<DetailedAppInfo>>()
val privateSpaceLocked = MutableLiveData<Boolean>()
private val profileAvailabilityBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// TODO: only update specific apps
// use Intent.EXTRA_USER
loadApps()
}
}
// TODO: only update specific apps // TODO: only update specific apps
private val launcherAppsCallback = object : LauncherApps.Callback() { private val launcherAppsCallback = object : LauncherApps.Callback() {
@ -84,12 +67,10 @@ class Application : android.app.Application() {
} }
var torchManager: TorchManager? = null var torchManager: TorchManager? = null
private var customAppNames: HashMap<AbstractAppInfo, String>? = null private var customAppNames: HashMap<AppInfo, String>? = null
private val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, pref -> private val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, pref ->
if (pref == getString(R.string.settings_apps_custom_names_key)) { if (pref == getString(R.string.settings_apps_custom_names_key)) {
customAppNames = LauncherPreferences.apps().customNames() 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 // Try to restore old preferences
migratePreferencesToNewVersion(this) migratePreferencesToNewVersion(this)
// First time opening the app: set defaults // First time opening the app: set defaults and start tutorial
// The tutorial is started from HomeActivity#onStart, as starting it here is blocked by android
if (!LauncherPreferences.internal().started()) { if (!LauncherPreferences.internal().started()) {
resetPreferences(this) 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 val launcherApps = getSystemService(LAUNCHER_APPS_SERVICE) as LauncherApps
launcherApps.registerCallback(launcherAppsCallback) 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() loadApps()
} }
fun getCustomAppNames(): HashMap<AbstractAppInfo, String> { fun getCustomAppNames(): HashMap<AppInfo, String> {
return (customAppNames ?: LauncherPreferences.apps().customNames() ?: HashMap()) return (customAppNames ?: LauncherPreferences.apps().customNames() ?: HashMap())
.also { customAppNames = it } .also { customAppNames = it }
} }
private fun loadApps() { private fun loadApps() {
privateSpaceLocked.postValue(isPrivateSpaceLocked(this)) AsyncTask.execute { apps.postValue(getApps(packageManager, applicationContext)) }
CoroutineScope(Dispatchers.Default).launch {
apps.postValue(getApps(packageManager, applicationContext))
}
} }
} }

View file

@ -3,74 +3,58 @@ package de.jrpie.android.launcher
import android.app.Activity import android.app.Activity
import android.app.Service import android.app.Service
import android.app.role.RoleManager import android.app.role.RoleManager
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.content.pm.LauncherApps.ShortcutQuery
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.UserHandle import android.os.UserHandle
import android.os.UserManager import android.os.UserManager
import android.provider.Settings import android.provider.Settings
import android.util.Log 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.Action
import de.jrpie.android.launcher.actions.Gesture 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.AppInfo
import de.jrpie.android.launcher.apps.DetailedAppInfo 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 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 const val REQUEST_SET_DEFAULT_HOME = 42
fun isDefaultHomeScreen(context: Context): Boolean { const val LOG_TAG = "Launcher"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
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) val roleManager = context.getSystemService(RoleManager::class.java)
return roleManager.isRoleHeld(RoleManager.ROLE_HOME) if (!roleManager.isRoleHeld(RoleManager.ROLE_HOME)) {
} else { context.startActivityForResult(
roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME),
REQUEST_SET_DEFAULT_HOME
)
}
return
}
if (checkDefault) {
val testIntent = Intent(Intent.ACTION_MAIN) val testIntent = Intent(Intent.ACTION_MAIN)
testIntent.addCategory(Intent.CATEGORY_HOME) testIntent.addCategory(Intent.CATEGORY_HOME)
val defaultHome = testIntent.resolveActivity(context.packageManager)?.packageName 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) val intent = Intent(Settings.ACTION_HOME_SETTINGS)
context.startActivity(intent) context.startActivity(intent)
} }
@ -84,91 +68,34 @@ fun getUserFromId(userId: Int?, context: Context): UserHandle {
return profiles.firstOrNull { it.hashCode() == userId } ?: profiles[0] 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<ShortcutInfo>? {
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<PinnedShortcutInfo> =
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) { 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) }) intent.putExtras(Bundle().apply { putBoolean("new_window", true) })
try { context.startActivity(intent)
context.startActivity(intent)
} catch (_: ActivityNotFoundException) {
Toast.makeText(context, R.string.toast_activity_not_found_browser, Toast.LENGTH_LONG).show()
}
} }
fun openTutorial(context: Context) { 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. * Load all apps.
*/ */
fun getApps( fun getApps(packageManager: PackageManager, context: Context): MutableList<DetailedAppInfo> {
packageManager: PackageManager, val start = System.currentTimeMillis()
context: Context val loadList = mutableListOf<DetailedAppInfo>()
): MutableList<AbstractDetailedAppInfo> {
var start = System.currentTimeMillis()
val loadList = mutableListOf<AbstractDetailedAppInfo>()
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
val userManager = context.getSystemService(Service.USER_SERVICE) as UserManager val userManager = context.getSystemService(Service.USER_SERVICE) as UserManager
val privateSpaceUser = getPrivateSpaceUser(context)
// TODO: shortcuts - launcherApps.getShortcuts() // TODO: shortcuts - launcherApps.getShortcuts()
val users = userManager.userProfiles val users = userManager.userProfiles
for (user in users) { 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 { 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) i.addCategory(Intent.CATEGORY_LAUNCHER)
val allApps = packageManager.queryIntentActivities(i, 0) val allApps = packageManager.queryIntentActivities(i, 0)
for (ri in allApps) { 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( val detailedAppInfo = DetailedAppInfo(
app, app,
ri.loadLabel(packageManager), ri.loadLabel(packageManager),
ri.activityInfo.loadIcon(packageManager), ri.activityInfo.loadIcon(packageManager)
false
) )
loadList.add(detailedAppInfo) 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)") 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 return loadList
} }
// used for the bug report button
fun getDeviceInfo(): String { // Used in Tutorial and Settings `ActivityOnResult`
return """ fun saveListActivityChoice(data: Intent?) {
µLauncher version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) val forGesture = data?.getStringExtra("forGesture") ?: return
Android version: ${Build.VERSION.RELEASE} (sdk ${Build.VERSION.SDK_INT}) Gesture.byId(forGesture)?.let { Action.setActionForGesture(it, Action.fromIntent(data)) }
Model: ${Build.MODEL}
Device: ${Build.DEVICE}
Brand: ${Build.BRAND}
Manufacturer: ${Build.MANUFACTURER}
""".trimIndent()
} }
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)
}

View file

@ -2,6 +2,7 @@ package de.jrpie.android.launcher.actions
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.SharedPreferences.Editor import android.content.SharedPreferences.Editor
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
@ -11,7 +12,6 @@ import de.jrpie.android.launcher.preferences.LauncherPreferences
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import androidx.core.content.edit
@Serializable @Serializable
@ -21,14 +21,15 @@ sealed interface Action {
fun getIcon(context: Context): Drawable? fun getIcon(context: Context): Drawable?
fun isAvailable(context: Context): Boolean fun isAvailable(context: Context): Boolean
// Can the action be used to reach µLauncher settings?
fun canReachSettings(): Boolean
fun bindToGesture(prefEditor: Editor, id: String) { fun bindToGesture(prefEditor: Editor, id: String) {
prefEditor.putString(id, Json.encodeToString(this)) prefEditor.putString(id, Json.encodeToString(this))
} }
fun writeToIntent(intent: Intent) {
intent.putExtra("action", Json.encodeToString(this))
}
companion object { companion object {
fun forGesture(gesture: Gesture): Action? { fun forGesture(gesture: Gesture): Action? {
@ -40,23 +41,20 @@ sealed interface Action {
} }
fun resetToDefaultActions(context: Context) { fun resetToDefaultActions(context: Context) {
LauncherPreferences.getSharedPreferences().edit { val editor = LauncherPreferences.getSharedPreferences().edit()
val boundActions = HashSet<String>() val boundActions = HashSet<String>()
Gesture.entries.forEach { gesture -> Gesture.entries.forEach { gesture ->
context.resources context.resources
.getStringArray(gesture.defaultsResource) .getStringArray(gesture.defaultsResource)
.filterNot { boundActions.contains(it) } .filterNot { boundActions.contains(it) }
.map { Pair(it, Json.decodeFromString<Action>(it)) } .map { Pair(it, Json.decodeFromString<Action>(it)) }
.firstOrNull { it.second.isAvailable(context) } .firstOrNull { it.second.isAvailable(context) }
?.apply { ?.apply {
// allow to bind CHOOSE to multiple gestures boundActions.add(first)
if (second != LauncherAction.CHOOSE) { second.bindToGesture(editor, gesture.id)
boundActions.add(first) }
}
second.bindToGesture(this@edit, gesture.id)
}
}
} }
editor.apply()
} }
fun setActionForGesture(gesture: Gesture, action: Action?) { fun setActionForGesture(gesture: Gesture, action: Action?) {
@ -64,15 +62,15 @@ sealed interface Action {
clearActionForGesture(gesture) clearActionForGesture(gesture)
return return
} }
LauncherPreferences.getSharedPreferences().edit { val editor = LauncherPreferences.getSharedPreferences().edit()
action.bindToGesture(this, gesture.id) action.bindToGesture(editor, gesture.id)
} editor.apply()
} }
fun clearActionForGesture(gesture: Gesture) { fun clearActionForGesture(gesture: Gesture) {
LauncherPreferences.getSharedPreferences().edit { LauncherPreferences.getSharedPreferences().edit()
remove(gesture.id) .remove(gesture.id)
} .apply()
} }
fun launch( fun launch(
@ -83,9 +81,6 @@ sealed interface Action {
) { ) {
if (action != null && action.invoke(context)) { if (action != null && action.invoke(context)) {
if (context is Activity) { 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) context.overridePendingTransition(animationIn, animationOut)
} }
} else { } else {
@ -96,5 +91,10 @@ sealed interface Action {
).show() ).show()
} }
} }
fun fromIntent(data: Intent): Action? {
val json = data.getStringExtra("action") ?: return null
return Json.decodeFromString(json)
}
} }
} }

View file

@ -2,7 +2,6 @@ package de.jrpie.android.launcher.actions
import android.app.AlertDialog import android.app.AlertDialog
import android.app.Service import android.app.Service
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
@ -11,7 +10,7 @@ import android.graphics.drawable.Drawable
import android.util.Log import android.util.Log
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.apps.AppInfo 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.apps.DetailedAppInfo
import de.jrpie.android.launcher.ui.list.apps.openSettings import de.jrpie.android.launcher.ui.list.apps.openSettings
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
@ -35,11 +34,7 @@ class AppAction(val app: AppInfo) : Action {
context.packageManager.getLaunchIntentForPackage(packageName)?.let { context.packageManager.getLaunchIntentForPackage(packageName)?.let {
it.addCategory(Intent.CATEGORY_LAUNCHER) it.addCategory(Intent.CATEGORY_LAUNCHER)
try { context.startActivity(it)
context.startActivity(it)
} catch (_: ActivityNotFoundException) {
return false
}
return true return true
} }
@ -67,15 +62,11 @@ class AppAction(val app: AppInfo) : Action {
} }
override fun getIcon(context: Context): Drawable? { 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 { override fun isAvailable(context: Context): Boolean {
// check if app is installed // check if app is installed
return DetailedAppInfo.fromAppInfo(app, context) != null return DetailedAppInfo.fromAppInfo(app, context) != null
} }
override fun canReachSettings(): Boolean {
return false
}
} }

View file

@ -1,7 +1,6 @@
package de.jrpie.android.launcher.actions package de.jrpie.android.launcher.actions
import android.content.Context import android.content.Context
import android.util.Log
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
@ -79,13 +78,6 @@ enum class Gesture(
R.array.default_up_right, R.array.default_up_right,
R.anim.bottom_up 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( SWIPE_UP_DOUBLE(
"action.double_up", "action.double_up",
R.string.settings_gesture_double_up, R.string.settings_gesture_double_up,
@ -114,13 +106,6 @@ enum class Gesture(
R.array.default_down_right, R.array.default_down_right,
R.anim.top_down 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( SWIPE_DOWN_DOUBLE(
"action.double_down", "action.double_down",
R.string.settings_gesture_double_down, R.string.settings_gesture_double_down,
@ -149,13 +134,6 @@ enum class Gesture(
R.array.default_messengers, R.array.default_messengers,
R.anim.right_left 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( SWIPE_LEFT_DOUBLE(
"action.double_left", "action.double_left",
R.string.settings_gesture_double_left, R.string.settings_gesture_double_left,
@ -184,73 +162,12 @@ enum class Gesture(
R.array.default_right_bottom, R.array.default_right_bottom,
R.anim.left_right 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( SWIPE_RIGHT_DOUBLE(
"action.double_right", "action.double_right",
R.string.settings_gesture_double_right, R.string.settings_gesture_double_right,
R.string.settings_gesture_description_double_right, R.string.settings_gesture_description_double_right,
R.array.default_double_right, R.array.default_double_right,
R.anim.left_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 { 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 { fun isDoubleVariant(): Boolean {
return when (this) { return when (this) {
SWIPE_UP_DOUBLE, SWIPE_UP_DOUBLE,
@ -355,14 +261,13 @@ enum class Gesture(
} }
operator fun invoke(context: Context) { operator fun invoke(context: Context) {
Log.i("Launcher", "Detected gesture: $this")
val action = Action.forGesture(this) val action = Action.forGesture(this)
Action.launch(action, context, this.animationIn, this.animationOut) Action.launch(action, context, this.animationIn, this.animationOut)
} }
companion object { companion object {
fun byId(id: String): Gesture? { fun byId(id: String): Gesture? {
return Gesture.entries.firstOrNull { it.id == id } return Gesture.values().firstOrNull { it.id == id }
} }
} }

View file

@ -9,15 +9,9 @@ import android.os.Build
import android.os.SystemClock import android.os.SystemClock
import android.view.KeyEvent import android.view.KeyEvent
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.R 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.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.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.list.ListActivity import de.jrpie.android.launcher.ui.list.ListActivity
import de.jrpie.android.launcher.ui.settings.SettingsActivity import de.jrpie.android.launcher.ui.settings.SettingsActivity
@ -39,88 +33,45 @@ enum class LauncherAction(
val id: String, val id: String,
val label: Int, val label: Int,
val icon: Int, val icon: Int,
val launch: (Context) -> Unit, val launch: (Context) -> Unit
private val canReachSettings: Boolean = false,
val available: (Context) -> Boolean = { true },
) : Action { ) : Action {
SETTINGS( SETTINGS(
"settings", "settings",
R.string.list_other_settings, R.string.list_other_settings,
R.drawable.baseline_settings_24, R.drawable.baseline_settings_24,
::openSettings, ::openSettings
true
), ),
CHOOSE( CHOOSE(
"choose", "choose",
R.string.list_other_list, R.string.list_other_list,
R.drawable.baseline_menu_24, R.drawable.baseline_menu_24,
::openAppsList, ::openAppsList
true
), ),
CHOOSE_FROM_FAVORITES( CHOOSE_FROM_FAVORITES(
"choose_from_favorites", "choose_from_favorites",
R.string.list_other_list_favorites, R.string.list_other_list_favorites,
R.drawable.baseline_favorite_24, R.drawable.baseline_favorite_24,
{ context -> openAppsList(context, favorite = true) }, { context -> openAppsList(context, true) }
true
),
CHOOSE_FROM_PRIVATE_SPACE(
"choose_from_private_space",
R.string.list_other_list_private_space,
R.drawable.baseline_security_24,
{ context ->
if ((context.applicationContext as Application).privateSpaceLocked.value != true
|| !hidePrivateSpaceWhenLocked(context)
) {
openAppsList(context, private = true)
}
},
available = { _ ->
isPrivateSpaceSupported()
}
),
TOGGLE_PRIVATE_SPACE_LOCK(
"toggle_private_space_lock",
R.string.list_other_toggle_private_space_lock,
R.drawable.baseline_security_24,
::togglePrivateSpaceLock,
available = { _ -> isPrivateSpaceSupported() }
), ),
VOLUME_UP( VOLUME_UP(
"volume_up", "volume_up",
R.string.list_other_volume_up, R.string.list_other_volume_up,
R.drawable.baseline_volume_up_24, R.drawable.baseline_volume_up_24, ::audioVolumeUp
{ context -> audioVolumeAdjust(context, AudioManager.ADJUST_RAISE) }
), ),
VOLUME_DOWN( VOLUME_DOWN(
"volume_down", "volume_down",
R.string.list_other_volume_down, R.string.list_other_volume_down,
R.drawable.baseline_volume_down_24, R.drawable.baseline_volume_down_24, ::audioVolumeDown
{ context -> audioVolumeAdjust(context, AudioManager.ADJUST_LOWER) }
),
VOLUME_ADJUST(
"volume_adjust",
R.string.list_other_volume_adjust,
R.drawable.baseline_volume_adjust_24,
{ context -> audioVolumeAdjust(context, AudioManager.ADJUST_SAME) }
),
TRACK_PLAY_PAUSE(
"play_pause_track",
R.string.list_other_track_play_pause,
R.drawable.baseline_play_arrow_24,
{ context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) }
), ),
TRACK_NEXT( TRACK_NEXT(
"next_track", "next_track",
R.string.list_other_track_next, R.string.list_other_track_next,
R.drawable.baseline_skip_next_24, R.drawable.baseline_skip_next_24, ::audioNextTrack
{ context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_NEXT) }
), ),
TRACK_PREV( TRACK_PREV(
"previous_track", "previous_track",
R.string.list_other_track_previous, R.string.list_other_track_previous,
R.drawable.baseline_skip_previous_24, R.drawable.baseline_skip_previous_24, ::audioPreviousTrack
{ context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PREVIOUS) }
), ),
EXPAND_NOTIFICATIONS_PANEL( EXPAND_NOTIFICATIONS_PANEL(
"expand_notifications_panel", "expand_notifications_panel",
@ -134,31 +85,17 @@ enum class LauncherAction(
R.drawable.baseline_settings_applications_24, R.drawable.baseline_settings_applications_24,
::expandSettingsPanel ::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(
"lock_screen", "lock_screen",
R.string.list_other_lock_screen, R.string.list_other_lock_screen,
R.drawable.baseline_lock_24, R.drawable.baseline_lock_24px,
{ c -> LauncherPreferences.actions().lockMethod().lockOrEnable(c) } { c -> LauncherPreferences.actions().lockMethod().lockOrEnable(c) }
), ),
TORCH( TORCH(
"toggle_torch", "toggle_torch",
R.string.list_other_torch, R.string.list_other_torch,
R.drawable.baseline_flashlight_on_24, R.drawable.baseline_flashlight_on_24,
::toggleTorch, ::toggleTorch
),
LAUNCH_OTHER_LAUNCHER(
"launcher_other_launcher",
R.string.list_other_launch_other_launcher,
R.drawable.baseline_home_24,
::launchOtherLauncher
), ),
NOP("nop", R.string.list_other_nop, R.drawable.baseline_not_interested_24, {}); NOP("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? { override fun getIcon(context: Context): Drawable? {
return AppCompatResources.getDrawable(context, icon) return context.getDrawable(icon)
} }
override fun isAvailable(context: Context): Boolean { override fun isAvailable(context: Context): Boolean {
return this.available(context) return true
}
override fun canReachSettings(): Boolean {
return this.canReachSettings
} }
companion object { companion object {
@ -192,28 +125,56 @@ enum class LauncherAction(
/* Media player actions */ /* 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 = val audioManager =
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
audioManager.adjustStreamVolume( audioManager.adjustStreamVolume(
AudioManager.STREAM_MUSIC, AudioManager.STREAM_MUSIC,
direction, AudioManager.ADJUST_RAISE,
AudioManager.FLAG_SHOW_UI 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 */ /* End media player actions */
private fun toggleTorch(context: Context) { private fun toggleTorch(context: Context) {
@ -246,7 +207,6 @@ private fun expandNotificationsPanel(context: Context) {
} }
} }
private fun expandSettingsPanel(context: Context) { private fun expandSettingsPanel(context: Context) {
/* https://stackoverflow.com/a/31898506 */ /* https://stackoverflow.com/a/31898506 */
try { 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) { private fun openSettings(context: Context) {
context.startActivity(Intent(context, SettingsActivity::class.java)) context.startActivity(Intent(context, SettingsActivity::class.java))
} }
fun openAppsList( fun openAppsList(context: Context, favorite: Boolean = false, hidden: Boolean = false) {
context: Context,
favorite: Boolean = false,
hidden: Boolean = false,
private: Boolean = false
) {
val intent = Intent(context, ListActivity::class.java) val intent = Intent(context, ListActivity::class.java)
intent.putExtra("intention", ListActivity.ListActivityIntention.VIEW.toString()) intent.putExtra("intention", ListActivity.ListActivityIntention.VIEW.toString())
intent.putExtra( intent.putExtra(
@ -301,16 +247,6 @@ fun openAppsList(
AppFilter.Companion.AppSetVisibility.HIDDEN 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) context.startActivity(intent)
} }
@ -324,7 +260,6 @@ private class LauncherActionSerializer : KSerializer<LauncherAction> {
) { ) {
element("value", String.serializer().descriptor) element("value", String.serializer().descriptor)
} }
override fun deserialize(decoder: Decoder): LauncherAction { override fun deserialize(decoder: Decoder): LauncherAction {
val s = decoder.decodeStructure(descriptor) { val s = decoder.decodeStructure(descriptor) {
decodeElementIndex(descriptor) decodeElementIndex(descriptor)
@ -338,4 +273,5 @@ private class LauncherActionSerializer : KSerializer<LauncherAction> {
encodeSerializableElement(descriptor, 0, String.serializer(), value.id) encodeSerializableElement(descriptor, 0, String.serializer(), value.id)
} }
} }
} }

View file

@ -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
}
}

View file

@ -22,44 +22,26 @@ class LauncherAccessibilityService : AccessibilityService() {
companion object { companion object {
private const val TAG = "Launcher Accessibility" 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_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 { try {
context.startService( context.startService(
Intent( Intent(
context, context,
LauncherAccessibilityService::class.java LauncherAccessibilityService::class.java
).apply { ).apply {
this.action = action action = ACTION_LOCK_SCREEN
}) })
} catch (_: Exception) { } catch (e: Exception) {
Toast.makeText( Toast.makeText(
context, context,
context.getString(failureMessageRes), context.getString(R.string.alert_lock_screen_failed),
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).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 { fun isEnabled(context: Context): Boolean {
val enabledServices = Settings.Secure.getString( val enabledServices = Settings.Secure.getString(
context.contentResolver, context.contentResolver,
@ -76,7 +58,7 @@ class LauncherAccessibilityService : AccessibilityService() {
setView(R.layout.dialog_consent_accessibility) setView(R.layout.dialog_consent_accessibility)
setTitle(R.string.dialog_consent_accessibility_title) setTitle(R.string.dialog_consent_accessibility_title)
setPositiveButton(R.string.dialog_consent_accessibility_ok) { _, _ -> setPositiveButton(R.string.dialog_consent_accessibility_ok) { _, _ ->
invoke(context, ACTION_REQUEST_ENABLE, R.string.alert_enable_accessibility_failed) lockScreen(context)
} }
setNegativeButton(R.string.dialog_cancel) { _, _ -> } setNegativeButton(R.string.dialog_cancel) { _, _ -> }
}.create().also { it.show() }.apply { }.create().also { it.show() }.apply {
@ -112,9 +94,7 @@ class LauncherAccessibilityService : AccessibilityService() {
} }
when (action) { when (action) {
ACTION_REQUEST_ENABLE -> {} // do nothing
ACTION_LOCK_SCREEN -> handleLockScreen() ACTION_LOCK_SCREEN -> handleLockScreen()
ACTION_RECENT_APPS -> performGlobalAction(GLOBAL_ACTION_RECENTS)
} }
} }
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)

View file

@ -6,10 +6,10 @@ import android.widget.Button
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
@Suppress("unused")
enum class LockMethod( enum class LockMethod(
private val lock: (Context) -> Unit, private val lock: (Context) -> Unit,
private val isEnabled: (Context) -> Boolean, private val isEnabled: (Context) -> Boolean,

View file

@ -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)
}
}
}

View file

@ -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<AbstractAppInfo, String>()
if (label.isNullOrEmpty()) {
map.remove(getRawInfo())
} else {
map[getRawInfo()] = label.toString()
}
LauncherPreferences.apps().customNames(map)
}
}

View file

@ -6,7 +6,6 @@ import android.os.Build
import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.AppAction import de.jrpie.android.launcher.actions.AppAction
import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.ShortcutAction
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import java.util.Locale import java.util.Locale
import kotlin.text.Regex.Companion.escape import kotlin.text.Regex.Companion.escape
@ -16,34 +15,26 @@ class AppFilter(
var query: String, var query: String,
var favoritesVisibility: AppSetVisibility = AppSetVisibility.VISIBLE, var favoritesVisibility: AppSetVisibility = AppSetVisibility.VISIBLE,
var hiddenVisibility: AppSetVisibility = AppSetVisibility.HIDDEN, var hiddenVisibility: AppSetVisibility = AppSetVisibility.HIDDEN,
var privateSpaceVisibility: AppSetVisibility = AppSetVisibility.VISIBLE
) { ) {
operator fun invoke(apps: List<AbstractDetailedAppInfo>): List<AbstractDetailedAppInfo> { operator fun invoke(apps: List<DetailedAppInfo>): List<DetailedAppInfo> {
var apps = 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 hidden = LauncherPreferences.apps().hidden() ?: setOf()
val favorites = LauncherPreferences.apps().favorites() ?: setOf() val favorites = LauncherPreferences.apps().favorites() ?: setOf()
val private = apps.filter { it.isPrivate() }
.map { it.getRawInfo() }.toSet()
apps = apps.filter { info -> apps = apps.filter { info ->
favoritesVisibility.predicate(favorites, info) favoritesVisibility.predicate(favorites, info)
&& hiddenVisibility.predicate(hidden, info) && hiddenVisibility.predicate(hidden, info)
&& privateSpaceVisibility.predicate(private, info)
} }
if (LauncherPreferences.apps().hideBoundApps()) { if (LauncherPreferences.apps().hideBoundApps()) {
val boundApps = Gesture.entries val boundApps = Gesture.entries
.filter(Gesture::isEnabled) .filter(Gesture::isEnabled)
.mapNotNull { g -> Action.forGesture(g) } .mapNotNull { g -> (Action.forGesture(g) as? AppAction)?.app }
.mapNotNull {
(it as? AppAction)?.app
?: (it as? ShortcutAction)?.shortcut
}
.toSet() .toSet()
apps = apps.filterNot { info -> boundApps.contains(info.getRawInfo()) } apps = apps.filterNot { info -> boundApps.contains(info.app) }
} }
// normalize text for search // normalize text for search
@ -63,11 +54,11 @@ class AppFilter(
if (query.isEmpty()) { if (query.isEmpty()) {
return apps return apps
} else { } else {
val r: MutableList<AbstractDetailedAppInfo> = ArrayList() val r: MutableList<DetailedAppInfo> = ArrayList()
val appsSecondary: MutableList<AbstractDetailedAppInfo> = ArrayList() val appsSecondary: MutableList<DetailedAppInfo> = ArrayList()
val normalizedQuery: String = normalize(query) val normalizedQuery: String = normalize(query)
for (item in apps) { 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(normalizedQuery)) {
r.add(item) r.add(item)
@ -83,11 +74,11 @@ class AppFilter(
companion object { companion object {
enum class AppSetVisibility( enum class AppSetVisibility(
val predicate: (set: Set<AbstractAppInfo>, AbstractDetailedAppInfo) -> Boolean val predicate: (set: Set<AppInfo>, DetailedAppInfo) -> Boolean
) { ) {
VISIBLE({ _, _ -> true }), VISIBLE({ _, _ -> true }),
HIDDEN({ set, appInfo -> !set.contains(appInfo.getRawInfo()) }), HIDDEN({ set, appInfo -> !set.contains(appInfo.app) }),
EXCLUSIVE({ set, appInfo -> set.contains(appInfo.getRawInfo()) }), EXCLUSIVE({ set, appInfo -> set.contains(appInfo.app) }),
; ;
} }

View file

@ -4,18 +4,33 @@ import android.app.Service
import android.content.Context import android.content.Context
import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.getUserFromId import de.jrpie.android.launcher.getUserFromId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
/** /**
* Represents an app installed on the users device. * Represents an app installed on the users device.
* Contains the minimal amount of data required to identify the app. * Contains the minimal amount of data required to identify the app.
*/ */
@Serializable @Serializable
@SerialName("app") class AppInfo(val packageName: String, val activityName: String?, val user: Int = INVALID_USER) {
data class AppInfo(val packageName: String, val activityName: String?, val user: Int = INVALID_USER): AbstractAppInfo {
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( fun getLauncherActivityInfo(
context: Context context: Context
@ -26,4 +41,17 @@ data class AppInfo(val packageName: String, val activityName: String?, val user:
return activityList.firstOrNull { app -> app.name == activityName } return activityList.firstOrNull { app -> app.name == activityName }
?: activityList.firstOrNull() ?: 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)
}
}
} }

View file

@ -4,23 +4,21 @@ import android.content.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherActivityInfo
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.UserHandle import android.util.Log
import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.actions.AppAction import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.getUserFromId
/** /**
* Stores information used to create [de.jrpie.android.launcher.ui.list.apps.AppsRecyclerAdapter] rows. * Stores information used to create [AppsRecyclerAdapter] rows.
*/ */
class DetailedAppInfo( class DetailedAppInfo(
private val app: AppInfo, val app: AppInfo,
private val label: CharSequence, val label: CharSequence,
private val icon: Drawable, val icon: Drawable,
private val privateSpace: Boolean, val isSystemApp: Boolean = false,
private val removable: Boolean = true, ) {
): AbstractDetailedAppInfo {
constructor(activityInfo: LauncherActivityInfo, private: Boolean) : this( constructor(activityInfo: LauncherActivityInfo) : this(
AppInfo( AppInfo(
activityInfo.applicationInfo.packageName, activityInfo.applicationInfo.packageName,
activityInfo.name, activityInfo.name,
@ -28,47 +26,32 @@ class DetailedAppInfo(
), ),
activityInfo.label, activityInfo.label,
activityInfo.getBadgedIcon(0), activityInfo.getBadgedIcon(0),
private, activityInfo.applicationInfo.flags.and(ApplicationInfo.FLAG_SYSTEM) != 0
// App can be uninstalled iff it is not a system app
activityInfo.applicationInfo.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0
) )
fun getCustomLabel(context: Context): CharSequence {
val map = (context.applicationContext as? Application)?.getCustomAppNames() ?: return label
return map[app] ?: label
override fun getLabel(): String {
return label.toString()
} }
override fun getIcon(context: Context): Drawable { fun setCustomLabel(label: CharSequence?) {
return icon
}
override fun getRawInfo(): AppInfo { Log.i("Launcher", "Setting custom label for ${this.app} to ${label}.")
return app val map = LauncherPreferences.apps().customNames() ?: HashMap<AppInfo, String>()
}
override fun getUser(context: Context): UserHandle { if (label.isNullOrEmpty()) {
return getUserFromId(app.user, context) map.remove(app)
} } else {
map[app] = label.toString()
}
override fun isPrivate(): Boolean { LauncherPreferences.apps().customNames(map)
return privateSpace
} }
override fun isRemovable(): Boolean {
return removable
}
override fun getAction(): Action {
return AppAction(app)
}
companion object { companion object {
fun fromAppInfo(appInfo: AppInfo, context: Context): DetailedAppInfo? { fun fromAppInfo(appInfo: AppInfo, context: Context): DetailedAppInfo? {
return appInfo.getLauncherActivityInfo(context)?.let { return appInfo.getLauncherActivityInfo(context)?.let { DetailedAppInfo(it) }
DetailedAppInfo(it, it.user == getPrivateSpaceUser(context))
}
} }
} }
} }

View file

@ -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)
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -17,7 +17,6 @@ import androidx.core.graphics.green
import androidx.core.graphics.red import androidx.core.graphics.red
import androidx.preference.Preference import androidx.preference.Preference
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import androidx.core.graphics.toColorInt
class ColorPreference(context: Context, attrs: AttributeSet?) : class ColorPreference(context: Context, attrs: AttributeSet?) :
Preference(context, attrs) { Preference(context, attrs) {
@ -53,7 +52,7 @@ class ColorPreference(context: Context, attrs: AttributeSet?) :
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
setView(R.layout.dialog_choose_color) setView(R.layout.dialog_choose_color)
setTitle(R.string.dialog_choose_color_title) setTitle(R.string.dialog_choose_color_title)
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(R.string.dialog_select_color_ok) { _, _ ->
persistInt(currentColor) persistInt(currentColor)
summary = currentColor.getHex() 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 onTextChanged(text: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun afterTextChanged(editable: Editable?) { override fun afterTextChanged(editable: Editable?) {
preview.hasFocus() || return preview.hasFocus() || return
val newText = editable?.toString() ?: return val newText = editable?.toString()
newText.isBlank() && return newText.isNullOrBlank() && return
try { try {
val newColor = newText.toColorInt() val newColor = Color.parseColor(newText.toString())
currentColor = newColor currentColor = newColor
updateColor(false) updateColor(false)
} catch (_: IllegalArgumentException) { } catch (_: IllegalArgumentException) {

View file

@ -5,9 +5,8 @@ import java.util.Set;
import de.jrpie.android.launcher.R; import de.jrpie.android.launcher.R;
import de.jrpie.android.launcher.actions.lock.LockMethod; import de.jrpie.android.launcher.actions.lock.LockMethod;
import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetAppInfoPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.MapAppInfoStringPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer;
import de.jrpie.android.launcher.preferences.theme.Background; import de.jrpie.android.launcher.preferences.theme.Background;
import de.jrpie.android.launcher.preferences.theme.ColorTheme; import de.jrpie.android.launcher.preferences.theme.ColorTheme;
import de.jrpie.android.launcher.preferences.theme.Font; import de.jrpie.android.launcher.preferences.theme.Font;
@ -21,24 +20,18 @@ import eu.jonahbauer.android.preference.annotations.Preferences;
r = R.class, r = R.class,
value = { value = {
@PreferenceGroup(name = "internal", prefix = "settings_internal_", suffix = "_key", 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", type = boolean.class, defaultValue = "false"),
@Preference(name = "started_time", type = long.class), @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"), @Preference(name = "version_code", type = int.class, defaultValue = "-1"),
}), }),
@PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = { @PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = {
@Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class), @Preference(name = "favorites", type = Set.class, serializer = SetAppInfoPreferenceSerializer.class),
@Preference(name = "hidden", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class), @Preference(name = "hidden", type = Set.class, serializer = SetAppInfoPreferenceSerializer.class),
@Preference(name = "pinned_shortcuts", type = Set.class, serializer = SetPinnedShortcutInfoPreferenceSerializer.class), @Preference(name = "custom_names", type = HashMap.class, serializer = MapAppInfoStringPreferenceSerializer.class),
@Preference(name = "custom_names", type = HashMap.class, serializer = MapAbstractAppInfoStringPreferenceSerializer.class),
@Preference(name = "hide_bound_apps", type = boolean.class, defaultValue = "false"), @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 = { @PreferenceGroup(name = "list", prefix = "settings_list_", suffix = "_key", value = {
@Preference(name = "layout", type = ListLayout.class, defaultValue = "DEFAULT"), @Preference(name = "layout", type = ListLayout.class, defaultValue = "DEFAULT")
@Preference(name = "reverse_layout", type = boolean.class, defaultValue = "false")
}), }),
@PreferenceGroup(name = "gestures", prefix = "settings_gesture_", suffix = "_key", value = { @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 = { @PreferenceGroup(name = "display", prefix = "settings_display_", suffix = "_key", value = {
@Preference(name = "screen_timeout_disabled", type = boolean.class, defaultValue = "false"), @Preference(name = "screen_timeout_disabled", type = boolean.class, defaultValue = "false"),
@Preference(name = "hide_status_bar", type = boolean.class, defaultValue = "true"), @Preference(name = "full_screen", type = boolean.class, defaultValue = "true"),
@Preference(name = "hide_navigation_bar", type = boolean.class, defaultValue = "false"),
@Preference(name = "rotate_screen", type = boolean.class, defaultValue = "true"), @Preference(name = "rotate_screen", type = boolean.class, defaultValue = "true"),
}), }),
@PreferenceGroup(name = "functionality", prefix = "settings_functionality_", suffix = "_key", value = { @PreferenceGroup(name = "functionality", prefix = "settings_functionality_", suffix = "_key", value = {

View file

@ -1,7 +1,6 @@
package de.jrpie.android.launcher.preferences package de.jrpie.android.launcher.preferences
import android.content.Context import android.content.Context
import android.util.TypedValue
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -28,10 +27,8 @@ enum class ListLayout(
GRID( GRID(
{ c -> { c ->
val displayMetrics = c.resources.displayMetrics val displayMetrics = c.resources.displayMetrics
val widthColumnPx = val widthSp = displayMetrics.widthPixels / displayMetrics.scaledDensity
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 90f, displayMetrics) GridLayoutManager(c, (widthSp / 90).toInt())
val numColumns = (displayMetrics.widthPixels / widthColumnPx).toInt()
GridLayoutManager(c, numColumns)
}, },
R.layout.list_apps_row_variant_grid, R.layout.list_apps_row_variant_grid,
false false

View file

@ -5,12 +5,8 @@ import android.util.Log
import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.apps.AppInfo 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.apps.DetailedAppInfo
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion1 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.preferences.legacy.migratePreferencesFromVersionUnknown
import de.jrpie.android.launcher.ui.HomeActivity 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 * Increase when breaking changes are introduced and write an appropriate case in
* `migratePreferencesToNewVersion` * `migratePreferencesToNewVersion`
*/ */
const val PREFERENCE_VERSION = 4 const val PREFERENCE_VERSION = 2
const val UNKNOWN_PREFERENCE_VERSION = -1 const val UNKNOWN_PREFERENCE_VERSION = -1
private const val TAG = "Launcher - Preferences" private const val TAG = "Launcher - Preferences"
@ -36,20 +32,13 @@ fun migratePreferencesToNewVersion(context: Context) {
UNKNOWN_PREFERENCE_VERSION -> { /* still using the old preferences file */ UNKNOWN_PREFERENCE_VERSION -> { /* still using the old preferences file */
migratePreferencesFromVersionUnknown(context) migratePreferencesFromVersionUnknown(context)
Log.i(TAG, "migration of preferences complete (${UNKNOWN_PREFERENCE_VERSION} -> ${PREFERENCE_VERSION}).")
Log.i(TAG, "migration of preferences complete.")
} }
1 -> { 1 -> {
migratePreferencesFromVersion1() migratePreferencesFromVersion1()
Log.i(TAG, "migration of preferences complete (1 -> ${PREFERENCE_VERSION}).") Log.i(TAG, "migration of preferences complete.")
}
2 -> {
migratePreferencesFromVersion2()
Log.i(TAG, "migration of preferences complete (2 -> ${PREFERENCE_VERSION}).")
}
3 -> {
migratePreferencesFromVersion3()
Log.i(TAG, "migration of preferences complete (3 -> ${PREFERENCE_VERSION}).")
} }
else -> { else -> {
@ -73,16 +62,16 @@ fun resetPreferences(context: Context) {
LauncherPreferences.internal().versionCode(PREFERENCE_VERSION) LauncherPreferences.internal().versionCode(PREFERENCE_VERSION)
val hidden: MutableSet<AbstractAppInfo> = mutableSetOf() val hidden: MutableSet<AppInfo> = mutableSetOf()
val launcher = DetailedAppInfo.fromAppInfo( val launcher = DetailedAppInfo.fromAppInfo(
AppInfo( AppInfo(
BuildConfig.APPLICATION_ID, BuildConfig.APPLICATION_ID,
HomeActivity::class.java.name, HomeActivity::class.java.name,
INVALID_USER AppInfo.INVALID_USER
), context ), context
) )
launcher?.getRawInfo()?.let { hidden.add(it) } launcher?.app?.let { hidden.add(it) }
Log.i(TAG,"Hiding ${launcher?.getRawInfo()}") Log.i(TAG,"Hiding ${launcher?.app}")
LauncherPreferences.apps().hidden(hidden) LauncherPreferences.apps().hidden(hidden)
Action.resetToDefaultActions(context) Action.resetToDefaultActions(context)

View file

@ -5,27 +5,14 @@ import de.jrpie.android.launcher.actions.AppAction
import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.apps.AppInfo 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.LauncherPreferences
import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION 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.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject 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<AppInfo, String>?): Set<String>? {
return value?.map { (key, value) ->
Json.encodeToString(LegacyMapEntry(key, value))
}?.toSet()
}
val oldLauncherActionIds: Map<String, LauncherAction> = val oldLauncherActionIds: Map<String, LauncherAction> =
mapOf( mapOf(
@ -81,28 +68,28 @@ private fun Action.Companion.legacyFromPreference(id: String): Action? {
val actionId = preferences.getString("$id.app", "")!! val actionId = preferences.getString("$id.app", "")!!
var u: Int? = preferences.getInt( var u: Int? = preferences.getInt(
"$id.user", "$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) return Action.legacyFromId(actionId, u)
} }
private fun migrateAppInfoStringMap(key: String) { private fun migrateAppInfoStringMap(key: String) {
val preferences = LauncherPreferences.getSharedPreferences() val preferences = LauncherPreferences.getSharedPreferences()
serializeMapAppInfo( MapAppInfoStringPreferenceSerializer().serialize(
preferences.getStringSet(key, setOf())?.mapNotNull { entry -> preferences.getStringSet(key, setOf())?.mapNotNull { entry ->
try { try {
val obj = JSONObject(entry) val obj = JSONObject(entry);
val info = AppInfo.legacyDeserialize(obj.getString("key")) val info = AppInfo.legacyDeserialize(obj.getString("key"))
val value = obj.getString("value") val value = obj.getString("value");
Pair(info, value) Pair(info, value)
} catch (_: JSONException) { } catch (_: JSONException) {
null null
} }
}?.toMap(HashMap()) }?.toMap(HashMap())
)?.let { )?.let {
preferences.edit { putStringSet(key, it) } preferences.edit().putStringSet(key, it as Set<String>).apply()
} }
} }
@ -111,16 +98,16 @@ private fun migrateAppInfoSet(key: String) {
.map(AppInfo.Companion::legacyDeserialize) .map(AppInfo.Companion::legacyDeserialize)
.map(AppInfo::serialize) .map(AppInfo::serialize)
.toSet() .toSet()
.let { LauncherPreferences.getSharedPreferences().edit { putStringSet(key, it) } } .let { LauncherPreferences.getSharedPreferences().edit().putStringSet(key, it).apply() }
} }
private fun migrateAction(key: String) { private fun migrateAction(key: String) {
Action.legacyFromPreference(key)?.let { action -> Action.legacyFromPreference(key)?.let { action ->
LauncherPreferences.getSharedPreferences().edit { LauncherPreferences.getSharedPreferences().edit()
putString(key, Json.encodeToString(action)) .putString(key, Json.encodeToString(action))
.remove("$key.app") .remove("$key.app")
.remove("$key.user") .remove("$key.user")
} .apply()
} }
} }
@ -130,12 +117,11 @@ private fun migrateAction(key: String) {
* (see [PREFERENCE_VERSION]) * (see [PREFERENCE_VERSION])
*/ */
fun migratePreferencesFromVersion1() { fun migratePreferencesFromVersion1() {
assert(PREFERENCE_VERSION == 2)
assert(LauncherPreferences.internal().versionCode() == 1) assert(LauncherPreferences.internal().versionCode() == 1)
Gesture.entries.forEach { g -> migrateAction(g.id) } Gesture.entries.forEach { g -> migrateAction(g.id) }
migrateAppInfoSet(LauncherPreferences.apps().keys().hidden()) migrateAppInfoSet(LauncherPreferences.apps().keys().hidden())
migrateAppInfoSet(LauncherPreferences.apps().keys().favorites()) migrateAppInfoSet(LauncherPreferences.apps().keys().favorites())
migrateAppInfoStringMap(LauncherPreferences.apps().keys().customNames()) migrateAppInfoStringMap(LauncherPreferences.apps().keys().customNames())
LauncherPreferences.internal().versionCode(2) LauncherPreferences.internal().versionCode(2)
migratePreferencesFromVersion2()
} }

View file

@ -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()
}

View file

@ -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<String>?): Set<AppInfo>? {
return value?.map {
Json.decodeFromString<AppInfo>(it)
}?.toHashSet()
}
fun deserializeMap(value: Set<String>?): HashMap<AppInfo, String>? {
return value?.associateTo(HashMap()) {
val entry = Json.decodeFromString<MapEntry>(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<AbstractAppInfo>()
deserializeSet(preferences.getStringSet(key, null))?.let {
set.addAll(it)
}
@Suppress("UNCHECKED_CAST")
editor.putStringSet(
key,
serializer.serialize(set as java.util.Set<AbstractAppInfo>) as Set<String>?
)
} 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<AbstractAppInfo, String>()
deserializeMap(preferences.getStringSet(key, null))?.let {
map.putAll(it)
}
@Suppress("UNCHECKED_CAST")
editor.putStringSet(key, serializer.serialize(map) as Set<String>?)
} 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)
}

View file

@ -4,9 +4,10 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log import android.util.Log
import de.jrpie.android.launcher.preferences.LauncherPreferences 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.Background
import de.jrpie.android.launcher.preferences.theme.ColorTheme import de.jrpie.android.launcher.preferences.theme.ColorTheme
import androidx.core.content.edit
private fun migrateStringPreference( private fun migrateStringPreference(
@ -49,6 +50,7 @@ private const val TAG = "Preferences ? -> 1"
* and a different file was used. * and a different file was used.
*/ */
fun migratePreferencesFromVersionUnknown(context: Context) { fun migratePreferencesFromVersionUnknown(context: Context) {
assert(PREFERENCE_VERSION == 2)
Log.i( Log.i(
TAG, TAG,
@ -64,317 +66,318 @@ fun migratePreferencesFromVersionUnknown(context: Context) {
return return
} }
LauncherPreferences.getSharedPreferences().edit { val newPrefs = LauncherPreferences.getSharedPreferences().edit()
migrateBooleanPreference( migrateBooleanPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"startedBefore", "startedBefore",
"internal.started_before", "internal.started_before",
false false
) )
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_volumeUpApp", "action_volumeUpApp",
"action.volume_up.app", "action.volume_up.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_volumeUpApp_user", "action_volumeUpApp_user",
"action.volume_up.user", "action.volume_up.user",
-1 -1
) )
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_volumeDownApp", "action_volumeDownApp",
"action.volume_down.app", "action.volume_down.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_volumeDownApp_user", "action_volumeDownApp_user",
"action.volume_down.user", "action.volume_down.user",
-1 -1
) )
migrateStringPreference(oldPrefs, this, "action_timeApp", "action.time.app", "") migrateStringPreference(oldPrefs, newPrefs, "action_timeApp", "action.time.app", "")
migrateIntPreference(oldPrefs, this, "action_timeApp_user", "action.time.user", -1) migrateIntPreference(oldPrefs, newPrefs, "action_timeApp_user", "action.time.user", -1)
migrateStringPreference(oldPrefs, this, "action_dateApp", "action.date.app", "") migrateStringPreference(oldPrefs, newPrefs, "action_dateApp", "action.date.app", "")
migrateIntPreference(oldPrefs, this, "action_dateApp_user", "action.date.user", -1) migrateIntPreference(oldPrefs, newPrefs, "action_dateApp_user", "action.date.user", -1)
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_longClickApp", "action_longClickApp",
"action.long_click.app", "action.long_click.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_longClickApp_user", "action_longClickApp_user",
"action.long_click.user", "action.long_click.user",
-1 -1
) )
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_doubleClickApp", "action_doubleClickApp",
"action.double_click.app", "action.double_click.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_doubleClickApp_user", "action_doubleClickApp_user",
"action.double_click.user", "action.double_click.user",
-1 -1
) )
migrateStringPreference(oldPrefs, this, "action_upApp", "action.up.app", "") migrateStringPreference(oldPrefs, newPrefs, "action_upApp", "action.up.app", "")
migrateIntPreference(oldPrefs, this, "action_upApp_user", "action.up.user", -1) migrateIntPreference(oldPrefs, newPrefs, "action_upApp_user", "action.up.user", -1)
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_up_leftApp", "action_up_leftApp",
"action.up_left.app", "action.up_left.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_up_leftApp_user", "action_up_leftApp_user",
"action.up_left.user", "action.up_left.user",
-1 -1
) )
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_up_rightApp", "action_up_rightApp",
"action.up_right.app", "action.up_right.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_up_rightApp_user", "action_up_rightApp_user",
"action.up_right.user", "action.up_right.user",
-1 -1
) )
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_doubleUpApp", "action_doubleUpApp",
"action.double_up.app", "action.double_up.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_doubleUpApp_user", "action_doubleUpApp_user",
"action.double_up.user", "action.double_up.user",
-1 -1
) )
migrateStringPreference(oldPrefs, this, "action_downApp", "action.down.app", "") migrateStringPreference(oldPrefs, newPrefs, "action_downApp", "action.down.app", "")
migrateIntPreference(oldPrefs, this, "action_downApp_user", "action.down.user", -1) migrateIntPreference(oldPrefs, newPrefs, "action_downApp_user", "action.down.user", -1)
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_down_leftApp", "action_down_leftApp",
"action.down_left.app", "action.down_left.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_down_leftApp_user", "action_down_leftApp_user",
"action.down_left.user", "action.down_left.user",
-1 -1
) )
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_down_rightApp", "action_down_rightApp",
"action.down_right.app", "action.down_right.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_down_rightApp_user", "action_down_rightApp_user",
"action.down_right.user", "action.down_right.user",
-1 -1
) )
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_doubleDownApp", "action_doubleDownApp",
"action.double_down.app", "action.double_down.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_doubleDownApp_user", "action_doubleDownApp_user",
"action.double_down.user", "action.double_down.user",
-1 -1
) )
migrateStringPreference(oldPrefs, this, "action_leftApp", "action.left.app", "") migrateStringPreference(oldPrefs, newPrefs, "action_leftApp", "action.left.app", "")
migrateIntPreference(oldPrefs, this, "action_leftApp_user", "action.left.user", -1) migrateIntPreference(oldPrefs, newPrefs, "action_leftApp_user", "action.left.user", -1)
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_left_topApp", "action_left_topApp",
"action.left_top.app", "action.left_top.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_left_topApp_user", "action_left_topApp_user",
"action.left_top.user", "action.left_top.user",
-1 -1
) )
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_left_bottomApp", "action_left_bottomApp",
"action.left_bottom.app", "action.left_bottom.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_left_bottomApp_user", "action_left_bottomApp_user",
"action.left_bottom.user", "action.left_bottom.user",
-1 -1
) )
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_doubleLeftApp", "action_doubleLeftApp",
"action.double_left.app", "action.double_left.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_doubleLeftApp_user", "action_doubleLeftApp_user",
"action.double_left.user", "action.double_left.user",
-1 -1
) )
migrateStringPreference(oldPrefs, this, "action_rightApp", "action.right.app", "") migrateStringPreference(oldPrefs, newPrefs, "action_rightApp", "action.right.app", "")
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_rightApp_user", "action_rightApp_user",
"action.right.user", "action.right.user",
-1 -1
) )
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_right_topApp", "action_right_topApp",
"action.right_top.app", "action.right_top.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_right_topApp_user", "action_right_topApp_user",
"action.right_top.user", "action.right_top.user",
-1 -1
) )
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_right_bottomApp", "action_right_bottomApp",
"action.right_bottom.app", "action.right_bottom.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_right_bottomApp_user", "action_right_bottomApp_user",
"action.right_bottom.user", "action.right_bottom.user",
-1 -1
) )
migrateStringPreference( migrateStringPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_doubleRightApp", "action_doubleRightApp",
"action.double_right.app", "action.double_right.app",
"" ""
) )
migrateIntPreference( migrateIntPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"action_doubleRightApp_user", "action_doubleRightApp_user",
"action.double_right.user", "action.double_right.user",
-1 -1
) )
migrateBooleanPreference(oldPrefs, this, "timeVisible", "clock.time_visible", true) migrateBooleanPreference(oldPrefs, newPrefs, "timeVisible", "clock.time_visible", true)
migrateBooleanPreference(oldPrefs, this, "dateVisible", "clock.date_visible", true) migrateBooleanPreference(oldPrefs, newPrefs, "dateVisible", "clock.date_visible", true)
migrateBooleanPreference( migrateBooleanPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"dateLocalized", "dateLocalized",
"clock.date_localized", "clock.date_localized",
false false
) )
migrateBooleanPreference( migrateBooleanPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"dateTimeFlip", "dateTimeFlip",
"clock.date_time_flip", "clock.date_time_flip",
false false
) )
migrateBooleanPreference( migrateBooleanPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"disableTimeout", "disableTimeout",
"display.disable_timeout", "display.disable_timeout",
false false
) )
migrateBooleanPreference( migrateBooleanPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"useFullScreen", "useFullScreen",
"display.use_full_screen", "display.use_full_screen",
true true
) )
migrateBooleanPreference( migrateBooleanPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"enableDoubleActions", "enableDoubleActions",
"enabled_gestures.double_actions", "enabled_gestures.double_actions",
true true
) )
migrateBooleanPreference( migrateBooleanPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"enableEdgeActions", "enableEdgeActions",
"enabled_gestures.edge_actions", "enabled_gestures.edge_actions",
true true
) )
migrateBooleanPreference( migrateBooleanPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"searchAutoLaunch", "searchAutoLaunch",
"functionality.search_auto_launch", "functionality.search_auto_launch",
true true
) )
migrateBooleanPreference( migrateBooleanPreference(
oldPrefs, oldPrefs,
this, newPrefs,
"searchAutoKeyboard", "searchAutoKeyboard",
"functionality.search_auto_keyboard", "functionality.search_auto_keyboard",
true true
) )
}
newPrefs.apply()
when (oldPrefs.getString("theme", "finn")) { when (oldPrefs.getString("theme", "finn")) {
"finn" -> { "finn" -> {

View file

@ -2,8 +2,7 @@
package de.jrpie.android.launcher.preferences.serialization package de.jrpie.android.launcher.preferences.serialization
import de.jrpie.android.launcher.apps.AbstractAppInfo import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -11,61 +10,40 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
// Serializers for [LauncherPreference$Config]
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class SetAbstractAppInfoPreferenceSerializer : class SetAppInfoPreferenceSerializer :
PreferenceSerializer<java.util.Set<AbstractAppInfo>?, java.util.Set<java.lang.String>?> { PreferenceSerializer<java.util.Set<AppInfo>?, java.util.Set<java.lang.String>?> {
@Throws(PreferenceSerializationException::class) @Throws(PreferenceSerializationException::class)
override fun serialize(value: java.util.Set<AbstractAppInfo>?): java.util.Set<java.lang.String> { override fun serialize(value: java.util.Set<AppInfo>?): java.util.Set<java.lang.String>? {
return value?.map(AbstractAppInfo::serialize) return value?.map(AppInfo::serialize)?.toHashSet() as java.util.Set<java.lang.String>
?.toHashSet() as java.util.Set<java.lang.String>
} }
@Throws(PreferenceSerializationException::class) @Throws(PreferenceSerializationException::class)
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.Set<AbstractAppInfo>? { override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.Set<AppInfo>? {
return value?.map(java.lang.String::toString)?.map(AbstractAppInfo::deserialize) return value?.map (java.lang.String::toString)?.map(AppInfo::deserialize)?.toHashSet() as? java.util.Set<AppInfo>
?.toHashSet() as? java.util.Set<AbstractAppInfo>
} }
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class SetPinnedShortcutInfoPreferenceSerializer : class MapAppInfoStringPreferenceSerializer :
PreferenceSerializer<java.util.Set<PinnedShortcutInfo>?, java.util.Set<java.lang.String>?> { PreferenceSerializer<java.util.HashMap<AppInfo, String>?, java.util.Set<java.lang.String>?> {
@Throws(PreferenceSerializationException::class)
override fun serialize(value: java.util.Set<PinnedShortcutInfo>?): java.util.Set<java.lang.String> { @Serializable()
return value?.map { Json.encodeToString<PinnedShortcutInfo>(it) } private class MapEntry(val key: AppInfo, val value: String)
?.toHashSet() as java.util.Set<java.lang.String>
}
@Throws(PreferenceSerializationException::class) @Throws(PreferenceSerializationException::class)
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.Set<PinnedShortcutInfo>? { override fun serialize(value: java.util.HashMap<AppInfo, String>?): java.util.Set<java.lang.String>? {
return value?.map(java.lang.String::toString)
?.map { Json.decodeFromString<PinnedShortcutInfo>(it) }
?.toHashSet() as? java.util.Set<PinnedShortcutInfo>
}
}
@Suppress("UNCHECKED_CAST")
class MapAbstractAppInfoStringPreferenceSerializer :
PreferenceSerializer<java.util.HashMap<AbstractAppInfo, String>?, java.util.Set<java.lang.String>?> {
@Serializable
private class MapEntry(val key: AbstractAppInfo, val value: String)
@Throws(PreferenceSerializationException::class)
override fun serialize(value: java.util.HashMap<AbstractAppInfo, String>?): java.util.Set<java.lang.String>? {
return value?.map { (key, value) -> return value?.map { (key, value) ->
Json.encodeToString(MapEntry(key, value)) Json.encodeToString(MapEntry(key, value))
}?.toHashSet() as? java.util.Set<java.lang.String> }?.toHashSet() as? java.util.Set<java.lang.String>
} }
@Throws(PreferenceSerializationException::class) @Throws(PreferenceSerializationException::class)
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.HashMap<AbstractAppInfo, String>? { override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.HashMap<AppInfo, String>? {
return value?.associateTo(HashMap()) { return value?.associateTo(HashMap()) {
val entry = Json.decodeFromString<MapEntry>(it.toString()) val entry = Json.decodeFromString<MapEntry>(it.toString())
Pair(entry.key, entry.value) Pair(entry.key, entry.value)
} }
} }
} }

View file

@ -16,13 +16,7 @@ enum class Background(val id: Int, val dim: Boolean = false, val blur: Boolean =
; ;
fun applyToTheme(theme: Resources.Theme) { fun applyToTheme(theme: Resources.Theme) {
var background = this theme.applyStyle(id, true)
// force a solid background when using the light theme
if (LauncherPreferences.theme().colorTheme() == ColorTheme.LIGHT) {
background = SOLID
}
theme.applyStyle(background.id, true)
} }
fun applyToWindow(window: Window) { fun applyToWindow(window: Window) {

View file

@ -2,9 +2,10 @@ package de.jrpie.android.launcher.preferences.theme
import android.content.Context import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import com.google.android.material.color.DynamicColors
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import com.google.android.material.color.DynamicColors
@Suppress("unused")
enum class ColorTheme( enum class ColorTheme(
private val id: Int, private val id: Int,
private val labelResource: Int, private val labelResource: Int,
@ -25,7 +26,7 @@ enum class ColorTheme(
R.style.colorThemeLight, R.style.colorThemeLight,
R.string.settings_theme_color_theme_item_light, R.string.settings_theme_color_theme_item_light,
R.style.textShadowLight, R.style.textShadowLight,
{ true }), { false }),
DYNAMIC( DYNAMIC(
R.style.colorThemeDynamic, R.style.colorThemeDynamic,
R.string.settings_theme_color_theme_item_dynamic, R.string.settings_theme_color_theme_item_dynamic,

View file

@ -27,14 +27,10 @@ fun View.blink(
} }
// Taken from: https://stackoverflow.com/a/30340794/12787264 // Taken from: https://stackoverflow.com/a/30340794/12787264
fun ImageView.transformGrayscale(grayscale: Boolean) { fun ImageView.transformGrayscale() {
this.colorFilter = if (grayscale) { this.colorFilter = ColorMatrixColorFilter(ColorMatrix().apply {
ColorMatrixColorFilter(ColorMatrix().apply { setSaturation(0f)
setSaturation(0f) })
})
} else {
null
}
} }

View file

@ -2,15 +2,18 @@ package de.jrpie.android.launcher.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.os.AsyncTask
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics
import android.view.GestureDetector
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.ViewConfiguration
import android.window.OnBackInvokedDispatcher import android.window.OnBackInvokedDispatcher
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.Action 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.databinding.HomeBinding
import de.jrpie.android.launcher.openTutorial import de.jrpie.android.launcher.openTutorial
import de.jrpie.android.launcher.preferences.LauncherPreferences 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 de.jrpie.android.launcher.ui.tutorial.TutorialActivity
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale 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, * [HomeActivity] is the actual application Launcher,
@ -34,10 +48,10 @@ import java.util.Locale
* - Setting global variables (preferences etc.) * - Setting global variables (preferences etc.)
* - Opening the [TutorialActivity] on new installations * - Opening the [TutorialActivity] on new installations
*/ */
class HomeActivity : UIObject, AppCompatActivity() { class HomeActivity : UIObject, AppCompatActivity(),
GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
private lateinit var binding: HomeBinding private lateinit var binding: HomeBinding
private var touchGestureDetector: TouchGestureDetector? = null
private var sharedPreferencesListener = private var sharedPreferencesListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
@ -46,20 +60,24 @@ class HomeActivity : UIObject, AppCompatActivity() {
) { ) {
recreate() 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super<AppCompatActivity>.onCreate(savedInstanceState) super<AppCompatActivity>.onCreate(savedInstanceState)
super<UIObject>.onCreate() super<UIObject>.onCreate()
// Initialise layout // Initialise layout
binding = HomeBinding.inflate(layoutInflater) binding = HomeBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// Handle back key / gesture on Android 13+, cf. onKeyDown() // Handle back key / gesture on Android 13+, cf. onKeyDown()
@ -70,72 +88,46 @@ class HomeActivity : UIObject, AppCompatActivity() {
handleBack() handleBack()
} }
} }
binding.buttonFallbackSettings.setOnClickListener {
LauncherAction.SETTINGS.invoke(this)
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
touchGestureDetector?.updateScreenSize(windowManager)
} }
override fun onStart() { override fun onStart() {
super<AppCompatActivity>.onStart() super<AppCompatActivity>.onStart()
super<UIObject>.onStart() mDetector = GestureDetectorCompat(this, this)
mDetector.setOnDoubleTapListener(this)
// If the tutorial was not finished, start it super<UIObject>.onStart()
if (!LauncherPreferences.internal().started()) {
openTutorial(this)
}
LauncherPreferences.getSharedPreferences() LauncherPreferences.getSharedPreferences()
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener) .registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
} }
override fun onWindowFocusChanged(hasFocus: Boolean) { private fun updateClock() {
super.onWindowFocusChanged(hasFocus) clockTimer.cancel()
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() {
val locale = Locale.getDefault() val locale = Locale.getDefault()
val dateVisible = LauncherPreferences.clock().dateVisible() val dateVisible = LauncherPreferences.clock().dateVisible()
val timeVisible = LauncherPreferences.clock().timeVisible() val timeVisible = LauncherPreferences.clock().timeVisible()
var dateFMT = "yyyy-MM-dd" var dateFMT = "yyyy-MM-dd"
var timeFMT = "HH:mm" var timeFMT = "HH:mm"
val period = 100L
if (LauncherPreferences.clock().showSeconds()) { if (LauncherPreferences.clock().showSeconds()) {
timeFMT += ":ss" 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()) { if (LauncherPreferences.clock().localized()) {
dateFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, dateFMT) dateFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, dateFMT)
timeFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, timeFMT) timeFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, timeFMT)
} }
var upperFormat = dateFMT var upperFormat = SimpleDateFormat(dateFMT, locale)
var lowerFormat = timeFMT var lowerFormat = SimpleDateFormat(timeFMT, locale)
var upperVisible = dateVisible var upperVisible = dateVisible
var lowerVisible = timeVisible var lowerVisible = timeVisible
@ -150,10 +142,21 @@ class HomeActivity : UIObject, AppCompatActivity() {
binding.homeUpperView.setTextColor(LauncherPreferences.clock().color()) binding.homeUpperView.setTextColor(LauncherPreferences.clock().color())
binding.homeLowerView.setTextColor(LauncherPreferences.clock().color()) binding.homeLowerView.setTextColor(LauncherPreferences.clock().color())
binding.homeLowerView.format24Hour = lowerFormat
binding.homeUpperView.format24Hour = upperFormat clockTimer = fixedRateTimer("clockTimer", true, 0L, period) {
binding.homeLowerView.format12Hour = lowerFormat this@HomeActivity.runOnUiThread {
binding.homeUpperView.format12Hour = upperFormat 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 { override fun getTheme(): Resources.Theme {
@ -171,31 +174,14 @@ class HomeActivity : UIObject, AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
/* This should be initialized in onCreate() edgeWidth = LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
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)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { updateClock()
binding.root.setOnApplyWindowInsetsListener { _, windowInsets -> }
@Suppress("deprecation") // required to support API 29
val insets = windowInsets.systemGestureInsets
touchGestureDetector?.setSystemGestureInsets(insets)
windowInsets override fun onPause() {
} super.onPause()
} clockTimer.cancel()
initClock()
updateSettingsFallbackButtonVisibility()
} }
override fun onDestroy() { override fun onDestroy() {
@ -211,7 +197,6 @@ class HomeActivity : UIObject, AppCompatActivity() {
// Only used pre Android 13, cf. onBackInvokedDispatcher // Only used pre Android 13, cf. onBackInvokedDispatcher
handleBack() handleBack()
} }
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> {
if (Action.forGesture(Gesture.VOLUME_UP) == LauncherAction.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 // Let the OS handle the key event. This works better with some custom ROMs
@ -232,11 +217,97 @@ class HomeActivity : UIObject, AppCompatActivity() {
return true return true
} }
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onFling(e1: MotionEvent?, e2: MotionEvent, dX: Float, dY: Float): Boolean {
touchGestureDetector?.onTouchEvent(event)
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 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() { override fun setOnClicks() {
binding.homeUpperView.setOnClickListener { binding.homeUpperView.setOnClickListener {
@ -258,10 +329,22 @@ class HomeActivity : UIObject, AppCompatActivity() {
private fun handleBack() { private fun handleBack() {
Gesture.BACK(this) LauncherAction.CHOOSE.launch(this)
} }
override fun isHomeScreen(): Boolean { override fun isHomeScreen(): Boolean {
return true 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 }
} }

View file

@ -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<AppCompatActivity>.onCreate(savedInstanceState)
super<UIObject>.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<RecyclerView>(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<AppCompatActivity>.onStart()
super<UIObject>.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<GestureRecyclerAdapter.ViewHolder>() {
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
}
}
}

View file

@ -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<Int, PointerPath>()
/* 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..<event.pointerCount).associateBy { event.getPointerId(it) }
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
synchronized(this@TouchGestureDetector) {
paths = HashMap()
cancelled = false
}
longPressHandler.postDelayed({
synchronized(this@TouchGestureDetector) {
if (cancelled) {
return@postDelayed
}
if (paths.entries.size == 1 && paths.entries.firstOrNull()?.value?.isTap() == true) {
cancelled = true
Gesture.LONG_CLICK.invoke(context)
}
}
}, LONG_PRESS_TIMEOUT.toLong())
}
// add new pointers
for (i in 0..<event.pointerCount) {
if (paths.containsKey(event.getPointerId(i))) {
continue
}
val index = pointerIdToIndex[i] ?: continue
paths[i] = PointerPath(
paths.entries.size,
Vector(event.getX(index), event.getY(index))
)
}
for (i in 0..<event.pointerCount) {
val index = pointerIdToIndex[i] ?: continue
for (j in 0..<event.historySize) {
paths[i]?.update(
Vector(
event.getHistoricalX(index, j),
event.getHistoricalY(index, j)
)
)
}
paths[i]?.update(Vector(event.getX(index), event.getY(index)))
}
if (event.actionMasked == MotionEvent.ACTION_UP) {
synchronized(this@TouchGestureDetector) {
// if the long press handler is still running, kill it
longPressHandler.removeCallbacksAndMessages(null)
// if the gesture was already detected as a long click, there is nothing to do
if (cancelled) {
return
}
}
classifyPaths(paths, event.downTime, event.eventTime)
}
return
}
private fun getGestureForDirection(direction: Vector): Gesture? {
return if (ANGULAR_THRESHOLD * abs(direction.x) > 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<Int, PointerPath>, 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
}
}

View file

@ -3,11 +3,7 @@ package de.jrpie.android.launcher.ui
import android.app.Activity import android.app.Activity
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Resources import android.content.res.Resources
import android.os.Build
import android.view.View
import android.view.Window import android.view.Window
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager import android.view.WindowManager
import de.jrpie.android.launcher.preferences.LauncherPreferences 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. * 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. * 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) { fun setWindowFlags(window: Window, homeScreen: Boolean) {
window.setFlags(0, 0) // clear flags window.setFlags(0, 0) // clear flags
// Display notification bar // Display notification bar
if (LauncherPreferences.display().hideStatusBar()) if (LauncherPreferences.display().fullScreen())
window.setFlags( window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN WindowManager.LayoutParams.FLAG_FULLSCREEN
@ -42,19 +36,17 @@ fun setWindowFlags(window: Window, homeScreen: Boolean) {
} }
interface UIObject { interface UIObject {
fun onCreate() { fun onCreate() {
if (this !is Activity) { if (this is Activity) {
return setWindowFlags(window, isHomeScreen())
}
setWindowFlags(window, isHomeScreen()) if (!LauncherPreferences.display().rotateScreen()) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
}
if (!LauncherPreferences.display().rotateScreen()) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
} }
} }
fun onStart() { fun onStart() {
setOnClicks() setOnClicks()
adjustLayout() adjustLayout()
@ -78,26 +70,4 @@ interface UIObject {
fun isHomeScreen(): Boolean { fun isHomeScreen(): Boolean {
return false 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)
}
}
} }

View file

@ -1,23 +1,25 @@
package de.jrpie.android.launcher.ui.list 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.content.res.Resources
import android.graphics.Rect import android.graphics.Rect
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast
import android.window.OnBackInvokedDispatcher import android.window.OnBackInvokedDispatcher
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter 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.R
import de.jrpie.android.launcher.REQUEST_UNINSTALL
import de.jrpie.android.launcher.actions.LauncherAction import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.apps.AppFilter 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.databinding.ListBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.UIObject 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 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: * The [ListActivity] is the most general purpose activity in Launcher:
* - used to view all apps and edit their settings * - 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 { class ListActivity : AppCompatActivity(), UIObject {
private lateinit var binding: ListBinding 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) { enum class ListActivityIntention(val titleResource: Int) {
@ -108,15 +68,10 @@ class ListActivity : AppCompatActivity(), UIObject {
?.let { ListActivityIntention.valueOf(it) } ?.let { ListActivityIntention.valueOf(it) }
?: ListActivityIntention.VIEW ?: ListActivityIntention.VIEW
@Suppress("deprecation") // required to support API level < 33
favoritesVisibility = bundle.getSerializable("favoritesVisibility") favoritesVisibility = bundle.getSerializable("favoritesVisibility")
as? AppFilter.Companion.AppSetVisibility ?: 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") hiddenVisibility = bundle.getSerializable("hiddenVisibility")
as? AppFilter.Companion.AppSetVisibility ?: hiddenVisibility as? AppFilter.Companion.AppSetVisibility ?: favoritesVisibility
if (intention != ListActivityIntention.VIEW) if (intention != ListActivityIntention.VIEW)
forGesture = bundle.getString("forGesture") 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. // android:windowSoftInputMode="adjustResize" doesn't work in full screen.
// workaround from https://stackoverflow.com/a/57623505 // workaround from https://stackoverflow.com/a/57623505
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
@ -153,7 +97,7 @@ class ListActivity : AppCompatActivity(), UIObject {
binding.listContainer.context.resources.displayMetrics.heightPixels binding.listContainer.context.resources.displayMetrics.heightPixels
val diff = height - r.bottom val diff = height - r.bottom
if (diff != 0 && if (diff != 0 &&
LauncherPreferences.display().hideStatusBar() LauncherPreferences.display().fullScreen()
) { ) {
if (binding.listContainer.paddingBottom != diff) { if (binding.listContainer.paddingBottom != diff) {
binding.listContainer.setPadding(0, 0, 0, diff) binding.listContainer.setPadding(0, 0, 0, diff)
@ -181,19 +125,30 @@ class ListActivity : AppCompatActivity(), UIObject {
finish() 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() { fun updateTitle() {
var titleResource = intention.titleResource var titleResource = intention.titleResource
if (intention == ListActivityIntention.VIEW) { if (intention == ListActivityIntention.VIEW) {
titleResource = titleResource = if (hiddenVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
if (hiddenVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { R.string.list_title_hidden
R.string.list_title_hidden } else if (favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
} else if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { R.string.list_title_favorite
R.string.list_title_private_space } else {
} else if (favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { R.string.list_title_view
R.string.list_title_favorite }
} else {
R.string.list_title_view
}
} }
binding.listHeading.text = getString(titleResource) binding.listHeading.text = getString(titleResource)
@ -206,12 +161,6 @@ class ListActivity : AppCompatActivity(), UIObject {
override fun setOnClicks() { override fun setOnClicks() {
binding.listClose.setOnClickListener { finish() } binding.listClose.setOnClickListener { finish() }
binding.listLock.setOnClickListener {
togglePrivateSpaceLock(this)
if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
finish()
}
}
} }
override fun adjustLayout() { override fun adjustLayout() {
@ -223,11 +172,11 @@ class ListActivity : AppCompatActivity(), UIObject {
updateTitle() updateTitle()
val sectionsPagerAdapter = ListSectionsPagerAdapter(this) val sectionsPagerAdapter = ListSectionsPagerAdapter(this, supportFragmentManager)
binding.listViewpager.let { val viewPager: ViewPager = findViewById(R.id.list_viewpager)
it.adapter = sectionsPagerAdapter viewPager.adapter = sectionsPagerAdapter
binding.listTabs.setupWithViewPager(it) 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, * The [ListSectionsPagerAdapter] returns the fragment,
* which corresponds to the selected tab in [ListActivity]. * 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 context: Context, fm: FragmentManager) :
class ListSectionsPagerAdapter(private val activity: ListActivity) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
FragmentPagerAdapter(activity.supportFragmentManager) {
override fun getItem(position: Int): Fragment { override fun getItem(position: Int): Fragment {
return when (position) { return when (position) {
@ -258,11 +201,11 @@ class ListSectionsPagerAdapter(private val activity: ListActivity) :
} }
override fun getPageTitle(position: Int): CharSequence { override fun getPageTitle(position: Int): CharSequence {
return activity.resources.getString(TAB_TITLES[position]) return context.resources.getString(TAB_TITLES[position])
} }
override fun getCount(): Int { override fun getCount(): Int {
return when (activity.intention) { return when (intention) {
ListActivity.ListActivityIntention.VIEW -> 1 ListActivity.ListActivityIntention.VIEW -> 1
else -> 2 else -> 2
} }

View file

@ -2,6 +2,7 @@ package de.jrpie.android.launcher.ui.list.apps
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -14,12 +15,12 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.AppAction
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
import de.jrpie.android.launcher.apps.AppFilter import de.jrpie.android.launcher.apps.AppFilter
import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.DetailedAppInfo 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.LauncherPreferences
import de.jrpie.android.launcher.preferences.ListLayout import de.jrpie.android.launcher.preferences.ListLayout
import de.jrpie.android.launcher.ui.list.ListActivity import de.jrpie.android.launcher.ui.list.ListActivity
@ -46,8 +47,7 @@ class AppsRecyclerAdapter(
RecyclerView.Adapter<AppsRecyclerAdapter.ViewHolder>() { RecyclerView.Adapter<AppsRecyclerAdapter.ViewHolder>() {
private val apps = (activity.applicationContext as Application).apps private val apps = (activity.applicationContext as Application).apps
private val appsListDisplayed: MutableList<AbstractDetailedAppInfo> = mutableListOf() private val appsListDisplayed: MutableList<DetailedAppInfo> = mutableListOf()
private val grayscale = LauncherPreferences.theme().monochromeIcons()
// temporarily disable auto launch // temporarily disable auto launch
var disableAutoLaunch: Boolean = false var disableAutoLaunch: Boolean = false
@ -68,7 +68,7 @@ class AppsRecyclerAdapter(
override fun onClick(v: View) { override fun onClick(v: View) {
val rect = Rect() val rect = Rect()
img.getGlobalVisibleRect(rect) img.getGlobalVisibleRect(rect)
selectItem(bindingAdapterPosition, rect) selectItem(adapterPosition, rect)
} }
init { init {
@ -80,19 +80,20 @@ class AppsRecyclerAdapter(
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
var appLabel = appsListDisplayed[i].getCustomLabel(activity) 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) { if (layout.useBadgedText) {
appLabel = activity.packageManager.getUserBadgedLabel( appLabel = activity.packageManager.getUserBadgedLabel(
appLabel, appLabel,
appsListDisplayed[i].getUser(activity) getUserFromId(appsListDisplayed[i].app.user, activity)
).toString() ).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 // decide when to show the options popup menu about
if (intention == ListActivity.ListActivityIntention.VIEW) { if (intention == ListActivity.ListActivityIntention.VIEW) {
@ -117,26 +118,22 @@ class AppsRecyclerAdapter(
@Suppress("SameReturnValue") @Suppress("SameReturnValue")
private fun showOptionsPopup( private fun showOptionsPopup(
viewHolder: ViewHolder, viewHolder: ViewHolder,
appInfo: AbstractDetailedAppInfo appInfo: DetailedAppInfo
): Boolean { ): Boolean {
//create the popup menu //create the popup menu
val popup = PopupMenu(activity, viewHolder.img) val popup = PopupMenu(activity, viewHolder.img)
popup.inflate(R.menu.menu_app) popup.inflate(R.menu.menu_app)
if (!appInfo.isRemovable()) { if (appInfo.isSystemApp) {
popup.menu.findItem(R.id.app_menu_delete).setVisible(false) popup.menu.findItem(R.id.app_menu_delete).setVisible(false)
} }
if (appInfo !is DetailedAppInfo) { if (LauncherPreferences.apps().hidden()?.contains(appInfo.app) == true) {
popup.menu.findItem(R.id.app_menu_info).setVisible(false)
}
if (LauncherPreferences.apps().hidden()?.contains(appInfo.getRawInfo()) == true) {
popup.menu.findItem(R.id.app_menu_hidden).setTitle(R.string.list_app_hidden_remove) 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) popup.menu.findItem(R.id.app_menu_favorite).setTitle(R.string.list_app_favorite_remove)
} }
@ -144,19 +141,19 @@ class AppsRecyclerAdapter(
popup.setOnMenuItemClickListener { popup.setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.app_menu_delete -> { R.id.app_menu_delete -> {
appInfo.getRawInfo().uninstall(activity); true appInfo.app.uninstall(activity); true
} }
R.id.app_menu_info -> { R.id.app_menu_info -> {
(appInfo.getRawInfo() as? AppInfo)?.openSettings(activity); true appInfo.app.openSettings(activity); true
} }
R.id.app_menu_favorite -> { R.id.app_menu_favorite -> {
appInfo.getRawInfo().toggleFavorite(); true appInfo.app.toggleFavorite(); true
} }
R.id.app_menu_hidden -> { R.id.app_menu_hidden -> {
appInfo.getRawInfo().toggleHidden(root); true appInfo.app.toggleHidden(root); true
} }
R.id.app_menu_rename -> { R.id.app_menu_rename -> {
@ -191,14 +188,15 @@ class AppsRecyclerAdapter(
val appInfo = appsListDisplayed[pos] val appInfo = appsListDisplayed[pos]
when (intention) { when (intention) {
ListActivity.ListActivityIntention.VIEW -> { ListActivity.ListActivityIntention.VIEW -> {
appInfo.getAction().invoke(activity, rect) AppAction(appInfo.app).invoke(activity, rect)
} }
ListActivity.ListActivityIntention.PICK -> { ListActivity.ListActivityIntention.PICK -> {
val returnIntent = Intent()
AppAction(appInfo.app).writeToIntent(returnIntent)
returnIntent.putExtra("forGesture", forGesture)
activity.setResult(REQUEST_CHOOSE_APP, returnIntent)
activity.finish() activity.finish()
forGesture ?: return
val gesture = Gesture.byId(forGesture) ?: return
Action.setActionForGesture(gesture, appInfo.getAction())
} }
} }
} }
@ -213,8 +211,8 @@ class AppsRecyclerAdapter(
&& !disableAutoLaunch && !disableAutoLaunch
&& LauncherPreferences.functionality().searchAutoLaunch() && LauncherPreferences.functionality().searchAutoLaunch()
) { ) {
val app = appsListDisplayed[0] val info = appsListDisplayed[0]
app.getAction().invoke(activity) AppAction(info.app).invoke(activity)
val inputMethodManager = val inputMethodManager =
activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager

View file

@ -1,11 +1,11 @@
package de.jrpie.android.launcher.ui.list.apps package de.jrpie.android.launcher.ui.list.apps
import android.app.Activity
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
@ -13,13 +13,11 @@ import android.widget.EditText
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import de.jrpie.android.launcher.R 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.AppInfo
import de.jrpie.android.launcher.apps.AbstractAppInfo import de.jrpie.android.launcher.apps.DetailedAppInfo
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
import de.jrpie.android.launcher.getUserFromId import de.jrpie.android.launcher.getUserFromId
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import androidx.core.net.toUri
private const val LOG_TAG = "AppContextMenu" private const val LOG_TAG = "AppContextMenu"
@ -34,29 +32,27 @@ fun AppInfo.openSettings(
} }
} }
fun AbstractAppInfo.uninstall(activity: Activity) { fun AppInfo.uninstall(activity: android.app.Activity) {
if (this is AppInfo) { val packageName = this.packageName.toString()
val packageName = this.packageName val userId = this.user
val userId = this.user
Log.i(LOG_TAG, "uninstalling $this") Log.i(LOG_TAG, "uninstalling $this")
val intent = Intent(Intent.ACTION_DELETE) val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE)
intent.data = "package:$packageName".toUri() intent.data = Uri.parse("package:$packageName")
getUserFromId(userId, activity).let { user -> getUserFromId(userId, activity).let { user ->
intent.putExtra(Intent.EXTRA_USER, 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)
} }
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
activity.startActivityForResult(
intent,
REQUEST_UNINSTALL
)
} }
fun AbstractAppInfo.toggleFavorite() { fun AppInfo.toggleFavorite() {
val favorites: MutableSet<AbstractAppInfo> = val favorites: MutableSet<AppInfo> =
LauncherPreferences.apps().favorites() ?: mutableSetOf() LauncherPreferences.apps().favorites() ?: mutableSetOf()
if (favorites.contains(this)) { if (favorites.contains(this)) {
@ -73,8 +69,8 @@ fun AbstractAppInfo.toggleFavorite() {
/** /**
* @param view: used to show a snackbar letting the user undo the action * @param view: used to show a snackbar letting the user undo the action
*/ */
fun AbstractAppInfo.toggleHidden(view: View) { fun AppInfo.toggleHidden(view: View) {
val hidden: MutableSet<AbstractAppInfo> = val hidden: MutableSet<AppInfo> =
LauncherPreferences.apps().hidden() ?: mutableSetOf() LauncherPreferences.apps().hidden() ?: mutableSetOf()
if (hidden.contains(this)) { if (hidden.contains(this)) {
hidden.remove(this) hidden.remove(this)
@ -91,12 +87,12 @@ fun AbstractAppInfo.toggleHidden(view: View) {
LauncherPreferences.apps().hidden(hidden) LauncherPreferences.apps().hidden(hidden)
} }
fun AbstractDetailedAppInfo.showRenameDialog(context: Context) { fun DetailedAppInfo.showRenameDialog(context: Context) {
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { 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) setView(R.layout.dialog_rename_app)
setNegativeButton(android.R.string.cancel) { d, _ -> d.cancel() } setNegativeButton(R.string.dialog_cancel) { d, _ -> d.cancel() }
setPositiveButton(android.R.string.ok) { d, _ -> setPositiveButton(R.string.dialog_rename_ok) { d, _ ->
setCustomLabel( setCustomLabel(
(d as? AlertDialog) (d as? AlertDialog)
?.findViewById<EditText>(R.id.dialog_rename_app_edit_text) ?.findViewById<EditText>(R.id.dialog_rename_app_edit_text)
@ -106,7 +102,7 @@ fun AbstractDetailedAppInfo.showRenameDialog(context: Context) {
}.create().also { it.show() }.apply { }.create().also { it.show() }.apply {
val input = findViewById<EditText>(R.id.dialog_rename_app_edit_text) val input = findViewById<EditText>(R.id.dialog_rename_app_edit_text)
input?.setText(getCustomLabel(context)) input?.setText(getCustomLabel(context))
input?.hint = getLabel() input?.hint = label
} }
} }

View file

@ -1,22 +1,22 @@
package de.jrpie.android.launcher.ui.list.apps package de.jrpie.android.launcher.ui.list.apps
import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment 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.R
import de.jrpie.android.launcher.apps.AppFilter import de.jrpie.android.launcher.apps.AppFilter
import de.jrpie.android.launcher.databinding.ListAppsBinding import de.jrpie.android.launcher.databinding.ListAppsBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.ui.list.ListActivity 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 import de.jrpie.android.launcher.ui.openSoftKeyboard
@ -49,7 +49,7 @@ class ListFragmentApps : Fragment(), UIObject {
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener) .registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
binding.listAppsCheckBoxFavorites.isChecked = binding.listAppsCheckBoxFavorites.isChecked =
((activity as? ListActivity)?.favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) (favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE)
} }
override fun onStop() { override fun onStop() {
@ -62,33 +62,24 @@ class ListFragmentApps : Fragment(), UIObject {
override fun setOnClicks() {} override fun setOnClicks() {}
override fun adjustLayout() { override fun adjustLayout() {
val listActivity = activity as? ListActivity ?: return
appsRecyclerAdapter = appsRecyclerAdapter =
AppsRecyclerAdapter( AppsRecyclerAdapter(
listActivity, binding.root, listActivity.intention, listActivity.forGesture, requireActivity(), binding.root, intention, forGesture,
appFilter = AppFilter( appFilter = AppFilter(
requireContext(), requireContext(),
"", "",
favoritesVisibility = listActivity.favoritesVisibility, favoritesVisibility = favoritesVisibility,
privateSpaceVisibility = listActivity.privateSpaceVisibility, hiddenVisibility = hiddenVisibility
hiddenVisibility = listActivity.hiddenVisibility
), ),
layout = LauncherPreferences.list().layout() layout = LauncherPreferences.list().layout()
) )
// set up the list / recycler // set up the list / recycler
binding.listAppsRview.apply { binding.listAppsRview.apply {
// improve performance (since content changes don't change the layout size) // improve performance (since content changes don't change the layout size)
setHasFixedSize(true) setHasFixedSize(true)
layoutManager = LauncherPreferences.list().layout().layoutManager(context) layoutManager = LauncherPreferences.list().layout().layoutManager(context)
.also {
if (LauncherPreferences.list().reverseLayout()) {
(it as? LinearLayoutManager)?.reverseLayout = true
(it as? GridLayoutManager)?.reverseLayout = true
}
}
adapter = appsRecyclerAdapter adapter = appsRecyclerAdapter
} }
@ -100,16 +91,7 @@ class ListFragmentApps : Fragment(), UIObject {
if (LauncherPreferences.functionality().searchWeb()) { if (LauncherPreferences.functionality().searchWeb()) {
val i = Intent(Intent.ACTION_WEB_SEARCH).putExtra("query", query) val i = Intent(Intent.ACTION_WEB_SEARCH).putExtra("query", query)
try { activity?.startActivity(i)
activity?.startActivity(i)
} catch (_: ActivityNotFoundException) {
Toast.makeText(
requireContext(),
R.string.toast_activity_not_found_search_web,
Toast.LENGTH_LONG
).show()
}
} else { } else {
appsRecyclerAdapter.selectItem(0) appsRecyclerAdapter.selectItem(0)
} }
@ -120,8 +102,7 @@ class ListFragmentApps : Fragment(), UIObject {
if (newText == " " && if (newText == " " &&
!appsRecyclerAdapter.disableAutoLaunch && !appsRecyclerAdapter.disableAutoLaunch &&
(activity as? ListActivity)?.intention intention == ListActivity.ListActivityIntention.VIEW &&
== ListActivity.ListActivityIntention.VIEW &&
LauncherPreferences.functionality().searchAutoLaunch() LauncherPreferences.functionality().searchAutoLaunch()
) { ) {
appsRecyclerAdapter.disableAutoLaunch = true appsRecyclerAdapter.disableAutoLaunch = true
@ -138,17 +119,17 @@ class ListFragmentApps : Fragment(), UIObject {
}) })
binding.listAppsCheckBoxFavorites.setOnClickListener { binding.listAppsCheckBoxFavorites.setOnClickListener {
listActivity.favoritesVisibility = favoritesVisibility =
if (binding.listAppsCheckBoxFavorites.isChecked) { if (binding.listAppsCheckBoxFavorites.isChecked) {
AppFilter.Companion.AppSetVisibility.EXCLUSIVE AppFilter.Companion.AppSetVisibility.EXCLUSIVE
} else { } else {
AppFilter.Companion.AppSetVisibility.VISIBLE AppFilter.Companion.AppSetVisibility.VISIBLE
} }
appsRecyclerAdapter.setFavoritesVisibility(listActivity.favoritesVisibility) appsRecyclerAdapter.setFavoritesVisibility(favoritesVisibility)
(activity as? ListActivity)?.updateTitle() (activity as? ListActivity)?.updateTitle()
} }
if (listActivity.intention == ListActivity.ListActivityIntention.VIEW if (intention == ListActivity.ListActivityIntention.VIEW
&& LauncherPreferences.functionality().searchAutoOpenKeyboard() && LauncherPreferences.functionality().searchAutoOpenKeyboard()
) { ) {
binding.listAppsSearchview.openSoftKeyboard(requireContext()) binding.listAppsSearchview.openSoftKeyboard(requireContext())

View file

@ -1,6 +1,7 @@
package de.jrpie.android.launcher.ui.list.other package de.jrpie.android.launcher.ui.list.other
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -8,10 +9,9 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction 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, * 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) : class OtherRecyclerAdapter(val activity: Activity) :
RecyclerView.Adapter<OtherRecyclerAdapter.ViewHolder>() { RecyclerView.Adapter<OtherRecyclerAdapter.ViewHolder>() {
private val othersList: Array<LauncherAction> = private val othersList: Array<LauncherAction> = LauncherAction.values()
LauncherAction.entries.filter { it.isAvailable(activity) }.toTypedArray()
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener { View.OnClickListener {
@ -33,13 +32,10 @@ class OtherRecyclerAdapter(val activity: Activity) :
override fun onClick(v: View) { override fun onClick(v: View) {
val pos = bindingAdapterPosition val pos = adapterPosition
val content = othersList[pos] val content = othersList[pos]
activity.finish() forGesture?.let { returnChoiceIntent(it, content) }
val gestureId = (activity as? ListActivity)?.forGesture ?: return
val gesture = Gesture.byId(gestureId) ?: return
Action.setActionForGesture(gesture, content)
} }
init { init {
@ -64,4 +60,12 @@ class OtherRecyclerAdapter(val activity: Activity) :
val view: View = inflater.inflate(R.layout.list_other_row, parent, false) val view: View = inflater.inflate(R.layout.list_other_row, parent, false)
return ViewHolder(view) 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()
}
} }

View file

@ -1,5 +1,6 @@
package de.jrpie.android.launcher.ui.settings package de.jrpie.android.launcher.ui.settings
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources import android.content.res.Resources
@ -7,14 +8,15 @@ import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.fragment.app.FragmentPagerAdapter
import com.google.android.material.tabs.TabLayoutMediator import androidx.viewpager.widget.ViewPager
import com.google.android.material.tabs.TabLayout
import de.jrpie.android.launcher.R 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.databinding.SettingsBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.theme.Background import de.jrpie.android.launcher.saveListActivityChoice
import de.jrpie.android.launcher.preferences.theme.ColorTheme
import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.ui.settings.actions.SettingsFragmentActions import de.jrpie.android.launcher.ui.settings.actions.SettingsFragmentActions
import de.jrpie.android.launcher.ui.settings.launcher.SettingsFragmentLauncher 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 { class SettingsActivity : AppCompatActivity(), UIObject {
private val solidBackground = LauncherPreferences.theme().background() == Background.SOLID private var sharedPreferencesListener =
|| LauncherPreferences.theme().colorTheme() == ColorTheme.LIGHT
private val sharedPreferencesListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
if (solidBackground && if (prefKey?.startsWith("theme.") == true ||
(prefKey == LauncherPreferences.theme().keys().background() || prefKey?.startsWith("display.") == true
prefKey == LauncherPreferences.theme().keys().colorTheme())
) { ) {
// Switching from solid background to a transparent background using `recreate()` 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()
}
} }
private lateinit var binding: SettingsBinding private lateinit var binding: SettingsBinding
@ -67,14 +53,12 @@ class SettingsActivity : AppCompatActivity(), UIObject {
setContentView(binding.root) setContentView(binding.root)
// set up tabs and swiping in settings // set up tabs and swiping in settings
val sectionsPagerAdapter = SettingsSectionsPagerAdapter(this) val sectionsPagerAdapter = SettingsSectionsPagerAdapter(this, supportFragmentManager)
binding.settingsViewpager.apply { val viewPager: ViewPager = findViewById(R.id.settings_viewpager)
adapter = sectionsPagerAdapter viewPager.adapter = sectionsPagerAdapter
setCurrentItem(intent.getIntExtra(EXTRA_TAB, 0), false)
} val tabs: TabLayout = findViewById(R.id.settings_tabs)
TabLayoutMediator(binding.settingsTabs, binding.settingsViewpager) { tab, position -> tabs.setupWithViewPager(viewPager)
tab.text = sectionsPagerAdapter.getPageTitle(position)
}.attach()
} }
override fun onStart() { override fun onStart() {
@ -103,21 +87,24 @@ class SettingsActivity : AppCompatActivity(), UIObject {
} }
} }
companion object { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
private const val EXTRA_TAB = "tab" when (requestCode) {
REQUEST_CHOOSE_APP -> saveListActivityChoice(data)
else -> super.onActivityResult(requestCode, resultCode, data)
}
} }
} }
private val TAB_TITLES = arrayOf( private val TAB_TITLES = arrayOf(
R.string.settings_tab_actions, R.string.settings_tab_app,
R.string.settings_tab_launcher, R.string.settings_tab_launcher,
R.string.settings_tab_meta R.string.settings_tab_meta
) )
class SettingsSectionsPagerAdapter(private val activity: FragmentActivity) : class SettingsSectionsPagerAdapter(private val context: Context, fm: FragmentManager) :
FragmentStateAdapter(activity) { FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun createFragment(position: Int): Fragment { override fun getItem(position: Int): Fragment {
return when (position) { return when (position) {
0 -> SettingsFragmentActions() 0 -> SettingsFragmentActions()
1 -> SettingsFragmentLauncher() 1 -> SettingsFragmentLauncher()
@ -126,11 +113,11 @@ class SettingsSectionsPagerAdapter(private val activity: FragmentActivity) :
} }
} }
fun getPageTitle(position: Int): CharSequence { override fun getPageTitle(position: Int): CharSequence {
return activity.resources.getString(TAB_TITLES[position]) return context.resources.getString(TAB_TITLES[position])
} }
override fun getItemCount(): Int { override fun getCount(): Int {
return 3 return 3
} }
} }

View file

@ -11,11 +11,11 @@ import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import de.jrpie.android.launcher.R 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.Action
import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.apps.AppFilter import de.jrpie.android.launcher.apps.AppFilter
@ -94,8 +94,6 @@ class SettingsFragmentActionsRecycler : Fragment(), UIObject {
class ActionsRecyclerAdapter(val activity: Activity) : class ActionsRecyclerAdapter(val activity: Activity) :
RecyclerView.Adapter<ActionsRecyclerAdapter.ViewHolder>() { RecyclerView.Adapter<ActionsRecyclerAdapter.ViewHolder>() {
private val drawableUnknown = AppCompatResources.getDrawable(activity, R.drawable.baseline_question_mark_24)
private val gesturesList: ArrayList<Gesture> = private val gesturesList: ArrayList<Gesture> =
Gesture.entries.filter(Gesture::isEnabled) as ArrayList<Gesture> Gesture.entries.filter(Gesture::isEnabled) as ArrayList<Gesture>
@ -117,18 +115,15 @@ class ActionsRecyclerAdapter(val activity: Activity) :
private fun updateViewHolder(gesture: Gesture, viewHolder: ViewHolder) { private fun updateViewHolder(gesture: Gesture, viewHolder: ViewHolder) {
val action = Action.forGesture(gesture) val action = Action.forGesture(gesture)
val drawable = action?.getIcon(activity)
if (action == null) { if (action == null || drawable == null) {
viewHolder.img.visibility = View.INVISIBLE viewHolder.img.visibility = View.INVISIBLE
viewHolder.removeAction.visibility = View.GONE viewHolder.removeAction.visibility = View.GONE
viewHolder.chooseButton.visibility = View.VISIBLE viewHolder.chooseButton.visibility = View.VISIBLE
return 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.img.visibility = View.VISIBLE
viewHolder.removeAction.visibility = View.VISIBLE viewHolder.removeAction.visibility = View.VISIBLE
viewHolder.chooseButton.visibility = View.INVISIBLE viewHolder.chooseButton.visibility = View.INVISIBLE
@ -142,7 +137,9 @@ class ActionsRecyclerAdapter(val activity: Activity) :
val description = gesture.getDescription(activity) val description = gesture.getDescription(activity)
viewHolder.descriptionTextView.text = description viewHolder.descriptionTextView.text = description
viewHolder.img.transformGrayscale(LauncherPreferences.theme().monochromeIcons())
if (LauncherPreferences.theme().monochromeIcons())
viewHolder.img.transformGrayscale()
updateViewHolder(gesture, viewHolder) updateViewHolder(gesture, viewHolder)
viewHolder.img.setOnClickListener { chooseApp(gesture) } viewHolder.img.setOnClickListener { chooseApp(gesture) }
@ -178,6 +175,9 @@ class ActionsRecyclerAdapter(val activity: Activity) :
intent.putExtra("intention", ListActivity.ListActivityIntention.PICK.toString()) intent.putExtra("intention", ListActivity.ListActivityIntention.PICK.toString())
intent.putExtra("hiddenVisibility", AppFilter.Companion.AppSetVisibility.VISIBLE) intent.putExtra("hiddenVisibility", AppFilter.Companion.AppSetVisibility.VISIBLE)
intent.putExtra("forGesture", gesture.id) // for which action we choose the app intent.putExtra("forGesture", gesture.id) // for which action we choose the app
activity.startActivity(intent) activity.startActivityForResult(
intent,
REQUEST_CHOOSE_APP
)
} }
} }

View file

@ -34,17 +34,6 @@ class SettingsFragmentLauncher : PreferenceFragmentCompat() {
) )
val timeVisible = LauncherPreferences.clock().timeVisible() val timeVisible = LauncherPreferences.clock().timeVisible()
showSeconds?.isVisible = timeVisible showSeconds?.isVisible = timeVisible
val background = findPreference<androidx.preference.Preference>(
LauncherPreferences.theme().keys().background()
)
val lightTheme = LauncherPreferences.theme().colorTheme() == ColorTheme.LIGHT
background?.isVisible = !lightTheme
val hidePausedApps = findPreference<androidx.preference.Preference>(
LauncherPreferences.apps().keys().hidePausedApps()
)
hidePausedApps?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
} }
override fun onStart() { override fun onStart() {

View file

@ -2,23 +2,19 @@ package de.jrpie.android.launcher.ui.settings.meta
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.copyToClipboard
import de.jrpie.android.launcher.databinding.SettingsMetaBinding import de.jrpie.android.launcher.databinding.SettingsMetaBinding
import de.jrpie.android.launcher.getDeviceInfo
import de.jrpie.android.launcher.openInBrowser import de.jrpie.android.launcher.openInBrowser
import de.jrpie.android.launcher.openTutorial
import de.jrpie.android.launcher.preferences.resetPreferences import de.jrpie.android.launcher.preferences.resetPreferences
import de.jrpie.android.launcher.ui.LegalInfoActivity import de.jrpie.android.launcher.ui.LegalInfoActivity
import de.jrpie.android.launcher.ui.UIObject 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. * The [SettingsFragmentMeta] is a used as a tab in the SettingsActivity.
@ -44,19 +40,25 @@ class SettingsFragmentMeta : Fragment(), UIObject {
super<UIObject>.onStart() super<UIObject>.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() { override fun setOnClicks() {
fun bindURL(view: View, urlRes: Int) {
view.setOnClickListener {
openInBrowser(
getString(urlRes),
requireContext()
)
}
}
binding.settingsMetaButtonViewTutorial.setOnClickListener { binding.settingsMetaButtonViewTutorial.setOnClickListener {
openTutorial(requireContext()) startActivity(Intent(this.context, TutorialActivity::class.java))
} }
// prompting for settings-reset confirmation // prompting for settings-reset confirmation
@ -77,62 +79,60 @@ class SettingsFragmentMeta : Fragment(), UIObject {
// view code // 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 // report a bug
binding.settingsMetaButtonReportBug.setOnClickListener { binding.settingsMetaButtonReportBug.setOnClickListener {
val deviceInfo = getDeviceInfo() openInBrowser(
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { getString(R.string.settings_meta_report_bug_link),
setView(R.layout.dialog_report_bug) requireContext()
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<TextView>(R.id.dialog_report_bug_device_info)
val buttonClipboard = findViewById<Button>(R.id.dialog_report_bug_button_clipboard)
val buttonSecurity = findViewById<Button>(R.id.dialog_report_bug_button_security)
info.text = deviceInfo
buttonClipboard.setOnClickListener {
copyToClipboard(requireContext(), deviceInfo)
}
info.setOnClickListener {
copyToClipboard(requireContext(), deviceInfo)
}
buttonSecurity.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_report_vulnerability_link),
requireContext()
)
}
}
} }
// join chat // join chat
bindURL(binding.settingsMetaButtonJoinChat, R.string.settings_meta_chat_url) binding.settingsMetaButtonJoinChat.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_chat_url),
requireContext()
)
}
// contact developer // contact developer
// bindURL(binding.settingsMetaButtonContact, R.string.settings_meta_contact_url) binding.settingsMetaButtonContact.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_contact_url),
requireContext()
)
}
// contact fork developer // contact fork developer
bindURL(binding.settingsMetaButtonForkContact, R.string.settings_meta_fork_contact_url) binding.settingsMetaButtonForkContact.setOnClickListener {
openInBrowser(
// donate getString(R.string.settings_meta_fork_contact_url),
bindURL(binding.settingsMetaButtonDonate, R.string.settings_meta_donate_url) requireContext()
)
}
// privacy policy // privacy policy
bindURL(binding.settingsMetaButtonPrivacy, R.string.settings_meta_privacy_url) binding.settingsMetaButtonPrivacy.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_privacy_url),
requireContext()
)
}
// legal info // legal info
binding.settingsMetaButtonLicenses.setOnClickListener { binding.settingsMetaButtonLicenses.setOnClickListener {
startActivity(Intent(this.context, LegalInfoActivity::class.java)) startActivity(Intent(this.context, LegalInfoActivity::class.java))
} }
binding.settingsMetaTextVersion.text = BuildConfig.VERSION_NAME
} }
} }

View file

@ -1,26 +1,24 @@
package de.jrpie.android.launcher.ui.tutorial package de.jrpie.android.launcher.ui.tutorial
import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.window.OnBackInvokedDispatcher
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager.widget.ViewPager
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayout
import de.jrpie.android.launcher.databinding.TutorialBinding import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.saveListActivityChoice
import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.ui.blink import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentConcept
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment0Start import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentFinish
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment1Concept import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentSetup
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment2Usage import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentStart
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment3AppList import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentUsage
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment4Setup
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment5Finish
/** /**
* The [TutorialActivity] is displayed automatically on new installations. * The [TutorialActivity] is displayed automatically on new installations.
@ -31,75 +29,19 @@ import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment5Finish
*/ */
class TutorialActivity : AppCompatActivity(), UIObject { class TutorialActivity : AppCompatActivity(), UIObject {
private lateinit var binding: TutorialBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super<AppCompatActivity>.onCreate(savedInstanceState) super<AppCompatActivity>.onCreate(savedInstanceState)
super<UIObject>.onCreate() super<UIObject>.onCreate()
// Initialise layout // Initialise layout
binding = TutorialBinding.inflate(layoutInflater) setContentView(R.layout.tutorial)
setContentView(binding.root)
// Handle back key / gesture on Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_OVERLAY
) {
// prevent going back when the tutorial is shown for the first time
if (!LauncherPreferences.internal().started()) {
return@registerOnBackInvokedCallback
}
finish()
}
}
// set up tabs and swiping in settings // set up tabs and swiping in settings
val sectionsPagerAdapter = TutorialSectionsPagerAdapter(this) val sectionsPagerAdapter = TutorialSectionsPagerAdapter(supportFragmentManager)
binding.tutorialViewpager.apply { val viewPager: ViewPager = findViewById(R.id.tutorial_viewpager)
adapter = sectionsPagerAdapter viewPager.adapter = sectionsPagerAdapter
currentItem = 0 val tabs: TabLayout = findViewById(R.id.tutorial_tabs)
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { tabs.setupWithViewPager(viewPager)
override fun onPageSelected(position: Int) {
binding.tutorialButtonNext.apply {
val lastItem = sectionsPagerAdapter.itemCount - 1
visibility = if (position == lastItem) {
View.INVISIBLE
} else {
View.VISIBLE
}
if (position == 0) {
blink()
} else {
clearAnimation()
}
}
binding.tutorialButtonBack.apply {
visibility = if (position == 0) {
View.INVISIBLE
} else {
View.VISIBLE
}
}
}
})
}
TabLayoutMediator(binding.tutorialTabs, binding.tutorialViewpager) { _, _ -> }.attach()
binding.tutorialButtonNext.setOnClickListener {
binding.tutorialViewpager.apply {
setCurrentItem(
(currentItem + 1).coerceAtMost(sectionsPagerAdapter.itemCount - 1),
true
)
}
}
binding.tutorialButtonBack.setOnClickListener {
binding.tutorialViewpager.apply {
setCurrentItem((currentItem - 1).coerceAtLeast(0), true)
}
}
} }
override fun getTheme(): Resources.Theme { override fun getTheme(): Resources.Theme {
@ -111,9 +53,14 @@ class TutorialActivity : AppCompatActivity(), UIObject {
super<UIObject>.onStart() super<UIObject>.onStart()
} }
// prevent going back when the tutorial is shown for the first time override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@Deprecated("Deprecated in Java", ReplaceWith("use anyway")) when (requestCode) {
@Suppress("deprecation") // support API level < 33 REQUEST_CHOOSE_APP -> saveListActivityChoice(data)
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
// Default: prevent going back, allow if viewed again later
override fun onBackPressed() { override fun onBackPressed() {
if (LauncherPreferences.internal().started()) if (LauncherPreferences.internal().started())
super.onBackPressed() super.onBackPressed()
@ -127,22 +74,26 @@ class TutorialActivity : AppCompatActivity(), UIObject {
* *
* Tabs: (Start | Concept | Usage | Setup | Finish) * Tabs: (Start | Concept | Usage | Setup | Finish)
*/ */
class TutorialSectionsPagerAdapter(activity: FragmentActivity) : class TutorialSectionsPagerAdapter(fm: FragmentManager) :
FragmentStateAdapter(activity) { FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItemCount(): Int { override fun getItem(position: Int): Fragment {
return 6
}
override fun createFragment(position: Int): Fragment {
return when (position) { return when (position) {
0 -> TutorialFragment0Start() 0 -> TutorialFragmentStart()
1 -> TutorialFragment1Concept() 1 -> TutorialFragmentConcept()
2 -> TutorialFragment2Usage() 2 -> TutorialFragmentUsage()
3 -> TutorialFragment3AppList() 3 -> TutorialFragmentSetup()
4 -> TutorialFragment4Setup() 4 -> TutorialFragmentFinish()
5 -> TutorialFragment5Finish()
else -> Fragment() else -> Fragment()
} }
} }
/* We don't use titles here, as we have the dots */
override fun getPageTitle(position: Int): CharSequence {
return ""
}
override fun getCount(): Int {
return 5
}
} }

View file

@ -1,30 +0,0 @@
package de.jrpie.android.launcher.ui.tutorial.tabs
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.ui.UIObject
/**
* The [TutorialFragment3AppList] is a used as a tab in the TutorialActivity.
*
* Tells the user how his screen will look and how the app can be used
*/
class TutorialFragment3AppList : Fragment(), UIObject {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.tutorial_3_app_list, container, false)
}
override fun onStart() {
super<Fragment>.onStart()
super<UIObject>.onStart()
}
}

View file

@ -6,22 +6,22 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.databinding.Tutorial1ConceptBinding import de.jrpie.android.launcher.databinding.TutorialConceptBinding
import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.UIObject
/** /**
* The [TutorialFragment1Concept] is a used as a tab in the TutorialActivity. * The [TutorialFragmentConcept] is a used as a tab in the TutorialActivity.
* *
* It is used to display info about Launchers concept (open source, efficiency ...) * It is used to display info about Launchers concept (open source, efficiency ...)
*/ */
class TutorialFragment1Concept : Fragment(), UIObject { class TutorialFragmentConcept : Fragment(), UIObject {
private lateinit var binding: Tutorial1ConceptBinding private lateinit var binding: TutorialConceptBinding
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = Tutorial1ConceptBinding.inflate(inflater, container, false) val binding = TutorialConceptBinding.inflate(inflater, container, false)
binding.tutorialConceptBadgeVersion.text = BuildConfig.VERSION_NAME binding.tutorialConceptBadgeVersion.text = BuildConfig.VERSION_NAME
return binding.root return binding.root
} }

View file

@ -6,25 +6,25 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.BuildConfig.VERSION_CODE import de.jrpie.android.launcher.BuildConfig.VERSION_CODE
import de.jrpie.android.launcher.databinding.Tutorial5FinishBinding import de.jrpie.android.launcher.databinding.TutorialFinishBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.setDefaultHomeScreen import de.jrpie.android.launcher.setDefaultHomeScreen
import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.UIObject
/** /**
* The [TutorialFragment5Finish] is a used as a tab in the TutorialActivity. * The [TutorialFragmentFinish] is a used as a tab in the TutorialActivity.
* *
* It is used to display further resources and let the user start Launcher * It is used to display further resources and let the user start Launcher
*/ */
class TutorialFragment5Finish : Fragment(), UIObject { class TutorialFragmentFinish : Fragment(), UIObject {
private lateinit var binding: Tutorial5FinishBinding private lateinit var binding: TutorialFinishBinding
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = Tutorial5FinishBinding.inflate(inflater, container, false) binding = TutorialFinishBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }

View file

@ -9,17 +9,17 @@ import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.UIObject
/** /**
* The [TutorialFragment4Setup] is a used as a tab in the TutorialActivity. * The [TutorialFragmentSetup] is a used as a tab in the TutorialActivity.
* *
* It is used to display info in the tutorial * It is used to display info in the tutorial
*/ */
class TutorialFragment4Setup : Fragment(), UIObject { class TutorialFragmentSetup : Fragment(), UIObject {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
return inflater.inflate(R.layout.tutorial_4_setup, container, false) return inflater.inflate(R.layout.tutorial_setup, container, false)
} }
override fun onStart() { override fun onStart() {

View file

@ -5,22 +5,24 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.databinding.Tutorial0StartBinding import de.jrpie.android.launcher.databinding.TutorialStartBinding
import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.ui.blink
/** /**
* The [TutorialFragment0Start] is a used as a tab in the TutorialActivity. * The [TutorialFragmentStart] is a used as a tab in the TutorialActivity.
* *
* It displays info about the app and gets the user into the tutorial * It displays info about the app and gets the user into the tutorial
*/ */
class TutorialFragment0Start : Fragment(), UIObject { class TutorialFragmentStart : Fragment(), UIObject {
private lateinit var binding: Tutorial0StartBinding private lateinit var binding: TutorialStartBinding
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = Tutorial0StartBinding.inflate(inflater, container, false) binding = TutorialStartBinding.inflate(inflater, container, false)
binding.tutorialStartIconRight.blink()
return binding.root return binding.root
} }

View file

@ -9,17 +9,17 @@ import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.UIObject
/** /**
* The [TutorialFragment2Usage] is a used as a tab in the TutorialActivity. * The [TutorialFragmentUsage] is a used as a tab in the TutorialActivity.
* *
* Tells the user how his screen will look and how the app can be used * Tells the user how his screen will look and how the app can be used
*/ */
class TutorialFragment2Usage : Fragment(), UIObject { class TutorialFragmentUsage : Fragment(), UIObject {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
return inflater.inflate(R.layout.tutorial_2_usage, container, false) return inflater.inflate(R.layout.tutorial_usage, container, false)
} }
override fun onStart() { override fun onStart() {

View file

@ -12,7 +12,6 @@ class HtmlTextView(context: Context, attr: AttributeSet?, int: Int) :
constructor(context: Context) : this(context, null, 0) constructor(context: Context) : this(context, null, 0)
init { init {
@Suppress("deprecation") // required to support API level < 24
text = Html.fromHtml(text.toString()) text = Html.fromHtml(text.toString())
movementMethod = LinkMovementMethod.getInstance() movementMethod = LinkMovementMethod.getInstance()
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View file

@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z" />
</vector>

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?android:textColor"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">

View file

@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="?android:textColor"
android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,400Q160,367 183.5,343.5Q207,320 240,320L280,320L280,240Q280,157 338.5,98.5Q397,40 480,40Q563,40 621.5,98.5Q680,157 680,240L680,320L720,320Q753,320 776.5,343.5Q800,367 800,400L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM240,800L720,800Q720,800 720,800Q720,800 720,800L720,400Q720,400 720,400Q720,400 720,400L240,400Q240,400 240,400Q240,400 240,400L240,800Q240,800 240,800Q240,800 240,800ZM480,680Q513,680 536.5,656.5Q560,633 560,600Q560,567 536.5,543.5Q513,520 480,520Q447,520 423.5,543.5Q400,567 400,600Q400,633 423.5,656.5Q447,680 480,680ZM360,320L600,320L600,240Q600,190 565,155Q530,120 480,120Q430,120 395,155Q360,190 360,240L360,320ZM240,800Q240,800 240,800Q240,800 240,800L240,400Q240,400 240,400Q240,400 240,400L240,400Q240,400 240,400Q240,400 240,400L240,800Q240,800 240,800Q240,800 240,800Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="?android:textColor"
android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,400Q160,367 183.5,343.5Q207,320 240,320L280,320L280,240Q280,157 338.5,98.5Q397,40 480,40Q563,40 621.5,98.5Q680,157 680,240L680,320L720,320Q753,320 776.5,343.5Q800,367 800,400L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM240,800L720,800Q720,800 720,800Q720,800 720,800L720,400Q720,400 720,400Q720,400 720,400L240,400Q240,400 240,400Q240,400 240,400L240,800Q240,800 240,800Q240,800 240,800ZM480,680Q513,680 536.5,656.5Q560,633 560,600Q560,567 536.5,543.5Q513,520 480,520Q447,520 423.5,543.5Q400,567 400,600Q400,633 423.5,656.5Q447,680 480,680ZM360,320L600,320L600,240Q600,190 565,155Q530,120 480,120Q430,120 395,155Q360,190 360,240L360,320ZM240,800Q240,800 240,800Q240,800 240,800L240,400Q240,400 240,400Q240,400 240,400L240,400Q240,400 240,400Q240,400 240,400L240,800Q240,800 240,800Q240,800 240,800Z"/>
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z" />
</vector>

View file

@ -1,11 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?android:textColor" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path <path android:fillColor="?android:textColor" android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
android:fillColor="?android:textColor"
android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
</vector> </vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="?android:textColor" android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

View file

@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z" />
</vector>

View file

@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z" />
</vector>

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="M8,5v14l11,-7z" />
</vector>

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11.07,12.85c0.77,-1.39 2.25,-2.21 3.11,-3.44c0.91,-1.29 0.4,-3.7 -2.18,-3.7c-1.69,0 -2.52,1.28 -2.87,2.34L6.54,6.96C7.25,4.83 9.18,3 11.99,3c2.35,0 3.96,1.07 4.78,2.41c0.7,1.15 1.11,3.3 0.03,4.9c-1.2,1.77 -2.35,2.31 -2.97,3.45c-0.25,0.46 -0.35,0.76 -0.35,2.24h-2.89C10.58,15.22 10.46,13.95 11.07,12.85zM14,20c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2S14,18.9 14,20z" />
</vector>

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">

View file

@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="?android:textColor"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94L12,12L5,12L5,6.3l7,-3.11v8.8z" />
</vector>

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">

View file

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">

View file

@ -1,16 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="m 3,9 v 6 h 4 l 5,5 V 4 L 7,9 Z m 13.5,3 C 16.5,10.23 15.48,8.71 14,7.97 v 8.05 c 1.48,-0.73 2.5,-2.25 2.5,-4.02 z" />
<path
android:fillAlpha="0.5"
android:fillColor="?android:textColor"
android:pathData="m 14,3.23 v 2.06 c 2.89,0.86 5,3.54 5,6.71 0,3.17 -2.11,5.85 -5,6.71 v 2.06 C 18.01,19.86 21,16.28 21,12 21,7.72 18.01,4.14 14,3.23 Z"
android:strokeAlpha="0.5" />
</vector>

View file

@ -2,6 +2,7 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true" android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">

View file

@ -2,6 +2,7 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true" android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#252827"
android:pathData="M0,0h108v108h-108z" />
</vector>

View file

@ -1,122 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/list_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".ui.PinShortcutActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/pin_shortcut_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:gravity="center"
app:elevation="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/list_heading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:minHeight="?actionBarSize"
android:padding="@dimen/appbar_padding"
android:text="@string/pin_shortcut_title"
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
android:textSize="30sp"
app:layout_constraintEnd_toStartOf="@id/pin_shortcut_close"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:id="@+id/pin_shortcut_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="center"
android:includeFontPadding="true"
android:paddingLeft="16sp"
android:paddingRight="16sp"
android:src="@drawable/baseline_close_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/pin_shortcut_appbar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="20dp"
android:orientation="vertical">
<TextView
android:id="@+id/pin_shortcut_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="50dp"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:minHeight="40dp"
tools:drawableLeft="@drawable/baseline_settings_24"
tools:text="Shortcut name" />
<Space
android:layout_width="match_parent"
android:layout_height="10dp" />
<CheckBox
android:id="@+id/pin_shortcut_switch_visible"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:textColor"
android:checked="true"
android:text="@string/pin_shortcut_switch_visible" />
<Space
android:layout_width="match_parent"
android:layout_height="10dp" />
<Button
android:id="@+id/pin_shortcut_button_bind"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pin_shortcut_button_bind" />
<Space
android:layout_width="match_parent"
android:layout_height="10dp" />
</LinearLayout>
</ScrollView>
<Button
android:id="@+id/pin_shortcut_button_ok"
android:layout_margin="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@android:string/ok"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/AlertDialogDanger"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/dialog_report_bug_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dialog_report_bug_info" />
<Space
android:layout_width="match_parent"
android:layout_height="20dp" />
<TextView
android:id="@+id/dialog_report_bug_device_info"
android:padding="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Version: ?\nAndroid Version: ?" />
<Button
android:id="@+id/dialog_report_bug_button_clipboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dialog_report_bug_button_clipboard" />
<Space
android:layout_width="match_parent"
android:layout_height="20dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dialog_report_bug_security_info"/>
<Button
android:id="@+id/dialog_report_bug_button_security"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dialog_report_bug_button_security" />
</LinearLayout>
</ScrollView>

View file

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/dialog_select_gesture"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/dialog_select_gesture_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_weight="1"
android:fadeScrollbars="false"
app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/fast_scroll_thumb_drawable"
app:fastScrollHorizontalTrackDrawable="@drawable/fast_scroll_track_drawable"
app:fastScrollVerticalThumbDrawable="@drawable/fast_scroll_thumb_drawable"
app:fastScrollVerticalTrackDrawable="@drawable/fast_scroll_track_drawable" >
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>

View file

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/dialog_select_gesture_row_container"
style="@style/AlertDialogCustom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:gravity="start"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/dialog_select_gesture_row_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/dialog_select_gesture_row_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start"
android:textSize="15sp"
tools:text="Action label" />
<TextView
android:id="@+id/dialog_select_gesture_row_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start"
android:textSize="11sp"
android:visibility="visible"
tools:text="A verbose description of how to perform the action" />
</LinearLayout>
<ImageView
android:id="@+id/dialog_select_gesture_row_icon"
android:layout_width="@dimen/app_icon_side"
android:layout_height="@dimen/app_icon_side"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/baseline_flashlight_on_24"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -10,7 +10,7 @@
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
tools:context=".ui.HomeActivity"> tools:context=".ui.HomeActivity">
<TextClock <TextView
android:id="@+id/home_upper_view" android:id="@+id/home_upper_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -23,7 +23,7 @@
app:layout_constraintVertical_bias="0.45" app:layout_constraintVertical_bias="0.45"
tools:text="2024-12-24" /> tools:text="2024-12-24" />
<TextClock <TextView
android:id="@+id/home_lower_view" android:id="@+id/home_lower_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -34,18 +34,4 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<!-- only shown when µLauncher settings can't be reached by a gesture -->
<ImageView
android:id="@+id/button_fallback_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/settings"
android:visibility="gone"
tools:visibility="visible"
android:padding="20dp"
android:alpha="0.4"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/baseline_settings_24"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -32,7 +32,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" android:layout_marginLeft="8dp"
android:contentDescription="@string/settings" android:contentDescription="@string/settings"
android:gravity="center" android:gravity="center"
android:paddingLeft="16sp" android:paddingLeft="16sp"
@ -40,7 +40,8 @@
android:src="@drawable/baseline_settings_24" android:src="@drawable/baseline_settings_24"
custom:layout_constraintBottom_toBottomOf="parent" custom:layout_constraintBottom_toBottomOf="parent"
custom:layout_constraintStart_toStartOf="parent" custom:layout_constraintStart_toStartOf="parent"
custom:layout_constraintTop_toTopOf="parent" /> custom:layout_constraintTop_toTopOf="parent"
custom:type="solid" />
<TextView <TextView
android:id="@+id/list_heading" android:id="@+id/list_heading"
@ -52,8 +53,8 @@
android:text="@string/list_title_pick" android:text="@string/list_title_pick"
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title" android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
android:textSize="30sp" android:textSize="30sp"
app:layout_constraintEnd_toStartOf="@id/list_lock" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/list_settings" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<ImageView <ImageView
@ -61,7 +62,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginStart="8dp" android:layout_marginRight="8dp"
android:gravity="center" android:gravity="center"
android:includeFontPadding="true" android:includeFontPadding="true"
android:paddingLeft="16sp" android:paddingLeft="16sp"
@ -69,21 +70,8 @@
android:src="@drawable/baseline_close_24" android:src="@drawable/baseline_close_24"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
<ImageView custom:type="solid" />
android:id="@+id/list_lock"
android:visibility="gone"
tools:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="true"
android:paddingLeft="0sp"
android:paddingRight="0sp"
android:src="@drawable/baseline_lock_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/list_close"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
@ -95,11 +83,6 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<!--
Should be replaced by androidx.viewpager2.widget.ViewPager2
but there is an issue with opening the keyboard:
https://github.com/jrpie/launcher/issues/130
-->
<androidx.viewpager.widget.ViewPager <androidx.viewpager.widget.ViewPager
android:id="@+id/list_viewpager" android:id="@+id/list_viewpager"
android:layout_width="0dp" android:layout_width="0dp"

View file

@ -15,21 +15,18 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@mipmap/ic_launcher_round"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
<TextView <TextView
android:id="@+id/list_apps_row_name" android:id="@+id/list_apps_row_name"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="20sp" android:layout_marginStart="60sp"
android:gravity="start" android:gravity="start"
android:text="" android:text=""
android:textSize="20sp" android:textSize="20sp"
tools:text="@string/app_name"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/list_apps_row_icon" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -15,7 +15,6 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
tools:src="@mipmap/ic_launcher_round"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
<TextView <TextView
@ -26,7 +25,7 @@
android:paddingTop="5dp" android:paddingTop="5dp"
android:text="" android:text=""
android:textSize="11sp" android:textSize="11sp"
tools:text="@string/app_name" tools:text="some app"
app:layout_constraintTop_toBottomOf="@id/list_apps_row_icon" app:layout_constraintTop_toBottomOf="@id/list_apps_row_icon"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View file

@ -25,13 +25,12 @@
android:gravity="start|center_horizontal" android:gravity="start|center_horizontal"
android:padding="5dp" android:padding="5dp"
android:paddingStart="20dp" android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="" android:text=""
android:textSize="20sp" android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="@string/app_name" /> tools:text="some app" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -22,7 +22,9 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="60sp" android:layout_marginStart="60sp"
android:layout_marginLeft="60sp"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"
android:layout_marginRight="5dp"
android:gravity="start" android:gravity="start"
android:text="" android:text=""
android:textSize="20sp" android:textSize="20sp"

View file

@ -39,7 +39,7 @@
android:id="@+id/settings_close" android:id="@+id/settings_close"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginRight="8dp"
android:gravity="center" android:gravity="center"
android:includeFontPadding="true" android:includeFontPadding="true"
android:paddingLeft="16sp" android:paddingLeft="16sp"
@ -47,14 +47,15 @@
android:src="@drawable/baseline_close_24" android:src="@drawable/baseline_close_24"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
custom:type="solid" />
<ImageView <ImageView
android:id="@+id/settings_system" android:id="@+id/settings_system"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" android:layout_marginLeft="8dp"
android:gravity="center" android:gravity="center"
android:includeFontPadding="true" android:includeFontPadding="true"
android:paddingLeft="16sp" android:paddingLeft="16sp"
@ -62,7 +63,8 @@
android:src="@drawable/baseline_settings_applications_24" android:src="@drawable/baseline_settings_applications_24"
custom:layout_constraintBottom_toBottomOf="parent" custom:layout_constraintBottom_toBottomOf="parent"
custom:layout_constraintStart_toStartOf="parent" custom:layout_constraintStart_toStartOf="parent"
custom:layout_constraintTop_toTopOf="parent" /> custom:layout_constraintTop_toTopOf="parent"
custom:type="solid" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
@ -73,7 +75,7 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager.widget.ViewPager
android:id="@+id/settings_viewpager" android:id="@+id/settings_viewpager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

Some files were not shown because too many files have changed in this diff Show more