mirror of
https://github.com/jrpie/Launcher.git
synced 2025-04-11 14:54:31 +02:00
Compare commits
113 commits
Author | SHA1 | Date | |
---|---|---|---|
653d16b269 | |||
5d695ec0ea | |||
b4608ef153 | |||
8e140e2e69 | |||
7fc58fe384 | |||
54409b6312 | |||
865cd47583 | |||
58ddd3c8cc | |||
0baa889de5 | |||
fa34cbae90 | |||
7ac09bd465 | |||
![]() |
c8d7a1cc3e | ||
7094d55484 | |||
b3e4d8834a | |||
008d0242ee | |||
![]() |
65034bf2fb | ||
![]() |
3cec2c36c6 | ||
00350d4c3a | |||
0941062270 | |||
c783a51658 | |||
da115bb2d9 | |||
90434617e7 | |||
47940811b4 | |||
![]() |
232046e986 | ||
![]() |
ff108ee323 | ||
![]() |
943867d938 | ||
![]() |
59f4a29044 | ||
![]() |
bd70b822cf | ||
72f9c0595f | |||
75b22400c5 | |||
c1511cd475 | |||
3597baee1f | |||
e02ca4091f | |||
541e60356c | |||
492749a340 | |||
55af392706 | |||
077ee4381a | |||
e250a58ef4 | |||
c7af387a94 | |||
6cd17343fc | |||
b156b68d53 | |||
c9ee2c6304 | |||
bf45b6602e | |||
d7dd1aa71a | |||
3664159782 | |||
8df9aae029 | |||
![]() |
f776fbb88e | ||
![]() |
a5ec8bb796 | ||
3b2dca9af9 | |||
1b12032750 | |||
55a54fb9a5 | |||
![]() |
e39ff62613 | ||
9fe1a37ed6 | |||
1f825d6f00 | |||
5ea03d39fa | |||
ae119ac4ce | |||
8948b34243 | |||
f18811bfa2 | |||
bd1f999a0e | |||
941b06b258 | |||
![]() |
9935386ad8 | ||
![]() |
d44224071f | ||
![]() |
1f8f75dec8 | ||
86528f4e27 | |||
3aee137a3c | |||
befa3afc5d | |||
a295c0ab4b | |||
![]() |
c448c51164 | ||
![]() |
68b79724e8 | ||
![]() |
bef38c2657 | ||
![]() |
d0b0c27b2c | ||
![]() |
4508e4ee5c | ||
![]() |
e959e9d957 | ||
![]() |
958d4879f5 | ||
![]() |
7841a99415 | ||
![]() |
18b4fca933 | ||
![]() |
5792c7f38c | ||
88a78749c2 | |||
7257d4ca35 | |||
47ae0bf35f | |||
5669279c64 | |||
0c0d90a357 | |||
012f13c827 | |||
757486771d | |||
9c5500aa83 | |||
d69e3caf71 | |||
944eb89fef | |||
fa2f1c4127 | |||
8699b92246 | |||
d6355afc54 | |||
0a9890111c | |||
b2e7e4cacf | |||
d62815be12 | |||
768d27a7bb | |||
a227a40b6e | |||
f280d36667 | |||
![]() |
3973f1338f | ||
9f3142cb69 | |||
ecd58b91bb | |||
2fc2b76fba | |||
![]() |
c7895830e7 | ||
![]() |
14766fe1d9 | ||
![]() |
b9a59c9e37 | ||
![]() |
2d03bdbbef | ||
![]() |
09b12834a9 | ||
23f8cfb70e | |||
6d385e4e87 | |||
785e024ddb | |||
679c90130d | |||
95c9fcd292 | |||
a188e06342 | |||
5696ea73da | |||
a7ce5b9222 |
166 changed files with 4442 additions and 1651 deletions
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
|
@ -1,3 +1,3 @@
|
|||
# How you can support finnmglas/Launcher
|
||||
# How you can support jrpie/Launcher
|
||||
|
||||
custom: sponsor.finnmglas.com
|
||||
custom: https://s.jrpie.de/launcher-donate
|
||||
|
|
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,38 +0,0 @@
|
|||
---
|
||||
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. -->
|
50
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
50
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
|
@ -0,0 +1,50 @@
|
|||
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
|
|
@ -9,7 +9,14 @@ KEYSTORE_ACCRESCENT_PASS=$(keepassxc-password "android_keys/launcher-accrescent"
|
|||
|
||||
if [[ $(git status --porcelain) ]]; then
|
||||
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
|
||||
|
||||
rm -rf "$OUTPUT_DIR"
|
||||
|
@ -23,13 +30,17 @@ echo "======================="
|
|||
|
||||
./gradlew clean
|
||||
./gradlew assembleDefaultRelease
|
||||
mv app/build/outputs/apk/default/release/app-default-release-unsigned.apk "$OUTPUT_DIR/unsigned.apk"
|
||||
$BUILD_TOOLS_DIR/apksigner sign --ks "$KEYSTORE" \
|
||||
mv app/build/outputs/apk/default/release/app-default-release-unsigned.apk "$OUTPUT_DIR/app-release.apk"
|
||||
"$BUILD_TOOLS_DIR/apksigner" sign --ks "$KEYSTORE" \
|
||||
--ks-key-alias key0 \
|
||||
--ks-pass="pass:$KEYSTORE_PASS" \
|
||||
--key-pass="pass:$KEYSTORE_PASS" \
|
||||
--v1-signing-enabled=true --v2-signing-enabled=true --v3-signing-enabled=true --v4-signing-enabled=true \
|
||||
"$OUTPUT_DIR/unsigned.apk"
|
||||
--alignment-preserved \
|
||||
--v1-signing-enabled=true \
|
||||
--v2-signing-enabled=true \
|
||||
--v3-signing-enabled=true \
|
||||
--v4-signing-enabled=true \
|
||||
"$OUTPUT_DIR/app-release.apk"
|
||||
|
||||
echo
|
||||
echo "======================="
|
||||
|
@ -38,17 +49,15 @@ echo "======================="
|
|||
|
||||
./gradlew clean
|
||||
./gradlew bundleDefaultRelease
|
||||
mv app/build/outputs/bundle/defaultRelease/app-default-release.aab $OUTPUT_DIR/app-release.aab
|
||||
$BUILD_TOOLS_DIR/apksigner sign --ks "$KEYSTORE" \
|
||||
mv app/build/outputs/bundle/defaultRelease/app-default-release.aab "$OUTPUT_DIR/app-release.aab"
|
||||
"$BUILD_TOOLS_DIR/apksigner" sign --ks "$KEYSTORE" \
|
||||
--ks-key-alias key0 \
|
||||
--ks-pass="pass:$KEYSTORE_PASS" \
|
||||
--key-pass="pass:$KEYSTORE_PASS" \
|
||||
--v1-signing-enabled=true --v2-signing-enabled=true --v3-signing-enabled=true --v4-signing-enabled=true \
|
||||
--min-sdk-version=21 \
|
||||
--target-sdk-version=35 \
|
||||
"$OUTPUT_DIR/app-release.aab"
|
||||
|
||||
|
||||
echo
|
||||
echo "======================="
|
||||
echo " Accrescent (apks) "
|
||||
|
@ -56,8 +65,10 @@ echo "======================="
|
|||
|
||||
./gradlew clean
|
||||
./gradlew bundleAccrescentRelease
|
||||
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 \
|
||||
mv app/build/outputs/bundle/accrescentRelease/app-accrescent-release.aab "$OUTPUT_DIR/app-accrescent-release.aab"
|
||||
|
||||
# build apks using bundletool from https://github.com/google/bundletool/releases
|
||||
"$JAVA_HOME/bin/java" -jar /opt/android/bundletool.jar build-apks \
|
||||
--bundle="$OUTPUT_DIR/app-accrescent-release.aab" --output="$OUTPUT_DIR/launcher-accrescent.apks" \
|
||||
--ks="$KEYSTORE_ACCRESCENT" \
|
||||
--ks-pass="pass:$KEYSTORE_ACCRESCENT_PASS" \
|
||||
|
|
79
README.md
79
README.md
|
@ -14,19 +14,12 @@
|
|||
µLauncher is an Android home screen that lets you launch apps using swipe gestures and button presses.
|
||||
It is *minimal, efficient and free of distraction*.
|
||||
|
||||
Your home screen only displays the date, time and a wallpaper.
|
||||
Pressing back or swiping up (this can be configured) opens a list
|
||||
of all installed apps, which can be searched efficiently.
|
||||
|
||||
This is a fork of [finnmglas's app Launcher][original-repo].
|
||||
|
||||
|
||||
<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://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.
|
||||
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg"
|
||||
|
@ -52,6 +45,46 @@ You can also [get it on Google Play](https://play.google.com/store/apps/details?
|
|||
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
|
||||
|
||||
There are several ways to contribute to this app:
|
||||
|
@ -60,36 +93,14 @@ There are several ways to contribute to this app:
|
|||
* If you find a bug or have an idea for a new feature you can [join the chat][chat] or open an [issue][issues]. Please note that I work on this project in my free time. Thus I might not respond immediately and not all ideas will be implemented.
|
||||
* You can implement a new feature yourself:
|
||||
- Create a fork of this repository: [![][shield-gh-fork]][fork]
|
||||
- Create a new branch named `feature/<your feature>` of `fix/<your fix>` and commit your changes.
|
||||
- Create a new branch named `feature/<your feature>` or `fix/<your fix>` and commit your changes.
|
||||
- Open a new pull request.
|
||||
|
||||
|
||||
See [BUILD.md](BUILD.md) for instructions how to build this project.
|
||||
See [build.md](docs/build.md) for instructions how to build this project.
|
||||
The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds.
|
||||
Note that those are not signed.
|
||||
|
||||
## Notable changes compared to [Finn's Launcher][original-repo]:
|
||||
|
||||
* Edge gestures: There is a setting to allow distinguishing swiping at the edges of the screen from swiping in the center.
|
||||
* Compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used.
|
||||
* The home button now works as expected.
|
||||
|
||||
### Visual
|
||||
* This app uses the system wallpaper instead of a custom solution.
|
||||
* The font has been changed to [Hack][hack-font].
|
||||
* Font Awesome Icons were replaced by Material icons.
|
||||
* The gear button on the home screen was removed. Instead pressing back opens the list of applications and the app settings are accessible from there.
|
||||
|
||||
|
||||
### Search
|
||||
* The search algorithm was modified to prefer matches at the beginning of the app name, i.e. when searching for `"te"`, `"termux"` is sorted before `"notes"`.
|
||||
* The search bar was moved to the bottom of the screen.
|
||||
|
||||
### Technical
|
||||
* Small improvements to the gesture detection.
|
||||
* Different apps set as default.
|
||||
* Package name was changed to `de.jrpie.android.launcher` to avoid clashing with the original app.
|
||||
* Dropped support for API < 21 (i.e. pre Lollypop)
|
||||
* Some refactoring
|
||||
---
|
||||
---
|
||||
[hack-font]: https://sourcefoundry.org/hack/
|
||||
[original-repo]: https://github.com/finnmglas/Launcher
|
||||
|
|
|
@ -23,8 +23,8 @@ android {
|
|||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
compileSdk 35
|
||||
versionCode 36
|
||||
versionName "j-0.0.20"
|
||||
versionCode 43
|
||||
versionName "0.1.3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
@ -43,8 +43,8 @@ android {
|
|||
|
||||
buildTypes {
|
||||
release {
|
||||
// minifyEnabled true
|
||||
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
|
@ -85,22 +85,23 @@ android {
|
|||
// Disables dependency metadata when building Android App Bundles.
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.activity:activity-ktx:1.8.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.core:core-ktx:1.15.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.palette:palette-ktx:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.4.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
|
||||
|
|
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
|
@ -1,4 +1,6 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
-dontobfuscate
|
||||
-dontoptimize
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
|
@ -25,3 +27,4 @@
|
|||
# This is generated automatically by the Android Gradle plugin.
|
||||
-dontwarn javax.annotation.processing.AbstractProcessor
|
||||
-dontwarn javax.annotation.processing.SupportedAnnotationTypes
|
||||
-dontwarn javax.annotation.processing.SupportedSourceVersion
|
||||
|
|
|
@ -19,6 +19,16 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/launcherBaseTheme"
|
||||
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
|
||||
android:name=".ui.HomeActivity"
|
||||
android:clearTaskOnLaunch="true"
|
||||
|
@ -75,7 +85,7 @@
|
|||
<service
|
||||
android:name=".actions.lock.LauncherAccessibilityService"
|
||||
android:exported="true"
|
||||
android:label="@string/accessibility_service_name"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
|
@ -87,4 +97,4 @@
|
|||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
|
@ -1,23 +1,40 @@
|
|||
package de.jrpie.android.launcher
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.LauncherApps
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.UserHandle
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.preference.PreferenceManager
|
||||
import de.jrpie.android.launcher.actions.TorchManager
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.DetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.isPrivateSpaceLocked
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion
|
||||
import de.jrpie.android.launcher.preferences.resetPreferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class Application : android.app.Application() {
|
||||
val apps = MutableLiveData<List<DetailedAppInfo>>()
|
||||
val apps = MutableLiveData<List<AbstractDetailedAppInfo>>()
|
||||
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
|
||||
private val launcherAppsCallback = object : LauncherApps.Callback() {
|
||||
|
@ -67,10 +84,12 @@ class Application : android.app.Application() {
|
|||
}
|
||||
|
||||
var torchManager: TorchManager? = null
|
||||
private var customAppNames: HashMap<AppInfo, String>? = null
|
||||
private var customAppNames: HashMap<AbstractAppInfo, String>? = null
|
||||
private val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, pref ->
|
||||
if (pref == getString(R.string.settings_apps_custom_names_key)) {
|
||||
customAppNames = LauncherPreferences.apps().customNames()
|
||||
} else if (pref == LauncherPreferences.apps().keys().pinnedShortcuts()) {
|
||||
loadApps()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,12 +110,10 @@ class Application : android.app.Application() {
|
|||
// Try to restore old preferences
|
||||
migratePreferencesToNewVersion(this)
|
||||
|
||||
// First time opening the app: set defaults and start tutorial
|
||||
// First time opening the app: set defaults
|
||||
// The tutorial is started from HomeActivity#onStart, as starting it here is blocked by android
|
||||
if (!LauncherPreferences.internal().started()) {
|
||||
resetPreferences(this)
|
||||
|
||||
LauncherPreferences.internal().started(true)
|
||||
openTutorial(this)
|
||||
}
|
||||
|
||||
|
||||
|
@ -107,15 +124,37 @@ class Application : android.app.Application() {
|
|||
val launcherApps = getSystemService(LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
launcherApps.registerCallback(launcherAppsCallback)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
|
||||
val filter = IntentFilter().also {
|
||||
if (Build.VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
it.addAction(Intent.ACTION_PROFILE_AVAILABLE)
|
||||
it.addAction(Intent.ACTION_PROFILE_UNAVAILABLE)
|
||||
} else {
|
||||
it.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
|
||||
it.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
|
||||
}
|
||||
}
|
||||
ContextCompat.registerReceiver(
|
||||
this, profileAvailabilityBroadcastReceiver, filter,
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
|
||||
removeUnusedShortcuts(this)
|
||||
}
|
||||
loadApps()
|
||||
}
|
||||
|
||||
fun getCustomAppNames(): HashMap<AppInfo, String> {
|
||||
fun getCustomAppNames(): HashMap<AbstractAppInfo, String> {
|
||||
return (customAppNames ?: LauncherPreferences.apps().customNames() ?: HashMap())
|
||||
.also { customAppNames = it }
|
||||
}
|
||||
|
||||
private fun loadApps() {
|
||||
AsyncTask.execute { apps.postValue(getApps(packageManager, applicationContext)) }
|
||||
privateSpaceLocked.postValue(isPrivateSpaceLocked(this))
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
apps.postValue(getApps(packageManager, applicationContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,58 +3,74 @@ package de.jrpie.android.launcher
|
|||
import android.app.Activity
|
||||
import android.app.Service
|
||||
import android.app.role.RoleManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.LauncherApps
|
||||
import android.content.pm.LauncherApps.ShortcutQuery
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.UserHandle
|
||||
import android.os.UserManager
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.actions.ShortcutAction
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
|
||||
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.DetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.DetailedPinnedShortcutInfo
|
||||
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
|
||||
import de.jrpie.android.launcher.apps.getPrivateSpaceUser
|
||||
import de.jrpie.android.launcher.apps.isPrivateSpaceSupported
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
|
||||
import androidx.core.net.toUri
|
||||
|
||||
|
||||
/* REQUEST CODES */
|
||||
|
||||
const val REQUEST_CHOOSE_APP = 1
|
||||
const val REQUEST_UNINSTALL = 2
|
||||
|
||||
const val REQUEST_SET_DEFAULT_HOME = 42
|
||||
|
||||
const val LOG_TAG = "Launcher"
|
||||
|
||||
fun setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) {
|
||||
const val REQUEST_SET_DEFAULT_HOME = 42
|
||||
|
||||
if (checkDefault
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
&& context is Activity
|
||||
) {
|
||||
fun isDefaultHomeScreen(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val roleManager = context.getSystemService(RoleManager::class.java)
|
||||
if (!roleManager.isRoleHeld(RoleManager.ROLE_HOME)) {
|
||||
context.startActivityForResult(
|
||||
roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME),
|
||||
REQUEST_SET_DEFAULT_HOME
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (checkDefault) {
|
||||
return roleManager.isRoleHeld(RoleManager.ROLE_HOME)
|
||||
} else {
|
||||
val testIntent = Intent(Intent.ACTION_MAIN)
|
||||
testIntent.addCategory(Intent.CATEGORY_HOME)
|
||||
val defaultHome = testIntent.resolveActivity(context.packageManager)?.packageName
|
||||
if (defaultHome == context.packageName) {
|
||||
// Launcher is already the default home app
|
||||
return
|
||||
}
|
||||
return defaultHome == context.packageName
|
||||
}
|
||||
}
|
||||
|
||||
fun setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) {
|
||||
val isDefault = isDefaultHomeScreen(context)
|
||||
if (checkDefault && isDefault) {
|
||||
// Launcher is already the default home app
|
||||
return
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
&& context is Activity
|
||||
&& checkDefault // using role manager only works when µLauncher is not already the default.
|
||||
) {
|
||||
val roleManager = context.getSystemService(RoleManager::class.java)
|
||||
context.startActivityForResult(
|
||||
roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME),
|
||||
REQUEST_SET_DEFAULT_HOME
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val intent = Intent(Settings.ACTION_HOME_SETTINGS)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
@ -68,34 +84,91 @@ fun getUserFromId(userId: Int?, context: Context): UserHandle {
|
|||
return profiles.firstOrNull { it.hashCode() == userId } ?: profiles[0]
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
fun removeUnusedShortcuts(context: Context) {
|
||||
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
fun getShortcuts(profile: UserHandle): List<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) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
intent.putExtras(Bundle().apply { putBoolean("new_window", true) })
|
||||
context.startActivity(intent)
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.toast_activity_not_found_browser, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun openTutorial(context: Context) {
|
||||
context.startActivity(Intent(context, TutorialActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
})
|
||||
context.startActivity(Intent(context, TutorialActivity::class.java))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load all apps.
|
||||
*/
|
||||
fun getApps(packageManager: PackageManager, context: Context): MutableList<DetailedAppInfo> {
|
||||
val start = System.currentTimeMillis()
|
||||
val loadList = mutableListOf<DetailedAppInfo>()
|
||||
fun getApps(
|
||||
packageManager: PackageManager,
|
||||
context: Context
|
||||
): MutableList<AbstractDetailedAppInfo> {
|
||||
var start = System.currentTimeMillis()
|
||||
val loadList = mutableListOf<AbstractDetailedAppInfo>()
|
||||
|
||||
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
val userManager = context.getSystemService(Service.USER_SERVICE) as UserManager
|
||||
|
||||
val privateSpaceUser = getPrivateSpaceUser(context)
|
||||
|
||||
// TODO: shortcuts - launcherApps.getShortcuts()
|
||||
val users = userManager.userProfiles
|
||||
for (user in users) {
|
||||
// don't load apps from a user profile that has quiet mode enabled
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
if (userManager.isQuietModeEnabled(user)) {
|
||||
// hide paused apps
|
||||
if (LauncherPreferences.apps().hidePausedApps()) {
|
||||
continue
|
||||
}
|
||||
// hide apps from private space
|
||||
if (isPrivateSpaceSupported() &&
|
||||
launcherApps.getLauncherUserInfo(user)?.userType == UserManager.USER_TYPE_PROFILE_PRIVATE
|
||||
) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
launcherApps.getActivityList(null, user).forEach {
|
||||
loadList.add(DetailedAppInfo(it))
|
||||
loadList.add(DetailedAppInfo(it, it.user == privateSpaceUser))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,27 +179,48 @@ fun getApps(packageManager: PackageManager, context: Context): MutableList<Detai
|
|||
i.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
val allApps = packageManager.queryIntentActivities(i, 0)
|
||||
for (ri in allApps) {
|
||||
val app = AppInfo(ri.activityInfo.packageName, null, AppInfo.INVALID_USER)
|
||||
val app = AppInfo(ri.activityInfo.packageName, null, INVALID_USER)
|
||||
val detailedAppInfo = DetailedAppInfo(
|
||||
app,
|
||||
ri.loadLabel(packageManager),
|
||||
ri.activityInfo.loadIcon(packageManager)
|
||||
ri.activityInfo.loadIcon(packageManager),
|
||||
false
|
||||
)
|
||||
loadList.add(detailedAppInfo)
|
||||
}
|
||||
}
|
||||
loadList.sortBy { it.getCustomLabel(context).toString() }
|
||||
loadList.sortBy { it.getCustomLabel(context) }
|
||||
|
||||
val end = System.currentTimeMillis()
|
||||
var end = System.currentTimeMillis()
|
||||
Log.i(LOG_TAG, "${loadList.size} apps loaded (${end - start}ms)")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
start = System.currentTimeMillis()
|
||||
LauncherPreferences.apps().pinnedShortcuts()
|
||||
?.mapNotNull { DetailedPinnedShortcutInfo.fromPinnedShortcutInfo(it, context) }
|
||||
?.let {
|
||||
end = System.currentTimeMillis()
|
||||
Log.i(LOG_TAG, "${it.size} shortcuts loaded (${end - start}ms)")
|
||||
loadList.addAll(it)
|
||||
}
|
||||
}
|
||||
|
||||
return loadList
|
||||
}
|
||||
|
||||
|
||||
// Used in Tutorial and Settings `ActivityOnResult`
|
||||
fun saveListActivityChoice(data: Intent?) {
|
||||
val forGesture = data?.getStringExtra("forGesture") ?: return
|
||||
Gesture.byId(forGesture)?.let { Action.setActionForGesture(it, Action.fromIntent(data)) }
|
||||
// used for the bug report button
|
||||
fun getDeviceInfo(): String {
|
||||
return """
|
||||
µLauncher version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})
|
||||
Android version: ${Build.VERSION.RELEASE} (sdk ${Build.VERSION.SDK_INT})
|
||||
Model: ${Build.MODEL}
|
||||
Device: ${Build.DEVICE}
|
||||
Brand: ${Build.BRAND}
|
||||
Manufacturer: ${Build.MANUFACTURER}
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
fun copyToClipboard(context: Context, text: String) {
|
||||
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clipData = ClipData.newPlainText("Debug Info", text)
|
||||
clipboardManager.setPrimaryClip(clipData)
|
||||
}
|
|
@ -2,7 +2,6 @@ package de.jrpie.android.launcher.actions
|
|||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences.Editor
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
|
@ -12,6 +11,7 @@ import de.jrpie.android.launcher.preferences.LauncherPreferences
|
|||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import androidx.core.content.edit
|
||||
|
||||
|
||||
@Serializable
|
||||
|
@ -21,15 +21,14 @@ sealed interface Action {
|
|||
fun getIcon(context: Context): Drawable?
|
||||
fun isAvailable(context: Context): Boolean
|
||||
|
||||
// Can the action be used to reach µLauncher settings?
|
||||
fun canReachSettings(): Boolean
|
||||
|
||||
|
||||
fun bindToGesture(prefEditor: Editor, id: String) {
|
||||
prefEditor.putString(id, Json.encodeToString(this))
|
||||
}
|
||||
|
||||
fun writeToIntent(intent: Intent) {
|
||||
intent.putExtra("action", Json.encodeToString(this))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun forGesture(gesture: Gesture): Action? {
|
||||
|
@ -41,20 +40,23 @@ sealed interface Action {
|
|||
}
|
||||
|
||||
fun resetToDefaultActions(context: Context) {
|
||||
val editor = LauncherPreferences.getSharedPreferences().edit()
|
||||
val boundActions = HashSet<String>()
|
||||
Gesture.entries.forEach { gesture ->
|
||||
context.resources
|
||||
.getStringArray(gesture.defaultsResource)
|
||||
.filterNot { boundActions.contains(it) }
|
||||
.map { Pair(it, Json.decodeFromString<Action>(it)) }
|
||||
.firstOrNull { it.second.isAvailable(context) }
|
||||
?.apply {
|
||||
boundActions.add(first)
|
||||
second.bindToGesture(editor, gesture.id)
|
||||
}
|
||||
LauncherPreferences.getSharedPreferences().edit {
|
||||
val boundActions = HashSet<String>()
|
||||
Gesture.entries.forEach { gesture ->
|
||||
context.resources
|
||||
.getStringArray(gesture.defaultsResource)
|
||||
.filterNot { boundActions.contains(it) }
|
||||
.map { Pair(it, Json.decodeFromString<Action>(it)) }
|
||||
.firstOrNull { it.second.isAvailable(context) }
|
||||
?.apply {
|
||||
// allow to bind CHOOSE to multiple gestures
|
||||
if (second != LauncherAction.CHOOSE) {
|
||||
boundActions.add(first)
|
||||
}
|
||||
second.bindToGesture(this@edit, gesture.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
fun setActionForGesture(gesture: Gesture, action: Action?) {
|
||||
|
@ -62,15 +64,15 @@ sealed interface Action {
|
|||
clearActionForGesture(gesture)
|
||||
return
|
||||
}
|
||||
val editor = LauncherPreferences.getSharedPreferences().edit()
|
||||
action.bindToGesture(editor, gesture.id)
|
||||
editor.apply()
|
||||
LauncherPreferences.getSharedPreferences().edit {
|
||||
action.bindToGesture(this, gesture.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearActionForGesture(gesture: Gesture) {
|
||||
LauncherPreferences.getSharedPreferences().edit()
|
||||
.remove(gesture.id)
|
||||
.apply()
|
||||
LauncherPreferences.getSharedPreferences().edit {
|
||||
remove(gesture.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun launch(
|
||||
|
@ -81,6 +83,9 @@ sealed interface Action {
|
|||
) {
|
||||
if (action != null && action.invoke(context)) {
|
||||
if (context is Activity) {
|
||||
// There does not seem to be a good alternative to overridePendingTransition.
|
||||
// Note that we can't use overrideActivityTransition here.
|
||||
@Suppress("deprecation")
|
||||
context.overridePendingTransition(animationIn, animationOut)
|
||||
}
|
||||
} else {
|
||||
|
@ -91,10 +96,5 @@ sealed interface Action {
|
|||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun fromIntent(data: Intent): Action? {
|
||||
val json = data.getStringExtra("action") ?: return null
|
||||
return Json.decodeFromString(json)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package de.jrpie.android.launcher.actions
|
|||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Service
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.LauncherApps
|
||||
|
@ -10,7 +11,7 @@ import android.graphics.drawable.Drawable
|
|||
import android.util.Log
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.AppInfo.Companion.INVALID_USER
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
|
||||
import de.jrpie.android.launcher.apps.DetailedAppInfo
|
||||
import de.jrpie.android.launcher.ui.list.apps.openSettings
|
||||
import kotlinx.serialization.SerialName
|
||||
|
@ -34,7 +35,11 @@ class AppAction(val app: AppInfo) : Action {
|
|||
|
||||
context.packageManager.getLaunchIntentForPackage(packageName)?.let {
|
||||
it.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
context.startActivity(it)
|
||||
try {
|
||||
context.startActivity(it)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -62,11 +67,15 @@ class AppAction(val app: AppInfo) : Action {
|
|||
}
|
||||
|
||||
override fun getIcon(context: Context): Drawable? {
|
||||
return DetailedAppInfo.fromAppInfo(app, context)?.icon
|
||||
return DetailedAppInfo.fromAppInfo(app, context)?.getIcon(context)
|
||||
}
|
||||
|
||||
override fun isAvailable(context: Context): Boolean {
|
||||
// check if app is installed
|
||||
return DetailedAppInfo.fromAppInfo(app, context) != null
|
||||
}
|
||||
|
||||
override fun canReachSettings(): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package de.jrpie.android.launcher.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
|
||||
|
@ -78,6 +79,13 @@ enum class Gesture(
|
|||
R.array.default_up_right,
|
||||
R.anim.bottom_up
|
||||
),
|
||||
TAP_AND_SWIPE_UP(
|
||||
"action.tap_up",
|
||||
R.string.settings_gesture_tap_up,
|
||||
R.string.settings_gesture_description_tap_up,
|
||||
R.array.default_up,
|
||||
R.anim.bottom_up
|
||||
),
|
||||
SWIPE_UP_DOUBLE(
|
||||
"action.double_up",
|
||||
R.string.settings_gesture_double_up,
|
||||
|
@ -106,6 +114,13 @@ enum class Gesture(
|
|||
R.array.default_down_right,
|
||||
R.anim.top_down
|
||||
),
|
||||
TAP_AND_SWIPE_DOWN(
|
||||
"action.tap_down",
|
||||
R.string.settings_gesture_tap_down,
|
||||
R.string.settings_gesture_description_tap_down,
|
||||
R.array.default_down,
|
||||
R.anim.bottom_up
|
||||
),
|
||||
SWIPE_DOWN_DOUBLE(
|
||||
"action.double_down",
|
||||
R.string.settings_gesture_double_down,
|
||||
|
@ -134,6 +149,13 @@ enum class Gesture(
|
|||
R.array.default_messengers,
|
||||
R.anim.right_left
|
||||
),
|
||||
TAP_AND_SWIPE_LEFT(
|
||||
"action.tap_left",
|
||||
R.string.settings_gesture_tap_left,
|
||||
R.string.settings_gesture_description_tap_left,
|
||||
R.array.default_messengers,
|
||||
R.anim.right_left
|
||||
),
|
||||
SWIPE_LEFT_DOUBLE(
|
||||
"action.double_left",
|
||||
R.string.settings_gesture_double_left,
|
||||
|
@ -162,12 +184,73 @@ enum class Gesture(
|
|||
R.array.default_right_bottom,
|
||||
R.anim.left_right
|
||||
),
|
||||
TAP_AND_SWIPE_RIGHT(
|
||||
"action.tap_right",
|
||||
R.string.settings_gesture_tap_right,
|
||||
R.string.settings_gesture_description_tap_right,
|
||||
R.array.default_right,
|
||||
R.anim.left_right
|
||||
),
|
||||
SWIPE_RIGHT_DOUBLE(
|
||||
"action.double_right",
|
||||
R.string.settings_gesture_double_right,
|
||||
R.string.settings_gesture_description_double_right,
|
||||
R.array.default_double_right,
|
||||
R.anim.left_right
|
||||
),
|
||||
SWIPE_LARGER(
|
||||
"action.larger",
|
||||
R.string.settings_gesture_swipe_larger,
|
||||
R.string.settings_gesture_description_swipe_larger,
|
||||
R.array.no_default
|
||||
),
|
||||
SWIPE_LARGER_REVERSE(
|
||||
"action.larger_reverse",
|
||||
R.string.settings_gesture_swipe_larger_reverse,
|
||||
R.string.settings_gesture_description_swipe_larger_reverse,
|
||||
R.array.no_default
|
||||
),
|
||||
SWIPE_SMALLER(
|
||||
"action.smaller",
|
||||
R.string.settings_gesture_swipe_smaller,
|
||||
R.string.settings_gesture_description_swipe_smaller,
|
||||
R.array.no_default
|
||||
),
|
||||
SWIPE_SMALLER_REVERSE(
|
||||
"action.smaller_reverse",
|
||||
R.string.settings_gesture_swipe_smaller_reverse,
|
||||
R.string.settings_gesture_description_swipe_smaller_reverse,
|
||||
R.array.no_default
|
||||
),
|
||||
SWIPE_LAMBDA(
|
||||
"action.lambda",
|
||||
R.string.settings_gesture_swipe_lambda,
|
||||
R.string.settings_gesture_description_swipe_lambda,
|
||||
R.array.no_default
|
||||
),
|
||||
SWIPE_LAMBDA_REVERSE(
|
||||
"action.lambda_reverse",
|
||||
R.string.settings_gesture_swipe_lambda_reverse,
|
||||
R.string.settings_gesture_description_swipe_lambda_reverse,
|
||||
R.array.no_default
|
||||
),
|
||||
SWIPE_V(
|
||||
"action.v",
|
||||
R.string.settings_gesture_swipe_v,
|
||||
R.string.settings_gesture_description_swipe_v,
|
||||
R.array.no_default
|
||||
),
|
||||
SWIPE_V_REVERSE(
|
||||
"action.v_reverse",
|
||||
R.string.settings_gesture_swipe_v_reverse,
|
||||
R.string.settings_gesture_description_swipe_v_reverse,
|
||||
R.array.no_default
|
||||
),
|
||||
BACK(
|
||||
"action.back",
|
||||
R.string.settings_gesture_back,
|
||||
R.string.settings_gesture_description_back,
|
||||
R.array.default_back
|
||||
);
|
||||
|
||||
enum class Edge {
|
||||
|
@ -224,6 +307,17 @@ enum class Gesture(
|
|||
}
|
||||
}
|
||||
|
||||
fun getTapComboVariant(): Gesture {
|
||||
return when (this) {
|
||||
SWIPE_UP -> TAP_AND_SWIPE_UP
|
||||
SWIPE_DOWN -> TAP_AND_SWIPE_DOWN
|
||||
SWIPE_LEFT -> TAP_AND_SWIPE_LEFT
|
||||
SWIPE_RIGHT -> TAP_AND_SWIPE_RIGHT
|
||||
else -> this
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun isDoubleVariant(): Boolean {
|
||||
return when (this) {
|
||||
SWIPE_UP_DOUBLE,
|
||||
|
@ -261,13 +355,14 @@ enum class Gesture(
|
|||
}
|
||||
|
||||
operator fun invoke(context: Context) {
|
||||
Log.i("Launcher", "Detected gesture: $this")
|
||||
val action = Action.forGesture(this)
|
||||
Action.launch(action, context, this.animationIn, this.animationOut)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun byId(id: String): Gesture? {
|
||||
return Gesture.values().firstOrNull { it.id == id }
|
||||
return Gesture.entries.firstOrNull { it.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,9 +9,15 @@ import android.os.Build
|
|||
import android.os.SystemClock
|
||||
import android.view.KeyEvent
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import de.jrpie.android.launcher.Application
|
||||
import de.jrpie.android.launcher.BuildConfig
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService
|
||||
import de.jrpie.android.launcher.apps.AppFilter
|
||||
import de.jrpie.android.launcher.apps.hidePrivateSpaceWhenLocked
|
||||
import de.jrpie.android.launcher.apps.isPrivateSpaceSupported
|
||||
import de.jrpie.android.launcher.apps.togglePrivateSpaceLock
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.ui.list.ListActivity
|
||||
import de.jrpie.android.launcher.ui.settings.SettingsActivity
|
||||
|
@ -33,45 +39,88 @@ enum class LauncherAction(
|
|||
val id: String,
|
||||
val label: Int,
|
||||
val icon: Int,
|
||||
val launch: (Context) -> Unit
|
||||
val launch: (Context) -> Unit,
|
||||
private val canReachSettings: Boolean = false,
|
||||
val available: (Context) -> Boolean = { true },
|
||||
) : Action {
|
||||
SETTINGS(
|
||||
"settings",
|
||||
R.string.list_other_settings,
|
||||
R.drawable.baseline_settings_24,
|
||||
::openSettings
|
||||
::openSettings,
|
||||
true
|
||||
),
|
||||
CHOOSE(
|
||||
"choose",
|
||||
R.string.list_other_list,
|
||||
R.drawable.baseline_menu_24,
|
||||
::openAppsList
|
||||
::openAppsList,
|
||||
true
|
||||
),
|
||||
CHOOSE_FROM_FAVORITES(
|
||||
"choose_from_favorites",
|
||||
R.string.list_other_list_favorites,
|
||||
R.drawable.baseline_favorite_24,
|
||||
{ context -> openAppsList(context, true) }
|
||||
{ context -> openAppsList(context, favorite = true) },
|
||||
true
|
||||
),
|
||||
CHOOSE_FROM_PRIVATE_SPACE(
|
||||
"choose_from_private_space",
|
||||
R.string.list_other_list_private_space,
|
||||
R.drawable.baseline_security_24,
|
||||
{ context ->
|
||||
if ((context.applicationContext as Application).privateSpaceLocked.value != true
|
||||
|| !hidePrivateSpaceWhenLocked(context)
|
||||
) {
|
||||
openAppsList(context, private = true)
|
||||
}
|
||||
},
|
||||
available = { _ ->
|
||||
isPrivateSpaceSupported()
|
||||
}
|
||||
),
|
||||
TOGGLE_PRIVATE_SPACE_LOCK(
|
||||
"toggle_private_space_lock",
|
||||
R.string.list_other_toggle_private_space_lock,
|
||||
R.drawable.baseline_security_24,
|
||||
::togglePrivateSpaceLock,
|
||||
available = { _ -> isPrivateSpaceSupported() }
|
||||
),
|
||||
VOLUME_UP(
|
||||
"volume_up",
|
||||
R.string.list_other_volume_up,
|
||||
R.drawable.baseline_volume_up_24, ::audioVolumeUp
|
||||
R.drawable.baseline_volume_up_24,
|
||||
{ context -> audioVolumeAdjust(context, AudioManager.ADJUST_RAISE) }
|
||||
),
|
||||
VOLUME_DOWN(
|
||||
"volume_down",
|
||||
R.string.list_other_volume_down,
|
||||
R.drawable.baseline_volume_down_24, ::audioVolumeDown
|
||||
R.drawable.baseline_volume_down_24,
|
||||
{ context -> audioVolumeAdjust(context, AudioManager.ADJUST_LOWER) }
|
||||
),
|
||||
VOLUME_ADJUST(
|
||||
"volume_adjust",
|
||||
R.string.list_other_volume_adjust,
|
||||
R.drawable.baseline_volume_adjust_24,
|
||||
{ context -> audioVolumeAdjust(context, AudioManager.ADJUST_SAME) }
|
||||
),
|
||||
TRACK_PLAY_PAUSE(
|
||||
"play_pause_track",
|
||||
R.string.list_other_track_play_pause,
|
||||
R.drawable.baseline_play_arrow_24,
|
||||
{ context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) }
|
||||
),
|
||||
TRACK_NEXT(
|
||||
"next_track",
|
||||
R.string.list_other_track_next,
|
||||
R.drawable.baseline_skip_next_24, ::audioNextTrack
|
||||
R.drawable.baseline_skip_next_24,
|
||||
{ context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_NEXT) }
|
||||
),
|
||||
TRACK_PREV(
|
||||
"previous_track",
|
||||
R.string.list_other_track_previous,
|
||||
R.drawable.baseline_skip_previous_24, ::audioPreviousTrack
|
||||
R.drawable.baseline_skip_previous_24,
|
||||
{ context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PREVIOUS) }
|
||||
),
|
||||
EXPAND_NOTIFICATIONS_PANEL(
|
||||
"expand_notifications_panel",
|
||||
|
@ -85,17 +134,31 @@ enum class LauncherAction(
|
|||
R.drawable.baseline_settings_applications_24,
|
||||
::expandSettingsPanel
|
||||
),
|
||||
RECENT_APPS(
|
||||
"recent_apps",
|
||||
R.string.list_other_recent_apps,
|
||||
R.drawable.baseline_apps_24,
|
||||
LauncherAccessibilityService::openRecentApps,
|
||||
false,
|
||||
{ _ -> BuildConfig.USE_ACCESSIBILITY_SERVICE }
|
||||
),
|
||||
LOCK_SCREEN(
|
||||
"lock_screen",
|
||||
R.string.list_other_lock_screen,
|
||||
R.drawable.baseline_lock_24px,
|
||||
R.drawable.baseline_lock_24,
|
||||
{ c -> LauncherPreferences.actions().lockMethod().lockOrEnable(c) }
|
||||
),
|
||||
TORCH(
|
||||
"toggle_torch",
|
||||
R.string.list_other_torch,
|
||||
R.drawable.baseline_flashlight_on_24,
|
||||
::toggleTorch
|
||||
::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, {});
|
||||
|
||||
|
@ -109,11 +172,15 @@ enum class LauncherAction(
|
|||
}
|
||||
|
||||
override fun getIcon(context: Context): Drawable? {
|
||||
return context.getDrawable(icon)
|
||||
return AppCompatResources.getDrawable(context, icon)
|
||||
}
|
||||
|
||||
override fun isAvailable(context: Context): Boolean {
|
||||
return true
|
||||
return this.available(context)
|
||||
}
|
||||
|
||||
override fun canReachSettings(): Boolean {
|
||||
return this.canReachSettings
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -125,56 +192,28 @@ enum class LauncherAction(
|
|||
|
||||
|
||||
/* Media player actions */
|
||||
|
||||
private fun audioNextTrack(context: Context) {
|
||||
|
||||
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, KeyEvent.KEYCODE_MEDIA_NEXT, 0)
|
||||
KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, key, 0)
|
||||
mAudioManager.dispatchMediaKeyEvent(downEvent)
|
||||
|
||||
val upEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT, 0)
|
||||
val upEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, key, 0)
|
||||
mAudioManager.dispatchMediaKeyEvent(upEvent)
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
private fun audioVolumeAdjust(context: Context, direction: Int) {
|
||||
val audioManager =
|
||||
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
audioManager.adjustStreamVolume(
|
||||
AudioManager.STREAM_MUSIC,
|
||||
AudioManager.ADJUST_RAISE,
|
||||
direction,
|
||||
AudioManager.FLAG_SHOW_UI
|
||||
)
|
||||
}
|
||||
|
||||
private fun audioVolumeDown(context: Context) {
|
||||
val audioManager =
|
||||
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
audioManager.adjustStreamVolume(
|
||||
AudioManager.STREAM_MUSIC,
|
||||
AudioManager.ADJUST_LOWER,
|
||||
AudioManager.FLAG_SHOW_UI
|
||||
)
|
||||
}
|
||||
/* End media player actions */
|
||||
|
||||
private fun toggleTorch(context: Context) {
|
||||
|
@ -207,6 +246,7 @@ private fun expandNotificationsPanel(context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private fun expandSettingsPanel(context: Context) {
|
||||
/* https://stackoverflow.com/a/31898506 */
|
||||
try {
|
||||
|
@ -224,11 +264,25 @@ private fun expandSettingsPanel(context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun launchOtherLauncher(context: Context) {
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) },
|
||||
context.getString(R.string.list_other_launch_other_launcher)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun openSettings(context: Context) {
|
||||
context.startActivity(Intent(context, SettingsActivity::class.java))
|
||||
}
|
||||
|
||||
fun openAppsList(context: Context, favorite: Boolean = false, hidden: Boolean = false) {
|
||||
fun openAppsList(
|
||||
context: Context,
|
||||
favorite: Boolean = false,
|
||||
hidden: Boolean = false,
|
||||
private: Boolean = false
|
||||
) {
|
||||
val intent = Intent(context, ListActivity::class.java)
|
||||
intent.putExtra("intention", ListActivity.ListActivityIntention.VIEW.toString())
|
||||
intent.putExtra(
|
||||
|
@ -247,6 +301,16 @@ fun openAppsList(context: Context, favorite: Boolean = false, hidden: Boolean =
|
|||
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)
|
||||
}
|
||||
|
@ -260,6 +324,7 @@ private class LauncherActionSerializer : KSerializer<LauncherAction> {
|
|||
) {
|
||||
element("value", String.serializer().descriptor)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): LauncherAction {
|
||||
val s = decoder.decodeStructure(descriptor) {
|
||||
decodeElementIndex(descriptor)
|
||||
|
@ -273,5 +338,4 @@ private class LauncherActionSerializer : KSerializer<LauncherAction> {
|
|||
encodeSerializableElement(descriptor, 0, String.serializer(), value.id)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -22,26 +22,44 @@ class LauncherAccessibilityService : AccessibilityService() {
|
|||
|
||||
companion object {
|
||||
private const val TAG = "Launcher Accessibility"
|
||||
private const val ACTION_REQUEST_ENABLE = "ACTION_REQUEST_ENABLE"
|
||||
const val ACTION_LOCK_SCREEN = "ACTION_LOCK_SCREEN"
|
||||
const val ACTION_RECENT_APPS = "ACTION_RECENT_APPS"
|
||||
|
||||
fun lockScreen(context: Context) {
|
||||
private fun invoke(context: Context, action: String, failureMessageRes: Int) {
|
||||
try {
|
||||
context.startService(
|
||||
Intent(
|
||||
context,
|
||||
LauncherAccessibilityService::class.java
|
||||
).apply {
|
||||
action = ACTION_LOCK_SCREEN
|
||||
this.action = action
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.alert_lock_screen_failed),
|
||||
context.getString(failureMessageRes),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun lockScreen(context: Context) {
|
||||
if (!isEnabled(context)) {
|
||||
showEnableDialog(context)
|
||||
} else {
|
||||
invoke(context, ACTION_LOCK_SCREEN, R.string.alert_lock_screen_failed)
|
||||
}
|
||||
}
|
||||
|
||||
fun openRecentApps(context: Context) {
|
||||
if (!isEnabled(context)) {
|
||||
showEnableDialog(context)
|
||||
} else {
|
||||
invoke(context, ACTION_RECENT_APPS, R.string.alert_recent_apps_failed)
|
||||
}
|
||||
}
|
||||
|
||||
fun isEnabled(context: Context): Boolean {
|
||||
val enabledServices = Settings.Secure.getString(
|
||||
context.contentResolver,
|
||||
|
@ -58,7 +76,7 @@ class LauncherAccessibilityService : AccessibilityService() {
|
|||
setView(R.layout.dialog_consent_accessibility)
|
||||
setTitle(R.string.dialog_consent_accessibility_title)
|
||||
setPositiveButton(R.string.dialog_consent_accessibility_ok) { _, _ ->
|
||||
lockScreen(context)
|
||||
invoke(context, ACTION_REQUEST_ENABLE, R.string.alert_enable_accessibility_failed)
|
||||
}
|
||||
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
|
||||
}.create().also { it.show() }.apply {
|
||||
|
@ -94,7 +112,9 @@ class LauncherAccessibilityService : AccessibilityService() {
|
|||
}
|
||||
|
||||
when (action) {
|
||||
ACTION_REQUEST_ENABLE -> {} // do nothing
|
||||
ACTION_LOCK_SCREEN -> handleLockScreen()
|
||||
ACTION_RECENT_APPS -> performGlobalAction(GLOBAL_ACTION_RECENTS)
|
||||
}
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
|
|
|
@ -6,10 +6,10 @@ import android.widget.Button
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import de.jrpie.android.launcher.BuildConfig
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
|
||||
|
||||
@Suppress("unused")
|
||||
enum class LockMethod(
|
||||
private val lock: (Context) -> Unit,
|
||||
private val isEnabled: (Context) -> Boolean,
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -6,6 +6,7 @@ import android.os.Build
|
|||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.AppAction
|
||||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.actions.ShortcutAction
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import java.util.Locale
|
||||
import kotlin.text.Regex.Companion.escape
|
||||
|
@ -15,26 +16,34 @@ class AppFilter(
|
|||
var query: String,
|
||||
var favoritesVisibility: AppSetVisibility = AppSetVisibility.VISIBLE,
|
||||
var hiddenVisibility: AppSetVisibility = AppSetVisibility.HIDDEN,
|
||||
var privateSpaceVisibility: AppSetVisibility = AppSetVisibility.VISIBLE
|
||||
) {
|
||||
|
||||
operator fun invoke(apps: List<DetailedAppInfo>): List<DetailedAppInfo> {
|
||||
operator fun invoke(apps: List<AbstractDetailedAppInfo>): List<AbstractDetailedAppInfo> {
|
||||
var apps =
|
||||
apps.sortedBy { app -> app.getCustomLabel(context).toString().lowercase(Locale.ROOT) }
|
||||
apps.sortedBy { app -> app.getCustomLabel(context).lowercase(Locale.ROOT) }
|
||||
|
||||
val hidden = LauncherPreferences.apps().hidden() ?: setOf()
|
||||
val favorites = LauncherPreferences.apps().favorites() ?: setOf()
|
||||
val private = apps.filter { it.isPrivate() }
|
||||
.map { it.getRawInfo() }.toSet()
|
||||
|
||||
apps = apps.filter { info ->
|
||||
favoritesVisibility.predicate(favorites, info)
|
||||
&& hiddenVisibility.predicate(hidden, info)
|
||||
&& privateSpaceVisibility.predicate(private, info)
|
||||
}
|
||||
|
||||
if (LauncherPreferences.apps().hideBoundApps()) {
|
||||
val boundApps = Gesture.entries
|
||||
.filter(Gesture::isEnabled)
|
||||
.mapNotNull { g -> (Action.forGesture(g) as? AppAction)?.app }
|
||||
.mapNotNull { g -> Action.forGesture(g) }
|
||||
.mapNotNull {
|
||||
(it as? AppAction)?.app
|
||||
?: (it as? ShortcutAction)?.shortcut
|
||||
}
|
||||
.toSet()
|
||||
apps = apps.filterNot { info -> boundApps.contains(info.app) }
|
||||
apps = apps.filterNot { info -> boundApps.contains(info.getRawInfo()) }
|
||||
}
|
||||
|
||||
// normalize text for search
|
||||
|
@ -54,11 +63,11 @@ class AppFilter(
|
|||
if (query.isEmpty()) {
|
||||
return apps
|
||||
} else {
|
||||
val r: MutableList<DetailedAppInfo> = ArrayList()
|
||||
val appsSecondary: MutableList<DetailedAppInfo> = ArrayList()
|
||||
val r: MutableList<AbstractDetailedAppInfo> = ArrayList()
|
||||
val appsSecondary: MutableList<AbstractDetailedAppInfo> = ArrayList()
|
||||
val normalizedQuery: String = normalize(query)
|
||||
for (item in apps) {
|
||||
val itemLabel: String = normalize(item.getCustomLabel(context).toString())
|
||||
val itemLabel: String = normalize(item.getCustomLabel(context))
|
||||
|
||||
if (itemLabel.startsWith(normalizedQuery)) {
|
||||
r.add(item)
|
||||
|
@ -74,11 +83,11 @@ class AppFilter(
|
|||
|
||||
companion object {
|
||||
enum class AppSetVisibility(
|
||||
val predicate: (set: Set<AppInfo>, DetailedAppInfo) -> Boolean
|
||||
val predicate: (set: Set<AbstractAppInfo>, AbstractDetailedAppInfo) -> Boolean
|
||||
) {
|
||||
VISIBLE({ _, _ -> true }),
|
||||
HIDDEN({ set, appInfo -> !set.contains(appInfo.app) }),
|
||||
EXCLUSIVE({ set, appInfo -> set.contains(appInfo.app) }),
|
||||
HIDDEN({ set, appInfo -> !set.contains(appInfo.getRawInfo()) }),
|
||||
EXCLUSIVE({ set, appInfo -> set.contains(appInfo.getRawInfo()) }),
|
||||
;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,33 +4,18 @@ import android.app.Service
|
|||
import android.content.Context
|
||||
import android.content.pm.LauncherActivityInfo
|
||||
import android.content.pm.LauncherApps
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
|
||||
import de.jrpie.android.launcher.getUserFromId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* Represents an app installed on the users device.
|
||||
* Contains the minimal amount of data required to identify the app.
|
||||
*/
|
||||
@Serializable
|
||||
class AppInfo(val packageName: String, val activityName: String?, val user: Int = INVALID_USER) {
|
||||
|
||||
fun serialize(): String {
|
||||
return Json.encodeToString(this)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if(other is AppInfo) {
|
||||
return other.user == user && other.packageName == packageName
|
||||
&& other.activityName == activityName
|
||||
}
|
||||
return super.equals(other)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return packageName.hashCode()
|
||||
}
|
||||
@SerialName("app")
|
||||
data class AppInfo(val packageName: String, val activityName: String?, val user: Int = INVALID_USER): AbstractAppInfo {
|
||||
|
||||
fun getLauncherActivityInfo(
|
||||
context: Context
|
||||
|
@ -41,17 +26,4 @@ class AppInfo(val packageName: String, val activityName: String?, val user: Int
|
|||
return activityList.firstOrNull { app -> app.name == activityName }
|
||||
?: activityList.firstOrNull()
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "AppInfo {package=$packageName, activity=$activityName, user=$user}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val INVALID_USER = -1
|
||||
|
||||
fun deserialize(serialized: String): AppInfo {
|
||||
return Json.decodeFromString(serialized)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,21 +4,23 @@ import android.content.Context
|
|||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.LauncherActivityInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import de.jrpie.android.launcher.Application
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import android.os.UserHandle
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.AppAction
|
||||
import de.jrpie.android.launcher.getUserFromId
|
||||
|
||||
/**
|
||||
* Stores information used to create [AppsRecyclerAdapter] rows.
|
||||
* Stores information used to create [de.jrpie.android.launcher.ui.list.apps.AppsRecyclerAdapter] rows.
|
||||
*/
|
||||
class DetailedAppInfo(
|
||||
val app: AppInfo,
|
||||
val label: CharSequence,
|
||||
val icon: Drawable,
|
||||
val isSystemApp: Boolean = false,
|
||||
) {
|
||||
private val app: AppInfo,
|
||||
private val label: CharSequence,
|
||||
private val icon: Drawable,
|
||||
private val privateSpace: Boolean,
|
||||
private val removable: Boolean = true,
|
||||
): AbstractDetailedAppInfo {
|
||||
|
||||
constructor(activityInfo: LauncherActivityInfo) : this(
|
||||
constructor(activityInfo: LauncherActivityInfo, private: Boolean) : this(
|
||||
AppInfo(
|
||||
activityInfo.applicationInfo.packageName,
|
||||
activityInfo.name,
|
||||
|
@ -26,32 +28,47 @@ class DetailedAppInfo(
|
|||
),
|
||||
activityInfo.label,
|
||||
activityInfo.getBadgedIcon(0),
|
||||
activityInfo.applicationInfo.flags.and(ApplicationInfo.FLAG_SYSTEM) != 0
|
||||
private,
|
||||
// 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()
|
||||
}
|
||||
|
||||
fun setCustomLabel(label: CharSequence?) {
|
||||
|
||||
Log.i("Launcher", "Setting custom label for ${this.app} to ${label}.")
|
||||
val map = LauncherPreferences.apps().customNames() ?: HashMap<AppInfo, String>()
|
||||
|
||||
if (label.isNullOrEmpty()) {
|
||||
map.remove(app)
|
||||
} else {
|
||||
map[app] = label.toString()
|
||||
}
|
||||
|
||||
LauncherPreferences.apps().customNames(map)
|
||||
override fun getIcon(context: Context): Drawable {
|
||||
return icon
|
||||
}
|
||||
|
||||
override fun getRawInfo(): AppInfo {
|
||||
return app
|
||||
}
|
||||
|
||||
override fun getUser(context: Context): UserHandle {
|
||||
return getUserFromId(app.user, context)
|
||||
}
|
||||
|
||||
override fun isPrivate(): Boolean {
|
||||
return privateSpace
|
||||
}
|
||||
|
||||
override fun isRemovable(): Boolean {
|
||||
return removable
|
||||
}
|
||||
|
||||
override fun getAction(): Action {
|
||||
return AppAction(app)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromAppInfo(appInfo: AppInfo, context: Context): DetailedAppInfo? {
|
||||
return appInfo.getLauncherActivityInfo(context)?.let { DetailedAppInfo(it) }
|
||||
return appInfo.getLauncherActivityInfo(context)?.let {
|
||||
DetailedAppInfo(it, it.user == getPrivateSpaceUser(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
140
app/src/main/java/de/jrpie/android/launcher/apps/PrivateSpace.kt
Normal file
140
app/src/main/java/de/jrpie/android/launcher/apps/PrivateSpace.kt
Normal file
|
@ -0,0 +1,140 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import androidx.core.graphics.green
|
|||
import androidx.core.graphics.red
|
||||
import androidx.preference.Preference
|
||||
import de.jrpie.android.launcher.R
|
||||
import androidx.core.graphics.toColorInt
|
||||
|
||||
class ColorPreference(context: Context, attrs: AttributeSet?) :
|
||||
Preference(context, attrs) {
|
||||
|
@ -52,7 +53,7 @@ class ColorPreference(context: Context, attrs: AttributeSet?) :
|
|||
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
|
||||
setView(R.layout.dialog_choose_color)
|
||||
setTitle(R.string.dialog_choose_color_title)
|
||||
setPositiveButton(R.string.dialog_select_color_ok) { _, _ ->
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
persistInt(currentColor)
|
||||
summary = currentColor.getHex()
|
||||
}
|
||||
|
@ -83,10 +84,10 @@ class ColorPreference(context: Context, attrs: AttributeSet?) :
|
|||
override fun onTextChanged(text: CharSequence?, p1: Int, p2: Int, p3: Int) {}
|
||||
override fun afterTextChanged(editable: Editable?) {
|
||||
preview.hasFocus() || return
|
||||
val newText = editable?.toString()
|
||||
newText.isNullOrBlank() && return
|
||||
val newText = editable?.toString() ?: return
|
||||
newText.isBlank() && return
|
||||
try {
|
||||
val newColor = Color.parseColor(newText.toString())
|
||||
val newColor = newText.toColorInt()
|
||||
currentColor = newColor
|
||||
updateColor(false)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
|
|
|
@ -5,8 +5,9 @@ import java.util.Set;
|
|||
|
||||
import de.jrpie.android.launcher.R;
|
||||
import de.jrpie.android.launcher.actions.lock.LockMethod;
|
||||
import de.jrpie.android.launcher.preferences.serialization.SetAppInfoPreferenceSerializer;
|
||||
import de.jrpie.android.launcher.preferences.serialization.MapAppInfoStringPreferenceSerializer;
|
||||
import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer;
|
||||
import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer;
|
||||
import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer;
|
||||
import de.jrpie.android.launcher.preferences.theme.Background;
|
||||
import de.jrpie.android.launcher.preferences.theme.ColorTheme;
|
||||
import de.jrpie.android.launcher.preferences.theme.Font;
|
||||
|
@ -20,18 +21,24 @@ import eu.jonahbauer.android.preference.annotations.Preferences;
|
|||
r = R.class,
|
||||
value = {
|
||||
@PreferenceGroup(name = "internal", prefix = "settings_internal_", suffix = "_key", value = {
|
||||
// set after the user finished the tutorial
|
||||
@Preference(name = "started", type = boolean.class, defaultValue = "false"),
|
||||
@Preference(name = "started_time", type = long.class),
|
||||
// see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt
|
||||
@Preference(name = "version_code", type = int.class, defaultValue = "-1"),
|
||||
}),
|
||||
@PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = {
|
||||
@Preference(name = "favorites", type = Set.class, serializer = SetAppInfoPreferenceSerializer.class),
|
||||
@Preference(name = "hidden", type = Set.class, serializer = SetAppInfoPreferenceSerializer.class),
|
||||
@Preference(name = "custom_names", type = HashMap.class, serializer = MapAppInfoStringPreferenceSerializer.class),
|
||||
@Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class),
|
||||
@Preference(name = "hidden", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class),
|
||||
@Preference(name = "pinned_shortcuts", type = Set.class, serializer = SetPinnedShortcutInfoPreferenceSerializer.class),
|
||||
@Preference(name = "custom_names", type = HashMap.class, serializer = MapAbstractAppInfoStringPreferenceSerializer.class),
|
||||
@Preference(name = "hide_bound_apps", type = boolean.class, defaultValue = "false"),
|
||||
@Preference(name = "hide_paused_apps", type = boolean.class, defaultValue = "false"),
|
||||
@Preference(name = "hide_private_space_apps", type = boolean.class, defaultValue = "false"),
|
||||
}),
|
||||
@PreferenceGroup(name = "list", prefix = "settings_list_", suffix = "_key", value = {
|
||||
@Preference(name = "layout", type = ListLayout.class, defaultValue = "DEFAULT")
|
||||
@Preference(name = "layout", type = ListLayout.class, defaultValue = "DEFAULT"),
|
||||
@Preference(name = "reverse_layout", type = boolean.class, defaultValue = "false")
|
||||
}),
|
||||
@PreferenceGroup(name = "gestures", prefix = "settings_gesture_", suffix = "_key", value = {
|
||||
}),
|
||||
|
@ -57,7 +64,8 @@ import eu.jonahbauer.android.preference.annotations.Preferences;
|
|||
}),
|
||||
@PreferenceGroup(name = "display", prefix = "settings_display_", suffix = "_key", value = {
|
||||
@Preference(name = "screen_timeout_disabled", type = boolean.class, defaultValue = "false"),
|
||||
@Preference(name = "full_screen", type = boolean.class, defaultValue = "true"),
|
||||
@Preference(name = "hide_status_bar", type = boolean.class, defaultValue = "true"),
|
||||
@Preference(name = "hide_navigation_bar", type = boolean.class, defaultValue = "false"),
|
||||
@Preference(name = "rotate_screen", type = boolean.class, defaultValue = "true"),
|
||||
}),
|
||||
@PreferenceGroup(name = "functionality", prefix = "settings_functionality_", suffix = "_key", value = {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package de.jrpie.android.launcher.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -27,8 +28,10 @@ enum class ListLayout(
|
|||
GRID(
|
||||
{ c ->
|
||||
val displayMetrics = c.resources.displayMetrics
|
||||
val widthSp = displayMetrics.widthPixels / displayMetrics.scaledDensity
|
||||
GridLayoutManager(c, (widthSp / 90).toInt())
|
||||
val widthColumnPx =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 90f, displayMetrics)
|
||||
val numColumns = (displayMetrics.widthPixels / widthColumnPx).toInt()
|
||||
GridLayoutManager(c, numColumns)
|
||||
},
|
||||
R.layout.list_apps_row_variant_grid,
|
||||
false
|
||||
|
|
|
@ -5,8 +5,12 @@ import android.util.Log
|
|||
import de.jrpie.android.launcher.BuildConfig
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
|
||||
import de.jrpie.android.launcher.apps.DetailedAppInfo
|
||||
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion1
|
||||
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion2
|
||||
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3
|
||||
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown
|
||||
import de.jrpie.android.launcher.ui.HomeActivity
|
||||
|
||||
|
@ -14,7 +18,7 @@ import de.jrpie.android.launcher.ui.HomeActivity
|
|||
* Increase when breaking changes are introduced and write an appropriate case in
|
||||
* `migratePreferencesToNewVersion`
|
||||
*/
|
||||
const val PREFERENCE_VERSION = 2
|
||||
const val PREFERENCE_VERSION = 4
|
||||
const val UNKNOWN_PREFERENCE_VERSION = -1
|
||||
private const val TAG = "Launcher - Preferences"
|
||||
|
||||
|
@ -32,13 +36,20 @@ fun migratePreferencesToNewVersion(context: Context) {
|
|||
|
||||
UNKNOWN_PREFERENCE_VERSION -> { /* still using the old preferences file */
|
||||
migratePreferencesFromVersionUnknown(context)
|
||||
|
||||
Log.i(TAG, "migration of preferences complete.")
|
||||
Log.i(TAG, "migration of preferences complete (${UNKNOWN_PREFERENCE_VERSION} -> ${PREFERENCE_VERSION}).")
|
||||
}
|
||||
|
||||
1 -> {
|
||||
migratePreferencesFromVersion1()
|
||||
Log.i(TAG, "migration of preferences complete.")
|
||||
Log.i(TAG, "migration of preferences complete (1 -> ${PREFERENCE_VERSION}).")
|
||||
}
|
||||
2 -> {
|
||||
migratePreferencesFromVersion2()
|
||||
Log.i(TAG, "migration of preferences complete (2 -> ${PREFERENCE_VERSION}).")
|
||||
}
|
||||
3 -> {
|
||||
migratePreferencesFromVersion3()
|
||||
Log.i(TAG, "migration of preferences complete (3 -> ${PREFERENCE_VERSION}).")
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
@ -62,16 +73,16 @@ fun resetPreferences(context: Context) {
|
|||
LauncherPreferences.internal().versionCode(PREFERENCE_VERSION)
|
||||
|
||||
|
||||
val hidden: MutableSet<AppInfo> = mutableSetOf()
|
||||
val hidden: MutableSet<AbstractAppInfo> = mutableSetOf()
|
||||
val launcher = DetailedAppInfo.fromAppInfo(
|
||||
AppInfo(
|
||||
BuildConfig.APPLICATION_ID,
|
||||
HomeActivity::class.java.name,
|
||||
AppInfo.INVALID_USER
|
||||
INVALID_USER
|
||||
), context
|
||||
)
|
||||
launcher?.app?.let { hidden.add(it) }
|
||||
Log.i(TAG,"Hiding ${launcher?.app}")
|
||||
launcher?.getRawInfo()?.let { hidden.add(it) }
|
||||
Log.i(TAG,"Hiding ${launcher?.getRawInfo()}")
|
||||
LauncherPreferences.apps().hidden(hidden)
|
||||
|
||||
Action.resetToDefaultActions(context)
|
||||
|
|
|
@ -5,14 +5,27 @@ import de.jrpie.android.launcher.actions.AppAction
|
|||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.actions.LauncherAction
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.AppInfo.Companion.INVALID_USER
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
|
||||
import de.jrpie.android.launcher.preferences.serialization.MapAppInfoStringPreferenceSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import androidx.core.content.edit
|
||||
|
||||
|
||||
@Serializable
|
||||
@Suppress("unused")
|
||||
private class LegacyMapEntry(val key: AppInfo, val value: String)
|
||||
|
||||
private fun serializeMapAppInfo(value: Map<AppInfo, String>?): Set<String>? {
|
||||
return value?.map { (key, value) ->
|
||||
Json.encodeToString(LegacyMapEntry(key, value))
|
||||
}?.toSet()
|
||||
}
|
||||
|
||||
|
||||
val oldLauncherActionIds: Map<String, LauncherAction> =
|
||||
mapOf(
|
||||
|
@ -68,28 +81,28 @@ private fun Action.Companion.legacyFromPreference(id: String): Action? {
|
|||
val actionId = preferences.getString("$id.app", "")!!
|
||||
var u: Int? = preferences.getInt(
|
||||
"$id.user",
|
||||
AppInfo.INVALID_USER
|
||||
INVALID_USER
|
||||
)
|
||||
u = if (u == AppInfo.INVALID_USER) null else u
|
||||
u = if (u == INVALID_USER) null else u
|
||||
|
||||
return Action.legacyFromId(actionId, u)
|
||||
}
|
||||
|
||||
private fun migrateAppInfoStringMap(key: String) {
|
||||
val preferences = LauncherPreferences.getSharedPreferences()
|
||||
MapAppInfoStringPreferenceSerializer().serialize(
|
||||
serializeMapAppInfo(
|
||||
preferences.getStringSet(key, setOf())?.mapNotNull { entry ->
|
||||
try {
|
||||
val obj = JSONObject(entry);
|
||||
val obj = JSONObject(entry)
|
||||
val info = AppInfo.legacyDeserialize(obj.getString("key"))
|
||||
val value = obj.getString("value");
|
||||
val value = obj.getString("value")
|
||||
Pair(info, value)
|
||||
} catch (_: JSONException) {
|
||||
null
|
||||
}
|
||||
}?.toMap(HashMap())
|
||||
)?.let {
|
||||
preferences.edit().putStringSet(key, it as Set<String>).apply()
|
||||
preferences.edit { putStringSet(key, it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,16 +111,16 @@ private fun migrateAppInfoSet(key: String) {
|
|||
.map(AppInfo.Companion::legacyDeserialize)
|
||||
.map(AppInfo::serialize)
|
||||
.toSet()
|
||||
.let { LauncherPreferences.getSharedPreferences().edit().putStringSet(key, it).apply() }
|
||||
.let { LauncherPreferences.getSharedPreferences().edit { putStringSet(key, it) } }
|
||||
}
|
||||
|
||||
private fun migrateAction(key: String) {
|
||||
Action.legacyFromPreference(key)?.let { action ->
|
||||
LauncherPreferences.getSharedPreferences().edit()
|
||||
.putString(key, Json.encodeToString(action))
|
||||
.remove("$key.app")
|
||||
.remove("$key.user")
|
||||
.apply()
|
||||
LauncherPreferences.getSharedPreferences().edit {
|
||||
putString(key, Json.encodeToString(action))
|
||||
.remove("$key.app")
|
||||
.remove("$key.user")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -117,11 +130,12 @@ private fun migrateAction(key: String) {
|
|||
* (see [PREFERENCE_VERSION])
|
||||
*/
|
||||
fun migratePreferencesFromVersion1() {
|
||||
assert(PREFERENCE_VERSION == 2)
|
||||
assert(LauncherPreferences.internal().versionCode() == 1)
|
||||
Gesture.entries.forEach { g -> migrateAction(g.id) }
|
||||
migrateAppInfoSet(LauncherPreferences.apps().keys().hidden())
|
||||
migrateAppInfoSet(LauncherPreferences.apps().keys().favorites())
|
||||
migrateAppInfoStringMap(LauncherPreferences.apps().keys().customNames())
|
||||
LauncherPreferences.internal().versionCode(2)
|
||||
|
||||
migratePreferencesFromVersion2()
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
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()
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
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)
|
||||
}
|
|
@ -4,10 +4,9 @@ import android.content.Context
|
|||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
|
||||
import de.jrpie.android.launcher.preferences.theme.Background
|
||||
import de.jrpie.android.launcher.preferences.theme.ColorTheme
|
||||
|
||||
import androidx.core.content.edit
|
||||
|
||||
|
||||
private fun migrateStringPreference(
|
||||
|
@ -50,7 +49,6 @@ private const val TAG = "Preferences ? -> 1"
|
|||
* and a different file was used.
|
||||
*/
|
||||
fun migratePreferencesFromVersionUnknown(context: Context) {
|
||||
assert(PREFERENCE_VERSION == 2)
|
||||
|
||||
Log.i(
|
||||
TAG,
|
||||
|
@ -66,318 +64,317 @@ fun migratePreferencesFromVersionUnknown(context: Context) {
|
|||
return
|
||||
}
|
||||
|
||||
val newPrefs = LauncherPreferences.getSharedPreferences().edit()
|
||||
LauncherPreferences.getSharedPreferences().edit {
|
||||
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"startedBefore",
|
||||
"internal.started_before",
|
||||
false
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"startedBefore",
|
||||
"internal.started_before",
|
||||
false
|
||||
)
|
||||
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_volumeUpApp",
|
||||
"action.volume_up.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_volumeUpApp_user",
|
||||
"action.volume_up.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_volumeDownApp",
|
||||
"action.volume_down.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_volumeDownApp_user",
|
||||
"action.volume_down.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(oldPrefs, newPrefs, "action_timeApp", "action.time.app", "")
|
||||
migrateIntPreference(oldPrefs, newPrefs, "action_timeApp_user", "action.time.user", -1)
|
||||
migrateStringPreference(oldPrefs, newPrefs, "action_dateApp", "action.date.app", "")
|
||||
migrateIntPreference(oldPrefs, newPrefs, "action_dateApp_user", "action.date.user", -1)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_longClickApp",
|
||||
"action.long_click.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_longClickApp_user",
|
||||
"action.long_click.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_doubleClickApp",
|
||||
"action.double_click.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_doubleClickApp_user",
|
||||
"action.double_click.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(oldPrefs, newPrefs, "action_upApp", "action.up.app", "")
|
||||
migrateIntPreference(oldPrefs, newPrefs, "action_upApp_user", "action.up.user", -1)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_up_leftApp",
|
||||
"action.up_left.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_up_leftApp_user",
|
||||
"action.up_left.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_up_rightApp",
|
||||
"action.up_right.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_up_rightApp_user",
|
||||
"action.up_right.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_doubleUpApp",
|
||||
"action.double_up.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_doubleUpApp_user",
|
||||
"action.double_up.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(oldPrefs, newPrefs, "action_downApp", "action.down.app", "")
|
||||
migrateIntPreference(oldPrefs, newPrefs, "action_downApp_user", "action.down.user", -1)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_down_leftApp",
|
||||
"action.down_left.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_down_leftApp_user",
|
||||
"action.down_left.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_down_rightApp",
|
||||
"action.down_right.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_down_rightApp_user",
|
||||
"action.down_right.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_doubleDownApp",
|
||||
"action.double_down.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_doubleDownApp_user",
|
||||
"action.double_down.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(oldPrefs, newPrefs, "action_leftApp", "action.left.app", "")
|
||||
migrateIntPreference(oldPrefs, newPrefs, "action_leftApp_user", "action.left.user", -1)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_left_topApp",
|
||||
"action.left_top.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_left_topApp_user",
|
||||
"action.left_top.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_left_bottomApp",
|
||||
"action.left_bottom.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_left_bottomApp_user",
|
||||
"action.left_bottom.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_doubleLeftApp",
|
||||
"action.double_left.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_doubleLeftApp_user",
|
||||
"action.double_left.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(oldPrefs, newPrefs, "action_rightApp", "action.right.app", "")
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_rightApp_user",
|
||||
"action.right.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_right_topApp",
|
||||
"action.right_top.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_right_topApp_user",
|
||||
"action.right_top.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_right_bottomApp",
|
||||
"action.right_bottom.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_right_bottomApp_user",
|
||||
"action.right_bottom.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_doubleRightApp",
|
||||
"action.double_right.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"action_doubleRightApp_user",
|
||||
"action.double_right.user",
|
||||
-1
|
||||
)
|
||||
migrateBooleanPreference(oldPrefs, newPrefs, "timeVisible", "clock.time_visible", true)
|
||||
migrateBooleanPreference(oldPrefs, newPrefs, "dateVisible", "clock.date_visible", true)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"dateLocalized",
|
||||
"clock.date_localized",
|
||||
false
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"dateTimeFlip",
|
||||
"clock.date_time_flip",
|
||||
false
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"disableTimeout",
|
||||
"display.disable_timeout",
|
||||
false
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"useFullScreen",
|
||||
"display.use_full_screen",
|
||||
true
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"enableDoubleActions",
|
||||
"enabled_gestures.double_actions",
|
||||
true
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"enableEdgeActions",
|
||||
"enabled_gestures.edge_actions",
|
||||
true
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"searchAutoLaunch",
|
||||
"functionality.search_auto_launch",
|
||||
true
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
newPrefs,
|
||||
"searchAutoKeyboard",
|
||||
"functionality.search_auto_keyboard",
|
||||
true
|
||||
)
|
||||
|
||||
newPrefs.apply()
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_volumeUpApp",
|
||||
"action.volume_up.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_volumeUpApp_user",
|
||||
"action.volume_up.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_volumeDownApp",
|
||||
"action.volume_down.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_volumeDownApp_user",
|
||||
"action.volume_down.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(oldPrefs, this, "action_timeApp", "action.time.app", "")
|
||||
migrateIntPreference(oldPrefs, this, "action_timeApp_user", "action.time.user", -1)
|
||||
migrateStringPreference(oldPrefs, this, "action_dateApp", "action.date.app", "")
|
||||
migrateIntPreference(oldPrefs, this, "action_dateApp_user", "action.date.user", -1)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_longClickApp",
|
||||
"action.long_click.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_longClickApp_user",
|
||||
"action.long_click.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_doubleClickApp",
|
||||
"action.double_click.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_doubleClickApp_user",
|
||||
"action.double_click.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(oldPrefs, this, "action_upApp", "action.up.app", "")
|
||||
migrateIntPreference(oldPrefs, this, "action_upApp_user", "action.up.user", -1)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_up_leftApp",
|
||||
"action.up_left.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_up_leftApp_user",
|
||||
"action.up_left.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_up_rightApp",
|
||||
"action.up_right.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_up_rightApp_user",
|
||||
"action.up_right.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_doubleUpApp",
|
||||
"action.double_up.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_doubleUpApp_user",
|
||||
"action.double_up.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(oldPrefs, this, "action_downApp", "action.down.app", "")
|
||||
migrateIntPreference(oldPrefs, this, "action_downApp_user", "action.down.user", -1)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_down_leftApp",
|
||||
"action.down_left.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_down_leftApp_user",
|
||||
"action.down_left.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_down_rightApp",
|
||||
"action.down_right.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_down_rightApp_user",
|
||||
"action.down_right.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_doubleDownApp",
|
||||
"action.double_down.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_doubleDownApp_user",
|
||||
"action.double_down.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(oldPrefs, this, "action_leftApp", "action.left.app", "")
|
||||
migrateIntPreference(oldPrefs, this, "action_leftApp_user", "action.left.user", -1)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_left_topApp",
|
||||
"action.left_top.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_left_topApp_user",
|
||||
"action.left_top.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_left_bottomApp",
|
||||
"action.left_bottom.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_left_bottomApp_user",
|
||||
"action.left_bottom.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_doubleLeftApp",
|
||||
"action.double_left.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_doubleLeftApp_user",
|
||||
"action.double_left.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(oldPrefs, this, "action_rightApp", "action.right.app", "")
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_rightApp_user",
|
||||
"action.right.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_right_topApp",
|
||||
"action.right_top.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_right_topApp_user",
|
||||
"action.right_top.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_right_bottomApp",
|
||||
"action.right_bottom.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_right_bottomApp_user",
|
||||
"action.right_bottom.user",
|
||||
-1
|
||||
)
|
||||
migrateStringPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_doubleRightApp",
|
||||
"action.double_right.app",
|
||||
""
|
||||
)
|
||||
migrateIntPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"action_doubleRightApp_user",
|
||||
"action.double_right.user",
|
||||
-1
|
||||
)
|
||||
migrateBooleanPreference(oldPrefs, this, "timeVisible", "clock.time_visible", true)
|
||||
migrateBooleanPreference(oldPrefs, this, "dateVisible", "clock.date_visible", true)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"dateLocalized",
|
||||
"clock.date_localized",
|
||||
false
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"dateTimeFlip",
|
||||
"clock.date_time_flip",
|
||||
false
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"disableTimeout",
|
||||
"display.disable_timeout",
|
||||
false
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"useFullScreen",
|
||||
"display.use_full_screen",
|
||||
true
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"enableDoubleActions",
|
||||
"enabled_gestures.double_actions",
|
||||
true
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"enableEdgeActions",
|
||||
"enabled_gestures.edge_actions",
|
||||
true
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"searchAutoLaunch",
|
||||
"functionality.search_auto_launch",
|
||||
true
|
||||
)
|
||||
migrateBooleanPreference(
|
||||
oldPrefs,
|
||||
this,
|
||||
"searchAutoKeyboard",
|
||||
"functionality.search_auto_keyboard",
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
when (oldPrefs.getString("theme", "finn")) {
|
||||
"finn" -> {
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
package de.jrpie.android.launcher.preferences.serialization
|
||||
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo
|
||||
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
|
||||
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException
|
||||
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
|
@ -10,40 +11,61 @@ import kotlinx.serialization.encodeToString
|
|||
import kotlinx.serialization.json.Json
|
||||
|
||||
|
||||
// Serializers for [LauncherPreference$Config]
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class SetAppInfoPreferenceSerializer :
|
||||
PreferenceSerializer<java.util.Set<AppInfo>?, java.util.Set<java.lang.String>?> {
|
||||
class SetAbstractAppInfoPreferenceSerializer :
|
||||
PreferenceSerializer<java.util.Set<AbstractAppInfo>?, java.util.Set<java.lang.String>?> {
|
||||
@Throws(PreferenceSerializationException::class)
|
||||
override fun serialize(value: java.util.Set<AppInfo>?): java.util.Set<java.lang.String>? {
|
||||
return value?.map(AppInfo::serialize)?.toHashSet() as java.util.Set<java.lang.String>
|
||||
override fun serialize(value: java.util.Set<AbstractAppInfo>?): java.util.Set<java.lang.String> {
|
||||
return value?.map(AbstractAppInfo::serialize)
|
||||
?.toHashSet() as java.util.Set<java.lang.String>
|
||||
}
|
||||
|
||||
@Throws(PreferenceSerializationException::class)
|
||||
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.Set<AppInfo>? {
|
||||
return value?.map (java.lang.String::toString)?.map(AppInfo::deserialize)?.toHashSet() as? java.util.Set<AppInfo>
|
||||
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.Set<AbstractAppInfo>? {
|
||||
return value?.map(java.lang.String::toString)?.map(AbstractAppInfo::deserialize)
|
||||
?.toHashSet() as? java.util.Set<AbstractAppInfo>
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class MapAppInfoStringPreferenceSerializer :
|
||||
PreferenceSerializer<java.util.HashMap<AppInfo, String>?, java.util.Set<java.lang.String>?> {
|
||||
|
||||
@Serializable()
|
||||
private class MapEntry(val key: AppInfo, val value: String)
|
||||
class SetPinnedShortcutInfoPreferenceSerializer :
|
||||
PreferenceSerializer<java.util.Set<PinnedShortcutInfo>?, java.util.Set<java.lang.String>?> {
|
||||
@Throws(PreferenceSerializationException::class)
|
||||
override fun serialize(value: java.util.Set<PinnedShortcutInfo>?): java.util.Set<java.lang.String> {
|
||||
return value?.map { Json.encodeToString<PinnedShortcutInfo>(it) }
|
||||
?.toHashSet() as java.util.Set<java.lang.String>
|
||||
}
|
||||
|
||||
@Throws(PreferenceSerializationException::class)
|
||||
override fun serialize(value: java.util.HashMap<AppInfo, String>?): java.util.Set<java.lang.String>? {
|
||||
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.Set<PinnedShortcutInfo>? {
|
||||
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) ->
|
||||
Json.encodeToString(MapEntry(key, value))
|
||||
}?.toHashSet() as? java.util.Set<java.lang.String>
|
||||
}
|
||||
|
||||
@Throws(PreferenceSerializationException::class)
|
||||
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.HashMap<AppInfo, String>? {
|
||||
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.HashMap<AbstractAppInfo, String>? {
|
||||
return value?.associateTo(HashMap()) {
|
||||
val entry = Json.decodeFromString<MapEntry>(it.toString())
|
||||
Pair(entry.key, entry.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,13 @@ enum class Background(val id: Int, val dim: Boolean = false, val blur: Boolean =
|
|||
;
|
||||
|
||||
fun applyToTheme(theme: Resources.Theme) {
|
||||
theme.applyStyle(id, true)
|
||||
var background = this
|
||||
|
||||
// force a solid background when using the light theme
|
||||
if (LauncherPreferences.theme().colorTheme() == ColorTheme.LIGHT) {
|
||||
background = SOLID
|
||||
}
|
||||
theme.applyStyle(background.id, true)
|
||||
}
|
||||
|
||||
fun applyToWindow(window: Window) {
|
||||
|
|
|
@ -2,10 +2,9 @@ package de.jrpie.android.launcher.preferences.theme
|
|||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import de.jrpie.android.launcher.R
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import de.jrpie.android.launcher.R
|
||||
|
||||
@Suppress("unused")
|
||||
enum class ColorTheme(
|
||||
private val id: Int,
|
||||
private val labelResource: Int,
|
||||
|
@ -26,7 +25,7 @@ enum class ColorTheme(
|
|||
R.style.colorThemeLight,
|
||||
R.string.settings_theme_color_theme_item_light,
|
||||
R.style.textShadowLight,
|
||||
{ false }),
|
||||
{ true }),
|
||||
DYNAMIC(
|
||||
R.style.colorThemeDynamic,
|
||||
R.string.settings_theme_color_theme_item_dynamic,
|
||||
|
|
|
@ -27,10 +27,14 @@ fun View.blink(
|
|||
}
|
||||
|
||||
// Taken from: https://stackoverflow.com/a/30340794/12787264
|
||||
fun ImageView.transformGrayscale() {
|
||||
this.colorFilter = ColorMatrixColorFilter(ColorMatrix().apply {
|
||||
setSaturation(0f)
|
||||
})
|
||||
fun ImageView.transformGrayscale(grayscale: Boolean) {
|
||||
this.colorFilter = if (grayscale) {
|
||||
ColorMatrixColorFilter(ColorMatrix().apply {
|
||||
setSaturation(0f)
|
||||
})
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -2,18 +2,15 @@ package de.jrpie.android.launcher.ui
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.GestureDetector
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.View
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import androidx.core.view.isVisible
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
|
@ -22,19 +19,8 @@ import de.jrpie.android.launcher.actions.LauncherAction
|
|||
import de.jrpie.android.launcher.databinding.HomeBinding
|
||||
import de.jrpie.android.launcher.openTutorial
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion
|
||||
import de.jrpie.android.launcher.preferences.resetPreferences
|
||||
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.Timer
|
||||
import kotlin.concurrent.fixedRateTimer
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.tan
|
||||
|
||||
|
||||
/**
|
||||
* [HomeActivity] is the actual application Launcher,
|
||||
|
@ -48,10 +34,10 @@ import kotlin.math.tan
|
|||
* - Setting global variables (preferences etc.)
|
||||
* - Opening the [TutorialActivity] on new installations
|
||||
*/
|
||||
class HomeActivity : UIObject, AppCompatActivity(),
|
||||
GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
|
||||
class HomeActivity : UIObject, AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: HomeBinding
|
||||
private lateinit var touchGestureDetector: TouchGestureDetector
|
||||
|
||||
private var sharedPreferencesListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
|
||||
|
@ -60,26 +46,39 @@ class HomeActivity : UIObject, AppCompatActivity(),
|
|||
) {
|
||||
recreate()
|
||||
}
|
||||
|
||||
if (prefKey?.startsWith("action.") == true) {
|
||||
updateSettingsFallbackButtonVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private var edgeWidth = 0.15f
|
||||
|
||||
private var bufferedPointerCount = 1 // how many fingers on screen
|
||||
private var pointerBufferTimer = Timer()
|
||||
|
||||
private lateinit var mDetector: GestureDetectorCompat
|
||||
|
||||
// timers
|
||||
private var clockTimer = Timer()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super<AppCompatActivity>.onCreate(savedInstanceState)
|
||||
super<UIObject>.onCreate()
|
||||
|
||||
touchGestureDetector = TouchGestureDetector(
|
||||
this, 0, 0,
|
||||
LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
|
||||
)
|
||||
touchGestureDetector.updateScreenSize(windowManager)
|
||||
|
||||
// Initialise layout
|
||||
binding = HomeBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
binding.root.setOnApplyWindowInsetsListener { _, windowInsets ->
|
||||
@Suppress("deprecation") // required to support API 29
|
||||
val insets = windowInsets.systemGestureInsets
|
||||
touchGestureDetector.setSystemGestureInsets(insets)
|
||||
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Handle back key / gesture on Android 13+, cf. onKeyDown()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
|
@ -88,46 +87,72 @@ class HomeActivity : UIObject, AppCompatActivity(),
|
|||
handleBack()
|
||||
}
|
||||
}
|
||||
binding.buttonFallbackSettings.setOnClickListener {
|
||||
LauncherAction.SETTINGS.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
touchGestureDetector.updateScreenSize(windowManager)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<AppCompatActivity>.onStart()
|
||||
|
||||
mDetector = GestureDetectorCompat(this, this)
|
||||
mDetector.setOnDoubleTapListener(this)
|
||||
|
||||
super<UIObject>.onStart()
|
||||
|
||||
// If the tutorial was not finished, start it
|
||||
if (!LauncherPreferences.internal().started()) {
|
||||
openTutorial(this)
|
||||
}
|
||||
|
||||
LauncherPreferences.getSharedPreferences()
|
||||
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
|
||||
}
|
||||
|
||||
private fun updateClock() {
|
||||
clockTimer.cancel()
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
|
||||
if (hasFocus && LauncherPreferences.display().hideNavigationBar()) {
|
||||
hideNavigationBar()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updateSettingsFallbackButtonVisibility() {
|
||||
// If µLauncher settings can not be reached from any action bound to an enabled gesture,
|
||||
// show the fallback button.
|
||||
binding.buttonFallbackSettings.visibility = if (
|
||||
!Gesture.entries.any { g ->
|
||||
g.isEnabled() && Action.forGesture(g)?.canReachSettings() == true
|
||||
}
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun initClock() {
|
||||
val locale = Locale.getDefault()
|
||||
val dateVisible = LauncherPreferences.clock().dateVisible()
|
||||
val timeVisible = LauncherPreferences.clock().timeVisible()
|
||||
|
||||
var dateFMT = "yyyy-MM-dd"
|
||||
var timeFMT = "HH:mm"
|
||||
val period = 100L
|
||||
if (LauncherPreferences.clock().showSeconds()) {
|
||||
timeFMT += ":ss"
|
||||
}
|
||||
/*
|
||||
I thought about adding an option to show microseconds as well ( timeFMT += ".SSS" ).
|
||||
However setting period ot 1L (or even 10L) causes high CPU load,
|
||||
so that doesn't seem to be a good idea.
|
||||
*/
|
||||
|
||||
if (LauncherPreferences.clock().localized()) {
|
||||
dateFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, dateFMT)
|
||||
timeFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, timeFMT)
|
||||
}
|
||||
|
||||
var upperFormat = SimpleDateFormat(dateFMT, locale)
|
||||
var lowerFormat = SimpleDateFormat(timeFMT, locale)
|
||||
var upperFormat = dateFMT
|
||||
var lowerFormat = timeFMT
|
||||
var upperVisible = dateVisible
|
||||
var lowerVisible = timeVisible
|
||||
|
||||
|
@ -142,21 +167,10 @@ class HomeActivity : UIObject, AppCompatActivity(),
|
|||
binding.homeUpperView.setTextColor(LauncherPreferences.clock().color())
|
||||
binding.homeLowerView.setTextColor(LauncherPreferences.clock().color())
|
||||
|
||||
|
||||
clockTimer = fixedRateTimer("clockTimer", true, 0L, period) {
|
||||
this@HomeActivity.runOnUiThread {
|
||||
if (lowerVisible) {
|
||||
val t = lowerFormat.format(Date())
|
||||
if (binding.homeLowerView.text != t)
|
||||
binding.homeLowerView.text = t
|
||||
}
|
||||
if (upperVisible) {
|
||||
val d = upperFormat.format(Date())
|
||||
if (binding.homeUpperView.text != d)
|
||||
binding.homeUpperView.text = d
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.homeLowerView.format24Hour = lowerFormat
|
||||
binding.homeUpperView.format24Hour = upperFormat
|
||||
binding.homeLowerView.format12Hour = lowerFormat
|
||||
binding.homeUpperView.format12Hour = upperFormat
|
||||
}
|
||||
|
||||
override fun getTheme(): Resources.Theme {
|
||||
|
@ -174,14 +188,11 @@ class HomeActivity : UIObject, AppCompatActivity(),
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
edgeWidth = LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
|
||||
touchGestureDetector.edgeWidth =
|
||||
LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
|
||||
|
||||
updateClock()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
clockTimer.cancel()
|
||||
initClock()
|
||||
updateSettingsFallbackButtonVisibility()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -197,6 +208,7 @@ class HomeActivity : UIObject, AppCompatActivity(),
|
|||
// Only used pre Android 13, cf. onBackInvokedDispatcher
|
||||
handleBack()
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
if (Action.forGesture(Gesture.VOLUME_UP) == LauncherAction.VOLUME_UP) {
|
||||
// Let the OS handle the key event. This works better with some custom ROMs
|
||||
|
@ -217,95 +229,9 @@ class HomeActivity : UIObject, AppCompatActivity(),
|
|||
return true
|
||||
}
|
||||
|
||||
override fun onFling(e1: MotionEvent?, e2: MotionEvent, dX: Float, dY: Float): Boolean {
|
||||
|
||||
if (e1 == null) return false
|
||||
|
||||
|
||||
val displayMetrics: DisplayMetrics = DisplayMetrics()
|
||||
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
|
||||
val width = displayMetrics.widthPixels
|
||||
val height = displayMetrics.heightPixels
|
||||
|
||||
val diffX = e1.x - e2.x
|
||||
val diffY = e1.y - e2.y
|
||||
|
||||
val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe()
|
||||
val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe()
|
||||
|
||||
val threshold = ViewConfiguration.get(this).scaledTouchSlop
|
||||
val angularThreshold = tan(Math.PI / 6)
|
||||
|
||||
var gesture = if (angularThreshold * abs(diffX) > abs(diffY)) { // horizontal swipe
|
||||
if (diffX > threshold)
|
||||
Gesture.SWIPE_LEFT
|
||||
else if (diffX < -threshold)
|
||||
Gesture.SWIPE_RIGHT
|
||||
else null
|
||||
} else if (angularThreshold * abs(diffY) > abs(diffX)) { // vertical swipe
|
||||
// Only open if the swipe was not from the phones top edge
|
||||
// TODO: replace 100px by sensible dp value (e.g. twice the height of the status bar)
|
||||
if (diffY < -threshold && e1.y > 100)
|
||||
Gesture.SWIPE_DOWN
|
||||
else if (diffY > threshold)
|
||||
Gesture.SWIPE_UP
|
||||
else null
|
||||
} else null
|
||||
|
||||
if (doubleActions && bufferedPointerCount > 1) {
|
||||
gesture = gesture?.let(Gesture::getDoubleVariant)
|
||||
}
|
||||
|
||||
if (edgeActions) {
|
||||
if (max(e1.x, e2.x) < edgeWidth * width) {
|
||||
gesture = gesture?.getEdgeVariant(Gesture.Edge.LEFT)
|
||||
} else if (min(e1.x, e2.x) > (1 - edgeWidth) * width) {
|
||||
gesture = gesture?.getEdgeVariant(Gesture.Edge.RIGHT)
|
||||
}
|
||||
|
||||
if (max(e1.y, e2.y) < edgeWidth * height) {
|
||||
gesture = gesture?.getEdgeVariant(Gesture.Edge.TOP)
|
||||
} else if (min(e1.y, e2.y) > (1 - edgeWidth) * height) {
|
||||
gesture = gesture?.getEdgeVariant(Gesture.Edge.BOTTOM)
|
||||
}
|
||||
}
|
||||
gesture?.invoke(this)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onLongPress(event: MotionEvent) {
|
||||
Gesture.LONG_CLICK(this)
|
||||
}
|
||||
|
||||
override fun onDoubleTap(event: MotionEvent): Boolean {
|
||||
Gesture.DOUBLE_CLICK(this)
|
||||
return false
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
|
||||
// Buffer / Debounce the pointer count
|
||||
if (event.pointerCount > bufferedPointerCount) {
|
||||
bufferedPointerCount = event.pointerCount
|
||||
pointerBufferTimer = fixedRateTimer("pointerBufferTimer", true, 300, 1000) {
|
||||
bufferedPointerCount = 1
|
||||
this.cancel() // a non-recurring timer
|
||||
}
|
||||
}
|
||||
|
||||
return if (mDetector.onTouchEvent(event)) {
|
||||
false
|
||||
} else {
|
||||
super.onTouchEvent(event)
|
||||
}
|
||||
touchGestureDetector.onTouchEvent(event)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun setOnClicks() {
|
||||
|
@ -329,22 +255,10 @@ class HomeActivity : UIObject, AppCompatActivity(),
|
|||
|
||||
|
||||
private fun handleBack() {
|
||||
LauncherAction.CHOOSE.launch(this)
|
||||
Gesture.BACK(this)
|
||||
}
|
||||
|
||||
override fun isHomeScreen(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
/* TODO: Remove those. For now they are necessary
|
||||
* because this inherits from GestureDetector.OnGestureListener */
|
||||
override fun onDoubleTapEvent(event: MotionEvent): Boolean { return false }
|
||||
override fun onDown(event: MotionEvent): Boolean { return false }
|
||||
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dX: Float, dY: Float): Boolean { return false }
|
||||
override fun onShowPress(event: MotionEvent) {}
|
||||
override fun onSingleTapUp(event: MotionEvent): Boolean { return false }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,339 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -3,7 +3,11 @@ package de.jrpie.android.launcher.ui
|
|||
import android.app.Activity
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
|
||||
|
@ -11,10 +15,12 @@ import de.jrpie.android.launcher.preferences.LauncherPreferences
|
|||
* An interface implemented by every [Activity], Fragment etc. in Launcher.
|
||||
* It handles themes and window flags - a useful abstraction as it is the same everywhere.
|
||||
*/
|
||||
@Suppress("deprecation") // FLAG_FULLSCREEN is required to support API level < 30
|
||||
fun setWindowFlags(window: Window, homeScreen: Boolean) {
|
||||
window.setFlags(0, 0) // clear flags
|
||||
|
||||
// Display notification bar
|
||||
if (LauncherPreferences.display().fullScreen())
|
||||
if (LauncherPreferences.display().hideStatusBar())
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
|
@ -36,17 +42,19 @@ fun setWindowFlags(window: Window, homeScreen: Boolean) {
|
|||
|
||||
}
|
||||
|
||||
|
||||
interface UIObject {
|
||||
fun onCreate() {
|
||||
if (this is Activity) {
|
||||
setWindowFlags(window, isHomeScreen())
|
||||
|
||||
if (!LauncherPreferences.display().rotateScreen()) {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||
}
|
||||
if (this !is Activity) {
|
||||
return
|
||||
}
|
||||
setWindowFlags(window, isHomeScreen())
|
||||
|
||||
if (!LauncherPreferences.display().rotateScreen()) {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||
}
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
setOnClicks()
|
||||
adjustLayout()
|
||||
|
@ -70,4 +78,26 @@ interface UIObject {
|
|||
fun isHomeScreen(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun hideNavigationBar() {
|
||||
if (this !is Activity) {
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.insetsController?.apply {
|
||||
hide(WindowInsets.Type.navigationBars())
|
||||
systemBarsBehavior =
|
||||
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
} else {
|
||||
// Try to hide the navigation bar but do not hide the status bar
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
or View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +1,23 @@
|
|||
package de.jrpie.android.launcher.ui.list
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import de.jrpie.android.launcher.Application
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.REQUEST_UNINSTALL
|
||||
import de.jrpie.android.launcher.actions.LauncherAction
|
||||
import de.jrpie.android.launcher.apps.AppFilter
|
||||
import de.jrpie.android.launcher.apps.hidePrivateSpaceWhenLocked
|
||||
import de.jrpie.android.launcher.apps.isPrivateSpaceLocked
|
||||
import de.jrpie.android.launcher.apps.isPrivateSpaceSetUp
|
||||
import de.jrpie.android.launcher.apps.togglePrivateSpaceLock
|
||||
import de.jrpie.android.launcher.databinding.ListBinding
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
@ -27,12 +25,6 @@ import de.jrpie.android.launcher.ui.list.apps.ListFragmentApps
|
|||
import de.jrpie.android.launcher.ui.list.other.ListFragmentOther
|
||||
|
||||
|
||||
// TODO: Better solution for this intercommunication functionality (used in list-fragments)
|
||||
var intention = ListActivity.ListActivityIntention.VIEW
|
||||
var favoritesVisibility: AppFilter.Companion.AppSetVisibility = AppFilter.Companion.AppSetVisibility.VISIBLE
|
||||
var hiddenVisibility: AppFilter.Companion.AppSetVisibility = AppFilter.Companion.AppSetVisibility.HIDDEN
|
||||
var forGesture: String? = null
|
||||
|
||||
/**
|
||||
* The [ListActivity] is the most general purpose activity in Launcher:
|
||||
* - used to view all apps and edit their settings
|
||||
|
@ -42,6 +34,54 @@ var forGesture: String? = null
|
|||
*/
|
||||
class ListActivity : AppCompatActivity(), UIObject {
|
||||
private lateinit var binding: ListBinding
|
||||
var intention = ListActivityIntention.VIEW
|
||||
var favoritesVisibility: AppFilter.Companion.AppSetVisibility =
|
||||
AppFilter.Companion.AppSetVisibility.VISIBLE
|
||||
var privateSpaceVisibility: AppFilter.Companion.AppSetVisibility =
|
||||
AppFilter.Companion.AppSetVisibility.VISIBLE
|
||||
var hiddenVisibility: AppFilter.Companion.AppSetVisibility =
|
||||
AppFilter.Companion.AppSetVisibility.HIDDEN
|
||||
var forGesture: String? = null
|
||||
|
||||
|
||||
private fun updateLockIcon(locked: Boolean) {
|
||||
if (
|
||||
// only show lock for VIEW intention
|
||||
(intention != ListActivityIntention.VIEW)
|
||||
// hide lock when private space does not exist
|
||||
|| !isPrivateSpaceSetUp(this)
|
||||
// hide lock when private space apps are hidden from the main list and we are not in the private space list
|
||||
|| (LauncherPreferences.apps().hidePrivateSpaceApps()
|
||||
&& privateSpaceVisibility != AppFilter.Companion.AppSetVisibility.EXCLUSIVE)
|
||||
// hide lock when private space is locked and the hidden when locked setting is set
|
||||
|| (locked && hidePrivateSpaceWhenLocked(this))
|
||||
) {
|
||||
binding.listLock.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
binding.listLock.visibility = View.VISIBLE
|
||||
|
||||
binding.listLock.setImageDrawable(
|
||||
AppCompatResources.getDrawable(
|
||||
this,
|
||||
if (locked) {
|
||||
R.drawable.baseline_lock_24
|
||||
} else {
|
||||
R.drawable.baseline_lock_open_24
|
||||
}
|
||||
)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
binding.listLock.tooltipText = getString(
|
||||
if (locked) {
|
||||
R.string.tooltip_unlock_private_space
|
||||
} else {
|
||||
R.string.tooltip_lock_private_space
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum class ListActivityIntention(val titleResource: Int) {
|
||||
|
@ -68,10 +108,15 @@ class ListActivity : AppCompatActivity(), UIObject {
|
|||
?.let { ListActivityIntention.valueOf(it) }
|
||||
?: ListActivityIntention.VIEW
|
||||
|
||||
@Suppress("deprecation") // required to support API level < 33
|
||||
favoritesVisibility = bundle.getSerializable("favoritesVisibility")
|
||||
as? AppFilter.Companion.AppSetVisibility ?: favoritesVisibility
|
||||
@Suppress("deprecation") // required to support API level < 33
|
||||
privateSpaceVisibility = bundle.getSerializable("privateSpaceVisibility")
|
||||
as? AppFilter.Companion.AppSetVisibility ?: privateSpaceVisibility
|
||||
@Suppress("deprecation") // required to support API level < 33
|
||||
hiddenVisibility = bundle.getSerializable("hiddenVisibility")
|
||||
as? AppFilter.Companion.AppSetVisibility ?: favoritesVisibility
|
||||
as? AppFilter.Companion.AppSetVisibility ?: hiddenVisibility
|
||||
|
||||
if (intention != ListActivityIntention.VIEW)
|
||||
forGesture = bundle.getString("forGesture")
|
||||
|
@ -87,6 +132,17 @@ class ListActivity : AppCompatActivity(), UIObject {
|
|||
}
|
||||
|
||||
|
||||
if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
|
||||
isPrivateSpaceSetUp(this, showToast = true, launchSettings = true)
|
||||
if (isPrivateSpaceLocked(this)) {
|
||||
togglePrivateSpaceLock(this)
|
||||
}
|
||||
}
|
||||
updateLockIcon(isPrivateSpaceLocked(this))
|
||||
|
||||
val privateSpaceLocked = (this.applicationContext as Application).privateSpaceLocked
|
||||
privateSpaceLocked.observe(this) { updateLockIcon(it) }
|
||||
|
||||
// android:windowSoftInputMode="adjustResize" doesn't work in full screen.
|
||||
// workaround from https://stackoverflow.com/a/57623505
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
|
@ -97,7 +153,7 @@ class ListActivity : AppCompatActivity(), UIObject {
|
|||
binding.listContainer.context.resources.displayMetrics.heightPixels
|
||||
val diff = height - r.bottom
|
||||
if (diff != 0 &&
|
||||
LauncherPreferences.display().fullScreen()
|
||||
LauncherPreferences.display().hideStatusBar()
|
||||
) {
|
||||
if (binding.listContainer.paddingBottom != diff) {
|
||||
binding.listContainer.setPadding(0, 0, 0, diff)
|
||||
|
@ -125,30 +181,19 @@ class ListActivity : AppCompatActivity(), UIObject {
|
|||
finish()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == REQUEST_UNINSTALL) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
Toast.makeText(this, getString(R.string.list_removed), Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
} else if (resultCode == Activity.RESULT_FIRST_USER) {
|
||||
Toast.makeText(this, getString(R.string.list_not_removed), Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun updateTitle() {
|
||||
var titleResource = intention.titleResource
|
||||
if (intention == ListActivityIntention.VIEW) {
|
||||
titleResource = if (hiddenVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
|
||||
R.string.list_title_hidden
|
||||
} else if (favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
|
||||
R.string.list_title_favorite
|
||||
} else {
|
||||
R.string.list_title_view
|
||||
}
|
||||
titleResource =
|
||||
if (hiddenVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
|
||||
R.string.list_title_hidden
|
||||
} else if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
|
||||
R.string.list_title_private_space
|
||||
} else if (favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
|
||||
R.string.list_title_favorite
|
||||
} else {
|
||||
R.string.list_title_view
|
||||
}
|
||||
}
|
||||
|
||||
binding.listHeading.text = getString(titleResource)
|
||||
|
@ -161,6 +206,12 @@ class ListActivity : AppCompatActivity(), UIObject {
|
|||
|
||||
override fun setOnClicks() {
|
||||
binding.listClose.setOnClickListener { finish() }
|
||||
binding.listLock.setOnClickListener {
|
||||
togglePrivateSpaceLock(this)
|
||||
if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun adjustLayout() {
|
||||
|
@ -172,11 +223,11 @@ class ListActivity : AppCompatActivity(), UIObject {
|
|||
|
||||
updateTitle()
|
||||
|
||||
val sectionsPagerAdapter = ListSectionsPagerAdapter(this, supportFragmentManager)
|
||||
val viewPager: ViewPager = findViewById(R.id.list_viewpager)
|
||||
viewPager.adapter = sectionsPagerAdapter
|
||||
val tabs: TabLayout = findViewById(R.id.list_tabs)
|
||||
tabs.setupWithViewPager(viewPager)
|
||||
val sectionsPagerAdapter = ListSectionsPagerAdapter(this)
|
||||
binding.listViewpager.let {
|
||||
it.adapter = sectionsPagerAdapter
|
||||
binding.listTabs.setupWithViewPager(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,9 +239,15 @@ private val TAB_TITLES = arrayOf(
|
|||
/**
|
||||
* The [ListSectionsPagerAdapter] returns the fragment,
|
||||
* which corresponds to the selected tab in [ListActivity].
|
||||
*
|
||||
* This should eventually be replaced by a [FragmentStateAdapter]
|
||||
* However this keyboard does not open when using [ViewPager2]
|
||||
* so currently [ViewPager] is used here.
|
||||
* https://github.com/jrpie/launcher/issues/130
|
||||
*/
|
||||
class ListSectionsPagerAdapter(private val context: Context, fm: FragmentManager) :
|
||||
FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||
@Suppress("deprecation")
|
||||
class ListSectionsPagerAdapter(private val activity: ListActivity) :
|
||||
FragmentPagerAdapter(activity.supportFragmentManager) {
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
return when (position) {
|
||||
|
@ -201,11 +258,11 @@ class ListSectionsPagerAdapter(private val context: Context, fm: FragmentManager
|
|||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return context.resources.getString(TAB_TITLES[position])
|
||||
return activity.resources.getString(TAB_TITLES[position])
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return when (intention) {
|
||||
return when (activity.intention) {
|
||||
ListActivity.ListActivityIntention.VIEW -> 1
|
||||
else -> 2
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package de.jrpie.android.launcher.ui.list.apps
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -15,12 +14,12 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import de.jrpie.android.launcher.Application
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
|
||||
import de.jrpie.android.launcher.actions.AppAction
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.AppFilter
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.DetailedAppInfo
|
||||
import de.jrpie.android.launcher.getUserFromId
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.preferences.ListLayout
|
||||
import de.jrpie.android.launcher.ui.list.ListActivity
|
||||
|
@ -47,7 +46,8 @@ class AppsRecyclerAdapter(
|
|||
RecyclerView.Adapter<AppsRecyclerAdapter.ViewHolder>() {
|
||||
|
||||
private val apps = (activity.applicationContext as Application).apps
|
||||
private val appsListDisplayed: MutableList<DetailedAppInfo> = mutableListOf()
|
||||
private val appsListDisplayed: MutableList<AbstractDetailedAppInfo> = mutableListOf()
|
||||
private val grayscale = LauncherPreferences.theme().monochromeIcons()
|
||||
|
||||
// temporarily disable auto launch
|
||||
var disableAutoLaunch: Boolean = false
|
||||
|
@ -68,7 +68,7 @@ class AppsRecyclerAdapter(
|
|||
override fun onClick(v: View) {
|
||||
val rect = Rect()
|
||||
img.getGlobalVisibleRect(rect)
|
||||
selectItem(adapterPosition, rect)
|
||||
selectItem(bindingAdapterPosition, rect)
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -80,20 +80,19 @@ class AppsRecyclerAdapter(
|
|||
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
|
||||
var appLabel = appsListDisplayed[i].getCustomLabel(activity)
|
||||
|
||||
val appIcon = appsListDisplayed[i].getIcon(activity)
|
||||
|
||||
viewHolder.img.transformGrayscale(grayscale)
|
||||
viewHolder.img.setImageDrawable(appIcon.constantState?.newDrawable() ?: appIcon)
|
||||
|
||||
if (layout.useBadgedText) {
|
||||
appLabel = activity.packageManager.getUserBadgedLabel(
|
||||
appLabel,
|
||||
getUserFromId(appsListDisplayed[i].app.user, activity)
|
||||
appsListDisplayed[i].getUser(activity)
|
||||
).toString()
|
||||
}
|
||||
|
||||
val appIcon = appsListDisplayed[i].icon
|
||||
|
||||
viewHolder.textView.text = appLabel
|
||||
viewHolder.img.setImageDrawable(appIcon)
|
||||
|
||||
if (LauncherPreferences.theme().monochromeIcons())
|
||||
viewHolder.img.transformGrayscale()
|
||||
|
||||
// decide when to show the options popup menu about
|
||||
if (intention == ListActivity.ListActivityIntention.VIEW) {
|
||||
|
@ -118,22 +117,26 @@ class AppsRecyclerAdapter(
|
|||
@Suppress("SameReturnValue")
|
||||
private fun showOptionsPopup(
|
||||
viewHolder: ViewHolder,
|
||||
appInfo: DetailedAppInfo
|
||||
appInfo: AbstractDetailedAppInfo
|
||||
): Boolean {
|
||||
//create the popup menu
|
||||
|
||||
val popup = PopupMenu(activity, viewHolder.img)
|
||||
popup.inflate(R.menu.menu_app)
|
||||
|
||||
if (appInfo.isSystemApp) {
|
||||
if (!appInfo.isRemovable()) {
|
||||
popup.menu.findItem(R.id.app_menu_delete).setVisible(false)
|
||||
}
|
||||
|
||||
if (LauncherPreferences.apps().hidden()?.contains(appInfo.app) == true) {
|
||||
if (appInfo !is DetailedAppInfo) {
|
||||
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)
|
||||
}
|
||||
|
||||
if (LauncherPreferences.apps().favorites()?.contains(appInfo.app) == true) {
|
||||
if (LauncherPreferences.apps().favorites()?.contains(appInfo.getRawInfo()) == true) {
|
||||
popup.menu.findItem(R.id.app_menu_favorite).setTitle(R.string.list_app_favorite_remove)
|
||||
}
|
||||
|
||||
|
@ -141,19 +144,19 @@ class AppsRecyclerAdapter(
|
|||
popup.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.app_menu_delete -> {
|
||||
appInfo.app.uninstall(activity); true
|
||||
appInfo.getRawInfo().uninstall(activity); true
|
||||
}
|
||||
|
||||
R.id.app_menu_info -> {
|
||||
appInfo.app.openSettings(activity); true
|
||||
(appInfo.getRawInfo() as? AppInfo)?.openSettings(activity); true
|
||||
}
|
||||
|
||||
R.id.app_menu_favorite -> {
|
||||
appInfo.app.toggleFavorite(); true
|
||||
appInfo.getRawInfo().toggleFavorite(); true
|
||||
}
|
||||
|
||||
R.id.app_menu_hidden -> {
|
||||
appInfo.app.toggleHidden(root); true
|
||||
appInfo.getRawInfo().toggleHidden(root); true
|
||||
}
|
||||
|
||||
R.id.app_menu_rename -> {
|
||||
|
@ -188,15 +191,14 @@ class AppsRecyclerAdapter(
|
|||
val appInfo = appsListDisplayed[pos]
|
||||
when (intention) {
|
||||
ListActivity.ListActivityIntention.VIEW -> {
|
||||
AppAction(appInfo.app).invoke(activity, rect)
|
||||
appInfo.getAction().invoke(activity, rect)
|
||||
}
|
||||
|
||||
ListActivity.ListActivityIntention.PICK -> {
|
||||
val returnIntent = Intent()
|
||||
AppAction(appInfo.app).writeToIntent(returnIntent)
|
||||
returnIntent.putExtra("forGesture", forGesture)
|
||||
activity.setResult(REQUEST_CHOOSE_APP, returnIntent)
|
||||
activity.finish()
|
||||
forGesture ?: return
|
||||
val gesture = Gesture.byId(forGesture) ?: return
|
||||
Action.setActionForGesture(gesture, appInfo.getAction())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -211,8 +213,8 @@ class AppsRecyclerAdapter(
|
|||
&& !disableAutoLaunch
|
||||
&& LauncherPreferences.functionality().searchAutoLaunch()
|
||||
) {
|
||||
val info = appsListDisplayed[0]
|
||||
AppAction(info.app).invoke(activity)
|
||||
val app = appsListDisplayed[0]
|
||||
app.getAction().invoke(activity)
|
||||
|
||||
val inputMethodManager =
|
||||
activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package de.jrpie.android.launcher.ui.list.apps
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.LauncherApps
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
|
@ -13,11 +13,13 @@ import android.widget.EditText
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.REQUEST_UNINSTALL
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.DetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
|
||||
import de.jrpie.android.launcher.getUserFromId
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import androidx.core.net.toUri
|
||||
|
||||
private const val LOG_TAG = "AppContextMenu"
|
||||
|
||||
|
@ -32,27 +34,29 @@ fun AppInfo.openSettings(
|
|||
}
|
||||
}
|
||||
|
||||
fun AppInfo.uninstall(activity: android.app.Activity) {
|
||||
val packageName = this.packageName.toString()
|
||||
val userId = this.user
|
||||
fun AbstractAppInfo.uninstall(activity: Activity) {
|
||||
if (this is AppInfo) {
|
||||
val packageName = this.packageName
|
||||
val userId = this.user
|
||||
|
||||
Log.i(LOG_TAG, "uninstalling $this")
|
||||
Log.i(LOG_TAG, "uninstalling $this")
|
||||
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE)
|
||||
intent.data = Uri.parse("package:$packageName")
|
||||
getUserFromId(userId, activity).let { user ->
|
||||
intent.putExtra(Intent.EXTRA_USER, user)
|
||||
val intent = Intent(Intent.ACTION_DELETE)
|
||||
intent.data = "package:$packageName".toUri()
|
||||
getUserFromId(userId, activity).let { user ->
|
||||
intent.putExtra(Intent.EXTRA_USER, user)
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
|
||||
} else if(this is PinnedShortcutInfo) {
|
||||
val pinned = LauncherPreferences.apps().pinnedShortcuts() ?: mutableSetOf()
|
||||
pinned.remove(this)
|
||||
LauncherPreferences.apps().pinnedShortcuts(pinned)
|
||||
}
|
||||
|
||||
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||
activity.startActivityForResult(
|
||||
intent,
|
||||
REQUEST_UNINSTALL
|
||||
)
|
||||
}
|
||||
|
||||
fun AppInfo.toggleFavorite() {
|
||||
val favorites: MutableSet<AppInfo> =
|
||||
fun AbstractAppInfo.toggleFavorite() {
|
||||
val favorites: MutableSet<AbstractAppInfo> =
|
||||
LauncherPreferences.apps().favorites() ?: mutableSetOf()
|
||||
|
||||
if (favorites.contains(this)) {
|
||||
|
@ -69,8 +73,8 @@ fun AppInfo.toggleFavorite() {
|
|||
/**
|
||||
* @param view: used to show a snackbar letting the user undo the action
|
||||
*/
|
||||
fun AppInfo.toggleHidden(view: View) {
|
||||
val hidden: MutableSet<AppInfo> =
|
||||
fun AbstractAppInfo.toggleHidden(view: View) {
|
||||
val hidden: MutableSet<AbstractAppInfo> =
|
||||
LauncherPreferences.apps().hidden() ?: mutableSetOf()
|
||||
if (hidden.contains(this)) {
|
||||
hidden.remove(this)
|
||||
|
@ -87,12 +91,12 @@ fun AppInfo.toggleHidden(view: View) {
|
|||
LauncherPreferences.apps().hidden(hidden)
|
||||
}
|
||||
|
||||
fun DetailedAppInfo.showRenameDialog(context: Context) {
|
||||
fun AbstractDetailedAppInfo.showRenameDialog(context: Context) {
|
||||
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
|
||||
setTitle(context.getString(R.string.dialog_rename_title, label))
|
||||
setTitle(context.getString(R.string.dialog_rename_title, getLabel()))
|
||||
setView(R.layout.dialog_rename_app)
|
||||
setNegativeButton(R.string.dialog_cancel) { d, _ -> d.cancel() }
|
||||
setPositiveButton(R.string.dialog_rename_ok) { d, _ ->
|
||||
setNegativeButton(android.R.string.cancel) { d, _ -> d.cancel() }
|
||||
setPositiveButton(android.R.string.ok) { d, _ ->
|
||||
setCustomLabel(
|
||||
(d as? AlertDialog)
|
||||
?.findViewById<EditText>(R.id.dialog_rename_app_edit_text)
|
||||
|
@ -102,7 +106,7 @@ fun DetailedAppInfo.showRenameDialog(context: Context) {
|
|||
}.create().also { it.show() }.apply {
|
||||
val input = findViewById<EditText>(R.id.dialog_rename_app_edit_text)
|
||||
input?.setText(getCustomLabel(context))
|
||||
input?.hint = label
|
||||
input?.hint = getLabel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
package de.jrpie.android.launcher.ui.list.apps
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.apps.AppFilter
|
||||
import de.jrpie.android.launcher.databinding.ListAppsBinding
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
import de.jrpie.android.launcher.ui.list.ListActivity
|
||||
import de.jrpie.android.launcher.ui.list.favoritesVisibility
|
||||
import de.jrpie.android.launcher.ui.list.forGesture
|
||||
import de.jrpie.android.launcher.ui.list.hiddenVisibility
|
||||
import de.jrpie.android.launcher.ui.list.intention
|
||||
import de.jrpie.android.launcher.ui.openSoftKeyboard
|
||||
|
||||
|
||||
|
@ -49,7 +49,7 @@ class ListFragmentApps : Fragment(), UIObject {
|
|||
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
|
||||
binding.listAppsCheckBoxFavorites.isChecked =
|
||||
(favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE)
|
||||
((activity as? ListActivity)?.favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
@ -62,24 +62,33 @@ class ListFragmentApps : Fragment(), UIObject {
|
|||
override fun setOnClicks() {}
|
||||
|
||||
override fun adjustLayout() {
|
||||
val listActivity = activity as? ListActivity ?: return
|
||||
|
||||
appsRecyclerAdapter =
|
||||
AppsRecyclerAdapter(
|
||||
requireActivity(), binding.root, intention, forGesture,
|
||||
listActivity, binding.root, listActivity.intention, listActivity.forGesture,
|
||||
appFilter = AppFilter(
|
||||
requireContext(),
|
||||
"",
|
||||
favoritesVisibility = favoritesVisibility,
|
||||
hiddenVisibility = hiddenVisibility
|
||||
favoritesVisibility = listActivity.favoritesVisibility,
|
||||
privateSpaceVisibility = listActivity.privateSpaceVisibility,
|
||||
hiddenVisibility = listActivity.hiddenVisibility
|
||||
),
|
||||
layout = LauncherPreferences.list().layout()
|
||||
)
|
||||
|
||||
|
||||
// set up the list / recycler
|
||||
binding.listAppsRview.apply {
|
||||
// improve performance (since content changes don't change the layout size)
|
||||
setHasFixedSize(true)
|
||||
layoutManager = LauncherPreferences.list().layout().layoutManager(context)
|
||||
.also {
|
||||
if (LauncherPreferences.list().reverseLayout()) {
|
||||
(it as? LinearLayoutManager)?.reverseLayout = true
|
||||
(it as? GridLayoutManager)?.reverseLayout = true
|
||||
}
|
||||
}
|
||||
adapter = appsRecyclerAdapter
|
||||
}
|
||||
|
||||
|
@ -91,7 +100,16 @@ class ListFragmentApps : Fragment(), UIObject {
|
|||
|
||||
if (LauncherPreferences.functionality().searchWeb()) {
|
||||
val i = Intent(Intent.ACTION_WEB_SEARCH).putExtra("query", query)
|
||||
activity?.startActivity(i)
|
||||
try {
|
||||
activity?.startActivity(i)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.toast_activity_not_found_search_web,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
} else {
|
||||
appsRecyclerAdapter.selectItem(0)
|
||||
}
|
||||
|
@ -102,7 +120,8 @@ class ListFragmentApps : Fragment(), UIObject {
|
|||
|
||||
if (newText == " " &&
|
||||
!appsRecyclerAdapter.disableAutoLaunch &&
|
||||
intention == ListActivity.ListActivityIntention.VIEW &&
|
||||
(activity as? ListActivity)?.intention
|
||||
== ListActivity.ListActivityIntention.VIEW &&
|
||||
LauncherPreferences.functionality().searchAutoLaunch()
|
||||
) {
|
||||
appsRecyclerAdapter.disableAutoLaunch = true
|
||||
|
@ -119,17 +138,17 @@ class ListFragmentApps : Fragment(), UIObject {
|
|||
})
|
||||
|
||||
binding.listAppsCheckBoxFavorites.setOnClickListener {
|
||||
favoritesVisibility =
|
||||
listActivity.favoritesVisibility =
|
||||
if (binding.listAppsCheckBoxFavorites.isChecked) {
|
||||
AppFilter.Companion.AppSetVisibility.EXCLUSIVE
|
||||
} else {
|
||||
AppFilter.Companion.AppSetVisibility.VISIBLE
|
||||
}
|
||||
appsRecyclerAdapter.setFavoritesVisibility(favoritesVisibility)
|
||||
appsRecyclerAdapter.setFavoritesVisibility(listActivity.favoritesVisibility)
|
||||
(activity as? ListActivity)?.updateTitle()
|
||||
}
|
||||
|
||||
if (intention == ListActivity.ListActivityIntention.VIEW
|
||||
if (listActivity.intention == ListActivity.ListActivityIntention.VIEW
|
||||
&& LauncherPreferences.functionality().searchAutoOpenKeyboard()
|
||||
) {
|
||||
binding.listAppsSearchview.openSoftKeyboard(requireContext())
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package de.jrpie.android.launcher.ui.list.other
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -9,9 +8,10 @@ import android.widget.ImageView
|
|||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.actions.LauncherAction
|
||||
import de.jrpie.android.launcher.ui.list.forGesture
|
||||
import de.jrpie.android.launcher.ui.list.ListActivity
|
||||
|
||||
/**
|
||||
* The [OtherRecyclerAdapter] will only be displayed in the ListActivity,
|
||||
|
@ -23,7 +23,8 @@ import de.jrpie.android.launcher.ui.list.forGesture
|
|||
class OtherRecyclerAdapter(val activity: Activity) :
|
||||
RecyclerView.Adapter<OtherRecyclerAdapter.ViewHolder>() {
|
||||
|
||||
private val othersList: Array<LauncherAction> = LauncherAction.values()
|
||||
private val othersList: Array<LauncherAction> =
|
||||
LauncherAction.entries.filter { it.isAvailable(activity) }.toTypedArray()
|
||||
|
||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
|
||||
View.OnClickListener {
|
||||
|
@ -32,10 +33,13 @@ class OtherRecyclerAdapter(val activity: Activity) :
|
|||
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val pos = adapterPosition
|
||||
val pos = bindingAdapterPosition
|
||||
val content = othersList[pos]
|
||||
|
||||
forGesture?.let { returnChoiceIntent(it, content) }
|
||||
activity.finish()
|
||||
val gestureId = (activity as? ListActivity)?.forGesture ?: return
|
||||
val gesture = Gesture.byId(gestureId) ?: return
|
||||
Action.setActionForGesture(gesture, content)
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -60,12 +64,4 @@ class OtherRecyclerAdapter(val activity: Activity) :
|
|||
val view: View = inflater.inflate(R.layout.list_other_row, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
private fun returnChoiceIntent(forGesture: String, action: LauncherAction) {
|
||||
val returnIntent = Intent()
|
||||
returnIntent.putExtra("forGesture", forGesture)
|
||||
action.writeToIntent(returnIntent)
|
||||
activity.setResult(REQUEST_CHOOSE_APP, returnIntent)
|
||||
activity.finish()
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package de.jrpie.android.launcher.ui.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
|
@ -8,15 +7,14 @@ import android.os.Bundle
|
|||
import android.provider.Settings
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
|
||||
import de.jrpie.android.launcher.databinding.SettingsBinding
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.saveListActivityChoice
|
||||
import de.jrpie.android.launcher.preferences.theme.Background
|
||||
import de.jrpie.android.launcher.preferences.theme.ColorTheme
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
import de.jrpie.android.launcher.ui.settings.actions.SettingsFragmentActions
|
||||
import de.jrpie.android.launcher.ui.settings.launcher.SettingsFragmentLauncher
|
||||
|
@ -33,13 +31,29 @@ import de.jrpie.android.launcher.ui.settings.meta.SettingsFragmentMeta
|
|||
*/
|
||||
class SettingsActivity : AppCompatActivity(), UIObject {
|
||||
|
||||
private var sharedPreferencesListener =
|
||||
private val solidBackground = LauncherPreferences.theme().background() == Background.SOLID
|
||||
|| LauncherPreferences.theme().colorTheme() == ColorTheme.LIGHT
|
||||
|
||||
private val sharedPreferencesListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
|
||||
if (prefKey?.startsWith("theme.") == true ||
|
||||
prefKey?.startsWith("display.") == true
|
||||
if (solidBackground &&
|
||||
(prefKey == LauncherPreferences.theme().keys().background() ||
|
||||
prefKey == LauncherPreferences.theme().keys().colorTheme())
|
||||
) {
|
||||
recreate()
|
||||
}
|
||||
// Switching from solid background to a transparent background using `recreate()`
|
||||
// causes a very ugly glitch, making the settings unreadable.
|
||||
// This ugly workaround causes a jump to the top of the list, but at least
|
||||
// the text stays readable.
|
||||
val i = Intent(this, SettingsActivity::class.java)
|
||||
.also { it.putExtra(EXTRA_TAB, 1) }
|
||||
finish()
|
||||
startActivity(i)
|
||||
} else
|
||||
if (prefKey?.startsWith("theme.") == true ||
|
||||
prefKey?.startsWith("display.") == true
|
||||
) {
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
private lateinit var binding: SettingsBinding
|
||||
|
||||
|
@ -53,12 +67,14 @@ class SettingsActivity : AppCompatActivity(), UIObject {
|
|||
setContentView(binding.root)
|
||||
|
||||
// set up tabs and swiping in settings
|
||||
val sectionsPagerAdapter = SettingsSectionsPagerAdapter(this, supportFragmentManager)
|
||||
val viewPager: ViewPager = findViewById(R.id.settings_viewpager)
|
||||
viewPager.adapter = sectionsPagerAdapter
|
||||
|
||||
val tabs: TabLayout = findViewById(R.id.settings_tabs)
|
||||
tabs.setupWithViewPager(viewPager)
|
||||
val sectionsPagerAdapter = SettingsSectionsPagerAdapter(this)
|
||||
binding.settingsViewpager.apply {
|
||||
adapter = sectionsPagerAdapter
|
||||
setCurrentItem(intent.getIntExtra(EXTRA_TAB, 0), false)
|
||||
}
|
||||
TabLayoutMediator(binding.settingsTabs, binding.settingsViewpager) { tab, position ->
|
||||
tab.text = sectionsPagerAdapter.getPageTitle(position)
|
||||
}.attach()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -87,24 +103,21 @@ class SettingsActivity : AppCompatActivity(), UIObject {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_CHOOSE_APP -> saveListActivityChoice(data)
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
companion object {
|
||||
private const val EXTRA_TAB = "tab"
|
||||
}
|
||||
}
|
||||
|
||||
private val TAB_TITLES = arrayOf(
|
||||
R.string.settings_tab_app,
|
||||
R.string.settings_tab_actions,
|
||||
R.string.settings_tab_launcher,
|
||||
R.string.settings_tab_meta
|
||||
)
|
||||
|
||||
class SettingsSectionsPagerAdapter(private val context: Context, fm: FragmentManager) :
|
||||
FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||
class SettingsSectionsPagerAdapter(private val activity: FragmentActivity) :
|
||||
FragmentStateAdapter(activity) {
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> SettingsFragmentActions()
|
||||
1 -> SettingsFragmentLauncher()
|
||||
|
@ -113,11 +126,11 @@ class SettingsSectionsPagerAdapter(private val context: Context, fm: FragmentMan
|
|||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return context.resources.getString(TAB_TITLES[position])
|
||||
fun getPageTitle(position: Int): CharSequence {
|
||||
return activity.resources.getString(TAB_TITLES[position])
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
override fun getItemCount(): Int {
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,11 @@ import android.view.ViewGroup
|
|||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.apps.AppFilter
|
||||
|
@ -94,6 +94,8 @@ class SettingsFragmentActionsRecycler : Fragment(), UIObject {
|
|||
class ActionsRecyclerAdapter(val activity: Activity) :
|
||||
RecyclerView.Adapter<ActionsRecyclerAdapter.ViewHolder>() {
|
||||
|
||||
private val drawableUnknown = AppCompatResources.getDrawable(activity, R.drawable.baseline_question_mark_24)
|
||||
|
||||
private val gesturesList: ArrayList<Gesture> =
|
||||
Gesture.entries.filter(Gesture::isEnabled) as ArrayList<Gesture>
|
||||
|
||||
|
@ -115,15 +117,18 @@ class ActionsRecyclerAdapter(val activity: Activity) :
|
|||
|
||||
private fun updateViewHolder(gesture: Gesture, viewHolder: ViewHolder) {
|
||||
val action = Action.forGesture(gesture)
|
||||
val drawable = action?.getIcon(activity)
|
||||
|
||||
if (action == null || drawable == null) {
|
||||
if (action == null) {
|
||||
viewHolder.img.visibility = View.INVISIBLE
|
||||
viewHolder.removeAction.visibility = View.GONE
|
||||
viewHolder.chooseButton.visibility = View.VISIBLE
|
||||
return
|
||||
}
|
||||
|
||||
// Use the unknown icon if there is an action, but we can't find its icon.
|
||||
// Probably an app was uninstalled.
|
||||
val drawable = action.getIcon(activity) ?: drawableUnknown
|
||||
|
||||
viewHolder.img.visibility = View.VISIBLE
|
||||
viewHolder.removeAction.visibility = View.VISIBLE
|
||||
viewHolder.chooseButton.visibility = View.INVISIBLE
|
||||
|
@ -137,9 +142,7 @@ class ActionsRecyclerAdapter(val activity: Activity) :
|
|||
val description = gesture.getDescription(activity)
|
||||
viewHolder.descriptionTextView.text = description
|
||||
|
||||
|
||||
if (LauncherPreferences.theme().monochromeIcons())
|
||||
viewHolder.img.transformGrayscale()
|
||||
viewHolder.img.transformGrayscale(LauncherPreferences.theme().monochromeIcons())
|
||||
|
||||
updateViewHolder(gesture, viewHolder)
|
||||
viewHolder.img.setOnClickListener { chooseApp(gesture) }
|
||||
|
@ -175,9 +178,6 @@ class ActionsRecyclerAdapter(val activity: Activity) :
|
|||
intent.putExtra("intention", ListActivity.ListActivityIntention.PICK.toString())
|
||||
intent.putExtra("hiddenVisibility", AppFilter.Companion.AppSetVisibility.VISIBLE)
|
||||
intent.putExtra("forGesture", gesture.id) // for which action we choose the app
|
||||
activity.startActivityForResult(
|
||||
intent,
|
||||
REQUEST_CHOOSE_APP
|
||||
)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,17 @@ class SettingsFragmentLauncher : PreferenceFragmentCompat() {
|
|||
)
|
||||
val timeVisible = LauncherPreferences.clock().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() {
|
||||
|
|
|
@ -2,19 +2,23 @@ package de.jrpie.android.launcher.ui.settings.meta
|
|||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.BuildConfig
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.copyToClipboard
|
||||
import de.jrpie.android.launcher.databinding.SettingsMetaBinding
|
||||
import de.jrpie.android.launcher.getDeviceInfo
|
||||
import de.jrpie.android.launcher.openInBrowser
|
||||
import de.jrpie.android.launcher.openTutorial
|
||||
import de.jrpie.android.launcher.preferences.resetPreferences
|
||||
import de.jrpie.android.launcher.ui.LegalInfoActivity
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
|
||||
|
||||
/**
|
||||
* The [SettingsFragmentMeta] is a used as a tab in the SettingsActivity.
|
||||
|
@ -40,25 +44,19 @@ class SettingsFragmentMeta : Fragment(), UIObject {
|
|||
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() {
|
||||
|
||||
fun bindURL(view: View, urlRes: Int) {
|
||||
view.setOnClickListener {
|
||||
openInBrowser(
|
||||
getString(urlRes),
|
||||
requireContext()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
binding.settingsMetaButtonViewTutorial.setOnClickListener {
|
||||
startActivity(Intent(this.context, TutorialActivity::class.java))
|
||||
openTutorial(requireContext())
|
||||
}
|
||||
|
||||
// prompting for settings-reset confirmation
|
||||
|
@ -79,60 +77,62 @@ class SettingsFragmentMeta : Fragment(), UIObject {
|
|||
|
||||
|
||||
// view code
|
||||
binding.settingsMetaButtonViewCode.setOnClickListener {
|
||||
openInBrowser(
|
||||
getString(R.string.settings_meta_link_github),
|
||||
requireContext()
|
||||
)
|
||||
}
|
||||
bindURL(binding.settingsMetaButtonViewCode, R.string.settings_meta_link_github)
|
||||
|
||||
// report a bug
|
||||
binding.settingsMetaButtonReportBug.setOnClickListener {
|
||||
openInBrowser(
|
||||
getString(R.string.settings_meta_report_bug_link),
|
||||
requireContext()
|
||||
)
|
||||
val deviceInfo = getDeviceInfo()
|
||||
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
|
||||
setView(R.layout.dialog_report_bug)
|
||||
setTitle(R.string.dialog_report_bug_title)
|
||||
setPositiveButton(R.string.dialog_report_bug_create_report) { _, _ ->
|
||||
openInBrowser(
|
||||
getString(R.string.settings_meta_report_bug_link),
|
||||
requireContext()
|
||||
)
|
||||
}
|
||||
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
|
||||
}.create().also { it.show() }.apply {
|
||||
val info = findViewById<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
|
||||
binding.settingsMetaButtonJoinChat.setOnClickListener {
|
||||
openInBrowser(
|
||||
getString(R.string.settings_meta_chat_url),
|
||||
requireContext()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
bindURL(binding.settingsMetaButtonJoinChat, R.string.settings_meta_chat_url)
|
||||
|
||||
// contact developer
|
||||
binding.settingsMetaButtonContact.setOnClickListener {
|
||||
openInBrowser(
|
||||
getString(R.string.settings_meta_contact_url),
|
||||
requireContext()
|
||||
)
|
||||
}
|
||||
// bindURL(binding.settingsMetaButtonContact, R.string.settings_meta_contact_url)
|
||||
|
||||
// contact fork developer
|
||||
binding.settingsMetaButtonForkContact.setOnClickListener {
|
||||
openInBrowser(
|
||||
getString(R.string.settings_meta_fork_contact_url),
|
||||
requireContext()
|
||||
)
|
||||
}
|
||||
bindURL(binding.settingsMetaButtonForkContact, R.string.settings_meta_fork_contact_url)
|
||||
|
||||
// donate
|
||||
bindURL(binding.settingsMetaButtonDonate, R.string.settings_meta_donate_url)
|
||||
|
||||
// privacy policy
|
||||
binding.settingsMetaButtonPrivacy.setOnClickListener {
|
||||
openInBrowser(
|
||||
getString(R.string.settings_meta_privacy_url),
|
||||
requireContext()
|
||||
)
|
||||
}
|
||||
bindURL(binding.settingsMetaButtonPrivacy, R.string.settings_meta_privacy_url)
|
||||
|
||||
// legal info
|
||||
binding.settingsMetaButtonLicenses.setOnClickListener {
|
||||
startActivity(Intent(this.context, LegalInfoActivity::class.java))
|
||||
}
|
||||
|
||||
binding.settingsMetaTextVersion.text = BuildConfig.VERSION_NAME
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
package de.jrpie.android.launcher.ui.tutorial
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import de.jrpie.android.launcher.databinding.TutorialBinding
|
||||
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.tutorial.tabs.TutorialFragmentConcept
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentFinish
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentSetup
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentStart
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentUsage
|
||||
import de.jrpie.android.launcher.ui.blink
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment0Start
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment1Concept
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment2Usage
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment3AppList
|
||||
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.
|
||||
|
@ -29,19 +31,75 @@ import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentUsage
|
|||
*/
|
||||
class TutorialActivity : AppCompatActivity(), UIObject {
|
||||
|
||||
private lateinit var binding: TutorialBinding
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super<AppCompatActivity>.onCreate(savedInstanceState)
|
||||
super<UIObject>.onCreate()
|
||||
|
||||
// Initialise layout
|
||||
setContentView(R.layout.tutorial)
|
||||
binding = TutorialBinding.inflate(layoutInflater)
|
||||
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
|
||||
val sectionsPagerAdapter = TutorialSectionsPagerAdapter(supportFragmentManager)
|
||||
val viewPager: ViewPager = findViewById(R.id.tutorial_viewpager)
|
||||
viewPager.adapter = sectionsPagerAdapter
|
||||
val tabs: TabLayout = findViewById(R.id.tutorial_tabs)
|
||||
tabs.setupWithViewPager(viewPager)
|
||||
val sectionsPagerAdapter = TutorialSectionsPagerAdapter(this)
|
||||
binding.tutorialViewpager.apply {
|
||||
adapter = sectionsPagerAdapter
|
||||
currentItem = 0
|
||||
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
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 {
|
||||
|
@ -53,14 +111,9 @@ class TutorialActivity : AppCompatActivity(), UIObject {
|
|||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_CHOOSE_APP -> saveListActivityChoice(data)
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
// Default: prevent going back, allow if viewed again later
|
||||
// prevent going back when the tutorial is shown for the first time
|
||||
@Deprecated("Deprecated in Java", ReplaceWith("use anyway"))
|
||||
@Suppress("deprecation") // support API level < 33
|
||||
override fun onBackPressed() {
|
||||
if (LauncherPreferences.internal().started())
|
||||
super.onBackPressed()
|
||||
|
@ -74,26 +127,22 @@ class TutorialActivity : AppCompatActivity(), UIObject {
|
|||
*
|
||||
* Tabs: (Start | Concept | Usage | Setup | Finish)
|
||||
*/
|
||||
class TutorialSectionsPagerAdapter(fm: FragmentManager) :
|
||||
FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||
class TutorialSectionsPagerAdapter(activity: FragmentActivity) :
|
||||
FragmentStateAdapter(activity) {
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
override fun getItemCount(): Int {
|
||||
return 6
|
||||
}
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> TutorialFragmentStart()
|
||||
1 -> TutorialFragmentConcept()
|
||||
2 -> TutorialFragmentUsage()
|
||||
3 -> TutorialFragmentSetup()
|
||||
4 -> TutorialFragmentFinish()
|
||||
0 -> TutorialFragment0Start()
|
||||
1 -> TutorialFragment1Concept()
|
||||
2 -> TutorialFragment2Usage()
|
||||
3 -> TutorialFragment3AppList()
|
||||
4 -> TutorialFragment4Setup()
|
||||
5 -> TutorialFragment5Finish()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,24 +5,22 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.databinding.TutorialStartBinding
|
||||
import de.jrpie.android.launcher.databinding.Tutorial0StartBinding
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
import de.jrpie.android.launcher.ui.blink
|
||||
|
||||
/**
|
||||
* The [TutorialFragmentStart] is a used as a tab in the TutorialActivity.
|
||||
* The [TutorialFragment0Start] is a used as a tab in the TutorialActivity.
|
||||
*
|
||||
* It displays info about the app and gets the user into the tutorial
|
||||
*/
|
||||
class TutorialFragmentStart : Fragment(), UIObject {
|
||||
class TutorialFragment0Start : Fragment(), UIObject {
|
||||
|
||||
private lateinit var binding: TutorialStartBinding
|
||||
private lateinit var binding: Tutorial0StartBinding
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = TutorialStartBinding.inflate(inflater, container, false)
|
||||
binding.tutorialStartIconRight.blink()
|
||||
binding = Tutorial0StartBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
|
@ -6,22 +6,22 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.BuildConfig
|
||||
import de.jrpie.android.launcher.databinding.TutorialConceptBinding
|
||||
import de.jrpie.android.launcher.databinding.Tutorial1ConceptBinding
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
||||
/**
|
||||
* The [TutorialFragmentConcept] is a used as a tab in the TutorialActivity.
|
||||
* The [TutorialFragment1Concept] is a used as a tab in the TutorialActivity.
|
||||
*
|
||||
* It is used to display info about Launchers concept (open source, efficiency ...)
|
||||
*/
|
||||
class TutorialFragmentConcept : Fragment(), UIObject {
|
||||
private lateinit var binding: TutorialConceptBinding
|
||||
class TutorialFragment1Concept : Fragment(), UIObject {
|
||||
private lateinit var binding: Tutorial1ConceptBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = TutorialConceptBinding.inflate(inflater, container, false)
|
||||
binding = Tutorial1ConceptBinding.inflate(inflater, container, false)
|
||||
binding.tutorialConceptBadgeVersion.text = BuildConfig.VERSION_NAME
|
||||
return binding.root
|
||||
}
|
|
@ -9,17 +9,17 @@ import de.jrpie.android.launcher.R
|
|||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
||||
/**
|
||||
* The [TutorialFragmentUsage] is a used as a tab in the TutorialActivity.
|
||||
* The [TutorialFragment2Usage] 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 TutorialFragmentUsage : Fragment(), UIObject {
|
||||
class TutorialFragment2Usage : Fragment(), UIObject {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.tutorial_usage, container, false)
|
||||
return inflater.inflate(R.layout.tutorial_2_usage, container, false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
|
@ -0,0 +1,30 @@
|
|||
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()
|
||||
}
|
||||
|
||||
}
|
|
@ -9,17 +9,17 @@ import de.jrpie.android.launcher.R
|
|||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
||||
/**
|
||||
* The [TutorialFragmentSetup] is a used as a tab in the TutorialActivity.
|
||||
* The [TutorialFragment4Setup] is a used as a tab in the TutorialActivity.
|
||||
*
|
||||
* It is used to display info in the tutorial
|
||||
*/
|
||||
class TutorialFragmentSetup : Fragment(), UIObject {
|
||||
class TutorialFragment4Setup : Fragment(), UIObject {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.tutorial_setup, container, false)
|
||||
return inflater.inflate(R.layout.tutorial_4_setup, container, false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
|
@ -6,25 +6,25 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.BuildConfig.VERSION_CODE
|
||||
import de.jrpie.android.launcher.databinding.TutorialFinishBinding
|
||||
import de.jrpie.android.launcher.databinding.Tutorial5FinishBinding
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.setDefaultHomeScreen
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
||||
/**
|
||||
* The [TutorialFragmentFinish] is a used as a tab in the TutorialActivity.
|
||||
* The [TutorialFragment5Finish] is a used as a tab in the TutorialActivity.
|
||||
*
|
||||
* It is used to display further resources and let the user start Launcher
|
||||
*/
|
||||
class TutorialFragmentFinish : Fragment(), UIObject {
|
||||
class TutorialFragment5Finish : Fragment(), UIObject {
|
||||
|
||||
private lateinit var binding: TutorialFinishBinding
|
||||
private lateinit var binding: Tutorial5FinishBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = TutorialFinishBinding.inflate(inflater, container, false)
|
||||
binding = Tutorial5FinishBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ class HtmlTextView(context: Context, attr: AttributeSet?, int: Int) :
|
|||
constructor(context: Context) : this(context, null, 0)
|
||||
|
||||
init {
|
||||
@Suppress("deprecation") // required to support API level < 24
|
||||
text = Html.fromHtml(text.toString())
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
|
BIN
app/src/main/res/drawable-mdpi/tutorial_app_list.png
Normal file
BIN
app/src/main/res/drawable-mdpi/tutorial_app_list.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 124 KiB |
BIN
app/src/main/res/drawable-mdpi/tutorial_home_screen.png
Normal file
BIN
app/src/main/res/drawable-mdpi/tutorial_home_screen.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
11
app/src/main/res/drawable/baseline_apps_24.xml
Normal file
11
app/src/main/res/drawable/baseline_apps_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<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>
|
|
@ -1,7 +1,6 @@
|
|||
<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">
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<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">
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?android:textColor"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<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">
|
||||
|
||||
|
|
11
app/src/main/res/drawable/baseline_home_24.xml
Normal file
11
app/src/main/res/drawable/baseline_home_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<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>
|
9
app/src/main/res/drawable/baseline_lock_24.xml
Normal file
9
app/src/main/res/drawable/baseline_lock_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<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>
|
|
@ -1,10 +0,0 @@
|
|||
<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>
|
9
app/src/main/res/drawable/baseline_lock_open_24.xml
Normal file
9
app/src/main/res/drawable/baseline_lock_open_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<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>
|
|
@ -1,5 +1,11 @@
|
|||
<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">
|
||||
<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="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
|
||||
<path
|
||||
android:fillColor="?android:textColor"
|
||||
android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z" />
|
||||
|
||||
</vector>
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<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>
|
11
app/src/main/res/drawable/baseline_navigate_before_24.xml
Normal file
11
app/src/main/res/drawable/baseline_navigate_before_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<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>
|
11
app/src/main/res/drawable/baseline_navigate_next_24.xml
Normal file
11
app/src/main/res/drawable/baseline_navigate_next_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<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>
|
|
@ -1,7 +1,6 @@
|
|||
<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">
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<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">
|
||||
|
||||
|
|
12
app/src/main/res/drawable/baseline_play_arrow_24.xml
Normal file
12
app/src/main/res/drawable/baseline_play_arrow_24.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<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>
|
12
app/src/main/res/drawable/baseline_question_mark_24.xml
Normal file
12
app/src/main/res/drawable/baseline_question_mark_24.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<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>
|
|
@ -1,7 +1,6 @@
|
|||
<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">
|
||||
|
||||
|
|
11
app/src/main/res/drawable/baseline_security_24.xml
Normal file
11
app/src/main/res/drawable/baseline_security_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<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>
|
|
@ -1,7 +1,6 @@
|
|||
<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">
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<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">
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<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">
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<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">
|
||||
|
||||
|
|
16
app/src/main/res/drawable/baseline_volume_adjust_24.xml
Normal file
16
app/src/main/res/drawable/baseline_volume_adjust_24.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<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>
|
|
@ -2,7 +2,6 @@
|
|||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 159 KiB |
|
@ -1,10 +0,0 @@
|
|||
<?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>
|
122
app/src/main/res/layout/activity_pin_shortcut.xml
Normal file
122
app/src/main/res/layout/activity_pin_shortcut.xml
Normal file
|
@ -0,0 +1,122 @@
|
|||
<?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>
|
53
app/src/main/res/layout/dialog_report_bug.xml
Normal file
53
app/src/main/res/layout/dialog_report_bug.xml
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?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>
|
27
app/src/main/res/layout/dialog_select_gesture.xml
Normal file
27
app/src/main/res/layout/dialog_select_gesture.xml
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?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>
|
53
app/src/main/res/layout/dialog_select_gesture_row.xml
Normal file
53
app/src/main/res/layout/dialog_select_gesture_row.xml
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?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>
|
|
@ -10,7 +10,7 @@
|
|||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.HomeActivity">
|
||||
|
||||
<TextView
|
||||
<TextClock
|
||||
android:id="@+id/home_upper_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -23,7 +23,7 @@
|
|||
app:layout_constraintVertical_bias="0.45"
|
||||
tools:text="2024-12-24" />
|
||||
|
||||
<TextView
|
||||
<TextClock
|
||||
android:id="@+id/home_lower_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -34,4 +34,18 @@
|
|||
app:layout_constraintEnd_toEndOf="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>
|
|
@ -32,7 +32,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="@string/settings"
|
||||
android:gravity="center"
|
||||
android:paddingLeft="16sp"
|
||||
|
@ -40,8 +40,7 @@
|
|||
android:src="@drawable/baseline_settings_24"
|
||||
custom:layout_constraintBottom_toBottomOf="parent"
|
||||
custom:layout_constraintStart_toStartOf="parent"
|
||||
custom:layout_constraintTop_toTopOf="parent"
|
||||
custom:type="solid" />
|
||||
custom:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/list_heading"
|
||||
|
@ -53,8 +52,8 @@
|
|||
android:text="@string/list_title_pick"
|
||||
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
|
||||
android:textSize="30sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/list_lock"
|
||||
app:layout_constraintStart_toEndOf="@id/list_settings"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
|
@ -62,7 +61,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="true"
|
||||
android:paddingLeft="16sp"
|
||||
|
@ -70,8 +69,21 @@
|
|||
android:src="@drawable/baseline_close_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
custom:type="solid" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
<ImageView
|
||||
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>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
|
@ -83,6 +95,11 @@
|
|||
|
||||
</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
|
||||
android:id="@+id/list_viewpager"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -15,18 +15,21 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@mipmap/ic_launcher_round"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/list_apps_row_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="60sp"
|
||||
android:layout_marginStart="20sp"
|
||||
android:gravity="start"
|
||||
android:text=""
|
||||
android:textSize="20sp"
|
||||
tools:text="@string/app_name"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/list_apps_row_icon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -15,6 +15,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:src="@mipmap/ic_launcher_round"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
|
@ -25,7 +26,7 @@
|
|||
android:paddingTop="5dp"
|
||||
android:text=""
|
||||
android:textSize="11sp"
|
||||
tools:text="some app"
|
||||
tools:text="@string/app_name"
|
||||
app:layout_constraintTop_toBottomOf="@id/list_apps_row_icon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -25,12 +25,13 @@
|
|||
android:gravity="start|center_horizontal"
|
||||
android:padding="5dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text=""
|
||||
android:textSize="20sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="some app" />
|
||||
tools:text="@string/app_name" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -22,9 +22,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="60sp"
|
||||
android:layout_marginLeft="60sp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:gravity="start"
|
||||
android:text=""
|
||||
android:textSize="20sp"
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
android:id="@+id/settings_close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="true"
|
||||
android:paddingLeft="16sp"
|
||||
|
@ -47,15 +47,14 @@
|
|||
android:src="@drawable/baseline_close_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
custom:type="solid" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/settings_system"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="true"
|
||||
android:paddingLeft="16sp"
|
||||
|
@ -63,8 +62,7 @@
|
|||
android:src="@drawable/baseline_settings_applications_24"
|
||||
custom:layout_constraintBottom_toBottomOf="parent"
|
||||
custom:layout_constraintStart_toStartOf="parent"
|
||||
custom:layout_constraintTop_toTopOf="parent"
|
||||
custom:type="solid" />
|
||||
custom:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
|
@ -75,7 +73,7 @@
|
|||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/settings_viewpager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/settings_meta_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:context=".ui.settings.meta.SettingsFragmentMeta">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -59,12 +59,12 @@
|
|||
android:text="@string/settings_meta_join_chat"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
<!--<Button
|
||||
android:id="@+id/settings_meta_button_contact"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_meta_contact"
|
||||
android:textAllCaps="false" />
|
||||
android:textAllCaps="false" />-->
|
||||
|
||||
<Button
|
||||
android:id="@+id/settings_meta_button_fork_contact"
|
||||
|
@ -73,6 +73,13 @@
|
|||
android:text="@string/settings_meta_fork_contact"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/settings_meta_button_donate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_meta_donate"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64sp" />
|
||||
|
@ -91,5 +98,16 @@
|
|||
android:text="@string/settings_meta_licenses"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="32sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/settings_meta_text_version"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="textEnd"
|
||||
tools:text="v1.2.3" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue