Compare commits

...

152 commits

Author SHA1 Message Date
653d16b269
new action: launch other launchers
Some checks failed
Android CI / build (push) Has been cancelled
2025-03-29 21:09:15 +01:00
5d695ec0ea
fix #135 2025-03-29 18:45:53 +01:00
b4608ef153
add new action: show recent apps
Some checks failed
Android CI / build (push) Has been cancelled
2025-03-24 13:21:58 +01:00
8e140e2e69
rename tab "Apps" to "Actions" and "Volume Up/Down" to "Volume Up/Down Key"
Some checks failed
Android CI / build (push) Has been cancelled
2025-03-20 16:23:01 +01:00
7fc58fe384
0.1.3 2025-03-20 15:52:12 +01:00
54409b6312
fix #133 2025-03-20 14:55:22 +01:00
865cd47583
0.1.2 2025-03-20 14:16:29 +01:00
58ddd3c8cc
fix #130 - revert part of 9043461 as ViewPager2 causes an issue with opening the keyboard 2025-03-20 14:09:00 +01:00
0baa889de5
0.1.1
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-19 17:55:35 +01:00
fa34cbae90
Merge pull request #128 from toolatebot/weblate-jrpie-launcher-launcher
Some checks are pending
Android CI / build (push) Waiting to run
Translations update from Toolate
2025-03-19 17:05:33 +01:00
7ac09bd465 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (254 of 254 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/
2025-03-18 19:30:19 +00:00
class0068
c8d7a1cc3e Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (254 of 254 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/
2025-03-18 19:30:19 +00:00
7094d55484 Translated using Weblate (German)
Currently translated at 100.0% (254 of 254 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/de/
2025-03-18 19:30:19 +00:00
b3e4d8834a
Merge pull request #127 from toolatebot/weblate-jrpie-launcher-launcher
Some checks failed
Android CI / build (push) Has been cancelled
Translations update from Toolate
2025-03-17 23:01:16 +01:00
008d0242ee Translated using Weblate (German)
Currently translated at 96.8% (246 of 254 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/de/
2025-03-17 17:07:17 +00:00
toolatebot
65034bf2fb Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/
2025-03-16 16:28:08 +00:00
Anonymous
3cec2c36c6 Translated using Weblate (German)
Currently translated at 92.7% (242 of 261 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/de/
2025-03-16 16:28:08 +00:00
00350d4c3a Translated using Weblate (German)
Currently translated at 92.7% (242 of 261 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/de/
2025-03-16 16:28:08 +00:00
0941062270
Merge pull request #121 from toolatebot/weblate-jrpie-launcher-launcher
Some checks failed
Android CI / build (push) Has been cancelled
Translations update from Toolate
2025-03-16 17:04:56 +01:00
c783a51658
upgrade AGP 2025-03-16 16:52:46 +01:00
da115bb2d9
refactor: remove unused stuff, fix lint warnings
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-16 16:37:39 +01:00
90434617e7
replace (ViewPager, FragmentPagerAdapter) by (ViewPager2, FragmentStateAdapter) 2025-03-16 04:12:34 +01:00
47940811b4
fix #126
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-16 02:30:49 +01:00
toolatebot
232046e986 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/
2025-03-16 00:07:18 +00:00
toolatebot
ff108ee323 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/
2025-03-16 00:07:18 +00:00
anmoti
943867d938 Translated using Weblate (Japanese)
Currently translated at 78.4% (200 of 255 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/ja/
2025-03-16 00:07:18 +00:00
anmoti
59f4a29044 Translated using Weblate (Japanese)
Currently translated at 17.6% (3 of 17 strings)

Translation: jrpie-Launcher/metadata
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/ja/
2025-03-16 00:07:18 +00:00
class0068
bd70b822cf Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 98.4% (251 of 255 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/
2025-03-16 00:07:18 +00:00
72f9c0595f
handle MotionEvent.ACTION_CANCEL in TouchGestureDetector (see #126)
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-15 19:23:51 +01:00
75b22400c5
try to fix #125 2025-03-15 17:24:19 +01:00
c1511cd475
merge #124 - improve tutorial
Some checks are pending
Android CI / build (push) Waiting to run
* Add new "app list" section
* Rename fragments
* Replace screenshots
* Replace ViewPager by ViewPager2
* Add navigation buttons

Co-authored-by: Luke Wass <wassupluke@gmail.com>
2025-03-15 03:26:37 +01:00
3597baee1f
fix problem which switching from grayscale icons back to normal
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-14 22:33:08 +01:00
e02ca4091f
0.1.0 2025-03-14 16:35:41 +01:00
541e60356c
implement #98 - disable functionality because of Android bug: https://issuetracker.google.com/issues/352276244#comment5 2025-03-14 16:35:13 +01:00
492749a340
remove global variables from ListActivity 2025-03-14 16:03:59 +01:00
55af392706
add donate button
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-14 15:40:06 +01:00
077ee4381a
lint 2025-03-14 15:27:26 +01:00
e250a58ef4
add new action: adjust volume 2025-03-14 13:37:41 +01:00
c7af387a94
implement #98 - hide lock icon when 'hide private space when locked' setting is set 2025-03-14 04:11:47 +01:00
6cd17343fc
show questionmark when unkown app or shortcut is bound to gesture 2025-03-14 04:09:28 +01:00
b156b68d53
improve German translation
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-14 02:39:30 +01:00
c9ee2c6304
handle exception when acessing shortcuts 2025-03-14 02:00:42 +01:00
bf45b6602e
Merge pull request #112 from acanoe/features/hide-navigation-bar
Some checks are pending
Android CI / build (push) Waiting to run
feat: Add option to hide navigation bar on home screen
2025-03-13 16:30:34 +01:00
d7dd1aa71a
refactor hide navigation bar
* move code to UIObject
* remove listener
* rename 'full screen' to 'hide status bar'
2025-03-13 16:28:01 +01:00
3664159782
fix #116 2025-03-13 15:38:00 +01:00
8df9aae029
Merge pull request #118 from toolatebot/weblate-jrpie-launcher-launcher
Some checks are pending
Android CI / build (push) Waiting to run
Translations update from Toolate
2025-03-13 03:08:22 +01:00
anmoti
f776fbb88e Added translation using Weblate (Japanese) 2025-03-12 12:36:18 +00:00
Vossa Excelencia
a5ec8bb796 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (255 of 255 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-03-12 00:07:18 +00:00
3b2dca9af9
merge translation update
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-11 22:38:34 +01:00
1b12032750
add FUNDING.yml
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-11 15:58:07 +01:00
55a54fb9a5
implement #45: show pinned shortcuts in app list
Some checks failed
Android CI / build (push) Has been cancelled
2025-03-05 13:04:15 +01:00
Vossa Excelencia
e39ff62613 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (253 of 253 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-03-05 00:07:17 +00:00
9fe1a37ed6
implement #45: show pinned shortcuts in app list
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-05 00:31:43 +01:00
1f825d6f00
implement #113: option to reverse app list
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-04 16:49:29 +01:00
5ea03d39fa
fix blurred text in dialogs
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-03 21:26:39 +01:00
ae119ac4ce
remove unused code 2025-03-03 21:23:16 +01:00
8948b34243
0.0.23
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-02 23:04:59 +01:00
f18811bfa2
Merge pull request #111 from toolatebot/weblate-jrpie-launcher-launcher
Translations update from Toolate
2025-03-02 23:00:17 +01:00
bd1f999a0e
Merge branch 'master' of https://github.com/jrpie/Launcher 2025-03-02 22:41:13 +01:00
941b06b258
minor reformatting 2025-02-22 03:03:52 +01:00
Hendika N
9935386ad8 Add option to hide navigation bar on home screen 2025-02-22 07:35:28 +07:00
Symphonic9861
d44224071f Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 84.9% (214 of 252 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/
2025-02-21 09:07:16 +00:00
Symphonic9861
1f8f75dec8 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 76.9% (194 of 252 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/
2025-02-19 11:17:04 +00:00
86528f4e27
add tap-swipe combo gestures (see #110)
Some checks failed
Android CI / build (push) Has been cancelled
2025-02-18 19:20:53 +01:00
3aee137a3c
basic support for pinned shortcuts (see #45)
TODO: Show pinned shortcuts in app list
2025-02-18 19:02:25 +01:00
befa3afc5d
0.0.22
Some checks are pending
Android CI / build (push) Waiting to run
2025-02-17 22:39:49 +01:00
a295c0ab4b
Merge pull request #107 from toolatebot/weblate-jrpie-launcher-launcher
Translations update from Toolate
2025-02-17 22:29:35 +01:00
Ahmet Çeliker
c448c51164 Translated using Weblate (Turkish)
Currently translated at 86.6% (13 of 15 strings)

Translation: jrpie-Launcher/metadata
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/tr/
2025-02-17 00:07:18 +00:00
Vossa Excelencia
68b79724e8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 26.6% (4 of 15 strings)

Translation: jrpie-Launcher/metadata
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/pt_BR/
2025-02-17 00:07:18 +00:00
Vossa Excelencia
bef38c2657 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (225 of 225 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-02-17 00:07:18 +00:00
Vossa Excelencia
d0b0c27b2c Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (220 of 220 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-02-17 00:07:18 +00:00
Vossa Excelencia
4508e4ee5c Translated using Weblate (Portuguese (Brazil))
Currently translated at 20.0% (3 of 15 strings)

Translation: jrpie-Launcher/metadata
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/pt_BR/
2025-02-17 00:07:18 +00:00
Vossa Excelencia
e959e9d957 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (220 of 220 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-02-17 00:07:18 +00:00
Nicola Bortoletto
958d4879f5 Translated using Weblate (Italian)
Currently translated at 99.0% (216 of 218 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/
2025-02-17 00:07:18 +00:00
Vossa Excelencia
7841a99415 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (218 of 218 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-02-17 00:07:18 +00:00
Xanadul
18b4fca933 Translated using Weblate (German)
Currently translated at 13.3% (2 of 15 strings)

Translation: jrpie-Launcher/metadata
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/de/
2025-02-17 00:07:18 +00:00
Nicola Bortoletto
5792c7f38c Translated using Weblate (Italian)
Currently translated at 98.1% (214 of 218 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/
2025-02-17 00:07:18 +00:00
88a78749c2
add action: media play / pause 2025-02-17 00:31:18 +01:00
7257d4ca35
fix bug in gesture detection logic
Some checks are pending
Android CI / build (push) Waiting to run
2025-02-16 23:58:42 +01:00
47ae0bf35f
update README.md
Some checks are pending
Android CI / build (push) Waiting to run
2025-02-16 16:51:18 +01:00
5669279c64
add <,>,V,Λ gestures 2025-02-16 15:50:13 +01:00
0c0d90a357
improve gesture detection 2025-02-15 03:08:18 +01:00
012f13c827
update gradle wrapper
Some checks failed
Android CI / build (push) Has been cancelled
2025-02-11 18:06:06 +01:00
757486771d
reenable proguard
Some checks are pending
Android CI / build (push) Waiting to run
2025-02-10 22:56:45 +01:00
9c5500aa83
lint
Some checks are pending
Android CI / build (push) Waiting to run
2025-02-09 21:08:16 +01:00
d69e3caf71
implement #98 - add option to show private space in different list 2025-02-09 18:49:41 +01:00
944eb89fef
implement #93 - treat back button as a gesture
Some checks failed
Android CI / build (push) Has been cancelled
2025-02-06 22:27:58 +01:00
fa2f1c4127
fix #106 (ugly workaround)
Some checks are pending
Android CI / build (push) Waiting to run
2025-02-05 23:47:37 +01:00
8699b92246
update the bug report url 2025-02-05 22:07:23 +01:00
d6355afc54
updated bug report template
Some checks failed
Android CI / build (push) Has been cancelled
2025-01-31 02:01:23 +01:00
0a9890111c
add explanation for pressing space to disable auto launch 2025-01-31 01:38:20 +01:00
b2e7e4cacf
v0.0.21 2025-01-31 01:12:55 +01:00
d62815be12
improve German translation 2025-01-31 01:11:00 +01:00
768d27a7bb
Merge branch 'master' of https://github.com/jrpie/Launcher 2025-01-31 00:44:44 +01:00
a227a40b6e
Merge pull request #103 from toolatebot/weblate-jrpie-launcher-launcher
Translations update from Toolate
2025-01-31 00:44:27 +01:00
f280d36667
add handler for ActivityNotFoundException 2025-01-28 02:15:04 +01:00
Vossa Excelencia
3973f1338f Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (215 of 215 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-01-28 00:11:10 +00:00
9f3142cb69
Merge branch 'master' of https://github.com/jrpie/Launcher
Some checks failed
Android CI / build (push) Has been cancelled
2025-01-27 02:05:36 +01:00
ecd58b91bb
some refactoring 2025-01-27 00:50:31 +01:00
2fc2b76fba
Merge pull request #97 from toolatebot/weblate-jrpie-launcher-launcher
Some checks are pending
Android CI / build (push) Waiting to run
Translations update from Toolate
2025-01-26 21:57:19 +01:00
Vossa Excelencia
c7895830e7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.5% (201 of 202 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-01-25 00:07:17 +00:00
Vossa Excelencia
14766fe1d9 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (202 of 202 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-01-25 00:07:17 +00:00
Anonymous
b9a59c9e37 Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.0% (196 of 202 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-01-25 00:07:17 +00:00
Anonymous
2d03bdbbef Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 84.1% (170 of 202 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/
2025-01-25 00:07:17 +00:00
Anonymous
09b12834a9 Translated using Weblate (French)
Currently translated at 95.0% (192 of 202 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/fr/
2025-01-25 00:07:17 +00:00
23f8cfb70e
implement #102 - show version in settings and add bug report dialog
Some checks failed
Android CI / build (push) Has been cancelled
2025-01-24 23:45:19 +01:00
6d385e4e87
use TextClock instead of custom solution 2025-01-22 02:17:46 +01:00
785e024ddb
basic support for private space (#98)
* add an action to toggle private space lock
* hide apps from private space if private space is locked
2025-01-22 01:44:51 +01:00
679c90130d
re-enable the light theme
close #75
when the light theme is selected, the background is always set to solid, to prevent problems with dark wallpapers
2025-01-21 23:05:49 +01:00
95c9fcd292
shorten 'auto launch disabled' to 'no auto launch' 2025-01-13 16:51:49 +01:00
a188e06342
update README.md 2025-01-13 15:53:42 +01:00
5696ea73da
update short description 2025-01-13 03:01:28 +01:00
a7ce5b9222
update release script and BUILD.md 2025-01-13 02:50:41 +01:00
74b448cd0f
j-0.0.20 2025-01-12 22:50:22 +01:00
cd36fad8cd
add release script 2025-01-12 22:47:50 +01:00
64ccc4f300
update CI pipeline to use default build flavor 2025-01-12 21:08:03 +01:00
b5dcfeecec
update CI pipeline to use default build flavor 2025-01-12 21:00:05 +01:00
677453137a
Merge pull request #96 from toolatebot/weblate-jrpie-launcher-launcher
Translations update from Toolate
2025-01-12 20:59:39 +01:00
f08f357bb3
add buildFlavor for accrescent 2025-01-12 01:31:49 +01:00
Vossa Excelencia
567d08fb3a Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (200 of 200 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-01-12 00:07:16 +00:00
fa0c880342
update LICENSE 2025-01-11 23:17:03 +01:00
8fc69e10e9
add list of open source licenses 2025-01-11 23:15:50 +01:00
eff7cfda5e
update AGP 2025-01-11 21:37:22 +01:00
ddca29067e
Merge branch 'master' of https://github.com/jrpie/Launcher 2025-01-10 00:23:27 +01:00
c6d0477e3a
fix #95: unicode normalization for search 2025-01-10 00:21:57 +01:00
eb9f43ec49
Merge pull request #94 from toolatebot/weblate-jrpie-launcher-launcher
Translations update from Toolate
2025-01-09 01:09:44 +01:00
ce5fade39a
allow to prefix query with space to disable auto launch 2025-01-09 01:08:01 +01:00
09ca4410b9 Translated using Weblate (French)
Currently translated at 98.9% (197 of 199 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/fr/
2025-01-09 00:07:16 +00:00
92772d715c
Merge branch 'master' of https://github.com/jrpie/Launcher 2025-01-09 00:25:14 +01:00
36a04703c2
update changelog 2025-01-09 00:25:07 +01:00
31106158b3
Merge pull request #92 from toolatebot/weblate-jrpie-launcher-launcher
Translations update from Toolate
2025-01-09 00:24:03 +01:00
6d5e3b36f9
update accessibility service warning 2025-01-09 00:23:35 +01:00
Nin Dan
e53b7682d8 Translated using Weblate (French)
Currently translated at 98.9% (197 of 199 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/fr/
2025-01-08 22:07:15 +00:00
Alexandre Ancel
3e4d22d3cd Translated using Weblate (French)
Currently translated at 98.9% (197 of 199 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/fr/
2025-01-05 00:07:16 +00:00
Anonymous
954bef4aac Translated using Weblate (German)
Currently translated at 91.3% (180 of 197 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/de/
2025-01-03 00:07:16 +00:00
e39d72ef3c
j-0.0.19 2025-01-02 23:15:14 +01:00
9216837879
add an option to search the web from the app list 2025-01-02 23:05:24 +01:00
2d53562d58
change the package id for debug builds 2025-01-02 22:47:00 +01:00
220ad4d18f
warn about problems with device encryption when using an accessibility service 2024-12-31 00:28:06 +01:00
cbaa853b1c
remove white box around cursor in EditText
android:background was erroneously used instead of android:windowBackground.
This was causing a white box around the cursor.
Neither android:background nor android:windowBackground is needed.
2024-12-30 23:25:57 +01:00
ad5ee5e10e
Merge branch 'master' of https://github.com/jrpie/Launcher 2024-12-30 23:06:13 +01:00
4ddb893d41
improve json serialization 2024-12-29 02:40:06 +01:00
970c160f4a
serialize to json 2024-12-23 02:29:17 +01:00
96c924fba5
Merge pull request #89 from toolatebot/weblate-jrpie-launcher-launcher
Translations update from Toolate
2024-12-22 17:57:47 +00:00
9d96284719 Translated using Weblate (German)
Currently translated at 91.8% (181 of 197 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/de/
2024-12-22 00:03:32 +00:00
36ee8033ed
make device admin the preferred method for locking the screen 2024-12-21 15:05:24 +01:00
2b7999cfdc
use LiveData to store app list 2024-12-20 04:45:56 +01:00
3353719cc3
move stuff out of Functions.kt 2024-12-20 02:42:58 +01:00
c84675904d
update README.md 2024-12-16 22:39:40 +01:00
bf4131c603
Merge pull request #87 from toolatebot/weblate-jrpie-launcher-launcher
Translations update from Toolate
2024-12-15 22:37:51 +00:00
Vossa Excelencia
db4a52a780 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (197 of 197 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2024-12-15 06:23:34 +00:00
8309b7c290
make lock screen dialog scrollable 2024-12-13 16:34:30 +01:00
175 changed files with 5609 additions and 2219 deletions

4
.github/FUNDING.yml vendored
View file

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

View file

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

View file

@ -29,4 +29,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: launcher-debug-${{ github.sha }}.apk
path: app/build/outputs/apk/debug/app-debug.apk
path: app/build/outputs/apk/default/debug/app-default-debug.apk

76
.scripts/release.sh Executable file
View file

@ -0,0 +1,76 @@
#!/bin/bash
export JAVA_HOME="/usr/lib/jvm/java-23-openjdk/"
OUTPUT_DIR="$HOME/launcher-release"
BUILD_TOOLS_DIR="$HOME/Android/Sdk/build-tools/35.0.0"
KEYSTORE="$HOME/data/keys/launcher_jrpie.jks"
KEYSTORE_ACCRESCENT="$HOME/data/keys/launcher_jrpie_accrescent.jks"
KEYSTORE_PASS=$(keepassxc-password "android_keys/launcher")
KEYSTORE_ACCRESCENT_PASS=$(keepassxc-password "android_keys/launcher-accrescent")
if [[ $(git status --porcelain) ]]; then
echo "There are uncommitted changes."
read -p "Continue anyway? (y/n) " -n 1 -r
echo # (optional) move to a new line
if ! [[ $REPLY =~ ^[Yy]$ ]]
then
exit 1
fi
fi
rm -rf "$OUTPUT_DIR"
mkdir "$OUTPUT_DIR"
echo
echo "======================="
echo " Default Release (apk) "
echo "======================="
./gradlew clean
./gradlew assembleDefaultRelease
mv app/build/outputs/apk/default/release/app-default-release-unsigned.apk "$OUTPUT_DIR/app-release.apk"
"$BUILD_TOOLS_DIR/apksigner" sign --ks "$KEYSTORE" \
--ks-key-alias key0 \
--ks-pass="pass:$KEYSTORE_PASS" \
--key-pass="pass:$KEYSTORE_PASS" \
--alignment-preserved \
--v1-signing-enabled=true \
--v2-signing-enabled=true \
--v3-signing-enabled=true \
--v4-signing-enabled=true \
"$OUTPUT_DIR/app-release.apk"
echo
echo "======================="
echo " Default Release (aab) "
echo "======================="
./gradlew clean
./gradlew bundleDefaultRelease
mv app/build/outputs/bundle/defaultRelease/app-default-release.aab "$OUTPUT_DIR/app-release.aab"
"$BUILD_TOOLS_DIR/apksigner" sign --ks "$KEYSTORE" \
--ks-key-alias key0 \
--ks-pass="pass:$KEYSTORE_PASS" \
--key-pass="pass:$KEYSTORE_PASS" \
--v1-signing-enabled=true --v2-signing-enabled=true --v3-signing-enabled=true --v4-signing-enabled=true \
--min-sdk-version=21 \
"$OUTPUT_DIR/app-release.aab"
echo
echo "======================="
echo " Accrescent (apks) "
echo "======================="
./gradlew clean
./gradlew bundleAccrescentRelease
mv app/build/outputs/bundle/accrescentRelease/app-accrescent-release.aab "$OUTPUT_DIR/app-accrescent-release.aab"
# build apks using bundletool from https://github.com/google/bundletool/releases
"$JAVA_HOME/bin/java" -jar /opt/android/bundletool.jar build-apks \
--bundle="$OUTPUT_DIR/app-accrescent-release.aab" --output="$OUTPUT_DIR/launcher-accrescent.apks" \
--ks="$KEYSTORE_ACCRESCENT" \
--ks-pass="pass:$KEYSTORE_ACCRESCENT_PASS" \
--ks-key-alias="key0" \
--key-pass="pass:$KEYSTORE_ACCRESCENT_PASS"

View file

@ -1,7 +1,7 @@
MIT License
Original Code Copyright (c) 2020 Finn Glas (https://github.com/finnmglas/Launcher)
Modifications Copyright (c) 2023 Josia Pietsch
Modifications Copyright (c) 2025 Josia Pietsch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -14,22 +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>
<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://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>
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"
@ -55,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:
@ -63,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

View file

@ -1,9 +1,9 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlinx-serialization'
android {
dataBinding {
enabled = true
}
@ -23,8 +23,8 @@ android {
minSdkVersion 21
targetSdkVersion 35
compileSdk 35
versionCode 34
versionName "j-0.0.18"
versionCode 43
versionName "0.1.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -43,10 +43,37 @@ android {
buildTypes {
release {
// minifyEnabled true
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
}
flavorDimensions += "distribution"
productFlavors {
create("default") {
dimension = "distribution"
getIsDefault().set(true)
buildConfigField "boolean", "USE_ACCESSIBILITY_SERVICE", "true"
}
create("accrescent") {
dimension = "distribution"
applicationIdSuffix = ".accrescent"
versionNameSuffix = "+accrescent"
buildConfigField "boolean", "USE_ACCESSIBILITY_SERVICE", "false"
}
}
sourceSets {
accrescent {
manifest.srcFile 'src/accrescent/AndroidManifest.xml'
}
}
namespace 'de.jrpie.android.launcher'
buildFeatures {
buildConfig true
@ -58,24 +85,26 @@ 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")
implementation "eu.jonahbauer:android-preference-annotations:1.1.2"
annotationProcessor "eu.jonahbauer:android-preference-annotations:1.1.2"
annotationProcessor "com.android.databinding:compiler:$android_plugin_version"

View file

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

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:node="merge">
<application
android:name=".Application"
tools:node="merge">
<service
android:name=".actions.lock.LauncherAccessibilityService"
tools:strict="true"
tools:node="remove" />
</application>
</manifest>

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name" translatable="false">μLauncher [debug]</string>
</resources>

View file

@ -3,82 +3,98 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_HIDDEN_PROFILES" />
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
<application
android:name=".Application"
android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:enableOnBackInvokedCallback="true"
android:theme="@style/launcherBaseTheme"
tools:ignore="UnusedAttribute" >
tools:ignore="UnusedAttribute">
<activity
android:name=".ui.PinShortcutActivity"
android:autoRemoveFromRecents="true"
android:excludeFromRecents="true"
android:exported="false">
<intent-filter>
<action android:name="android.content.pm.action.CONFIRM_PIN_SHORTCUT" />
<action android:name="android.content.pm.action.CONFIRM_PIN_APPWIDGET" />
</intent-filter>
</activity>
<activity
android:name=".ui.HomeActivity"
android:exported="true"
android:excludeFromRecents="true"
android:clearTaskOnLaunch="true"
android:configChanges="orientation|screenSize"
android:theme="@style/launcherHomeTheme"
android:launchMode="singleTask" >
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/launcherHomeTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="de.jrpie.android.launcher.ui.tutorial.TutorialActivity"
android:configChanges="orientation|screenSize" >
</activity>
android:name=".ui.tutorial.TutorialActivity"
android:configChanges="orientation|screenSize" />
<activity
android:name="de.jrpie.android.launcher.ui.list.ListActivity"
android:name=".ui.list.ListActivity"
android:configChanges="orientation|screenSize"
android:windowSoftInputMode="adjustResize" >
</activity>
android:windowSoftInputMode="adjustResize" />
<activity
android:name="de.jrpie.android.launcher.ui.settings.SettingsActivity"
android:name=".ui.settings.SettingsActivity"
android:configChanges="orientation|screenSize"
android:windowSoftInputMode="adjustNothing"
android:exported="true" >
android:exported="true"
android:windowSoftInputMode="adjustNothing">
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES"/>
<category android:name="android.intent.category.DEFAULT"/>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ui.LegalInfoActivity"
android:exported="false" />
<receiver android:name=".actions.lock.LauncherDeviceAdmin"
<receiver
android:name=".actions.lock.LauncherDeviceAdmin"
android:description="@string/device_admin_description"
android:exported="true"
android:label="@string/app_name"
android:description="@string/device_admin_description"
android:permission="android.permission.BIND_DEVICE_ADMIN">
<meta-data android:name="android.app.device_admin"
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin_config" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
<service android:name=".actions.lock.LauncherAccessibilityService"
<service
android:name=".actions.lock.LauncherAccessibilityService"
android:exported="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
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" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest>

View file

@ -1,19 +1,95 @@
package de.jrpie.android.launcher
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
import android.os.Build
import android.os.Build.VERSION_CODES
import android.os.UserHandle
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import androidx.preference.PreferenceManager
import de.jrpie.android.launcher.actions.TorchManager
import de.jrpie.android.launcher.apps.AppInfo
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<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() {
override fun onPackageRemoved(p0: String?, p1: UserHandle?) {
loadApps()
}
override fun onPackageAdded(p0: String?, p1: UserHandle?) {
loadApps()
}
override fun onPackageChanged(p0: String?, p1: UserHandle?) {
loadApps()
}
override fun onPackagesAvailable(p0: Array<out String>?, p1: UserHandle?, p2: Boolean) {
// TODO
}
override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) {
// TODO
}
override fun onPackagesUnsuspended(packageNames: Array<out String>?, user: UserHandle?) {
// TODO
}
override fun onPackagesUnavailable(p0: Array<out String>?, p1: UserHandle?, p2: Boolean) {
// TODO
}
override fun onPackageLoadingProgressChanged(
packageName: String,
user: UserHandle,
progress: Float
) {
// TODO
}
override fun onShortcutsChanged(
packageName: String,
shortcuts: MutableList<ShortcutInfo>,
user: UserHandle
) {
// TODO
}
}
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()
}
}
@ -30,12 +106,55 @@ class Application : android.app.Application() {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
LauncherPreferences.init(preferences, this.resources)
// Try to restore old preferences
migratePreferencesToNewVersion(this)
// First time opening the app: set defaults
// The tutorial is started from HomeActivity#onStart, as starting it here is blocked by android
if (!LauncherPreferences.internal().started()) {
resetPreferences(this)
}
LauncherPreferences.getSharedPreferences()
.registerOnSharedPreferenceChangeListener(listener)
val launcherApps = getSystemService(LAUNCHER_APPS_SERVICE) as LauncherApps
launcherApps.registerCallback(launcherAppsCallback)
if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
val filter = IntentFilter().also {
if (Build.VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM) {
it.addAction(Intent.ACTION_PROFILE_AVAILABLE)
it.addAction(Intent.ACTION_PROFILE_UNAVAILABLE)
} else {
it.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
it.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
}
}
ContextCompat.registerReceiver(
this, profileAvailabilityBroadcastReceiver, filter,
ContextCompat.RECEIVER_EXPORTED
)
}
if (Build.VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
removeUnusedShortcuts(this)
}
loadApps()
}
fun getCustomAppNames(): HashMap<AppInfo, String> {
fun getCustomAppNames(): HashMap<AbstractAppInfo, String> {
return (customAppNames ?: LauncherPreferences.apps().customNames() ?: HashMap())
.also { customAppNames = it }
}
private fun loadApps() {
privateSpaceLocked.postValue(isPrivateSpaceLocked(this))
CoroutineScope(Dispatchers.Default).launch {
apps.postValue(getApps(packageManager, applicationContext))
}
}
}

View file

@ -3,147 +3,129 @@ 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.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.Rect
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.view.View
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
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.ui.list.apps.AppsRecyclerAdapter
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
/* Objects used by multiple activities */
val appsList: MutableList<DetailedAppInfo> = ArrayList()
/* 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"
/* Animate */
const val REQUEST_SET_DEFAULT_HOME = 42
// Taken from https://stackoverflow.com/questions/47293269
fun View.blink(
times: Int = Animation.INFINITE,
duration: Long = 1000L,
offset: Long = 20L,
minAlpha: Float = 0.2f,
maxAlpha: Float = 1.0f,
repeatMode: Int = Animation.REVERSE
) {
startAnimation(AlphaAnimation(minAlpha, maxAlpha).also {
it.duration = duration
it.startOffset = offset
it.repeatMode = repeatMode
it.repeatCount = times
})
}
fun setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) {
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)
}
fun getIntent(packageName: String, context: Context): Intent? {
val intent: Intent? = context.packageManager.getLaunchIntentForPackage(packageName)
intent?.addCategory(Intent.CATEGORY_LAUNCHER)
return intent
}
/* --- */
fun getUserFromId(user: Int?, context: Context): UserHandle {
fun getUserFromId(userId: Int?, context: Context): UserHandle {
/* TODO: this is an ugly hack.
Use userManager#getUserForSerialNumber instead (breaking change to SharedPreferences!)
*/
val userManager = context.getSystemService(Service.USER_SERVICE) as UserManager
val profiles = userManager.userProfiles
return profiles.firstOrNull { it.hashCode() == user } ?: profiles[0]
return profiles.firstOrNull { it.hashCode() == userId } ?: profiles[0]
}
fun uninstallApp(appInfo: AppInfo, activity: Activity) {
val packageName = appInfo.packageName.toString()
val user = appInfo.user
Log.i(LOG_TAG, "uninstalling $appInfo")
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE)
intent.data = Uri.parse("package:$packageName")
getUserFromId(user, activity).let { user ->
intent.putExtra(Intent.EXTRA_USER, user)
@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
}
}
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
activity.startActivityForResult(
intent,
REQUEST_UNINSTALL
)
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 openNewTabWindow(urls: String, context: Context) {
val uris = Uri.parse(urls)
val intents = Intent(Intent.ACTION_VIEW, uris)
val b = Bundle()
b.putBoolean("new_window", true)
intents.putExtras(b)
context.startActivity(intents)
}
fun openAppSettings(
appInfo: AppInfo,
context: Context,
sourceBounds: Rect? = null,
opts: Bundle? = null
) {
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
appInfo.getLauncherActivityInfo(context)?.let { app ->
launcherApps.startAppDetailsActivity(app.componentName, app.user, sourceBounds, opts)
fun openInBrowser(url: String, context: Context) {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
intent.putExtras(Bundle().apply { putBoolean("new_window", true) })
try {
context.startActivity(intent)
} catch (_: ActivityNotFoundException) {
Toast.makeText(context, R.string.toast_activity_not_found_browser, Toast.LENGTH_LONG).show()
}
}
@ -153,24 +135,43 @@ fun openTutorial(context: Context) {
/**
* [loadApps] is used to speed up the [AppsRecyclerAdapter] loading time,
* as it caches all the apps and allows for fast access to the data.
* Load all apps.
*/
fun loadApps(packageManager: PackageManager, context: Context) {
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))
}
}
// fallback option
if (loadList.isEmpty()) {
Log.w(LOG_TAG, "using fallback option to load packages")
@ -178,40 +179,48 @@ fun loadApps(packageManager: PackageManager, context: Context) {
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() }
appsList.clear()
appsList.addAll(loadList)
loadList.sortBy { it.getCustomLabel(context) }
var end = System.currentTimeMillis()
Log.i(LOG_TAG, "${loadList.size} apps loaded (${end - start}ms)")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
start = System.currentTimeMillis()
LauncherPreferences.apps().pinnedShortcuts()
?.mapNotNull { DetailedPinnedShortcutInfo.fromPinnedShortcutInfo(it, context) }
?.let {
end = System.currentTimeMillis()
Log.i(LOG_TAG, "${it.size} shortcuts loaded (${end - start}ms)")
loadList.addAll(it)
}
}
return loadList
}
// 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()
}
// Taken from https://stackoverflow.com/a/50743764/12787264
fun openSoftKeyboard(context: Context, view: View) {
view.requestFocus()
// open the soft keyboard
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
}
// Taken from: https://stackoverflow.com/a/30340794/12787264
fun transformGrayscale(imageView: ImageView) {
val matrix = ColorMatrix()
matrix.setSaturation(0f)
val filter = ColorMatrixColorFilter(matrix)
imageView.colorFilter = filter
}
fun copyToClipboard(context: Context, text: String) {
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("Debug Info", text)
clipboardManager.setPrimaryClip(clipData)
}

View file

@ -2,84 +2,61 @@ package de.jrpie.android.launcher.actions
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences.Editor
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.widget.Toast
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.AppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.apps.DetailedAppInfo
import de.jrpie.android.launcher.preferences.LauncherPreferences
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import androidx.core.content.edit
interface Action {
@Serializable
sealed interface Action {
fun invoke(context: Context, rect: Rect? = null): Boolean
fun bindToGesture(prefEditor: Editor, id: String)
fun label(context: Context): String
fun getIcon(context: Context): Drawable?
fun isAvailable(context: Context): Boolean
fun writeToIntent(intent: Intent)
// Can the action be used to reach µLauncher settings?
fun canReachSettings(): Boolean
fun bindToGesture(prefEditor: Editor, id: String) {
prefEditor.putString(id, Json.encodeToString(this))
}
companion object {
/**
* Get an action for a specific id.
* An id is of the form:
* - "launcher:${launcher_action_name}", see [LauncherAction]
* - "${package_name}", see [AppAction]
* - "${package_name}:${activity_name}", see [AppAction]
*
* @param id
* @param user a user id, ignored if the action is a [LauncherAction].
* @param context used to complete [AppInfo] if possible
*/
private fun fromId(id: String, user: Int?, context: Context? = null): Action? {
if (id.isEmpty()) {
return null
}
if (LauncherAction.isOtherAction(id)) {
return LauncherAction.byId(id)
}
val values = id.split(";")
var info = AppInfo(values[0], values.getOrNull(1), user ?: INVALID_USER)
// try to complete an incomplete AppInfo if a context is provided
if (context != null && (info.user == INVALID_USER || info.activityName == null)) {
info = DetailedAppInfo.fromAppInfo(info, context)?.app?:info
}
return AppAction(info)
}
fun forGesture(gesture: Gesture): Action? {
val id = gesture.id
val preferences = LauncherPreferences.getSharedPreferences()
val actionId = preferences.getString("$id.app", "")!!
var u: Int? = preferences.getInt("$id.user", INVALID_USER)
u = if (u == INVALID_USER) null else u
return fromId(actionId, u)
val json = preferences.getString(id, "null")!!
return Json.decodeFromString(json)
}
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, fromId(it, null, context)) }
.firstOrNull { it.second?.isAvailable(context) ?: false }
?.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?) {
@ -87,16 +64,15 @@ 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()
.putString(gesture.id + ".app", "")
.putInt(gesture.id + ".user", INVALID_USER)
.apply()
LauncherPreferences.getSharedPreferences().edit {
remove(gesture.id)
}
}
fun launch(
@ -107,6 +83,9 @@ 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 {
@ -117,11 +96,5 @@ interface Action {
).show()
}
}
fun fromIntent(data: Intent): Action? {
val value = data.getStringExtra("action_id") ?: return null
val user = data.getIntExtra("user", INVALID_USER)
return fromId(value, user)
}
}
}

View file

@ -2,42 +2,47 @@ package de.jrpie.android.launcher.actions
import android.app.AlertDialog
import android.app.Service
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.LauncherApps
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.util.Log
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.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.getIntent
import de.jrpie.android.launcher.openAppSettings
import de.jrpie.android.launcher.ui.list.apps.openSettings
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
class AppAction(val appInfo: AppInfo) : Action {
@Serializable
@SerialName("action:app")
class AppAction(val app: AppInfo) : Action {
override fun invoke(context: Context, rect: Rect?): Boolean {
val packageName = appInfo.packageName.toString()
if (appInfo.user != INVALID_USER) {
val packageName = app.packageName
if (app.user != INVALID_USER) {
val launcherApps =
context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
appInfo.getLauncherActivityInfo(context)?.let { app ->
Log.i("Launcher", "Starting $appInfo")
app.getLauncherActivityInfo(context)?.let { app ->
Log.i("Launcher", "Starting ${this.app}")
launcherApps.startMainActivity(app.componentName, app.user, rect, null)
return true
}
}
val intent = getIntent(packageName, context)
if (intent != null) {
context.startActivity(intent)
context.packageManager.getLaunchIntentForPackage(packageName)?.let {
it.addCategory(Intent.CATEGORY_LAUNCHER)
try {
context.startActivity(it)
} catch (_: ActivityNotFoundException) {
return false
}
return true
}
/* check if app is installed */
if (isAvailable(context)) {
AlertDialog.Builder(
@ -47,7 +52,7 @@ class AppAction(val appInfo: AppInfo) : Action {
.setTitle(context.getString(R.string.alert_cant_open_title))
.setMessage(context.getString(R.string.alert_cant_open_message))
.setPositiveButton(android.R.string.ok) { _, _ ->
openAppSettings(appInfo, context)
app.openSettings(context)
}
.setNegativeButton(android.R.string.cancel, null)
.setIcon(android.R.drawable.ic_dialog_info)
@ -58,33 +63,19 @@ class AppAction(val appInfo: AppInfo) : Action {
}
override fun label(context: Context): String {
return DetailedAppInfo.fromAppInfo(appInfo, context)?.getCustomLabel(context).toString()
return DetailedAppInfo.fromAppInfo(app, context)?.getCustomLabel(context).toString()
}
override fun getIcon(context: Context): Drawable? {
return DetailedAppInfo.fromAppInfo(appInfo, context)?.icon
return DetailedAppInfo.fromAppInfo(app, context)?.getIcon(context)
}
override fun isAvailable(context: Context): Boolean {
// check if app is installed
return DetailedAppInfo.fromAppInfo(appInfo, context) != null
return DetailedAppInfo.fromAppInfo(app, context) != null
}
override fun bindToGesture(editor: SharedPreferences.Editor, id: String) {
val u = appInfo.user
// TODO: replace this by AppInfo#serialize (breaking change to SharedPreferences!)
var app = appInfo.packageName.toString()
if (appInfo.activityName != null) {
app += ";${appInfo.activityName}"
}
editor
.putString("$id.app", app)
.putInt("$id.user", u)
}
override fun writeToIntent(intent: Intent) {
intent.putExtra("action_id", "${appInfo.packageName};${appInfo.activityName}")
intent.putExtra("user", appInfo.user)
override fun canReachSettings(): Boolean {
return false
}
}

View file

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

View file

@ -2,7 +2,6 @@ package de.jrpie.android.launcher.actions
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences.Editor
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.media.AudioManager
@ -10,83 +9,158 @@ 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.AppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.apps.hidePrivateSpaceWhenLocked
import de.jrpie.android.launcher.apps.isPrivateSpaceSupported
import de.jrpie.android.launcher.apps.togglePrivateSpaceLock
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.list.ListActivity
import de.jrpie.android.launcher.ui.settings.SettingsActivity
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure
@Serializable(with = LauncherActionSerializer::class)
@SerialName("action:launcher")
enum class LauncherAction(
val id: String,
val label: Int,
val icon: Int,
val launch: (Context) -> Unit
val launch: (Context) -> Unit,
private val canReachSettings: Boolean = false,
val available: (Context) -> Boolean = { true },
) : Action {
SETTINGS(
"launcher:settings",
"settings",
R.string.list_other_settings,
R.drawable.baseline_settings_24,
::openSettings
::openSettings,
true
),
CHOOSE(
"launcher:choose",
"choose",
R.string.list_other_list,
R.drawable.baseline_menu_24,
::openAppsList
::openAppsList,
true
),
CHOOSE_FROM_FAVORITES(
"launcher:chooseFromFavorites",
"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(
"launcher:volumeUp",
"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(
"launcher:volumeDown",
"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(
"launcher:nextTrack",
"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(
"launcher:previousTrack",
"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(
"launcher:expandNotificationsPanel",
"expand_notifications_panel",
R.string.list_other_expand_notifications_panel,
R.drawable.baseline_notifications_24,
::expandNotificationsPanel
),
EXPAND_SETTINGS_PANEL(
"launcher:expandSettingsPanel",
"expand_settings_panel",
R.string.list_other_expand_settings_panel,
R.drawable.baseline_settings_applications_24,
::expandSettingsPanel
),
RECENT_APPS(
"recent_apps",
R.string.list_other_recent_apps,
R.drawable.baseline_apps_24,
LauncherAccessibilityService::openRecentApps,
false,
{ _ -> BuildConfig.USE_ACCESSIBILITY_SERVICE }
),
LOCK_SCREEN(
"launcher:lockScreen",
"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(
"launcher:toggleTorch",
"toggle_torch",
R.string.list_other_torch,
R.drawable.baseline_flashlight_on_24,
::toggleTorch
::toggleTorch,
),
NOP("launcher:nop", R.string.list_other_nop, R.drawable.baseline_not_interested_24, {});
LAUNCH_OTHER_LAUNCHER(
"launcher_other_launcher",
R.string.list_other_launch_other_launcher,
R.drawable.baseline_home_24,
::launchOtherLauncher
),
NOP("nop", R.string.list_other_nop, R.drawable.baseline_not_interested_24, {});
override fun invoke(context: Context, rect: Rect?): Boolean {
launch(context)
@ -98,86 +172,48 @@ enum class LauncherAction(
}
override fun getIcon(context: Context): Drawable? {
return context.getDrawable(icon)
}
override fun bindToGesture(editor: Editor, id: String) {
editor
.putString("$id.app", this.id)
.putInt("$id.user", INVALID_USER)
}
override fun writeToIntent(intent: Intent) {
intent.putExtra("action_id", id)
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 {
fun byId(id: String): LauncherAction? {
return entries.singleOrNull { it.id == id }
}
fun isOtherAction(id: String): Boolean {
return id.startsWith("launcher")
}
}
}
/* Media player actions */
private fun 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) {
@ -210,6 +246,7 @@ private fun expandNotificationsPanel(context: Context) {
}
}
private fun expandSettingsPanel(context: Context) {
/* https://stackoverflow.com/a/31898506 */
try {
@ -227,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(
@ -250,6 +301,41 @@ 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)
}
/* A custom serializer is required to store type information,
see https://github.com/Kotlin/kotlinx.serialization/issues/1486
*/
private class LauncherActionSerializer : KSerializer<LauncherAction> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor(
"action:launcher",
) {
element("value", String.serializer().descriptor)
}
override fun deserialize(decoder: Decoder): LauncherAction {
val s = decoder.decodeStructure(descriptor) {
decodeElementIndex(descriptor)
decodeSerializableElement(descriptor, 0, String.serializer())
}
return LauncherAction.byId(s) ?: throw SerializationException()
}
override fun serialize(encoder: Encoder, value: LauncherAction) {
encoder.encodeStructure(descriptor) {
encodeSerializableElement(descriptor, 0, String.serializer(), value.id)
}
}
}

View file

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

View file

@ -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,9 +76,9 @@ 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_consent_accessibility_cancel) { _, _ -> }
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
}.create().also { it.show() }.apply {
val buttonOk = getButton(AlertDialog.BUTTON_POSITIVE)
val checkboxes = listOf(
@ -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)

View file

@ -2,13 +2,14 @@ package de.jrpie.android.launcher.actions.lock
import android.content.Context
import android.os.Build
import android.view.LayoutInflater
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,
@ -36,32 +37,28 @@ enum class LockMethod(
companion object {
fun chooseMethod(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
! BuildConfig.USE_ACCESSIBILITY_SERVICE) {
// only device admin is available
setMethod(context, DEVICE_ADMIN)
return
}
val builder = AlertDialog.Builder(context, R.style.AlertDialogCustom)
builder.setNegativeButton("cancel") { _, _ -> }
builder.setCustomTitle(
LayoutInflater.from(context).inflate(R.layout.dialog_select_lock_method, null)
)
builder.setItems(
arrayOf(
context.getString(R.string.screen_lock_method_use_accessibility),
context.getString(R.string.screen_lock_method_use_device_admin)
)
) { _, i ->
val method = when (i) {
0 -> ACCESSIBILITY_SERVICE
1 -> DEVICE_ADMIN
else -> return@setItems
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
setView(R.layout.dialog_select_lock_method)
// setTitle()
}.create().also { it.show() }.apply {
findViewById<Button>(R.id.dialog_select_lock_method_button_accessibility)
?.setOnClickListener {
setMethod(context, ACCESSIBILITY_SERVICE)
cancel()
}
findViewById<Button>(R.id.dialog_select_lock_method_button_device_admin)
?.setOnClickListener {
setMethod(context, DEVICE_ADMIN)
cancel()
}
setMethod(context, method)
}
builder.show()
return
}

View file

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

View file

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

View file

@ -1,41 +1,53 @@
package de.jrpie.android.launcher.apps
import android.content.Context
import android.icu.text.Normalizer2
import android.os.Build
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.AppAction
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.ShortcutAction
import de.jrpie.android.launcher.preferences.LauncherPreferences
import java.util.Locale
import kotlin.text.Regex.Companion.escape
class AppFilter(
var context: Context,
var search: String,
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)?.appInfo }
.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
val allowedSpecialCharacters = search
val allowedSpecialCharacters = unicodeNormalize(query)
.lowercase(Locale.ROOT)
.toCharArray()
.distinct()
@ -45,20 +57,21 @@ class AppFilter(
val disallowedCharsRegex = "[^\\p{L}$allowedSpecialCharacters]".toRegex()
fun normalize(text: String): String {
return text.lowercase(Locale.ROOT).replace(disallowedCharsRegex, "")
return unicodeNormalize(text).replace(disallowedCharsRegex, "")
}
if (search.isEmpty()) {
if (query.isEmpty()) {
return apps
} else {
val r: MutableList<DetailedAppInfo> = ArrayList()
val appsSecondary: MutableList<DetailedAppInfo> = ArrayList()
val normalizedText: String = normalize(search)
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(normalizedText)) {
if (itemLabel.startsWith(normalizedQuery)) {
r.add(item)
} else if (itemLabel.contains(normalizedText)) {
} else if (itemLabel.contains(normalizedQuery)) {
appsSecondary.add(item)
}
}
@ -70,12 +83,20 @@ 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()) }),
;
}
private fun unicodeNormalize(s: String): String {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val normalizer = Normalizer2.getNFKDInstance()
return normalizer.normalize(s.lowercase(Locale.ROOT))
}
return s.lowercase(Locale.ROOT)
}
}
}

View file

@ -4,60 +4,26 @@ 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
/**
* Represents an app installed on the users device.
* Contains the minimal amount of data required to identify the app.
*/
class AppInfo(val packageName: CharSequence, val activityName: CharSequence?, val user: Int = INVALID_USER) {
// TODO: make activityName non nullable (breaking change to SharedPreferences!)
fun serialize(): String {
val u = user
var ret = "$packageName;$u"
activityName?.let { ret += ";$activityName" }
return ret
}
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()
}
@Serializable
@SerialName("app")
data class AppInfo(val packageName: String, val activityName: String?, val user: Int = INVALID_USER): AbstractAppInfo {
fun getLauncherActivityInfo(
context: Context
): LauncherActivityInfo? {
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
val userHandle = getUserFromId(user, context)
val activityList = launcherApps.getActivityList(packageName.toString(), userHandle)
val activityList = launcherApps.getActivityList(packageName, userHandle)
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 {
val values = serialized.split(";")
val packageName = values[0]
val user = Integer.valueOf(values[1])
val activityName = values.getOrNull(2)
return AppInfo(packageName, activityName, user)
}
}
}

View file

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

View file

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

View file

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

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

View file

@ -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,11 +53,11 @@ 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()
}
setNegativeButton(R.string.dialog_select_color_cancel) { _, _ -> }
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
}.create().also { it.show() }.apply {
val preview = findViewById<EditText>(R.id.dialog_select_color_preview)
@ -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) {

View file

@ -1,23 +1,19 @@
package de.jrpie.android.launcher.preferences;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import de.jrpie.android.launcher.R;
import de.jrpie.android.launcher.actions.lock.LockMethod;
import de.jrpie.android.launcher.apps.AppInfo;
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;
import eu.jonahbauer.android.preference.annotations.Preference;
import eu.jonahbauer.android.preference.annotations.PreferenceGroup;
import eu.jonahbauer.android.preference.annotations.Preferences;
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException;
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer;
@Preferences(
name = "de.jrpie.android.launcher.preferences.LauncherPreferences",
@ -25,18 +21,24 @@ import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSeriali
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 = LauncherPreferences$Config.AppInfoSetSerializer.class),
@Preference(name = "hidden", type = Set.class, serializer = LauncherPreferences$Config.AppInfoSetSerializer.class),
@Preference(name = "custom_names", type = HashMap.class, serializer = LauncherPreferences$Config.MapAppInfoStringSerializer.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 = {
}),
@ -62,11 +64,13 @@ import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSeriali
}),
@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 = {
@Preference(name = "search_auto_launch", type = boolean.class, defaultValue = "true"),
@Preference(name = "search_web", type = boolean.class, description = "false"),
@Preference(name = "search_auto_open_keyboard", type = boolean.class, defaultValue = "true"),
}),
@PreferenceGroup(name = "enabled_gestures", prefix = "settings_enabled_gestures_", suffix = "_key", value = {
@ -78,73 +82,4 @@ import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSeriali
@Preference(name = "lock_method", type = LockMethod.class, defaultValue = "DEVICE_ADMIN"),
}),
})
public final class LauncherPreferences$Config {
public static class AppInfoSetSerializer implements PreferenceSerializer<Set<AppInfo>, Set<String>> {
@Override
public Set<String> serialize(Set<AppInfo> value) throws PreferenceSerializationException {
if (value == null) return null;
var serialized = new HashSet<String>(value.size());
for (var app : value) {
serialized.add(app.serialize());
}
return serialized;
}
@Override
public Set<AppInfo> deserialize(Set<String> value) throws PreferenceSerializationException {
if (value == null) return null;
var deserialized = new HashSet<AppInfo>(value.size());
for (var s : value) {
deserialized.add(AppInfo.Companion.deserialize(s));
}
return deserialized;
}
}
public static class MapAppInfoStringSerializer implements PreferenceSerializer<HashMap<AppInfo, String>, Set<String>> {
@Override
public Set<String> serialize(HashMap<AppInfo, String> value) throws PreferenceSerializationException {
if (value == null) return null;
var serialized = new HashSet<String>(value.size());
for (var entry : value.entrySet()) {
JSONObject obj = new JSONObject();
try {
obj.put("key", entry.getKey().serialize());
obj.put("value", entry.getValue());
serialized.add(obj.toString());
} catch (JSONException ignored) {
}
}
return serialized;
}
@Override
public HashMap<AppInfo, String> deserialize(Set<String> value) throws PreferenceSerializationException {
if (value == null) return null;
var deserialized = new HashMap<AppInfo, String>();
for (var entry : value) {
try {
JSONObject obj = new JSONObject(entry);
AppInfo info = AppInfo.Companion.deserialize(obj.getString("key"));
String s = obj.getString("value");
deserialized.put(info, s);
} catch (JSONException ignored) {
}
}
return deserialized;
}
}
}
public final class LauncherPreferences$Config {}

View file

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

View file

@ -1,415 +1,69 @@
package de.jrpie.android.launcher.preferences
import android.content.Context
import android.content.SharedPreferences
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.theme.Background
import de.jrpie.android.launcher.preferences.theme.ColorTheme
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
/* Current version of the structure of preferences.
* Increase when breaking changes are introduced and write an appropriate case in
* `migratePreferencesToNewVersion`
*/
const val PREFERENCE_VERSION = 1
const val PREFERENCE_VERSION = 4
const val UNKNOWN_PREFERENCE_VERSION = -1
private const val TAG = "Launcher - Preferences"
private fun migrateStringPreference(
oldPrefs: SharedPreferences,
newPreferences: SharedPreferences.Editor,
oldKey: String,
newKey: String,
default: String
) {
val s = oldPrefs.getString(oldKey, default)
newPreferences.putString(newKey, s)
}
private fun migrateIntPreference(
oldPrefs: SharedPreferences,
newPreferences: SharedPreferences.Editor,
oldKey: String,
newKey: String,
default: Int
) {
val s = oldPrefs.getInt(oldKey, default)
newPreferences.putInt(newKey, s)
}
private fun migrateBooleanPreference(
oldPrefs: SharedPreferences,
newPreferences: SharedPreferences.Editor,
oldKey: String,
newKey: String,
default: Boolean
) {
val s = oldPrefs.getBoolean(oldKey, default)
newPreferences.putBoolean(newKey, s)
}
/*
* Tries to detect preferences written by older versions of the app
* and migrate them to the current format.
*/
fun migratePreferencesToNewVersion(context: Context) {
when (LauncherPreferences.internal().versionCode()) {
// Check versions, make sure transitions between versions go well
PREFERENCE_VERSION -> { /* the version installed and used previously are the same */
}
UNKNOWN_PREFERENCE_VERSION -> { /* still using the old preferences file */
Log.i(
TAG,
"Unknown preference version, trying to restore preferences from old version."
)
val oldPrefs = context.getSharedPreferences(
"V3RYR4ND0MK3YCR4P",
Context.MODE_PRIVATE
)
if (!oldPrefs.contains("startedBefore")) {
Log.i(TAG, "No old preferences found.")
return
}
val newPrefs = LauncherPreferences.getSharedPreferences().edit()
migrateBooleanPreference(
oldPrefs,
newPrefs,
"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()
when (oldPrefs.getString("theme", "finn")) {
"finn" -> {
LauncherPreferences.theme().colorTheme(ColorTheme.DEFAULT)
LauncherPreferences.theme().monochromeIcons(false)
LauncherPreferences.theme().background(Background.DIM)
}
"dark" -> {
LauncherPreferences.theme().colorTheme(ColorTheme.DARK)
LauncherPreferences.theme().monochromeIcons(true)
LauncherPreferences.theme().background(Background.DIM)
}
try {
when (LauncherPreferences.internal().versionCode()) {
// Check versions, make sure transitions between versions go well
PREFERENCE_VERSION -> { /* the version installed and used previously are the same */
}
UNKNOWN_PREFERENCE_VERSION -> { /* still using the old preferences file */
migratePreferencesFromVersionUnknown(context)
Log.i(TAG, "migration of preferences complete (${UNKNOWN_PREFERENCE_VERSION} -> ${PREFERENCE_VERSION}).")
}
LauncherPreferences.internal().versionCode(PREFERENCE_VERSION)
Log.i(TAG, "migration of preferences complete.")
1 -> {
migratePreferencesFromVersion1()
Log.i(TAG, "migration of preferences complete (1 -> ${PREFERENCE_VERSION}).")
}
2 -> {
migratePreferencesFromVersion2()
Log.i(TAG, "migration of preferences complete (2 -> ${PREFERENCE_VERSION}).")
}
3 -> {
migratePreferencesFromVersion3()
Log.i(TAG, "migration of preferences complete (3 -> ${PREFERENCE_VERSION}).")
}
// show the new tutorial
// context.startActivity(Intent(context, TutorialActivity::class.java))
else -> {
Log.w(
TAG,
"Shared preferences were written by a newer version of the app (${
LauncherPreferences.internal().versionCode()
})!"
)
}
}
else -> {}
} catch (e: Exception) {
Log.e(TAG, "Unable to restore preferences:\n${e.stackTrace}")
resetPreferences(context)
}
}
@ -419,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)

View file

@ -0,0 +1,141 @@
package de.jrpie.android.launcher.preferences.legacy
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.LauncherAction
import de.jrpie.android.launcher.apps.AppInfo
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 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(
Pair("launcher:settings", LauncherAction.SETTINGS),
Pair("launcher:choose", LauncherAction.CHOOSE),
Pair("launcher:chooseFromFavorites", LauncherAction.CHOOSE_FROM_FAVORITES),
Pair("launcher:volumeUp", LauncherAction.VOLUME_UP),
Pair("launcher:volumeDown", LauncherAction.VOLUME_DOWN),
Pair("launcher:nextTrack", LauncherAction.TRACK_NEXT),
Pair("launcher:previousTrack", LauncherAction.TRACK_PREV),
Pair("launcher:expandNotificationsPanel", LauncherAction.EXPAND_NOTIFICATIONS_PANEL),
Pair("launcher:expandSettingsPanel", LauncherAction.EXPAND_SETTINGS_PANEL),
Pair("launcher:lockScreen", LauncherAction.LOCK_SCREEN),
Pair("launcher:toggleTorch", LauncherAction.TORCH),
Pair("launcher:nop", LauncherAction.NOP),
)
private fun AppInfo.Companion.legacyDeserialize(serialized: String): AppInfo {
val values = serialized.split(";")
val packageName = values[0]
val user = Integer.valueOf(values[1])
val activityName = values.getOrNull(2) ?: "" // TODO
return AppInfo(packageName, activityName, user)
}
/**
* Get an action for a specific id.
* An id is of the form:
* - "launcher:${launcher_action_name}", see [LauncherAction]
* - "${package_name}", see [AppAction]
* - "${package_name}:${activity_name}", see [AppAction]
*
* @param id
* @param user a user id, ignored if the action is a [LauncherAction].
*/
private fun Action.Companion.legacyFromId(id: String, user: Int?): Action? {
if (id.isEmpty()) {
return null
}
oldLauncherActionIds[id]?.let { return it }
val values = id.split(";")
return AppAction(
AppInfo(
values[0], values.getOrNull(1) ?: "", user ?: INVALID_USER
)
)
}
private fun Action.Companion.legacyFromPreference(id: String): Action? {
val preferences = LauncherPreferences.getSharedPreferences()
val actionId = preferences.getString("$id.app", "")!!
var u: Int? = preferences.getInt(
"$id.user",
INVALID_USER
)
u = if (u == INVALID_USER) null else u
return Action.legacyFromId(actionId, u)
}
private fun migrateAppInfoStringMap(key: String) {
val preferences = LauncherPreferences.getSharedPreferences()
serializeMapAppInfo(
preferences.getStringSet(key, setOf())?.mapNotNull { entry ->
try {
val obj = JSONObject(entry)
val info = AppInfo.legacyDeserialize(obj.getString("key"))
val value = obj.getString("value")
Pair(info, value)
} catch (_: JSONException) {
null
}
}?.toMap(HashMap())
)?.let {
preferences.edit { putStringSet(key, it) }
}
}
private fun migrateAppInfoSet(key: String) {
(LauncherPreferences.getSharedPreferences().getStringSet(key, setOf()) ?: return)
.map(AppInfo.Companion::legacyDeserialize)
.map(AppInfo::serialize)
.toSet()
.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")
}
}
}
/**
* Migrate preferences from version 1 (used until version j-0.0.18) to the current format
* (see [PREFERENCE_VERSION])
*/
fun migratePreferencesFromVersion1() {
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()
}

View file

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

View file

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

View file

@ -0,0 +1,396 @@
package de.jrpie.android.launcher.preferences.legacy
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.theme.Background
import de.jrpie.android.launcher.preferences.theme.ColorTheme
import androidx.core.content.edit
private fun migrateStringPreference(
oldPrefs: SharedPreferences,
newPreferences: SharedPreferences.Editor,
oldKey: String,
newKey: String,
default: String
) {
val s = oldPrefs.getString(oldKey, default)
newPreferences.putString(newKey, s)
}
private fun migrateIntPreference(
oldPrefs: SharedPreferences,
newPreferences: SharedPreferences.Editor,
oldKey: String,
newKey: String,
default: Int
) {
val s = oldPrefs.getInt(oldKey, default)
newPreferences.putInt(newKey, s)
}
private fun migrateBooleanPreference(
oldPrefs: SharedPreferences,
newPreferences: SharedPreferences.Editor,
oldKey: String,
newKey: String,
default: Boolean
) {
val s = oldPrefs.getBoolean(oldKey, default)
newPreferences.putBoolean(newKey, s)
}
private const val TAG = "Preferences ? -> 1"
/**
* Try to migrate from a very old preference version, where no version number was stored
* and a different file was used.
*/
fun migratePreferencesFromVersionUnknown(context: Context) {
Log.i(
TAG,
"Unknown preference version, trying to restore preferences from old version."
)
val oldPrefs = context.getSharedPreferences(
"V3RYR4ND0MK3YCR4P",
Context.MODE_PRIVATE
)
if (!oldPrefs.contains("startedBefore")) {
Log.i(TAG, "No old preferences found. Probably this is a fresh installation.")
return
}
LauncherPreferences.getSharedPreferences().edit {
migrateBooleanPreference(
oldPrefs,
this,
"startedBefore",
"internal.started_before",
false
)
migrateStringPreference(
oldPrefs,
this,
"action_volumeUpApp",
"action.volume_up.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_volumeUpApp_user",
"action.volume_up.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_volumeDownApp",
"action.volume_down.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_volumeDownApp_user",
"action.volume_down.user",
-1
)
migrateStringPreference(oldPrefs, this, "action_timeApp", "action.time.app", "")
migrateIntPreference(oldPrefs, this, "action_timeApp_user", "action.time.user", -1)
migrateStringPreference(oldPrefs, this, "action_dateApp", "action.date.app", "")
migrateIntPreference(oldPrefs, this, "action_dateApp_user", "action.date.user", -1)
migrateStringPreference(
oldPrefs,
this,
"action_longClickApp",
"action.long_click.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_longClickApp_user",
"action.long_click.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_doubleClickApp",
"action.double_click.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_doubleClickApp_user",
"action.double_click.user",
-1
)
migrateStringPreference(oldPrefs, this, "action_upApp", "action.up.app", "")
migrateIntPreference(oldPrefs, this, "action_upApp_user", "action.up.user", -1)
migrateStringPreference(
oldPrefs,
this,
"action_up_leftApp",
"action.up_left.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_up_leftApp_user",
"action.up_left.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_up_rightApp",
"action.up_right.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_up_rightApp_user",
"action.up_right.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_doubleUpApp",
"action.double_up.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_doubleUpApp_user",
"action.double_up.user",
-1
)
migrateStringPreference(oldPrefs, this, "action_downApp", "action.down.app", "")
migrateIntPreference(oldPrefs, this, "action_downApp_user", "action.down.user", -1)
migrateStringPreference(
oldPrefs,
this,
"action_down_leftApp",
"action.down_left.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_down_leftApp_user",
"action.down_left.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_down_rightApp",
"action.down_right.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_down_rightApp_user",
"action.down_right.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_doubleDownApp",
"action.double_down.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_doubleDownApp_user",
"action.double_down.user",
-1
)
migrateStringPreference(oldPrefs, this, "action_leftApp", "action.left.app", "")
migrateIntPreference(oldPrefs, this, "action_leftApp_user", "action.left.user", -1)
migrateStringPreference(
oldPrefs,
this,
"action_left_topApp",
"action.left_top.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_left_topApp_user",
"action.left_top.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_left_bottomApp",
"action.left_bottom.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_left_bottomApp_user",
"action.left_bottom.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_doubleLeftApp",
"action.double_left.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_doubleLeftApp_user",
"action.double_left.user",
-1
)
migrateStringPreference(oldPrefs, this, "action_rightApp", "action.right.app", "")
migrateIntPreference(
oldPrefs,
this,
"action_rightApp_user",
"action.right.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_right_topApp",
"action.right_top.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_right_topApp_user",
"action.right_top.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_right_bottomApp",
"action.right_bottom.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_right_bottomApp_user",
"action.right_bottom.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_doubleRightApp",
"action.double_right.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_doubleRightApp_user",
"action.double_right.user",
-1
)
migrateBooleanPreference(oldPrefs, this, "timeVisible", "clock.time_visible", true)
migrateBooleanPreference(oldPrefs, this, "dateVisible", "clock.date_visible", true)
migrateBooleanPreference(
oldPrefs,
this,
"dateLocalized",
"clock.date_localized",
false
)
migrateBooleanPreference(
oldPrefs,
this,
"dateTimeFlip",
"clock.date_time_flip",
false
)
migrateBooleanPreference(
oldPrefs,
this,
"disableTimeout",
"display.disable_timeout",
false
)
migrateBooleanPreference(
oldPrefs,
this,
"useFullScreen",
"display.use_full_screen",
true
)
migrateBooleanPreference(
oldPrefs,
this,
"enableDoubleActions",
"enabled_gestures.double_actions",
true
)
migrateBooleanPreference(
oldPrefs,
this,
"enableEdgeActions",
"enabled_gestures.edge_actions",
true
)
migrateBooleanPreference(
oldPrefs,
this,
"searchAutoLaunch",
"functionality.search_auto_launch",
true
)
migrateBooleanPreference(
oldPrefs,
this,
"searchAutoKeyboard",
"functionality.search_auto_keyboard",
true
)
}
when (oldPrefs.getString("theme", "finn")) {
"finn" -> {
LauncherPreferences.theme().colorTheme(ColorTheme.DEFAULT)
LauncherPreferences.theme().monochromeIcons(false)
LauncherPreferences.theme().background(Background.DIM)
}
"dark" -> {
LauncherPreferences.theme().colorTheme(ColorTheme.DARK)
LauncherPreferences.theme().monochromeIcons(true)
LauncherPreferences.theme().background(Background.DIM)
}
}
LauncherPreferences.internal().versionCode(1)
Log.i(TAG, "migrated preferences to version 1.")
migratePreferencesFromVersion1()
}

View file

@ -0,0 +1,71 @@
@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
package de.jrpie.android.launcher.preferences.serialization
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
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Suppress("UNCHECKED_CAST")
class SetAbstractAppInfoPreferenceSerializer :
PreferenceSerializer<java.util.Set<AbstractAppInfo>?, java.util.Set<java.lang.String>?> {
@Throws(PreferenceSerializationException::class)
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<AbstractAppInfo>? {
return value?.map(java.lang.String::toString)?.map(AbstractAppInfo::deserialize)
?.toHashSet() as? java.util.Set<AbstractAppInfo>
}
}
@Suppress("UNCHECKED_CAST")
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 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<AbstractAppInfo, String>? {
return value?.associateTo(HashMap()) {
val entry = Json.decodeFromString<MapEntry>(it.toString())
Pair(entry.key, entry.value)
}
}
}

View file

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

View file

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

View file

@ -0,0 +1,47 @@
package de.jrpie.android.launcher.ui
import android.content.Context
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.view.View
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
// Taken from https://stackoverflow.com/questions/47293269
fun View.blink(
times: Int = Animation.INFINITE,
duration: Long = 1000L,
offset: Long = 20L,
minAlpha: Float = 0.2f,
maxAlpha: Float = 1.0f,
repeatMode: Int = Animation.REVERSE
) {
startAnimation(AlphaAnimation(minAlpha, maxAlpha).also {
it.duration = duration
it.startOffset = offset
it.repeatMode = repeatMode
it.repeatCount = times
})
}
// Taken from: https://stackoverflow.com/a/30340794/12787264
fun ImageView.transformGrayscale(grayscale: Boolean) {
this.colorFilter = if (grayscale) {
ColorMatrixColorFilter(ColorMatrix().apply {
setSaturation(0f)
})
} else {
null
}
}
// Taken from https://stackoverflow.com/a/50743764/12787264
fun View.openSoftKeyboard(context: Context) {
this.requestFocus()
// open the soft keyboard
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
}

View file

@ -2,40 +2,25 @@ 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
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.databinding.HomeBinding
import de.jrpie.android.launcher.loadApps
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,
@ -49,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 ->
@ -61,40 +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()
// Try to restore old preferences
migratePreferencesToNewVersion(this)
// First time opening the app: set defaults and start tutorial
if (!LauncherPreferences.internal().started()) {
resetPreferences(this)
LauncherPreferences.internal().started(true)
openTutorial(this)
}
// Preload apps to speed up the Apps Recycler
AsyncTask.execute { loadApps(packageManager, applicationContext) }
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(
@ -103,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
@ -157,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 {
@ -189,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() {
@ -212,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
@ -232,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() {
@ -344,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 }
}

View file

@ -0,0 +1,46 @@
package de.jrpie.android.launcher.ui
import android.content.res.Resources
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.databinding.LegalInfoBinding
class LegalInfoActivity : AppCompatActivity(), UIObject {
private lateinit var binding: LegalInfoBinding
override fun onCreate(savedInstanceState: Bundle?) {
super<AppCompatActivity>.onCreate(savedInstanceState)
super<UIObject>.onCreate()
// Initialise layout
binding = LegalInfoBinding.inflate(layoutInflater)
setContentView(binding.root)
setTitle(R.string.legal_info_title)
setSupportActionBar(binding.legalInfoAppbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onStart() {
super<AppCompatActivity>.onStart()
super<UIObject>.onStart()
}
override fun getTheme(): Resources.Theme {
return modifyTheme(super.getTheme())
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
finish()
return true
}
else -> {
return super.onOptionsItemSelected(item)
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -2,36 +2,28 @@ 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.os.AsyncTask
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.ImageView
import android.widget.PopupMenu
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
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.appsList
import de.jrpie.android.launcher.getUserFromId
import de.jrpie.android.launcher.loadApps
import de.jrpie.android.launcher.openAppSettings
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.ListLayout
import de.jrpie.android.launcher.transformGrayscale
import de.jrpie.android.launcher.ui.list.ListActivity
import de.jrpie.android.launcher.uninstallApp
import de.jrpie.android.launcher.ui.transformGrayscale
/**
* A [RecyclerView] (efficient scrollable list) containing all apps on the users device.
@ -53,9 +45,19 @@ class AppsRecyclerAdapter(
) :
RecyclerView.Adapter<AppsRecyclerAdapter.ViewHolder>() {
private val LOG_TAG = "Launcher"
private val apps = (activity.applicationContext as Application).apps
private val appsListDisplayed: MutableList<AbstractDetailedAppInfo> = mutableListOf()
private val grayscale = LauncherPreferences.theme().monochromeIcons()
private val appsListDisplayed: MutableList<DetailedAppInfo> = mutableListOf()
// temporarily disable auto launch
var disableAutoLaunch: Boolean = false
init {
apps.observe(this.activity as AppCompatActivity) {
updateAppsList()
}
updateAppsList()
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
@ -66,7 +68,7 @@ class AppsRecyclerAdapter(
override fun onClick(v: View) {
val rect = Rect()
img.getGlobalVisibleRect(rect)
selectItem(adapterPosition, rect)
selectItem(bindingAdapterPosition, rect)
}
init {
@ -78,21 +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()) transformGrayscale(
viewHolder.img
)
// decide when to show the options popup menu about
if (intention == ListActivity.ListActivityIntention.VIEW) {
@ -117,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)
}
@ -140,73 +144,23 @@ class AppsRecyclerAdapter(
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.app_menu_delete -> {
uninstallApp(appInfo.app, activity)
true
appInfo.getRawInfo().uninstall(activity); true
}
R.id.app_menu_info -> {
openAppSettings(appInfo.app, activity)
true
(appInfo.getRawInfo() as? AppInfo)?.openSettings(activity); true
}
R.id.app_menu_favorite -> {
val favorites: MutableSet<AppInfo> =
LauncherPreferences.apps().favorites() ?: mutableSetOf()
if (favorites.contains(appInfo.app)) {
favorites.remove(appInfo.app)
Log.i(LOG_TAG, "Removing $appInfo from favorites.")
} else {
Log.i(LOG_TAG, "Adding $appInfo to favorites.")
favorites.add(appInfo.app)
}
LauncherPreferences.apps().favorites(favorites)
true
appInfo.getRawInfo().toggleFavorite(); true
}
R.id.app_menu_hidden -> {
val hidden: MutableSet<AppInfo> =
LauncherPreferences.apps().hidden() ?: mutableSetOf()
if (hidden.contains(appInfo.app)) {
hidden.remove(appInfo.app)
} else {
hidden.add(appInfo.app)
Snackbar.make(root, R.string.snackbar_app_hidden, Snackbar.LENGTH_LONG)
.setAction(R.string.undo) {
LauncherPreferences.apps().hidden(
LauncherPreferences.apps().hidden().minus(appInfo.app)
)
}.show()
}
LauncherPreferences.apps().hidden(hidden)
true
appInfo.getRawInfo().toggleHidden(root); true
}
R.id.app_menu_rename -> {
val builder = AlertDialog.Builder(activity, R.style.AlertDialogCustom)
val title = activity.getString(R.string.dialog_rename_title, appInfo.label)
builder.setTitle(title)
builder.setView(R.layout.dialog_rename_app)
builder.setNegativeButton(R.string.dialog_rename_cancel) { d, _ -> d.cancel() }
builder.setPositiveButton(R.string.dialog_rename_ok) { d, _ ->
appInfo.setCustomLabel(
(d as? AlertDialog)
?.findViewById<EditText>(R.id.dialog_rename_app_edit_text)
?.text.toString()
)
}
val dialog = builder.create()
dialog.show()
val input = dialog.findViewById<EditText>(R.id.dialog_rename_app_edit_text)
input?.setText(appInfo.getCustomLabel(activity))
input?.hint = appInfo.label
true
appInfo.showRenameDialog(activity); true
}
else -> false
@ -230,18 +184,6 @@ class AppsRecyclerAdapter(
return viewHolder
}
init {
// Load the apps
if (appsList.size == 0)
loadApps(activity.packageManager, activity)
else {
AsyncTask.execute { loadApps(activity.packageManager, activity) }
notifyDataSetChanged()
}
updateAppsList()
}
fun selectItem(pos: Int, rect: Rect = Rect()) {
if (pos >= appsListDisplayed.size) {
return
@ -249,30 +191,30 @@ 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())
}
}
}
fun updateAppsList(triggerAutoLaunch: Boolean = false) {
appsListDisplayed.clear()
appsListDisplayed.addAll(appFilter(appsList))
apps.value?.let { appsListDisplayed.addAll(appFilter(it)) }
if (triggerAutoLaunch &&
appsListDisplayed.size == 1
&& intention == ListActivity.ListActivityIntention.VIEW
&& !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
@ -286,7 +228,7 @@ class AppsRecyclerAdapter(
* The function [setSearchString] is used to search elements within this [RecyclerView].
*/
fun setSearchString(search: String) {
appFilter.search = search
appFilter.query = search
updateAppsList(true)
}

View file

@ -0,0 +1,113 @@
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.os.Bundle
import android.util.Log
import android.view.View
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.apps.AppInfo
import de.jrpie.android.launcher.apps.AbstractAppInfo
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
import de.jrpie.android.launcher.getUserFromId
import de.jrpie.android.launcher.preferences.LauncherPreferences
import androidx.core.net.toUri
private const val LOG_TAG = "AppContextMenu"
fun AppInfo.openSettings(
context: Context,
sourceBounds: Rect? = null,
opts: Bundle? = null
) {
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
this.getLauncherActivityInfo(context)?.let { app ->
launcherApps.startAppDetailsActivity(app.componentName, app.user, sourceBounds, opts)
}
}
fun AbstractAppInfo.uninstall(activity: Activity) {
if (this is AppInfo) {
val packageName = this.packageName
val userId = this.user
Log.i(LOG_TAG, "uninstalling $this")
val intent = Intent(Intent.ACTION_DELETE)
intent.data = "package:$packageName".toUri()
getUserFromId(userId, activity).let { user ->
intent.putExtra(Intent.EXTRA_USER, user)
}
activity.startActivity(intent)
} else if(this is PinnedShortcutInfo) {
val pinned = LauncherPreferences.apps().pinnedShortcuts() ?: mutableSetOf()
pinned.remove(this)
LauncherPreferences.apps().pinnedShortcuts(pinned)
}
}
fun AbstractAppInfo.toggleFavorite() {
val favorites: MutableSet<AbstractAppInfo> =
LauncherPreferences.apps().favorites() ?: mutableSetOf()
if (favorites.contains(this)) {
favorites.remove(this)
Log.i(LOG_TAG, "Removing $this from favorites.")
} else {
Log.i(LOG_TAG, "Adding $this to favorites.")
favorites.add(this)
}
LauncherPreferences.apps().favorites(favorites)
}
/**
* @param view: used to show a snackbar letting the user undo the action
*/
fun AbstractAppInfo.toggleHidden(view: View) {
val hidden: MutableSet<AbstractAppInfo> =
LauncherPreferences.apps().hidden() ?: mutableSetOf()
if (hidden.contains(this)) {
hidden.remove(this)
} else {
hidden.add(this)
Snackbar.make(view, R.string.snackbar_app_hidden, Snackbar.LENGTH_LONG)
.setAction(R.string.undo) {
LauncherPreferences.apps().hidden(
LauncherPreferences.apps().hidden().minus(this)
)
}.show()
}
LauncherPreferences.apps().hidden(hidden)
}
fun AbstractDetailedAppInfo.showRenameDialog(context: Context) {
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
setTitle(context.getString(R.string.dialog_rename_title, getLabel()))
setView(R.layout.dialog_rename_app)
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)
?.text.toString()
)
}
}.create().also { it.show() }.apply {
val input = findViewById<EditText>(R.id.dialog_rename_app_edit_text)
input?.setText(getCustomLabel(context))
input?.hint = getLabel()
}
}

View file

@ -1,21 +1,23 @@
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.openSoftKeyboard
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
/**
@ -25,11 +27,11 @@ import de.jrpie.android.launcher.ui.list.intention
*/
class ListFragmentApps : Fragment(), UIObject {
private lateinit var binding: ListAppsBinding
private lateinit var appsRViewAdapter: AppsRecyclerAdapter
private lateinit var appsRecyclerAdapter: AppsRecyclerAdapter
private var sharedPreferencesListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
appsRViewAdapter.updateAppsList()
appsRecyclerAdapter.updateAppsList()
}
override fun onCreateView(
@ -47,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() {
@ -60,57 +62,96 @@ class ListFragmentApps : Fragment(), UIObject {
override fun setOnClicks() {}
override fun adjustLayout() {
val listActivity = activity as? ListActivity ?: return
appsRViewAdapter =
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)
adapter = appsRViewAdapter
.also {
if (LauncherPreferences.list().reverseLayout()) {
(it as? LinearLayoutManager)?.reverseLayout = true
(it as? GridLayoutManager)?.reverseLayout = true
}
}
adapter = appsRecyclerAdapter
}
binding.listAppsSearchview.setOnQueryTextListener(object :
androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
appsRViewAdapter.setSearchString(query)
appsRViewAdapter.selectItem(0)
appsRecyclerAdapter.setSearchString(query)
if (LauncherPreferences.functionality().searchWeb()) {
val i = Intent(Intent.ACTION_WEB_SEARCH).putExtra("query", query)
try {
activity?.startActivity(i)
} catch (_: ActivityNotFoundException) {
Toast.makeText(
requireContext(),
R.string.toast_activity_not_found_search_web,
Toast.LENGTH_LONG
).show()
}
} else {
appsRecyclerAdapter.selectItem(0)
}
return true
}
override fun onQueryTextChange(newText: String): Boolean {
appsRViewAdapter.setSearchString(newText)
if (newText == " " &&
!appsRecyclerAdapter.disableAutoLaunch &&
(activity as? ListActivity)?.intention
== ListActivity.ListActivityIntention.VIEW &&
LauncherPreferences.functionality().searchAutoLaunch()
) {
appsRecyclerAdapter.disableAutoLaunch = true
binding.listAppsSearchview.apply {
queryHint = context.getString(R.string.list_apps_search_hint_no_auto_launch)
setQuery("", false)
}
return false
}
appsRecyclerAdapter.setSearchString(newText)
return false
}
})
binding.listAppsCheckBoxFavorites.setOnClickListener {
favoritesVisibility =
listActivity.favoritesVisibility =
if (binding.listAppsCheckBoxFavorites.isChecked) {
AppFilter.Companion.AppSetVisibility.EXCLUSIVE
} else {
AppFilter.Companion.AppSetVisibility.VISIBLE
}
appsRViewAdapter.setFavoritesVisibility(favoritesVisibility)
appsRecyclerAdapter.setFavoritesVisibility(listActivity.favoritesVisibility)
(activity as? ListActivity)?.updateTitle()
}
if (intention == ListActivity.ListActivityIntention.VIEW
if (listActivity.intention == ListActivity.ListActivityIntention.VIEW
&& LauncherPreferences.functionality().searchAutoOpenKeyboard()
) {
openSoftKeyboard(requireContext(), binding.listAppsSearchview)
binding.listAppsSearchview.openSoftKeyboard(requireContext())
}
}
}

View file

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

View file

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

View file

@ -1,5 +1,6 @@
package de.jrpie.android.launcher.ui.settings.actions
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
@ -10,19 +11,19 @@ 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
import de.jrpie.android.launcher.databinding.SettingsActionsRecyclerBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.transformGrayscale
import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.ui.list.ListActivity
import de.jrpie.android.launcher.ui.transformGrayscale
/**
* The [SettingsFragmentActionsRecycler] is a fragment containing the [ActionsRecyclerAdapter],
@ -93,7 +94,10 @@ class SettingsFragmentActionsRecycler : Fragment(), UIObject {
class ActionsRecyclerAdapter(val activity: Activity) :
RecyclerView.Adapter<ActionsRecyclerAdapter.ViewHolder>() {
private val gesturesList: ArrayList<Gesture>
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>
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener {
@ -113,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
@ -135,9 +142,8 @@ class ActionsRecyclerAdapter(val activity: Activity) :
val description = gesture.getDescription(activity)
viewHolder.descriptionTextView.text = description
viewHolder.img.transformGrayscale(LauncherPreferences.theme().monochromeIcons())
if (LauncherPreferences.theme().monochromeIcons())
transformGrayscale(viewHolder.img)
updateViewHolder(gesture, viewHolder)
viewHolder.img.setOnClickListener { chooseApp(gesture) }
viewHolder.chooseButton.setOnClickListener { chooseApp(gesture) }
@ -154,10 +160,7 @@ class ActionsRecyclerAdapter(val activity: Activity) :
return ViewHolder(view)
}
init {
gesturesList = Gesture.entries.filter(Gesture::isEnabled) as ArrayList<Gesture>
}
@SuppressLint("NotifyDataSetChanged")
fun updateActions() {
val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe()
val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe()
@ -170,15 +173,11 @@ class ActionsRecyclerAdapter(val activity: Activity) :
notifyDataSetChanged()
}
/* */
private fun chooseApp(gesture: Gesture) {
val intent = Intent(activity, ListActivity::class.java)
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)
}
}

View file

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

View file

@ -2,18 +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.openNewTabWindow
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.
@ -39,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
@ -78,55 +77,62 @@ class SettingsFragmentMeta : Fragment(), UIObject {
// view code
binding.settingsMetaButtonViewCode.setOnClickListener {
openNewTabWindow(
getString(R.string.settings_meta_link_github),
requireContext()
)
}
bindURL(binding.settingsMetaButtonViewCode, R.string.settings_meta_link_github)
// report a bug
binding.settingsMetaButtonReportBug.setOnClickListener {
openNewTabWindow(
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 {
openNewTabWindow(
getString(R.string.settings_meta_chat_url),
requireContext()
)
}
bindURL(binding.settingsMetaButtonJoinChat, R.string.settings_meta_chat_url)
// contact developer
binding.settingsMetaButtonContact.setOnClickListener {
openNewTabWindow(
getString(R.string.settings_meta_contact_url),
requireContext()
)
}
// bindURL(binding.settingsMetaButtonContact, R.string.settings_meta_contact_url)
// contact fork developer
binding.settingsMetaButtonForkContact.setOnClickListener {
openNewTabWindow(
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 {
openNewTabWindow(
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
}
}

View file

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

View file

@ -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.blink
import de.jrpie.android.launcher.databinding.TutorialStartBinding
import de.jrpie.android.launcher.databinding.Tutorial0StartBinding
import de.jrpie.android.launcher.ui.UIObject
/**
* 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
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

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

View file

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

View file

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

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

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

View file

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

View file

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

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

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

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

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

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

View file

@ -1,14 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content"
android:padding="10dp">
<de.jrpie.android.launcher.ui.util.HtmlTextView
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<de.jrpie.android.launcher.ui.util.HtmlTextView
android:id="@+id/dialog_select_lock_method_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/screen_lock_method_dialog_text"
android:textColor="@android:color/black" />
</LinearLayout>
<Button
android:id="@+id/dialog_select_lock_method_button_device_admin"
android:text="@string/screen_lock_method_use_device_admin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<Button
android:id="@+id/dialog_select_lock_method_button_accessibility"
android:text="@string/screen_lock_method_use_accessibility"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</ScrollView>

View file

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

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".ui.settings.SettingsActivity">
<com.google.android.material.appbar.AppBarLayout
android:background="@null"
app:elevation="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/legal_info_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".ui.LegalInfoActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<de.jrpie.android.launcher.ui.util.HtmlTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/legal_info_text" />
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

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