mirror of
https://github.com/jrpie/Launcher.git
synced 2025-04-18 18:00:50 +02:00
Compare commits
432 commits
Author | SHA1 | Date | |
---|---|---|---|
22633bdac3 | |||
4f795289d5 | |||
2774b74d9d | |||
3d49ec16a7 | |||
![]() |
a0b2417363 | ||
![]() |
20a01e9f03 | ||
![]() |
24250ad345 | ||
![]() |
7cce425339 | ||
![]() |
0877ca6772 | ||
![]() |
03a9833b51 | ||
![]() |
ce65741717 | ||
![]() |
c085087e1e | ||
![]() |
cbd23159da | ||
![]() |
940e5785dc | ||
![]() |
14ffbd1f6c | ||
![]() |
bfc84b57ca | ||
![]() |
8b1963f3e1 | ||
![]() |
4f801427a4 | ||
![]() |
8a487eb4c7 | ||
![]() |
e6dd2634ae | ||
0441b3fd3d | |||
e7c1d28576 | |||
653d16b269 | |||
5d695ec0ea | |||
b4608ef153 | |||
8e140e2e69 | |||
7fc58fe384 | |||
54409b6312 | |||
865cd47583 | |||
58ddd3c8cc | |||
0baa889de5 | |||
fa34cbae90 | |||
7ac09bd465 | |||
![]() |
c8d7a1cc3e | ||
7094d55484 | |||
b3e4d8834a | |||
008d0242ee | |||
![]() |
65034bf2fb | ||
![]() |
3cec2c36c6 | ||
00350d4c3a | |||
0941062270 | |||
c783a51658 | |||
da115bb2d9 | |||
90434617e7 | |||
47940811b4 | |||
![]() |
232046e986 | ||
![]() |
ff108ee323 | ||
![]() |
943867d938 | ||
![]() |
59f4a29044 | ||
![]() |
bd70b822cf | ||
72f9c0595f | |||
75b22400c5 | |||
c1511cd475 | |||
3597baee1f | |||
e02ca4091f | |||
541e60356c | |||
492749a340 | |||
55af392706 | |||
077ee4381a | |||
e250a58ef4 | |||
c7af387a94 | |||
6cd17343fc | |||
b156b68d53 | |||
c9ee2c6304 | |||
bf45b6602e | |||
d7dd1aa71a | |||
3664159782 | |||
8df9aae029 | |||
![]() |
f776fbb88e | ||
![]() |
a5ec8bb796 | ||
3b2dca9af9 | |||
1b12032750 | |||
55a54fb9a5 | |||
![]() |
e39ff62613 | ||
9fe1a37ed6 | |||
1f825d6f00 | |||
5ea03d39fa | |||
ae119ac4ce | |||
8948b34243 | |||
f18811bfa2 | |||
bd1f999a0e | |||
941b06b258 | |||
![]() |
9935386ad8 | ||
![]() |
d44224071f | ||
![]() |
1f8f75dec8 | ||
86528f4e27 | |||
3aee137a3c | |||
befa3afc5d | |||
a295c0ab4b | |||
![]() |
c448c51164 | ||
![]() |
68b79724e8 | ||
![]() |
bef38c2657 | ||
![]() |
d0b0c27b2c | ||
![]() |
4508e4ee5c | ||
![]() |
e959e9d957 | ||
![]() |
958d4879f5 | ||
![]() |
7841a99415 | ||
![]() |
18b4fca933 | ||
![]() |
5792c7f38c | ||
88a78749c2 | |||
7257d4ca35 | |||
47ae0bf35f | |||
5669279c64 | |||
0c0d90a357 | |||
012f13c827 | |||
757486771d | |||
9c5500aa83 | |||
d69e3caf71 | |||
944eb89fef | |||
fa2f1c4127 | |||
8699b92246 | |||
d6355afc54 | |||
0a9890111c | |||
b2e7e4cacf | |||
d62815be12 | |||
768d27a7bb | |||
a227a40b6e | |||
f280d36667 | |||
![]() |
3973f1338f | ||
9f3142cb69 | |||
ecd58b91bb | |||
2fc2b76fba | |||
![]() |
c7895830e7 | ||
![]() |
14766fe1d9 | ||
![]() |
b9a59c9e37 | ||
![]() |
2d03bdbbef | ||
![]() |
09b12834a9 | ||
23f8cfb70e | |||
6d385e4e87 | |||
785e024ddb | |||
679c90130d | |||
95c9fcd292 | |||
a188e06342 | |||
5696ea73da | |||
a7ce5b9222 | |||
74b448cd0f | |||
cd36fad8cd | |||
64ccc4f300 | |||
b5dcfeecec | |||
677453137a | |||
f08f357bb3 | |||
![]() |
567d08fb3a | ||
fa0c880342 | |||
8fc69e10e9 | |||
eff7cfda5e | |||
ddca29067e | |||
c6d0477e3a | |||
eb9f43ec49 | |||
ce5fade39a | |||
09ca4410b9 | |||
92772d715c | |||
36a04703c2 | |||
31106158b3 | |||
6d5e3b36f9 | |||
![]() |
e53b7682d8 | ||
![]() |
3e4d22d3cd | ||
![]() |
954bef4aac | ||
e39d72ef3c | |||
9216837879 | |||
2d53562d58 | |||
220ad4d18f | |||
cbaa853b1c | |||
ad5ee5e10e | |||
4ddb893d41 | |||
970c160f4a | |||
96c924fba5 | |||
9d96284719 | |||
36ee8033ed | |||
2b7999cfdc | |||
3353719cc3 | |||
c84675904d | |||
bf4131c603 | |||
![]() |
db4a52a780 | ||
8309b7c290 | |||
aca7fe57d2 | |||
9e514050e2 | |||
![]() |
ae9210a103 | ||
40a80755d5 | |||
![]() |
ef787f5417 | ||
97d10d09b5 | |||
46aaa91fe9 | |||
9423dcce11 | |||
![]() |
a88fb869cb | ||
6eb4fd6104 | |||
fc56d3c857 | |||
c4513236ec | |||
41011b7b35 | |||
8a8a148ebf | |||
![]() |
cbfde12981 | ||
![]() |
c68a59e4ee | ||
3e1fa822e1 | |||
a9f3196f8e | |||
3c59ad4c41 | |||
06777a4d34 | |||
08f0026dc3 | |||
9649b9b523 | |||
84c4fdfb47 | |||
77668d773f | |||
0692db8084 | |||
27ebd728ef | |||
2c4f5176b7 | |||
98274fbd4f | |||
![]() |
490bb0e3dc | ||
![]() |
9bfe61c4f0 | ||
![]() |
c44115a947 | ||
![]() |
987acc93e4 | ||
![]() |
79f3bf3d79 | ||
1a34b6234e | |||
8ca67103bf | |||
edc4927f1a | |||
897bfb9640 | |||
![]() |
cc64745c0f | ||
57c2eab61e | |||
3f0856b732 | |||
172de1f3dd | |||
80cb8e995a | |||
608f4c0bd0 | |||
3de4b7c56f | |||
7ee39ba3b8 | |||
89bf02fc06 | |||
a9e9f34260 | |||
c3a31a97ef | |||
d63461ea40 | |||
7f2a52c79c | |||
3ae2e36ee0 | |||
fac3991f96 | |||
b8ef2a07c2 | |||
3bba7cfe74 | |||
a2db262dbc | |||
ea964a50b9 | |||
58d5112f8a | |||
cef92d2331 | |||
28ed2c78b3 | |||
038eaccc9c | |||
659ac73c64 | |||
ef96ba720c | |||
68fa4190f3 | |||
![]() |
7b66611415 | ||
![]() |
c42b648ea2 | ||
![]() |
a8d42d6b50 | ||
![]() |
19d8db9ea8 | ||
![]() |
76accf40da | ||
b09c6a52b2 | |||
bcb3f74a21 | |||
70e35db394 | |||
6a42ef0747 | |||
acbcef5827 | |||
c1dcc0fe4e | |||
90f5d5f5c5 | |||
118352740c | |||
aba23c370b | |||
d1d3699233 | |||
60746c97be | |||
f6f09f1c2f | |||
7a3208ae23 | |||
d22d20d8f9 | |||
e915585dbb | |||
e0ce54bef1 | |||
f61f861950 | |||
9848785b3e | |||
cd600af09f | |||
592d2f6f8e | |||
d98258b316 | |||
ec529dd75a | |||
c509031954 | |||
6376845dc9 | |||
0543bcda55 | |||
4fc99c4337 | |||
a316ad21c0 | |||
7efe05011f | |||
d703a139f3 | |||
9b84d1ddcf | |||
589d5ec9ab | |||
0ee4814bd2 | |||
5b00c4b6bb | |||
3c1436e759 | |||
3037edfd83 | |||
ecdb388f68 | |||
![]() |
71ecf22ff7 | ||
19fe0fdd8a | |||
55b48779cf | |||
c1650fab84 | |||
9f235200fe | |||
4b14585c57 | |||
3f1263f648 | |||
2cc4d02587 | |||
874a2bcdad | |||
6b31f8dc3b | |||
3423534085 | |||
3c73a7f49d | |||
![]() |
9bc8d6bb6d | ||
6cabcf51bd | |||
5c28625b2a | |||
b6965b0b10 | |||
77800b27c3 | |||
3dced7ace6 | |||
beabc94146 | |||
145a2fc1f2 | |||
6bdd9ed340 | |||
97f0c4e67e | |||
![]() |
12501cad89 | ||
e3aedf250c | |||
![]() |
537db5e75c | ||
![]() |
a0c65d5ad4 | ||
![]() |
684ce6a29e | ||
![]() |
76de557e1b | ||
![]() |
0734d4df3b | ||
ea80bd9513 | |||
51184e7e8a | |||
ba5a784990 | |||
ab2ed14ab7 | |||
![]() |
3541c04e82 | ||
9dae07a8eb | |||
af69f875af | |||
88e13c04d1 | |||
e2fa2e2987 | |||
b67f87e93d | |||
4ee81bde4d | |||
3da241c70b | |||
04de9dcc3f | |||
![]() |
07f92a71c3 | ||
![]() |
3eebb00a81 | ||
335aef1d7c | |||
68acb43426 | |||
fe5bf8904b | |||
a6c72f100a | |||
525ffe6b53 | |||
7048c00ac0 | |||
662efd4ecb | |||
e4b1bccf85 | |||
![]() |
8a30d4acad | ||
![]() |
f798b54a23 | ||
![]() |
dade5d8e8f | ||
![]() |
b9f31e1ff2 | ||
![]() |
3537a2376c | ||
![]() |
98b2ad16c8 | ||
95e2f82736 | |||
5cd3d62e1a | |||
b0e4a0347a | |||
d547c89763 | |||
c40dce4ae1 | |||
6f241c0239 | |||
dddc2827b3 | |||
435250d704 | |||
1e1b89c0b7 | |||
738cddc51c | |||
3fee30bebb | |||
46ca5eada4 | |||
95e7b58c42 | |||
6d1e4a3780 | |||
6e28fbfea5 | |||
ac2aa49ca1 | |||
32c3c41266 | |||
c89e74205d | |||
9a3957be36 | |||
f788a11489 | |||
672be6c9a0 | |||
0e18eb1a78 | |||
89093f6b9e | |||
5dc2ee3901 | |||
99acdba262 | |||
b9dc66bc12 | |||
![]() |
5492039b89 | ||
870ee56b88 | |||
5ab54ea1cb | |||
25483f65ac | |||
25905f1116 | |||
9582113bcf | |||
764a75f680 | |||
1b4e2eb44d | |||
![]() |
4ce399ac62 | ||
![]() |
5b906389b5 | ||
![]() |
23ca9eb4cf | ||
ca53aaa8cb | |||
c7068a31ce | |||
![]() |
0d9d85f253 | ||
![]() |
1ff26ac036 | ||
![]() |
91a636bf03 | ||
861895750c | |||
fcf96e64ad | |||
ac30fc6ed5 | |||
7c6ea1a16e | |||
eeb31d6cf1 | |||
d4f672fc51 | |||
631dbf0cdb | |||
e86ed34fe5 | |||
![]() |
3dfa9d912e | ||
7ecab3d9ae | |||
849db934ac | |||
b0deb94b7a | |||
ef16d70576 | |||
2e82fec002 | |||
f0c06ba6e5 | |||
f792271a5a | |||
62c6e1fc2f | |||
bc0ecad1ac | |||
522353a62c | |||
522ca697b6 | |||
496dc17c2e | |||
![]() |
bd52784ce8 | ||
493af82004 | |||
79b63b6f5c | |||
![]() |
33137a5bff | ||
![]() |
35296db832 | ||
![]() |
fb4d3b5f17 | ||
![]() |
25c4472398 | ||
![]() |
ef71215676 | ||
![]() |
7dc6d1cf35 | ||
![]() |
f54b4494e2 | ||
31727d63c4 | |||
![]() |
6be67336a3 | ||
25c0205015 | |||
3b90b3f837 | |||
037b632a63 | |||
c6fe8dc405 | |||
0553d5eb4c | |||
888dc032c1 | |||
5e841a9106 | |||
bf4298ea58 | |||
9b14516433 | |||
1d19c05e92 | |||
70db5d5476 | |||
3e4ab2ee97 | |||
9a438df3f9 | |||
500062b29b | |||
7a60611ec5 | |||
a97de134d7 | |||
d9b11ea494 | |||
3935afa40d | |||
977c4618ef | |||
178865c6cf | |||
ebc2e5ca2a |
311 changed files with 11808 additions and 4197 deletions
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
|
@ -1,3 +1,3 @@
|
|||
# How you can support finnmglas/Launcher
|
||||
# How you can support jrpie/Launcher
|
||||
|
||||
custom: sponsor.finnmglas.com
|
||||
custom: https://s.jrpie.de/launcher-donate
|
||||
|
|
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help improve this app
|
||||
title: ''
|
||||
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. Samsung A7]
|
||||
- Android Version: [e.g. Marshmallow, 6.0 or API 23]
|
||||
|
||||
**Additional info**
|
||||
Add any other info or comments about the problem here.
|
50
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
50
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
|
@ -0,0 +1,50 @@
|
|||
name: Bug report
|
||||
description: Create a report to help improve this app
|
||||
title: '[bug] '
|
||||
labels: bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for helping to improve µLauncher!
|
||||
- type: textarea
|
||||
id: bug
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
description: What happened?
|
||||
placeholder: A clear and concise description of what the bug is.
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: What did you expect to happen instead?
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: What steps are required to reproduce the bug?
|
||||
render: markdown
|
||||
placeholder: |
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: device
|
||||
attributes:
|
||||
label: Your Device
|
||||
description: |
|
||||
What device are you using? Adding this information helps to reproduce the bug.
|
||||
You can copy this from µLauncher > Settings > Meta > Report Bug.
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,20 +1,24 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
title: '[feature] '
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
# <!--MANDATORY--> Please describe the problem to be solved
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
<!-- Add a clear and concise description of the addressed problem. Don't say "add a button such that ... " but **why** this button should be added. This is very important as it allows to discuss alternative solutions. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
# <!--OPTIONAL--> Describe the solution you would suggest
|
||||
|
||||
**Additional info**
|
||||
Add any other info, comments or screenshots about the feature request here.
|
||||
<!-- A description of the solution, e.g. "add a button to the settings activity. When clicking that button ..." -->
|
||||
|
||||
# <!--OPTIONAL--> Describe alternative solutions you've considered
|
||||
|
||||
<!-- A description of any alternative solutions or features you've considered. -->
|
||||
|
||||
# <!--OPTIONAL--> Additional info
|
||||
|
||||
<!-- Add any other info, comments or screenshots about the feature request here. -->
|
||||
|
|
32
.github/workflows/android.yml
vendored
Normal file
32
.github/workflows/android.yml
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
name: Android CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
|
||||
- name: upload apk
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: launcher-debug-${{ github.sha }}.apk
|
||||
path: app/build/outputs/apk/default/debug/app-default-debug.apk
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -15,7 +15,9 @@ bin/
|
|||
gen/
|
||||
out/
|
||||
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||
# release/
|
||||
release/
|
||||
app/release/
|
||||
.kotlin/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
|
@ -38,12 +40,15 @@ captures/
|
|||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/*
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/other.xml
|
||||
.idea/gradle.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/dictionaries
|
||||
.idea/libraries
|
||||
.idea/deploymentTargetSelector.xml
|
||||
# Android Studio 3 in .gitignore file.
|
||||
.idea/caches
|
||||
.idea/modules.xml
|
||||
|
|
138
.idea/codeStyles/Project.xml
generated
138
.idea/codeStyles/Project.xml
generated
|
@ -1,138 +0,0 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="java.util" alias="false" withSubpackages="false" />
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||
<value>
|
||||
<package name="" alias="false" withSubpackages="true" />
|
||||
<package name="java" alias="false" withSubpackages="true" />
|
||||
<package name="javax" alias="false" withSubpackages="true" />
|
||||
<package name="kotlin" alias="false" withSubpackages="true" />
|
||||
<package name="" alias="true" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
generated
5
.idea/codeStyles/codeStyleConfig.xml
generated
|
@ -1,5 +0,0 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
10
.idea/deploymentTargetSelector.xml
generated
10
.idea/deploymentTargetSelector.xml
generated
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
25
.idea/jarRepositories.xml
generated
25
.idea/jarRepositories.xml
generated
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="BintrayJCenter" />
|
||||
<option name="name" value="BintrayJCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
6
.idea/kotlinc.xml
generated
6
.idea/kotlinc.xml
generated
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.6.21" />
|
||||
</component>
|
||||
</project>
|
17
.idea/misc.xml
generated
17
.idea/misc.xml
generated
|
@ -1,17 +0,0 @@
|
|||
<project version="4">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="app/src/main/res/layout/list.xml" value="0.21331521739130435" />
|
||||
<entry key="app/src/main/res/layout/list_apps.xml" value="0.1757852077001013" />
|
||||
<entry key="app/src/main/res/layout/settings.xml" value="0.21331521739130435" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
263
.idea/other.xml
generated
263
.idea/other.xml
generated
|
@ -1,263 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="direct_access_persist.xml">
|
||||
<option name="deviceSelectionList">
|
||||
<list>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="27" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="F01L" />
|
||||
<option name="id" value="F01L" />
|
||||
<option name="manufacturer" value="FUJITSU" />
|
||||
<option name="name" value="F-01L" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1280" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="28" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="SH-01L" />
|
||||
<option name="id" value="SH-01L" />
|
||||
<option name="manufacturer" value="SHARP" />
|
||||
<option name="name" value="AQUOS sense2 SH-01L" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a51" />
|
||||
<option name="id" value="a51" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy A51" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="akita" />
|
||||
<option name="id" value="akita" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="b0q" />
|
||||
<option name="id" value="b0q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S22 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="32" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="bluejay" />
|
||||
<option name="id" value="bluejay" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="29" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="crownqlteue" />
|
||||
<option name="id" value="crownqlteue" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Note9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2220" />
|
||||
<option name="screenY" value="1080" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="dm3q" />
|
||||
<option name="id" value="dm3q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S23 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix_camera" />
|
||||
<option name="id" value="felix_camera" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold (Camera-enabled)" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts8uwifi" />
|
||||
<option name="id" value="gts8uwifi" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S8 Ultra" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1848" />
|
||||
<option name="screenY" value="2960" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="husky" />
|
||||
<option name="id" value="husky" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8 Pro" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="java" />
|
||||
<option name="id" value="java" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="G20" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="lynx" />
|
||||
<option name="id" value="lynx" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="oriole" />
|
||||
<option name="id" value="oriole" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="panther" />
|
||||
<option name="id" value="panther" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q2q" />
|
||||
<option name="id" value="q2q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold3" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1768" />
|
||||
<option name="screenY" value="2208" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q5q" />
|
||||
<option name="id" value="q5q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold5" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1812" />
|
||||
<option name="screenY" value="2176" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="r11" />
|
||||
<option name="id" value="r11" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Watch" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="384" />
|
||||
<option name="screenY" value="384" />
|
||||
<option name="type" value="WEAR_OS" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="redfin" />
|
||||
<option name="id" value="redfin" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 5" />
|
||||
<option name="screenDensity" value="440" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="shiba" />
|
||||
<option name="id" value="shiba" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tangorpro" />
|
||||
<option name="id" value="tangorpro" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Tablet" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="29" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="x1q" />
|
||||
<option name="id" value="x1q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S20" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3200" />
|
||||
</PersistentDeviceSelectionData>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
76
.scripts/release.sh
Executable file
76
.scripts/release.sh
Executable file
|
@ -0,0 +1,76 @@
|
|||
#!/bin/bash
|
||||
export JAVA_HOME="/usr/lib/jvm/java-21-openjdk/"
|
||||
OUTPUT_DIR="$HOME/launcher-release"
|
||||
BUILD_TOOLS_DIR="$HOME/Android/Sdk/build-tools/35.0.0"
|
||||
KEYSTORE="$HOME/data/keys/launcher_jrpie.jks"
|
||||
KEYSTORE_ACCRESCENT="$HOME/data/keys/launcher_jrpie_accrescent.jks"
|
||||
KEYSTORE_PASS=$(keepassxc-password "android_keys/launcher")
|
||||
KEYSTORE_ACCRESCENT_PASS=$(keepassxc-password "android_keys/launcher-accrescent")
|
||||
|
||||
if [[ $(git status --porcelain) ]]; then
|
||||
echo "There are uncommitted changes."
|
||||
|
||||
read -p "Continue anyway? (y/n) " -n 1 -r
|
||||
echo # (optional) move to a new line
|
||||
if ! [[ $REPLY =~ ^[Yy]$ ]]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
rm -rf "$OUTPUT_DIR"
|
||||
mkdir "$OUTPUT_DIR"
|
||||
|
||||
|
||||
echo
|
||||
echo "======================="
|
||||
echo " Default Release (apk) "
|
||||
echo "======================="
|
||||
|
||||
./gradlew clean
|
||||
./gradlew assembleDefaultRelease
|
||||
mv app/build/outputs/apk/default/release/app-default-release-unsigned.apk "$OUTPUT_DIR/app-release.apk"
|
||||
"$BUILD_TOOLS_DIR/apksigner" sign --ks "$KEYSTORE" \
|
||||
--ks-key-alias key0 \
|
||||
--ks-pass="pass:$KEYSTORE_PASS" \
|
||||
--key-pass="pass:$KEYSTORE_PASS" \
|
||||
--alignment-preserved \
|
||||
--v1-signing-enabled=true \
|
||||
--v2-signing-enabled=true \
|
||||
--v3-signing-enabled=true \
|
||||
--v4-signing-enabled=true \
|
||||
"$OUTPUT_DIR/app-release.apk"
|
||||
|
||||
echo
|
||||
echo "======================="
|
||||
echo " Default Release (aab) "
|
||||
echo "======================="
|
||||
|
||||
./gradlew clean
|
||||
./gradlew bundleDefaultRelease
|
||||
mv app/build/outputs/bundle/defaultRelease/app-default-release.aab "$OUTPUT_DIR/app-release.aab"
|
||||
"$BUILD_TOOLS_DIR/apksigner" sign --ks "$KEYSTORE" \
|
||||
--ks-key-alias key0 \
|
||||
--ks-pass="pass:$KEYSTORE_PASS" \
|
||||
--key-pass="pass:$KEYSTORE_PASS" \
|
||||
--v1-signing-enabled=true --v2-signing-enabled=true --v3-signing-enabled=true --v4-signing-enabled=true \
|
||||
--min-sdk-version=21 \
|
||||
"$OUTPUT_DIR/app-release.aab"
|
||||
|
||||
echo
|
||||
echo "======================="
|
||||
echo " Accrescent (apks) "
|
||||
echo "======================="
|
||||
|
||||
./gradlew clean
|
||||
./gradlew bundleAccrescentRelease
|
||||
mv app/build/outputs/bundle/accrescentRelease/app-accrescent-release.aab "$OUTPUT_DIR/app-accrescent-release.aab"
|
||||
|
||||
# build apks using bundletool from https://github.com/google/bundletool/releases
|
||||
"$JAVA_HOME/bin/java" -jar /opt/android/bundletool.jar build-apks \
|
||||
--bundle="$OUTPUT_DIR/app-accrescent-release.aab" --output="$OUTPUT_DIR/launcher-accrescent.apks" \
|
||||
--ks="$KEYSTORE_ACCRESCENT" \
|
||||
--ks-pass="pass:$KEYSTORE_ACCRESCENT_PASS" \
|
||||
--ks-key-alias="key0" \
|
||||
--key-pass="pass:$KEYSTORE_ACCRESCENT_PASS"
|
2
LICENSE
2
LICENSE
|
@ -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
|
||||
|
|
134
README.md
134
README.md
|
@ -1,38 +1,112 @@
|
|||
<!-- Shields from shields.io -->
|
||||
<!--[![][shield-release]][latest-release] -->
|
||||
[![][shield-release]][latest-release]
|
||||
[](https://github.com/jrpie/Launcher/actions/workflows/android.yml)
|
||||
[![][shield-license]][license]
|
||||
|
||||
<!-- ENGLISH README -->
|
||||
|
||||
# <a name="en"></a> Launcher
|
||||
|
||||
This is a fork of [finnmglas's app Launcher][original-repo].
|
||||
|
||||
## Notable changes:
|
||||
|
||||
* Edge gestures: There is a setting to allow distinguishing swiping at the edges of the screen from swiping in the center.
|
||||
|
||||
### 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.
|
||||
[][matrix]
|
||||
[][discord]
|
||||
|
||||
|
||||
### 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
|
||||
---
|
||||
|
||||
# μLauncher
|
||||
|
||||
|
||||
µ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*.
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
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"
|
||||
alt="screenshot"
|
||||
height="400">
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg"
|
||||
alt="screenshot"
|
||||
height="400">
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg"
|
||||
alt="screenshot"
|
||||
height="400">
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg"
|
||||
alt="screenshot"
|
||||
height="400">
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg"
|
||||
alt="screenshot"
|
||||
height="400">
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/7.jpg"
|
||||
alt="screenshot"
|
||||
height="400">
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/8.jpg"
|
||||
alt="screenshot"
|
||||
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:
|
||||
* You can add or improve [translations][toolate].
|
||||
<br><img src="https://toolate.othing.xyz/widget/jrpie-launcher/launcher/horizontal-auto.svg" alt="translation status">
|
||||
* 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>` or `fix/<your fix>` and commit your changes.
|
||||
- Open a new pull request.
|
||||
|
||||
|
||||
See [build.md](docs/build.md) for instructions how to build this project.
|
||||
The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds.
|
||||
Note that those are not signed.
|
||||
|
||||
---
|
||||
[hack-font]: https://sourcefoundry.org/hack/
|
||||
[original-repo]: https://github.com/finnmglas/Launcher
|
||||
[toolate]: https://toolate.othing.xyz/projects/jrpie-launcher/
|
||||
[issues]: https://github.com/jrpie/Launcher/issues/
|
||||
[fork]: https://github.com/jrpie/Launcher/fork/
|
||||
|
||||
|
||||
<!-- Download links / stores -->
|
||||
|
@ -44,14 +118,16 @@ This is a fork of [finnmglas's app Launcher][original-repo].
|
|||
<!-- Shields and Badges -->
|
||||
|
||||
[shield-release]: https://img.shields.io/github/v/release/jrpie/Launcher?style=flat
|
||||
[latest-release]: https://github.com/jrpie/Launcher/releases/latest
|
||||
[shield-contribute]: https://img.shields.io/badge/contributions-welcome-007ec6.svg?style=flat
|
||||
[shield-license]: https://img.shields.io/badge/license-MIT-007ec6?style=flat
|
||||
|
||||
[shield-gh-watch]: https://img.shields.io/github/watchers/jrpie/Launcher?label=Watch&style=social
|
||||
[shield-gh-star]: https://img.shields.io/github/stars/jrpie/Launcher?label=Star&style=social
|
||||
[shield-gh-fork]: https://img.shields.io/github/forks/jrpie/Launcher?label=Fork&style=social
|
||||
|
||||
|
||||
[matrix]: https://s.jrpie.de/launcher-matrix
|
||||
[discord]: https://s.jrpie.de/launcher-discord
|
||||
[chat]: https://s.jrpie.de/launcher-chat
|
||||
|
||||
<!-- Helpful resources -->
|
||||
|
||||
|
|
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Reporting Security Issues
|
||||
|
||||
For security related issues, please use the contact information
|
||||
from the [security.txt](https://jrpie.de/.well-known/security.txt) on my website
|
||||
or [report a vulnerability](https://github.com/jrpie/Launcher/security/advisories/new) on github.
|
|
@ -1,45 +1,115 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
android {
|
||||
compileSdkVersion 34
|
||||
buildToolsVersion "34.0.0"
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes.addAll(
|
||||
[
|
||||
"META-INF/LICENSE.md",
|
||||
"META-INF/NOTICE.md",
|
||||
"META-INF/LICENSE-notice.md"
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.jrpie.android.launcher"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
versionCode 16
|
||||
versionName "j-0.0.4"
|
||||
compileSdk 35
|
||||
versionCode 44
|
||||
versionName "0.1.4"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
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
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
// Disables dependency metadata when building APKs.
|
||||
includeInApk = false
|
||||
// Disables dependency metadata when building Android App Bundles.
|
||||
includeInBundle = false
|
||||
}
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.core:core-ktx:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.8.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.core:core-ktx:1.15.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.palette:palette-ktx:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.4.0'
|
||||
implementation 'androidx.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"
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
}
|
||||
|
||||
|
|
9
app/proguard-rules.pro
vendored
9
app/proguard-rules.pro
vendored
|
@ -1,4 +1,6 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
-dontobfuscate
|
||||
-dontoptimize
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
|
@ -19,3 +21,10 @@
|
|||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
# see app/build/outputs/mapping/release/missing_rules.txt
|
||||
# Please add these rules to your existing keep rules in order to suppress warnings.
|
||||
# This is generated automatically by the Android Gradle plugin.
|
||||
-dontwarn javax.annotation.processing.AbstractProcessor
|
||||
-dontwarn javax.annotation.processing.SupportedAnnotationTypes
|
||||
-dontwarn javax.annotation.processing.SupportedSourceVersion
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "de.jrpie.android.launcher",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 16,
|
||||
"versionName": "j-0.0.4",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File",
|
||||
"minSdkVersionForDexing": 21
|
||||
}
|
15
app/src/accrescent/AndroidManifest.xml
Normal file
15
app/src/accrescent/AndroidManifest.xml
Normal 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>
|
3
app/src/debug/res/values/donottranslate.xml
Normal file
3
app/src/debug/res/values/donottranslate.xml
Normal file
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name" translatable="false">μLauncher [debug]</string>
|
||||
</resources>
|
|
@ -3,43 +3,98 @@
|
|||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<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:theme="@style/launcherBaseTheme">
|
||||
|
||||
<activity android:name=".HomeActivity"
|
||||
android:theme="@style/launcherBaseTheme"
|
||||
tools:ignore="UnusedAttribute">
|
||||
<activity
|
||||
android:name=".ui.PinShortcutActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.pm.action.CONFIRM_PIN_SHORTCUT" />
|
||||
<action android:name="android.content.pm.action.CONFIRM_PIN_APPWIDGET" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.HomeActivity"
|
||||
android:clearTaskOnLaunch="true"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
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=".tutorial.TutorialActivity"
|
||||
android:screenOrientation="portrait"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
</activity>
|
||||
<activity android:name=".list.ListActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
</activity>
|
||||
<activity android:name=".settings.SettingsActivity"
|
||||
android:screenOrientation="portrait"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
<activity
|
||||
android:name=".ui.tutorial.TutorialActivity"
|
||||
android:configChanges="orientation|screenSize" />
|
||||
<activity
|
||||
android:name=".ui.list.ListActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name=".ui.settings.SettingsActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustNothing">
|
||||
<intent-filter>
|
||||
<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"
|
||||
android:description="@string/device_admin_description"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_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"
|
||||
android:exported="true"
|
||||
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>
|
||||
</manifest>
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 38 KiB |
160
app/src/main/java/de/jrpie/android/launcher/Application.kt
Normal file
160
app/src/main/java/de/jrpie/android/launcher/Application.kt
Normal file
|
@ -0,0 +1,160 @@
|
|||
package de.jrpie.android.launcher
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.LauncherApps
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.UserHandle
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.preference.PreferenceManager
|
||||
import de.jrpie.android.launcher.actions.TorchManager
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.isPrivateSpaceLocked
|
||||
import de.jrpie.android.launcher.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<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()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// TODO Error: Invalid resource ID 0x00000000.
|
||||
// DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
|
||||
|
||||
if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
|
||||
torchManager = TorchManager(this)
|
||||
}
|
||||
|
||||
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<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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,418 +1,226 @@
|
|||
package de.jrpie.android.launcher
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
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.SharedPreferences
|
||||
import android.content.pm.LauncherApps
|
||||
import android.content.pm.LauncherApps.ShortcutQuery
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.BlendModeColorFilter
|
||||
import android.graphics.ColorMatrix
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.os.UserHandle
|
||||
import android.os.UserManager
|
||||
import android.provider.Settings
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.Switch
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import de.jrpie.android.launcher.list.ListActivity
|
||||
import de.jrpie.android.launcher.list.apps.AppInfo
|
||||
import de.jrpie.android.launcher.list.apps.AppsRecyclerAdapter
|
||||
import de.jrpie.android.launcher.list.other.LauncherAction
|
||||
import de.jrpie.android.launcher.settings.SettingsActivity
|
||||
import de.jrpie.android.launcher.settings.intendedSettingsPause
|
||||
import de.jrpie.android.launcher.tutorial.TutorialActivity
|
||||
import androidx.annotation.RequiresApi
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.actions.ShortcutAction
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
|
||||
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.DetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.DetailedPinnedShortcutInfo
|
||||
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
|
||||
import de.jrpie.android.launcher.apps.getPrivateSpaceUser
|
||||
import de.jrpie.android.launcher.apps.isPrivateSpaceSupported
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
|
||||
import androidx.core.net.toUri
|
||||
|
||||
|
||||
/* Preference Key Constants */
|
||||
const val LOG_TAG = "Launcher"
|
||||
|
||||
const val PREF_DOMINANT = "custom_dominant"
|
||||
const val PREF_VIBRANT = "custom_vibrant"
|
||||
const val PREF_THEME = "theme"
|
||||
const val REQUEST_SET_DEFAULT_HOME = 42
|
||||
|
||||
const val PREF_SCREEN_TIMEOUT_DISABLED = "disableTimeout"
|
||||
const val PREF_SCREEN_FULLSCREEN = "useFullScreen"
|
||||
const val PREF_DATE_FORMAT = "dateFormat"
|
||||
|
||||
const val PREF_DOUBLE_ACTIONS_ENABLED = "enableDoubleActions"
|
||||
const val PREF_EDGE_ACTIONS_ENABLED = "enableEdgeActions"
|
||||
const val PREF_SEARCH_AUTO_LAUNCH = "searchAutoLaunch"
|
||||
const val PREF_SEARCH_AUTO_KEYBOARD = "searchAutoKeyboard"
|
||||
|
||||
const val PREF_SLIDE_SENSITIVITY = "slideSensitivity"
|
||||
|
||||
const val PREF_STARTED = "startedBefore"
|
||||
const val PREF_STARTED_TIME = "firstStartup"
|
||||
|
||||
const val PREF_VERSION = "version"
|
||||
|
||||
/* Objects used by multiple activities */
|
||||
val appsList: MutableList<AppInfo> = ArrayList()
|
||||
|
||||
/* Variables containing settings */
|
||||
val displayMetrics = DisplayMetrics()
|
||||
|
||||
var dominantColor = 0
|
||||
var vibrantColor = 0
|
||||
|
||||
/* REQUEST CODES */
|
||||
|
||||
const val REQUEST_CHOOSE_APP = 1
|
||||
const val REQUEST_UNINSTALL = 2
|
||||
|
||||
/* Animate */
|
||||
|
||||
// Taken from https://stackoverflow.com/questions/47293269
|
||||
fun View.blink(
|
||||
times: Int = Animation.INFINITE,
|
||||
duration: Long = 1000L,
|
||||
offset: Long = 20L,
|
||||
minAlpha: Float = 0.2f,
|
||||
maxAlpha: Float = 1.0f,
|
||||
repeatMode: Int = Animation.REVERSE
|
||||
) {
|
||||
startAnimation(AlphaAnimation(minAlpha, maxAlpha).also {
|
||||
it.duration = duration
|
||||
it.startOffset = offset
|
||||
it.repeatMode = repeatMode
|
||||
it.repeatCount = times
|
||||
})
|
||||
}
|
||||
|
||||
fun getPreferences(context: Context): SharedPreferences{
|
||||
return context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
}
|
||||
|
||||
/* Activity related */
|
||||
|
||||
fun isInstalled(uri: String, context: Context): Boolean {
|
||||
if (uri.startsWith("launcher:")) return true // All internal actions
|
||||
|
||||
try {
|
||||
context.packageManager.getPackageInfo(uri, PackageManager.GET_ACTIVITIES)
|
||||
return true
|
||||
} catch (_: PackageManager.NameNotFoundException) { }
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getIntent(packageName: String, context: Context): Intent? {
|
||||
val intent: Intent? = context.packageManager.getLaunchIntentForPackage(packageName)
|
||||
intent?.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun launch(
|
||||
data: String, activity: Activity,
|
||||
animationIn: Int = android.R.anim.fade_in, animationOut: Int = android.R.anim.fade_out
|
||||
) {
|
||||
|
||||
if (LauncherAction.isOtherAction(data)) { // [type]:[info]
|
||||
LauncherAction.byId(data)?.let {it.launch(activity) }
|
||||
}
|
||||
else launchApp(data, activity) // app
|
||||
|
||||
activity.overridePendingTransition(animationIn, animationOut)
|
||||
}
|
||||
|
||||
/* Media player actions */
|
||||
|
||||
fun audioNextTrack(activity: Activity) {
|
||||
|
||||
val mAudioManager = activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
val eventTime: Long = SystemClock.uptimeMillis()
|
||||
|
||||
val downEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT, 0)
|
||||
mAudioManager.dispatchMediaKeyEvent(downEvent)
|
||||
|
||||
val upEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT, 0)
|
||||
mAudioManager.dispatchMediaKeyEvent(upEvent)
|
||||
}
|
||||
|
||||
fun audioPreviousTrack(activity: Activity) {
|
||||
val mAudioManager = activity.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)
|
||||
}
|
||||
|
||||
fun audioVolumeUp(activity: Activity) {
|
||||
val audioManager =
|
||||
activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
audioManager.adjustStreamVolume(
|
||||
AudioManager.STREAM_MUSIC,
|
||||
AudioManager.ADJUST_RAISE,
|
||||
AudioManager.FLAG_SHOW_UI
|
||||
)
|
||||
}
|
||||
|
||||
fun audioVolumeDown(activity: Activity) {
|
||||
val audioManager =
|
||||
activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
audioManager.adjustStreamVolume(
|
||||
AudioManager.STREAM_MUSIC,
|
||||
AudioManager.ADJUST_LOWER,
|
||||
AudioManager.FLAG_SHOW_UI
|
||||
)
|
||||
}
|
||||
|
||||
/* --- */
|
||||
|
||||
fun launchApp(packageName: String, context: Context) {
|
||||
val intent = getIntent(packageName, context)
|
||||
|
||||
if (intent != null) {
|
||||
context.startActivity(intent)
|
||||
fun isDefaultHomeScreen(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val roleManager = context.getSystemService(RoleManager::class.java)
|
||||
return roleManager.isRoleHeld(RoleManager.ROLE_HOME)
|
||||
} else {
|
||||
if (isInstalled(packageName, context)){
|
||||
|
||||
AlertDialog.Builder(
|
||||
context,
|
||||
R.style.AlertDialogCustom
|
||||
)
|
||||
.setTitle(context.getString(R.string.alert_cant_open_title))
|
||||
.setMessage(context.getString(R.string.alert_cant_open_message))
|
||||
.setPositiveButton(android.R.string.ok
|
||||
) { _, _ ->
|
||||
openAppSettings(
|
||||
packageName,
|
||||
context
|
||||
)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setIcon(android.R.drawable.ic_dialog_info)
|
||||
.show()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.toast_cant_open_message),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
val testIntent = Intent(Intent.ACTION_MAIN)
|
||||
testIntent.addCategory(Intent.CATEGORY_HOME)
|
||||
val defaultHome = testIntent.resolveActivity(context.packageManager)?.packageName
|
||||
return defaultHome == context.packageName
|
||||
}
|
||||
}
|
||||
|
||||
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 setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) {
|
||||
val isDefault = isDefaultHomeScreen(context)
|
||||
if (checkDefault && isDefault) {
|
||||
// Launcher is already the default home app
|
||||
return
|
||||
}
|
||||
|
||||
/* Settings related functions */
|
||||
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
|
||||
}
|
||||
|
||||
fun getSavedTheme(context: Context) : String {
|
||||
return getPreferences(context).getString(PREF_THEME, "finn").toString()
|
||||
}
|
||||
|
||||
fun saveTheme(context: Context, themeName: String) : String {
|
||||
getPreferences(context).edit()
|
||||
.putString(PREF_THEME, themeName)
|
||||
.apply()
|
||||
|
||||
return themeName
|
||||
}
|
||||
|
||||
fun resetToDefaultTheme(activity: Activity) {
|
||||
dominantColor = activity.resources.getColor(R.color.finnmglasTheme_background_color)
|
||||
vibrantColor = activity.resources.getColor(R.color.finnmglasTheme_accent_color)
|
||||
|
||||
getPreferences(activity).edit()
|
||||
.putInt(PREF_DOMINANT, dominantColor)
|
||||
.putInt(PREF_VIBRANT, vibrantColor)
|
||||
.apply()
|
||||
|
||||
saveTheme(activity,"finn")
|
||||
loadSettings(activity)
|
||||
|
||||
intendedSettingsPause = true
|
||||
activity.recreate()
|
||||
}
|
||||
|
||||
fun resetToDarkTheme(activity: Activity) {
|
||||
dominantColor = activity.resources.getColor(R.color.darkTheme_background_color)
|
||||
vibrantColor = activity.resources.getColor(R.color.darkTheme_accent_color)
|
||||
|
||||
getPreferences(activity).edit()
|
||||
.putInt(PREF_DOMINANT, dominantColor)
|
||||
.putInt(PREF_VIBRANT, vibrantColor)
|
||||
.apply()
|
||||
|
||||
saveTheme(activity,"dark")
|
||||
|
||||
intendedSettingsPause = true
|
||||
activity.recreate()
|
||||
}
|
||||
|
||||
|
||||
fun openAppSettings(pkg: String, context: Context) {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intent.data = Uri.parse("package:$pkg")
|
||||
val intent = Intent(Settings.ACTION_HOME_SETTINGS)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun openSettings(activity: Activity) {
|
||||
activity.startActivity(Intent(activity, SettingsActivity::class.java))
|
||||
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() == userId } ?: profiles[0]
|
||||
}
|
||||
|
||||
fun openTutorial(activity: Activity){
|
||||
activity.startActivity(Intent(activity, TutorialActivity::class.java))
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
fun removeUnusedShortcuts(context: Context) {
|
||||
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
fun getShortcuts(profile: UserHandle): List<ShortcutInfo>? {
|
||||
return try {
|
||||
launcherApps.getShortcuts(
|
||||
ShortcutQuery().apply {
|
||||
setQueryFlags(ShortcutQuery.FLAG_MATCH_PINNED)
|
||||
},
|
||||
profile
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
// https://github.com/jrpie/launcher/issues/116
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
val userManager = context.getSystemService(Service.USER_SERVICE) as UserManager
|
||||
val boundActions: MutableSet<PinnedShortcutInfo> =
|
||||
Gesture.entries.mapNotNull { Action.forGesture(it) as? ShortcutAction }.map { it.shortcut }
|
||||
.toMutableSet()
|
||||
LauncherPreferences.apps().pinnedShortcuts()?.let { boundActions.addAll(it) }
|
||||
try {
|
||||
userManager.userProfiles.filter { !userManager.isQuietModeEnabled(it) }.forEach { profile ->
|
||||
getShortcuts(profile)?.groupBy { it.`package` }?.forEach { (p, shortcuts) ->
|
||||
launcherApps.pinShortcuts(p,
|
||||
shortcuts.filter { boundActions.contains(PinnedShortcutInfo(it)) }
|
||||
.map { it.id }.toList(),
|
||||
profile
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (_: SecurityException) { }
|
||||
}
|
||||
|
||||
fun openAppsList(activity: Activity){
|
||||
val intent = Intent(activity, ListActivity::class.java)
|
||||
intent.putExtra("intention", ListActivity.ListActivityIntention.VIEW.toString())
|
||||
intendedSettingsPause = true
|
||||
activity.startActivity(intent)
|
||||
fun openInBrowser(url: String, context: Context) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
intent.putExtras(Bundle().apply { putBoolean("new_window", true) })
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.toast_activity_not_found_browser, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun openTutorial(context: Context) {
|
||||
context.startActivity(Intent(context, TutorialActivity::class.java))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* [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) {
|
||||
val loadList = mutableListOf<AppInfo>()
|
||||
fun getApps(
|
||||
packageManager: PackageManager,
|
||||
context: Context
|
||||
): MutableList<AbstractDetailedAppInfo> {
|
||||
var start = System.currentTimeMillis()
|
||||
val loadList = mutableListOf<AbstractDetailedAppInfo>()
|
||||
|
||||
val i = Intent(Intent.ACTION_MAIN, null)
|
||||
i.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
val allApps = packageManager.queryIntentActivities(i, 0)
|
||||
for (ri in allApps) {
|
||||
val app = AppInfo()
|
||||
app.label = ri.loadLabel(packageManager)
|
||||
app.packageName = ri.activityInfo.packageName
|
||||
app.icon = ri.activityInfo.loadIcon(packageManager)
|
||||
loadList.add(app)
|
||||
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
val userManager = context.getSystemService(Service.USER_SERVICE) as UserManager
|
||||
|
||||
val privateSpaceUser = getPrivateSpaceUser(context)
|
||||
|
||||
// TODO: shortcuts - launcherApps.getShortcuts()
|
||||
val users = userManager.userProfiles
|
||||
for (user in users) {
|
||||
// don't load apps from a user profile that has quiet mode enabled
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
if (userManager.isQuietModeEnabled(user)) {
|
||||
// hide paused apps
|
||||
if (LauncherPreferences.apps().hidePausedApps()) {
|
||||
continue
|
||||
}
|
||||
// hide apps from private space
|
||||
if (isPrivateSpaceSupported() &&
|
||||
launcherApps.getLauncherUserInfo(user)?.userType == UserManager.USER_TYPE_PROFILE_PRIVATE
|
||||
) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
launcherApps.getActivityList(null, user).forEach {
|
||||
loadList.add(DetailedAppInfo(it, it.user == privateSpaceUser))
|
||||
}
|
||||
}
|
||||
loadList.sortBy { it.label.toString() }
|
||||
|
||||
appsList.clear()
|
||||
appsList.addAll(loadList)
|
||||
}
|
||||
|
||||
fun loadSettings(context: Context) {
|
||||
val preferences = getPreferences(context)
|
||||
dominantColor = preferences.getInt(PREF_DOMINANT, 0)
|
||||
vibrantColor = preferences.getInt(PREF_VIBRANT, 0)
|
||||
}
|
||||
|
||||
fun resetSettings(context: Context) {
|
||||
|
||||
val editor = getPreferences(context).edit()
|
||||
|
||||
// set default theme
|
||||
dominantColor = context.resources.getColor(R.color.finnmglasTheme_background_color)
|
||||
vibrantColor = context.resources.getColor(R.color.finnmglasTheme_accent_color)
|
||||
|
||||
editor
|
||||
.putInt(PREF_DOMINANT, dominantColor)
|
||||
.putInt(PREF_VIBRANT, vibrantColor)
|
||||
.putString(PREF_THEME, "finn")
|
||||
.putBoolean(PREF_SCREEN_TIMEOUT_DISABLED, false)
|
||||
.putBoolean(PREF_SEARCH_AUTO_LAUNCH, false)
|
||||
.putInt(PREF_DATE_FORMAT, 0)
|
||||
.putBoolean(PREF_SCREEN_FULLSCREEN, true)
|
||||
.putBoolean(PREF_DOUBLE_ACTIONS_ENABLED, false)
|
||||
.putInt(PREF_SLIDE_SENSITIVITY, 50)
|
||||
|
||||
Gesture.values().forEach { editor.putString(it.id, it.pickDefaultApp(context)) }
|
||||
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
fun setWindowFlags(window: Window) {
|
||||
window.setFlags(0, 0) // clear flags
|
||||
|
||||
val preferences = getPreferences(window.context)
|
||||
// Display notification bar
|
||||
if (preferences.getBoolean(PREF_SCREEN_FULLSCREEN, true))
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
else window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
|
||||
// Screen Timeout
|
||||
if (preferences.getBoolean(PREF_SCREEN_TIMEOUT_DISABLED, false))
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
)
|
||||
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
// Used in Tutorial and Settings `ActivityOnResult`
|
||||
fun saveListActivityChoice(context: Context, data: Intent?) {
|
||||
val value = data?.getStringExtra("value")
|
||||
val forGesture = data?.getStringExtra("forGesture") ?: return
|
||||
|
||||
Gesture.byId(forGesture)?.setApp(context, value.toString())
|
||||
|
||||
loadSettings(context)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
/* Bitmaps */
|
||||
|
||||
fun setButtonColor(btn: Button, color: Int) {
|
||||
if (Build.VERSION.SDK_INT >= 29)
|
||||
btn.background.colorFilter = BlendModeColorFilter(color, BlendMode.MULTIPLY)
|
||||
else {
|
||||
// tested with API 17 (Android 4.4.2 on S4 mini) -> fails
|
||||
// tested with API 28 (Android 9 on S8) -> necessary
|
||||
btn.background.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
|
||||
// fallback option
|
||||
if (loadList.isEmpty()) {
|
||||
Log.w(LOG_TAG, "using fallback option to load packages")
|
||||
val i = Intent(Intent.ACTION_MAIN, null)
|
||||
i.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
val allApps = packageManager.queryIntentActivities(i, 0)
|
||||
for (ri in allApps) {
|
||||
val app = AppInfo(ri.activityInfo.packageName, null, INVALID_USER)
|
||||
val detailedAppInfo = DetailedAppInfo(
|
||||
app,
|
||||
ri.loadLabel(packageManager),
|
||||
ri.activityInfo.loadIcon(packageManager),
|
||||
false
|
||||
)
|
||||
loadList.add(detailedAppInfo)
|
||||
}
|
||||
}
|
||||
// not setting it in any other case (yet), unable to find a good solution
|
||||
}
|
||||
loadList.sortBy { it.getCustomLabel(context) }
|
||||
|
||||
fun setSwitchColor(sw: Switch, trackColor: Int) {
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
sw.trackDrawable.colorFilter = BlendModeColorFilter(trackColor, BlendMode.MULTIPLY)
|
||||
}
|
||||
else {
|
||||
sw.trackDrawable.colorFilter = PorterDuffColorFilter(trackColor, PorterDuff.Mode.SRC_ATOP)
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
// used for the bug report button
|
||||
fun getDeviceInfo(): String {
|
||||
return """
|
||||
µLauncher version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})
|
||||
Android version: ${Build.VERSION.RELEASE} (sdk ${Build.VERSION.SDK_INT})
|
||||
Model: ${Build.MODEL}
|
||||
Device: ${Build.DEVICE}
|
||||
Brand: ${Build.BRAND}
|
||||
Manufacturer: ${Build.MANUFACTURER}
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
fun copyToClipboard(context: Context, text: String) {
|
||||
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clipData = ClipData.newPlainText("Debug Info", text)
|
||||
clipboardManager.setPrimaryClip(clipData)
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
package de.jrpie.android.launcher
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* @param id internal id to serialize the action. Used as a key in shared preferences.
|
||||
* @param defaultsResource res id of array of default actions for the gesture.
|
||||
* @param labelResource res id of the name of the gesture.
|
||||
* @param animationIn res id of transition animation (in) when using the gesture to launch an app.
|
||||
* @param animationOut res id of transition animation (out) when using the gesture to launch an app.
|
||||
*/
|
||||
enum class Gesture (val id: String, private val labelResource: Int,
|
||||
private val defaultsResource: Int,
|
||||
private val animationIn: Int = android.R.anim.fade_in,
|
||||
private val animationOut: Int = android.R.anim.fade_out){
|
||||
VOLUME_UP("action_volumeUpApp", R.string.settings_gesture_vol_up, R.array.default_volume_up, 0,0),
|
||||
VOLUME_DOWN("action_volumeDownApp", R.string.settings_gesture_vol_down, R.array.default_volume_down,0,0),
|
||||
TIME("action_timeApp", R.string.settings_gesture_time, R.array.default_time),
|
||||
DATE("action_dateApp", R.string.settings_gesture_date, R.array.default_date),
|
||||
LONG_CLICK("action_longClickApp", R.string.settings_gesture_long_click, R.array.default_long_click, 0,0),
|
||||
DOUBLE_CLICK("action_doubleClickApp", R.string.settings_gesture_double_click, R.array.default_double_click,0,0),
|
||||
SWIPE_UP("action_upApp", R.string.settings_gesture_up, R.array.default_up, R.anim.bottom_up),
|
||||
SWIPE_UP_LEFT_EDGE("action_up_leftApp", R.string.settings_gesture_up_left_edge, R.array.default_up_left, R.anim.bottom_up),
|
||||
SWIPE_UP_RIGHT_EDGE("action_up_rightApp", R.string.settings_gesture_up_right_edge, R.array.default_up_right, R.anim.bottom_up),
|
||||
SWIPE_UP_DOUBLE( "action_doubleUpApp", R.string.settings_gesture_double_up, R.array.default_double_up, R.anim.bottom_up),
|
||||
SWIPE_DOWN("action_downApp", R.string.settings_gesture_down, R.array.default_down, R.anim.top_down),
|
||||
SWIPE_DOWN_LEFT_EDGE("action_down_leftApp", R.string.settings_gesture_down_left_edge, R.array.default_down_left, R.anim.top_down),
|
||||
SWIPE_DOWN_RIGHT_EDGE("action_down_rightApp", R.string.settings_gesture_down_right_edge, R.array.default_down_right, R.anim.top_down),
|
||||
SWIPE_DOWN_DOUBLE("action_doubleDownApp", R.string.settings_gesture_double_down, R.array.default_double_down, R.anim.top_down),
|
||||
SWIPE_LEFT("action_leftApp", R.string.settings_gesture_left, R.array.default_left, R.anim.right_left),
|
||||
SWIPE_LEFT_TOP_EDGE("action_left_topApp", R.string.settings_gesture_left_top_edge, R.array.default_left_top, R.anim.right_left),
|
||||
SWIPE_LEFT_BOTTOM_EDGE("action_left_bottomApp", R.string.settings_gesture_left_bottom_edge, R.array.default_left_bottom, R.anim.right_left),
|
||||
SWIPE_LEFT_DOUBLE("action_doubleLeftApp", R.string.settings_gesture_double_left, R.array.default_double_left, R.anim.right_left),
|
||||
SWIPE_RIGHT("action_rightApp", R.string.settings_gesture_right, R.array.default_right, R.anim.left_right),
|
||||
SWIPE_RIGHT_TOP_EDGE("action_right_topApp", R.string.settings_gesture_right_top_edge, R.array.default_right_top, R.anim.left_right),
|
||||
SWIPE_RIGHT_BOTTOM_EDGE("action_right_bottomApp", R.string.settings_gesture_right_bottom_edge, R.array.default_right_bottom, R.anim.left_right),
|
||||
SWIPE_RIGHT_DOUBLE("action_doubleRightApp", R.string.settings_gesture_double_right, R.array.default_double_right, R.anim.left_right);
|
||||
|
||||
enum class Edge{
|
||||
TOP, BOTTOM, LEFT, RIGHT
|
||||
}
|
||||
|
||||
fun getApp(context: Context): String {
|
||||
return getPreferences(context).getString(this.id, "")!!
|
||||
}
|
||||
|
||||
fun removeApp(context: Context) {
|
||||
getPreferences(context).edit()
|
||||
.putString(this.id, "") // clear it
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun setApp(context: Context, app: String) {
|
||||
getPreferences(context).edit()
|
||||
.putString(this.id, app)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun getLabel(context: Context): String {
|
||||
return context.resources.getString(this.labelResource)
|
||||
}
|
||||
|
||||
fun pickDefaultApp(context: Context) : String {
|
||||
return context.resources
|
||||
.getStringArray(this.defaultsResource)
|
||||
.firstOrNull { isInstalled(it, context) }
|
||||
?: ""
|
||||
}
|
||||
|
||||
fun getDoubleVariant(): Gesture {
|
||||
return when(this) {
|
||||
SWIPE_UP -> SWIPE_UP_DOUBLE
|
||||
SWIPE_DOWN -> SWIPE_DOWN_DOUBLE
|
||||
SWIPE_LEFT -> SWIPE_LEFT_DOUBLE
|
||||
SWIPE_RIGHT -> SWIPE_RIGHT_DOUBLE
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
fun getEdgeVariant(edge: Edge): Gesture {
|
||||
return when(edge) {
|
||||
Edge.TOP ->
|
||||
when(this) {
|
||||
SWIPE_LEFT -> SWIPE_LEFT_TOP_EDGE
|
||||
SWIPE_RIGHT -> SWIPE_RIGHT_TOP_EDGE
|
||||
else -> this
|
||||
}
|
||||
Edge.BOTTOM ->
|
||||
when(this) {
|
||||
SWIPE_LEFT -> SWIPE_LEFT_BOTTOM_EDGE
|
||||
SWIPE_RIGHT -> SWIPE_RIGHT_BOTTOM_EDGE
|
||||
else -> this
|
||||
}
|
||||
Edge.LEFT ->
|
||||
when(this) {
|
||||
SWIPE_UP -> SWIPE_UP_LEFT_EDGE
|
||||
SWIPE_DOWN -> SWIPE_DOWN_LEFT_EDGE
|
||||
else -> this
|
||||
}
|
||||
Edge.RIGHT ->
|
||||
when(this) {
|
||||
SWIPE_UP -> SWIPE_UP_RIGHT_EDGE
|
||||
SWIPE_DOWN -> SWIPE_DOWN_RIGHT_EDGE
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isDoubleVariant(): Boolean {
|
||||
return when(this){
|
||||
SWIPE_UP_DOUBLE,
|
||||
SWIPE_DOWN_DOUBLE,
|
||||
SWIPE_LEFT_DOUBLE,
|
||||
SWIPE_RIGHT_DOUBLE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun isEdgeVariant(): Boolean {
|
||||
return when(this){
|
||||
SWIPE_UP_RIGHT_EDGE,
|
||||
SWIPE_UP_LEFT_EDGE,
|
||||
SWIPE_DOWN_LEFT_EDGE,
|
||||
SWIPE_DOWN_RIGHT_EDGE,
|
||||
SWIPE_LEFT_TOP_EDGE,
|
||||
SWIPE_LEFT_BOTTOM_EDGE,
|
||||
SWIPE_RIGHT_TOP_EDGE,
|
||||
SWIPE_RIGHT_BOTTOM_EDGE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
operator fun invoke(activity: Activity) {
|
||||
launch(this.getApp(activity), activity, this.animationIn, this.animationOut)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun byId(id: String): Gesture? {
|
||||
return Gesture.values().firstOrNull {it.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,249 +0,0 @@
|
|||
package de.jrpie.android.launcher
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.view.GestureDetector
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import de.jrpie.android.launcher.BuildConfig.VERSION_NAME
|
||||
import de.jrpie.android.launcher.list.other.LauncherAction
|
||||
import de.jrpie.android.launcher.tutorial.TutorialActivity
|
||||
import kotlinx.android.synthetic.main.home.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.concurrent.fixedRateTimer
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* [HomeActivity] is the actual application Launcher,
|
||||
* what makes this application special / unique.
|
||||
*
|
||||
* In this activity we display the date and time,
|
||||
* and we listen for actions like tapping, swiping or button presses.
|
||||
*
|
||||
* As it also is the first thing that is started when someone opens Launcher,
|
||||
* it also contains some logic related to the overall application:
|
||||
* - Setting global variables (preferences etc.)
|
||||
* - Opening the [TutorialActivity] on new installations
|
||||
*/
|
||||
class HomeActivity: UIObject, AppCompatActivity(),
|
||||
GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
|
||||
|
||||
private var bufferedPointerCount = 1 // how many fingers on screen
|
||||
private var pointerBufferTimer = Timer()
|
||||
|
||||
private lateinit var mDetector: GestureDetectorCompat
|
||||
|
||||
// timers
|
||||
private var clockTimer = Timer()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val preferences = getPreferences(this)
|
||||
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
|
||||
loadSettings(this)
|
||||
|
||||
// First time opening the app: show Tutorial, else: check versions
|
||||
if (!preferences.getBoolean(PREF_STARTED, false))
|
||||
startActivity(Intent(this, TutorialActivity::class.java))
|
||||
else when (preferences.getString(PREF_VERSION, "")) {
|
||||
// Check versions, make sure transitions between versions go well
|
||||
|
||||
VERSION_NAME -> { /* the version installed and used previously are the same */ }
|
||||
"" -> { /* The version used before was pre- v1.3.0,
|
||||
as version tracking started then */
|
||||
|
||||
/*
|
||||
* before, the dominant and vibrant color of the `finn` and `dark` theme
|
||||
* were not stored anywhere. Now they have to be stored:
|
||||
* -> we just reset them using newly implemented functions
|
||||
*/
|
||||
when (getSavedTheme(this)) {
|
||||
"finn" -> resetToDefaultTheme(this)
|
||||
"dark" -> resetToDarkTheme(this)
|
||||
}
|
||||
|
||||
preferences.edit()
|
||||
.putString(PREF_VERSION, VERSION_NAME) // save new version
|
||||
.apply()
|
||||
|
||||
// show the new tutorial
|
||||
startActivity(Intent(this, TutorialActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
// Preload apps to speed up the Apps Recycler
|
||||
AsyncTask.execute { loadApps(packageManager) }
|
||||
|
||||
// Initialise layout
|
||||
setContentView(R.layout.home)
|
||||
}
|
||||
|
||||
override fun onStart(){
|
||||
super<AppCompatActivity>.onStart()
|
||||
|
||||
mDetector = GestureDetectorCompat(this, this)
|
||||
mDetector.setOnDoubleTapListener(this)
|
||||
|
||||
// for if the settings changed
|
||||
loadSettings(this)
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Applying the date / time format (changeable in settings)
|
||||
val dFormat = getPreferences(this).getInt(PREF_DATE_FORMAT, 0)
|
||||
val upperFMT = resources.getStringArray(R.array.settings_launcher_time_formats_upper)
|
||||
val lowerFMT = resources.getStringArray(R.array.settings_launcher_time_formats_lower)
|
||||
|
||||
val dateFormat = SimpleDateFormat(upperFMT[dFormat], Locale.getDefault())
|
||||
val timeFormat = SimpleDateFormat(lowerFMT[dFormat], Locale.getDefault())
|
||||
|
||||
clockTimer = fixedRateTimer("clockTimer", true, 0L, 100) {
|
||||
this@HomeActivity.runOnUiThread {
|
||||
val t = timeFormat.format(Date())
|
||||
if (home_lower_view.text != t)
|
||||
home_lower_view.text = t
|
||||
|
||||
val d = dateFormat.format(Date())
|
||||
if (home_upper_view.text != d)
|
||||
home_upper_view.text = d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
clockTimer.cancel()
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_BACK -> LauncherAction.CHOOSE.launch(this)
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> Gesture.VOLUME_UP(this)
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> Gesture.VOLUME_DOWN(this)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onFling(e1: MotionEvent?, e2: MotionEvent, dX: Float, dY: Float): Boolean {
|
||||
|
||||
if (e1 == null) return false;
|
||||
|
||||
val width = displayMetrics.widthPixels
|
||||
val height = displayMetrics.heightPixels
|
||||
|
||||
val diffX = e1.x - e2.x
|
||||
val diffY = e1.y - e2.y
|
||||
|
||||
val preferences = getPreferences(this)
|
||||
|
||||
val doubleActions = preferences.getBoolean(PREF_DOUBLE_ACTIONS_ENABLED, false)
|
||||
val edgeActions = preferences.getBoolean(PREF_EDGE_ACTIONS_ENABLED, false)
|
||||
val edgeStrictness = 0.15
|
||||
// how distinguished the swipe has to be to launch something
|
||||
// strictness = opposite of sensitivity. TODO - May have to be adjusted
|
||||
val strictness = (4 / bufferedPointerCount) * ((100 - preferences.getInt(PREF_SLIDE_SENSITIVITY, 50)) / 50)
|
||||
|
||||
var gesture = if(abs(diffX) > abs(diffY)) { // horizontal swipe
|
||||
if (diffX > width / 4 && abs(diffX) > strictness * abs(diffY))
|
||||
Gesture.SWIPE_LEFT
|
||||
else if (diffX < -width / 4 && abs(diffX) > strictness * abs(diffY))
|
||||
Gesture.SWIPE_RIGHT
|
||||
else null
|
||||
} else { // vertical swipe
|
||||
// Only open if the swipe was not from the phones top edge
|
||||
if (diffY < -height / 8 && abs(diffY) > strictness * abs(diffX) && e1.y > 100)
|
||||
Gesture.SWIPE_DOWN
|
||||
else if (diffY > height / 8 && abs(diffY) > strictness * abs(diffX))
|
||||
Gesture.SWIPE_UP
|
||||
else null
|
||||
}
|
||||
|
||||
if (doubleActions && bufferedPointerCount > 1) {
|
||||
gesture = gesture?.let(Gesture::getDoubleVariant)
|
||||
}
|
||||
|
||||
if (edgeActions) {
|
||||
if(max(e1.x, e2.x) < edgeStrictness * width){
|
||||
gesture = gesture?.let{it.getEdgeVariant(Gesture.Edge.LEFT)};
|
||||
} else if (min(e1.x, e2.x) > (1-edgeStrictness) * width){
|
||||
gesture = gesture?.let{it.getEdgeVariant(Gesture.Edge.RIGHT)};
|
||||
}
|
||||
|
||||
if(max(e1.y, e2.y) < edgeStrictness * height){
|
||||
gesture = gesture?.let{it.getEdgeVariant(Gesture.Edge.TOP)};
|
||||
} else if (min(e1.y, e2.y) > (1-edgeStrictness) * height){
|
||||
gesture = gesture?.let{it.getEdgeVariant(Gesture.Edge.BOTTOM)};
|
||||
}
|
||||
}
|
||||
gesture?.invoke(this)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onLongPress(event: MotionEvent) {
|
||||
Gesture.LONG_CLICK(this)
|
||||
}
|
||||
|
||||
override fun onDoubleTap(event: MotionEvent): Boolean {
|
||||
Gesture.DOUBLE_CLICK(this)
|
||||
return false
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
|
||||
// Buffer / Debounce the pointer count
|
||||
if (event.pointerCount > bufferedPointerCount) {
|
||||
bufferedPointerCount = event.pointerCount
|
||||
pointerBufferTimer = fixedRateTimer("pointerBufferTimer", true, 300, 1000) {
|
||||
bufferedPointerCount = 1
|
||||
this.cancel() // a non-recurring timer
|
||||
}
|
||||
}
|
||||
|
||||
return if (mDetector.onTouchEvent(event)) { false } else { super.onTouchEvent(event) }
|
||||
}
|
||||
|
||||
override fun setOnClicks() {
|
||||
|
||||
val preferences = getPreferences(this)
|
||||
home_upper_view.setOnClickListener {
|
||||
when (preferences.getInt(PREF_DATE_FORMAT, 0)) {
|
||||
0 -> Gesture.DATE(this)
|
||||
else -> Gesture.TIME(this)
|
||||
}
|
||||
}
|
||||
|
||||
home_lower_view.setOnClickListener {
|
||||
when (preferences.getInt(PREF_DATE_FORMAT, 0)) {
|
||||
0 -> Gesture.TIME(this)
|
||||
else -> Gesture.DATE(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 }
|
||||
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package de.jrpie.android.launcher
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
interface UIObject {
|
||||
fun onStart() {
|
||||
if (this is Activity) setWindowFlags(window)
|
||||
|
||||
applyTheme()
|
||||
setOnClicks()
|
||||
adjustLayout()
|
||||
}
|
||||
|
||||
// Don't use actual themes, rather create them on the fly for faster theme-switching
|
||||
fun applyTheme() { }
|
||||
fun setOnClicks() { }
|
||||
fun adjustLayout() { }
|
||||
}
|
100
app/src/main/java/de/jrpie/android/launcher/actions/Action.kt
Normal file
100
app/src/main/java/de/jrpie/android/launcher/actions/Action.kt
Normal file
|
@ -0,0 +1,100 @@
|
|||
package de.jrpie.android.launcher.actions
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
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.preferences.LauncherPreferences
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import androidx.core.content.edit
|
||||
|
||||
|
||||
@Serializable
|
||||
sealed interface Action {
|
||||
fun invoke(context: Context, rect: Rect? = null): Boolean
|
||||
fun label(context: Context): String
|
||||
fun getIcon(context: Context): Drawable?
|
||||
fun isAvailable(context: Context): Boolean
|
||||
|
||||
// Can the action be used to reach µLauncher settings?
|
||||
fun canReachSettings(): Boolean
|
||||
|
||||
|
||||
fun bindToGesture(prefEditor: Editor, id: String) {
|
||||
prefEditor.putString(id, Json.encodeToString(this))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun forGesture(gesture: Gesture): Action? {
|
||||
val id = gesture.id
|
||||
|
||||
val preferences = LauncherPreferences.getSharedPreferences()
|
||||
val json = preferences.getString(id, "null")!!
|
||||
return Json.decodeFromString(json)
|
||||
}
|
||||
|
||||
fun resetToDefaultActions(context: Context) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setActionForGesture(gesture: Gesture, action: Action?) {
|
||||
if (action == null) {
|
||||
clearActionForGesture(gesture)
|
||||
return
|
||||
}
|
||||
LauncherPreferences.getSharedPreferences().edit {
|
||||
action.bindToGesture(this, gesture.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearActionForGesture(gesture: Gesture) {
|
||||
LauncherPreferences.getSharedPreferences().edit {
|
||||
remove(gesture.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun launch(
|
||||
action: Action?,
|
||||
context: Context,
|
||||
animationIn: Int = android.R.anim.fade_in,
|
||||
animationOut: Int = android.R.anim.fade_out
|
||||
) {
|
||||
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 {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.toast_cant_open_message),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package de.jrpie.android.launcher.actions
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Service
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.LauncherApps
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
|
||||
import de.jrpie.android.launcher.apps.DetailedAppInfo
|
||||
import de.jrpie.android.launcher.ui.list.apps.openSettings
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@SerialName("action:app")
|
||||
class AppAction(val app: AppInfo) : Action {
|
||||
|
||||
override fun invoke(context: Context, rect: Rect?): Boolean {
|
||||
val packageName = app.packageName
|
||||
if (app.user != INVALID_USER) {
|
||||
val launcherApps =
|
||||
context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
app.getLauncherActivityInfo(context)?.let { app ->
|
||||
Log.i("Launcher", "Starting ${this.app}")
|
||||
launcherApps.startMainActivity(app.componentName, app.user, rect, null)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
context.packageManager.getLaunchIntentForPackage(packageName)?.let {
|
||||
it.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
try {
|
||||
context.startActivity(it)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/* check if app is installed */
|
||||
if (isAvailable(context)) {
|
||||
AlertDialog.Builder(
|
||||
context,
|
||||
R.style.AlertDialogCustom
|
||||
)
|
||||
.setTitle(context.getString(R.string.alert_cant_open_title))
|
||||
.setMessage(context.getString(R.string.alert_cant_open_message))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
app.openSettings(context)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setIcon(android.R.drawable.ic_dialog_info)
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun label(context: Context): String {
|
||||
return DetailedAppInfo.fromAppInfo(app, context)?.getCustomLabel(context).toString()
|
||||
}
|
||||
|
||||
override fun getIcon(context: Context): Drawable? {
|
||||
return DetailedAppInfo.fromAppInfo(app, context)?.getIcon(context)
|
||||
}
|
||||
|
||||
override fun isAvailable(context: Context): Boolean {
|
||||
// check if app is installed
|
||||
return DetailedAppInfo.fromAppInfo(app, context) != null
|
||||
}
|
||||
|
||||
override fun canReachSettings(): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
369
app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt
Normal file
369
app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt
Normal file
|
@ -0,0 +1,369 @@
|
|||
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
|
||||
|
||||
/**
|
||||
* @param id internal id to serialize the action. Used as a key in shared preferences.
|
||||
* @param defaultsResource res id of array of default actions for the gesture.
|
||||
* @param labelResource res id of the name of the gesture.
|
||||
* @param animationIn res id of transition animation (in) when using the gesture to launch an app.
|
||||
* @param animationOut res id of transition animation (out) when using the gesture to launch an app.
|
||||
*/
|
||||
enum class Gesture(
|
||||
val id: String,
|
||||
private val labelResource: Int,
|
||||
private val descriptionResource: Int,
|
||||
internal val defaultsResource: Int,
|
||||
private val animationIn: Int = android.R.anim.fade_in,
|
||||
private val animationOut: Int = android.R.anim.fade_out
|
||||
) {
|
||||
VOLUME_UP(
|
||||
"action.volume_up",
|
||||
R.string.settings_gesture_vol_up,
|
||||
R.string.settings_gesture_description_vol_up,
|
||||
R.array.default_volume_up,
|
||||
0,
|
||||
0
|
||||
),
|
||||
VOLUME_DOWN(
|
||||
"action.volume_down",
|
||||
R.string.settings_gesture_vol_down,
|
||||
R.string.settings_gesture_description_vol_down,
|
||||
R.array.default_volume_down, 0, 0
|
||||
),
|
||||
TIME(
|
||||
"action.time",
|
||||
R.string.settings_gesture_time,
|
||||
R.string.settings_gesture_description_time,
|
||||
R.array.default_time
|
||||
),
|
||||
DATE(
|
||||
"action.date",
|
||||
R.string.settings_gesture_date,
|
||||
R.string.settings_gesture_description_date,
|
||||
R.array.default_date
|
||||
),
|
||||
LONG_CLICK(
|
||||
"action.long_click",
|
||||
R.string.settings_gesture_long_click,
|
||||
R.string.settings_gesture_description_long_click,
|
||||
R.array.default_long_click, 0, 0
|
||||
),
|
||||
DOUBLE_CLICK(
|
||||
"action.double_click",
|
||||
R.string.settings_gesture_double_click,
|
||||
R.string.settings_gesture_description_double_click,
|
||||
R.array.default_double_click, 0, 0
|
||||
),
|
||||
SWIPE_UP(
|
||||
"action.up",
|
||||
R.string.settings_gesture_up,
|
||||
R.string.settings_gesture_description_up,
|
||||
R.array.default_up,
|
||||
R.anim.bottom_up
|
||||
),
|
||||
SWIPE_UP_LEFT_EDGE(
|
||||
"action.up_left",
|
||||
R.string.settings_gesture_up_left_edge,
|
||||
R.string.settings_gesture_description_up_left_edge,
|
||||
R.array.default_up_left,
|
||||
R.anim.bottom_up
|
||||
),
|
||||
SWIPE_UP_RIGHT_EDGE(
|
||||
"action.up_right",
|
||||
R.string.settings_gesture_up_right_edge,
|
||||
R.string.settings_gesture_description_up_right_edge,
|
||||
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,
|
||||
R.string.settings_gesture_description_double_up,
|
||||
R.array.default_double_up,
|
||||
R.anim.bottom_up
|
||||
),
|
||||
SWIPE_DOWN(
|
||||
"action.down",
|
||||
R.string.settings_gesture_down,
|
||||
R.string.settings_gesture_description_down,
|
||||
R.array.default_down,
|
||||
R.anim.top_down
|
||||
),
|
||||
SWIPE_DOWN_LEFT_EDGE(
|
||||
"action.down_left",
|
||||
R.string.settings_gesture_down_left_edge,
|
||||
R.string.settings_gesture_description_down_left_edge,
|
||||
R.array.default_down_left,
|
||||
R.anim.top_down
|
||||
),
|
||||
SWIPE_DOWN_RIGHT_EDGE(
|
||||
"action.down_right",
|
||||
R.string.settings_gesture_down_right_edge,
|
||||
R.string.settings_gesture_description_down_right_edge,
|
||||
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,
|
||||
R.string.settings_gesture_description_double_down,
|
||||
R.array.default_double_down,
|
||||
R.anim.top_down
|
||||
),
|
||||
SWIPE_LEFT(
|
||||
"action.left",
|
||||
R.string.settings_gesture_left,
|
||||
R.string.settings_gesture_description_left,
|
||||
R.array.default_messengers,
|
||||
R.anim.right_left
|
||||
),
|
||||
SWIPE_LEFT_TOP_EDGE(
|
||||
"action.left_top",
|
||||
R.string.settings_gesture_left_top_edge,
|
||||
R.string.settings_gesture_description_left_top_edge,
|
||||
R.array.default_messengers,
|
||||
R.anim.right_left
|
||||
),
|
||||
SWIPE_LEFT_BOTTOM_EDGE(
|
||||
"action.left_bottom",
|
||||
R.string.settings_gesture_left_bottom_edge,
|
||||
R.string.settings_gesture_description_left_bottom_edge,
|
||||
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,
|
||||
R.string.settings_gesture_description_double_left,
|
||||
R.array.default_messengers,
|
||||
R.anim.right_left
|
||||
),
|
||||
SWIPE_RIGHT(
|
||||
"action.right",
|
||||
R.string.settings_gesture_right,
|
||||
R.string.settings_gesture_description_right,
|
||||
R.array.default_right,
|
||||
R.anim.left_right
|
||||
),
|
||||
SWIPE_RIGHT_TOP_EDGE(
|
||||
"action.right_top",
|
||||
R.string.settings_gesture_right_top_edge,
|
||||
R.string.settings_gesture_description_right_top_edge,
|
||||
R.array.default_right_top,
|
||||
R.anim.left_right
|
||||
),
|
||||
SWIPE_RIGHT_BOTTOM_EDGE(
|
||||
"action.right_bottom",
|
||||
R.string.settings_gesture_right_bottom_edge,
|
||||
R.string.settings_gesture_description_right_bottom_edge,
|
||||
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 {
|
||||
TOP, BOTTOM, LEFT, RIGHT
|
||||
}
|
||||
|
||||
fun getLabel(context: Context): String {
|
||||
return context.resources.getString(this.labelResource)
|
||||
}
|
||||
|
||||
fun getDescription(context: Context): String {
|
||||
return context.resources.getString(this.descriptionResource)
|
||||
}
|
||||
|
||||
fun getDoubleVariant(): Gesture {
|
||||
return when (this) {
|
||||
SWIPE_UP -> SWIPE_UP_DOUBLE
|
||||
SWIPE_DOWN -> SWIPE_DOWN_DOUBLE
|
||||
SWIPE_LEFT -> SWIPE_LEFT_DOUBLE
|
||||
SWIPE_RIGHT -> SWIPE_RIGHT_DOUBLE
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
fun getEdgeVariant(edge: Edge): Gesture {
|
||||
return when (edge) {
|
||||
Edge.TOP ->
|
||||
when (this) {
|
||||
SWIPE_LEFT -> SWIPE_LEFT_TOP_EDGE
|
||||
SWIPE_RIGHT -> SWIPE_RIGHT_TOP_EDGE
|
||||
else -> this
|
||||
}
|
||||
|
||||
Edge.BOTTOM ->
|
||||
when (this) {
|
||||
SWIPE_LEFT -> SWIPE_LEFT_BOTTOM_EDGE
|
||||
SWIPE_RIGHT -> SWIPE_RIGHT_BOTTOM_EDGE
|
||||
else -> this
|
||||
}
|
||||
|
||||
Edge.LEFT ->
|
||||
when (this) {
|
||||
SWIPE_UP -> SWIPE_UP_LEFT_EDGE
|
||||
SWIPE_DOWN -> SWIPE_DOWN_LEFT_EDGE
|
||||
else -> this
|
||||
}
|
||||
|
||||
Edge.RIGHT ->
|
||||
when (this) {
|
||||
SWIPE_UP -> SWIPE_UP_RIGHT_EDGE
|
||||
SWIPE_DOWN -> SWIPE_DOWN_RIGHT_EDGE
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
SWIPE_DOWN_DOUBLE,
|
||||
SWIPE_LEFT_DOUBLE,
|
||||
SWIPE_RIGHT_DOUBLE -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun isEdgeVariant(): Boolean {
|
||||
return when (this) {
|
||||
SWIPE_UP_RIGHT_EDGE,
|
||||
SWIPE_UP_LEFT_EDGE,
|
||||
SWIPE_DOWN_LEFT_EDGE,
|
||||
SWIPE_DOWN_RIGHT_EDGE,
|
||||
SWIPE_LEFT_TOP_EDGE,
|
||||
SWIPE_LEFT_BOTTOM_EDGE,
|
||||
SWIPE_RIGHT_TOP_EDGE,
|
||||
SWIPE_RIGHT_BOTTOM_EDGE -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun isEnabled(): Boolean {
|
||||
if (isEdgeVariant()) {
|
||||
return LauncherPreferences.enabled_gestures().edgeSwipe()
|
||||
}
|
||||
if (isDoubleVariant()) {
|
||||
return LauncherPreferences.enabled_gestures().doubleSwipe()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
operator fun invoke(context: Context) {
|
||||
Log.i("Launcher", "Detected gesture: $this")
|
||||
val action = Action.forGesture(this)
|
||||
Action.launch(action, context, this.animationIn, this.animationOut)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun byId(id: String): Gesture? {
|
||||
return Gesture.entries.firstOrNull { it.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,341 @@
|
|||
package de.jrpie.android.launcher.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.view.KeyEvent
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import de.jrpie.android.launcher.Application
|
||||
import de.jrpie.android.launcher.BuildConfig
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService
|
||||
import de.jrpie.android.launcher.apps.AppFilter
|
||||
import de.jrpie.android.launcher.apps.hidePrivateSpaceWhenLocked
|
||||
import de.jrpie.android.launcher.apps.isPrivateSpaceSupported
|
||||
import de.jrpie.android.launcher.apps.togglePrivateSpaceLock
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.ui.list.ListActivity
|
||||
import de.jrpie.android.launcher.ui.settings.SettingsActivity
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.encoding.decodeStructure
|
||||
import kotlinx.serialization.encoding.encodeStructure
|
||||
|
||||
@Serializable(with = LauncherActionSerializer::class)
|
||||
@SerialName("action:launcher")
|
||||
enum class LauncherAction(
|
||||
val id: String,
|
||||
val label: Int,
|
||||
val icon: Int,
|
||||
val launch: (Context) -> Unit,
|
||||
private val canReachSettings: Boolean = false,
|
||||
val available: (Context) -> Boolean = { true },
|
||||
) : Action {
|
||||
SETTINGS(
|
||||
"settings",
|
||||
R.string.list_other_settings,
|
||||
R.drawable.baseline_settings_24,
|
||||
::openSettings,
|
||||
true
|
||||
),
|
||||
CHOOSE(
|
||||
"choose",
|
||||
R.string.list_other_list,
|
||||
R.drawable.baseline_menu_24,
|
||||
::openAppsList,
|
||||
true
|
||||
),
|
||||
CHOOSE_FROM_FAVORITES(
|
||||
"choose_from_favorites",
|
||||
R.string.list_other_list_favorites,
|
||||
R.drawable.baseline_favorite_24,
|
||||
{ context -> openAppsList(context, favorite = true) },
|
||||
true
|
||||
),
|
||||
CHOOSE_FROM_PRIVATE_SPACE(
|
||||
"choose_from_private_space",
|
||||
R.string.list_other_list_private_space,
|
||||
R.drawable.baseline_security_24,
|
||||
{ context ->
|
||||
if ((context.applicationContext as Application).privateSpaceLocked.value != true
|
||||
|| !hidePrivateSpaceWhenLocked(context)
|
||||
) {
|
||||
openAppsList(context, private = true)
|
||||
}
|
||||
},
|
||||
available = { _ ->
|
||||
isPrivateSpaceSupported()
|
||||
}
|
||||
),
|
||||
TOGGLE_PRIVATE_SPACE_LOCK(
|
||||
"toggle_private_space_lock",
|
||||
R.string.list_other_toggle_private_space_lock,
|
||||
R.drawable.baseline_security_24,
|
||||
::togglePrivateSpaceLock,
|
||||
available = { _ -> isPrivateSpaceSupported() }
|
||||
),
|
||||
VOLUME_UP(
|
||||
"volume_up",
|
||||
R.string.list_other_volume_up,
|
||||
R.drawable.baseline_volume_up_24,
|
||||
{ context -> audioVolumeAdjust(context, AudioManager.ADJUST_RAISE) }
|
||||
),
|
||||
VOLUME_DOWN(
|
||||
"volume_down",
|
||||
R.string.list_other_volume_down,
|
||||
R.drawable.baseline_volume_down_24,
|
||||
{ context -> audioVolumeAdjust(context, AudioManager.ADJUST_LOWER) }
|
||||
),
|
||||
VOLUME_ADJUST(
|
||||
"volume_adjust",
|
||||
R.string.list_other_volume_adjust,
|
||||
R.drawable.baseline_volume_adjust_24,
|
||||
{ context -> audioVolumeAdjust(context, AudioManager.ADJUST_SAME) }
|
||||
),
|
||||
TRACK_PLAY_PAUSE(
|
||||
"play_pause_track",
|
||||
R.string.list_other_track_play_pause,
|
||||
R.drawable.baseline_play_arrow_24,
|
||||
{ context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) }
|
||||
),
|
||||
TRACK_NEXT(
|
||||
"next_track",
|
||||
R.string.list_other_track_next,
|
||||
R.drawable.baseline_skip_next_24,
|
||||
{ context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_NEXT) }
|
||||
),
|
||||
TRACK_PREV(
|
||||
"previous_track",
|
||||
R.string.list_other_track_previous,
|
||||
R.drawable.baseline_skip_previous_24,
|
||||
{ context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PREVIOUS) }
|
||||
),
|
||||
EXPAND_NOTIFICATIONS_PANEL(
|
||||
"expand_notifications_panel",
|
||||
R.string.list_other_expand_notifications_panel,
|
||||
R.drawable.baseline_notifications_24,
|
||||
::expandNotificationsPanel
|
||||
),
|
||||
EXPAND_SETTINGS_PANEL(
|
||||
"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(
|
||||
"lock_screen",
|
||||
R.string.list_other_lock_screen,
|
||||
R.drawable.baseline_lock_24,
|
||||
{ c -> LauncherPreferences.actions().lockMethod().lockOrEnable(c) }
|
||||
),
|
||||
TORCH(
|
||||
"toggle_torch",
|
||||
R.string.list_other_torch,
|
||||
R.drawable.baseline_flashlight_on_24,
|
||||
::toggleTorch,
|
||||
),
|
||||
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)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun label(context: Context): String {
|
||||
return context.getString(label)
|
||||
}
|
||||
|
||||
override fun getIcon(context: Context): Drawable? {
|
||||
return AppCompatResources.getDrawable(context, icon)
|
||||
}
|
||||
|
||||
override fun isAvailable(context: Context): Boolean {
|
||||
return this.available(context)
|
||||
}
|
||||
|
||||
override fun canReachSettings(): Boolean {
|
||||
return this.canReachSettings
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun byId(id: String): LauncherAction? {
|
||||
return entries.singleOrNull { it.id == id }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Media player actions */
|
||||
private fun audioManagerPressKey(context: Context, key: Int) {
|
||||
val mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val eventTime: Long = SystemClock.uptimeMillis()
|
||||
val downEvent =
|
||||
KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, key, 0)
|
||||
mAudioManager.dispatchMediaKeyEvent(downEvent)
|
||||
val upEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, key, 0)
|
||||
mAudioManager.dispatchMediaKeyEvent(upEvent)
|
||||
|
||||
}
|
||||
|
||||
private fun audioVolumeAdjust(context: Context, direction: Int) {
|
||||
val audioManager =
|
||||
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
audioManager.adjustStreamVolume(
|
||||
AudioManager.STREAM_MUSIC,
|
||||
direction,
|
||||
AudioManager.FLAG_SHOW_UI
|
||||
)
|
||||
}
|
||||
|
||||
/* End media player actions */
|
||||
|
||||
private fun toggleTorch(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.alert_requires_android_m),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
return
|
||||
}
|
||||
|
||||
(context.applicationContext as Application).torchManager?.toggleTorch(context)
|
||||
}
|
||||
|
||||
private fun expandNotificationsPanel(context: Context) {
|
||||
/* https://stackoverflow.com/a/15582509 */
|
||||
try {
|
||||
@Suppress("SpellCheckingInspection")
|
||||
val statusBarService: Any? = context.getSystemService("statusbar")
|
||||
val statusBarManager = Class.forName("android.app.StatusBarManager")
|
||||
val showStatusBar = statusBarManager.getMethod("expandNotificationsPanel")
|
||||
showStatusBar.invoke(statusBarService)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.alert_cant_expand_status_bar_panel),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun expandSettingsPanel(context: Context) {
|
||||
/* https://stackoverflow.com/a/31898506 */
|
||||
try {
|
||||
@Suppress("SpellCheckingInspection")
|
||||
val statusBarService: Any? = context.getSystemService("statusbar")
|
||||
val statusBarManager = Class.forName("android.app.StatusBarManager")
|
||||
val showStatusBar = statusBarManager.getMethod("expandSettingsPanel")
|
||||
showStatusBar.invoke(statusBarService)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.alert_cant_expand_status_bar_panel),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchOtherLauncher(context: Context) {
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) },
|
||||
context.getString(R.string.list_other_launch_other_launcher)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun openSettings(context: Context) {
|
||||
context.startActivity(Intent(context, SettingsActivity::class.java))
|
||||
}
|
||||
|
||||
fun openAppsList(
|
||||
context: Context,
|
||||
favorite: Boolean = false,
|
||||
hidden: Boolean = false,
|
||||
private: Boolean = false
|
||||
) {
|
||||
val intent = Intent(context, ListActivity::class.java)
|
||||
intent.putExtra("intention", ListActivity.ListActivityIntention.VIEW.toString())
|
||||
intent.putExtra(
|
||||
"favoritesVisibility",
|
||||
if (favorite) {
|
||||
AppFilter.Companion.AppSetVisibility.EXCLUSIVE
|
||||
} else {
|
||||
AppFilter.Companion.AppSetVisibility.VISIBLE
|
||||
}
|
||||
)
|
||||
intent.putExtra(
|
||||
"hiddenVisibility",
|
||||
if (hidden) {
|
||||
AppFilter.Companion.AppSetVisibility.EXCLUSIVE
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
package de.jrpie.android.launcher.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.camera2.CameraAccessException
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import de.jrpie.android.launcher.R
|
||||
|
||||
@RequiresApi(VERSION_CODES.M)
|
||||
class TorchManager(context: Context) {
|
||||
|
||||
private val camera = getCameraId(context)
|
||||
private var torchEnabled = false
|
||||
|
||||
private val torchCallback = object : CameraManager.TorchCallback() {
|
||||
override fun onTorchModeChanged(cameraId: String, enabled: Boolean) {
|
||||
synchronized(this@TorchManager) {
|
||||
if (cameraId == camera) {
|
||||
torchEnabled = enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
registerCallback(context)
|
||||
}
|
||||
|
||||
private fun getCameraId(context: Context): String? {
|
||||
val cameraManager =
|
||||
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
|
||||
return cameraManager.cameraIdList.firstOrNull { c ->
|
||||
cameraManager
|
||||
.getCameraCharacteristics(c)
|
||||
.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerCallback(context: Context) {
|
||||
val cameraManager =
|
||||
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
|
||||
cameraManager.registerTorchCallback(
|
||||
torchCallback,
|
||||
Handler(Looper.getMainLooper())
|
||||
)
|
||||
}
|
||||
|
||||
fun toggleTorch(context: Context) {
|
||||
synchronized(this) {
|
||||
val cameraManager =
|
||||
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
|
||||
if (camera == null) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.alert_no_torch_found),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (!torchEnabled && Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
|
||||
cameraManager.turnOnTorchWithStrengthLevel(
|
||||
camera,
|
||||
cameraManager.getCameraCharacteristics(camera)
|
||||
.get(CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL) ?: 1
|
||||
)
|
||||
} else {
|
||||
cameraManager.setTorchMode(camera, !torchEnabled)
|
||||
}
|
||||
|
||||
} catch (e: CameraAccessException) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.alert_torch_access_exception),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package de.jrpie.android.launcher.actions.lock
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.widget.CheckBox
|
||||
import android.widget.Toast
|
||||
import de.jrpie.android.launcher.R
|
||||
|
||||
class LauncherAccessibilityService : AccessibilityService() {
|
||||
override fun onInterrupt() {}
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
||||
// Intentionally left blank, we are not interested in any AccessibilityEvents.
|
||||
// DO NOT ADD ANY CODE HERE!
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "Launcher Accessibility"
|
||||
private const val ACTION_REQUEST_ENABLE = "ACTION_REQUEST_ENABLE"
|
||||
const val ACTION_LOCK_SCREEN = "ACTION_LOCK_SCREEN"
|
||||
const val ACTION_RECENT_APPS = "ACTION_RECENT_APPS"
|
||||
|
||||
private fun invoke(context: Context, action: String, failureMessageRes: Int) {
|
||||
try {
|
||||
context.startService(
|
||||
Intent(
|
||||
context,
|
||||
LauncherAccessibilityService::class.java
|
||||
).apply {
|
||||
this.action = action
|
||||
})
|
||||
} catch (_: Exception) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
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,
|
||||
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
|
||||
) ?: return false
|
||||
|
||||
return enabledServices.split(":")
|
||||
.contains("${context.packageName}/${LauncherAccessibilityService::class.java.name}")
|
||||
.also { Log.d(TAG, "Accessibility Service enabled: $it") }
|
||||
}
|
||||
|
||||
fun showEnableDialog(context: Context) {
|
||||
AlertDialog.Builder(context, R.style.AlertDialogDanger).apply {
|
||||
setView(R.layout.dialog_consent_accessibility)
|
||||
setTitle(R.string.dialog_consent_accessibility_title)
|
||||
setPositiveButton(R.string.dialog_consent_accessibility_ok) { _, _ ->
|
||||
invoke(context, ACTION_REQUEST_ENABLE, R.string.alert_enable_accessibility_failed)
|
||||
}
|
||||
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
|
||||
}.create().also { it.show() }.apply {
|
||||
val buttonOk = getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
val checkboxes = listOf(
|
||||
findViewById<CheckBox>(R.id.dialog_consent_accessibility_checkbox_1),
|
||||
findViewById(R.id.dialog_consent_accessibility_checkbox_2),
|
||||
findViewById(R.id.dialog_consent_accessibility_checkbox_3),
|
||||
findViewById(R.id.dialog_consent_accessibility_checkbox_4),
|
||||
)
|
||||
val update = {
|
||||
buttonOk.isEnabled = checkboxes.map { b -> b?.isChecked == true }.all { it }
|
||||
}
|
||||
update()
|
||||
checkboxes.forEach { c ->
|
||||
c?.setOnClickListener { _ -> update() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
intent?.action?.let { action ->
|
||||
if (!isEnabled(this)) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.toast_accessibility_service_not_enabled),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
requestEnable()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
when (action) {
|
||||
ACTION_REQUEST_ENABLE -> {} // do nothing
|
||||
ACTION_LOCK_SCREEN -> handleLockScreen()
|
||||
ACTION_RECENT_APPS -> performGlobalAction(GLOBAL_ACTION_RECENTS)
|
||||
}
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
private fun requestEnable() {
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleLockScreen() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
getText(R.string.toast_lock_screen_not_supported),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return
|
||||
}
|
||||
|
||||
val success = performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN)
|
||||
if (!success) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
getText(R.string.alert_lock_screen_failed),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
requestEnable()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package de.jrpie.android.launcher.actions.lock
|
||||
|
||||
import android.app.admin.DeviceAdminReceiver
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import de.jrpie.android.launcher.R
|
||||
|
||||
class LauncherDeviceAdmin : DeviceAdminReceiver() {
|
||||
companion object {
|
||||
private fun getComponentName(context: Context): ComponentName {
|
||||
return ComponentName(context, LauncherDeviceAdmin::class.java)
|
||||
}
|
||||
|
||||
private fun requestDeviceAdmin(context: Context) {
|
||||
|
||||
val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN).apply {
|
||||
putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, getComponentName(context))
|
||||
putExtra(
|
||||
DevicePolicyManager.EXTRA_ADD_EXPLANATION,
|
||||
context.getString(R.string.device_admin_explanation)
|
||||
)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun isDeviceAdmin(context: Context): Boolean {
|
||||
val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
|
||||
return dpm.isAdminActive(getComponentName(context))
|
||||
}
|
||||
|
||||
private fun assertDeviceAdmin(context: Context): Boolean {
|
||||
val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
|
||||
if (!dpm.isAdminActive(getComponentName(context))) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.toast_device_admin_not_enabled),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
requestDeviceAdmin(context)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun lockScreen(context: Context) {
|
||||
|
||||
assertDeviceAdmin(context) || return
|
||||
|
||||
val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
|
||||
dpm.lockNow()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package de.jrpie.android.launcher.actions.lock
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.widget.Button
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import de.jrpie.android.launcher.BuildConfig
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
|
||||
|
||||
enum class LockMethod(
|
||||
private val lock: (Context) -> Unit,
|
||||
private val isEnabled: (Context) -> Boolean,
|
||||
private val enable: (Context) -> Unit
|
||||
) {
|
||||
DEVICE_ADMIN(
|
||||
LauncherDeviceAdmin::lockScreen,
|
||||
LauncherDeviceAdmin::isDeviceAdmin,
|
||||
LauncherDeviceAdmin::lockScreen
|
||||
),
|
||||
ACCESSIBILITY_SERVICE(
|
||||
LauncherAccessibilityService::lockScreen,
|
||||
LauncherAccessibilityService::isEnabled,
|
||||
LauncherAccessibilityService::showEnableDialog
|
||||
),
|
||||
;
|
||||
|
||||
fun lockOrEnable(context: Context) {
|
||||
if (!this.isEnabled(context)) {
|
||||
chooseMethod(context)
|
||||
return
|
||||
}
|
||||
this.lock(context)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun chooseMethod(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
|
||||
! BuildConfig.USE_ACCESSIBILITY_SERVICE) {
|
||||
// only device admin is available
|
||||
setMethod(context, DEVICE_ADMIN)
|
||||
return
|
||||
}
|
||||
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
|
||||
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
|
||||
setView(R.layout.dialog_select_lock_method)
|
||||
// setTitle()
|
||||
}.create().also { it.show() }.apply {
|
||||
findViewById<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()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
private fun setMethod(context: Context, m: LockMethod) {
|
||||
LauncherPreferences.actions().lockMethod(m)
|
||||
if (!m.isEnabled(context))
|
||||
m.enable(context)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package de.jrpie.android.launcher.apps
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* This interface is implemented by [AppInfo] and [PinnedShortcutInfo].
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface AbstractAppInfo {
|
||||
fun serialize(): String {
|
||||
return Json.encodeToString(this)
|
||||
}
|
||||
companion object {
|
||||
const val INVALID_USER = -1
|
||||
|
||||
fun deserialize(serialized: String): AbstractAppInfo {
|
||||
return Json.decodeFromString(serialized)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package de.jrpie.android.launcher.apps
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.UserHandle
|
||||
import android.util.Log
|
||||
import de.jrpie.android.launcher.Application
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
|
||||
/**
|
||||
* This interface is implemented by [DetailedAppInfo] and [DetailedPinnedShortcutInfo]
|
||||
*/
|
||||
sealed interface AbstractDetailedAppInfo {
|
||||
fun getRawInfo(): AbstractAppInfo
|
||||
fun getLabel(): String
|
||||
fun getIcon(context: Context): Drawable
|
||||
fun getUser(context: Context): UserHandle
|
||||
fun isPrivate(): Boolean
|
||||
fun isRemovable(): Boolean
|
||||
fun getAction(): Action
|
||||
|
||||
|
||||
fun getCustomLabel(context: Context): String {
|
||||
val map = (context.applicationContext as? Application)?.getCustomAppNames()
|
||||
return map?.get(getRawInfo()) ?: getLabel()
|
||||
}
|
||||
|
||||
|
||||
fun setCustomLabel(label: CharSequence?) {
|
||||
Log.i("Launcher", "Setting custom label for ${this.getRawInfo()} to ${label}.")
|
||||
val map = LauncherPreferences.apps().customNames() ?: HashMap<AbstractAppInfo, String>()
|
||||
|
||||
if (label.isNullOrEmpty()) {
|
||||
map.remove(getRawInfo())
|
||||
} else {
|
||||
map[getRawInfo()] = label.toString()
|
||||
}
|
||||
LauncherPreferences.apps().customNames(map)
|
||||
}
|
||||
|
||||
}
|
102
app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt
Normal file
102
app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt
Normal file
|
@ -0,0 +1,102 @@
|
|||
package de.jrpie.android.launcher.apps
|
||||
|
||||
import android.content.Context
|
||||
import android.icu.text.Normalizer2
|
||||
import android.os.Build
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.AppAction
|
||||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.actions.ShortcutAction
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import java.util.Locale
|
||||
import kotlin.text.Regex.Companion.escape
|
||||
|
||||
class AppFilter(
|
||||
var context: Context,
|
||||
var query: String,
|
||||
var favoritesVisibility: AppSetVisibility = AppSetVisibility.VISIBLE,
|
||||
var hiddenVisibility: AppSetVisibility = AppSetVisibility.HIDDEN,
|
||||
var privateSpaceVisibility: AppSetVisibility = AppSetVisibility.VISIBLE
|
||||
) {
|
||||
|
||||
operator fun invoke(apps: List<AbstractDetailedAppInfo>): List<AbstractDetailedAppInfo> {
|
||||
var apps =
|
||||
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) }
|
||||
.mapNotNull {
|
||||
(it as? AppAction)?.app
|
||||
?: (it as? ShortcutAction)?.shortcut
|
||||
}
|
||||
.toSet()
|
||||
apps = apps.filterNot { info -> boundApps.contains(info.getRawInfo()) }
|
||||
}
|
||||
|
||||
// normalize text for search
|
||||
val allowedSpecialCharacters = unicodeNormalize(query)
|
||||
.lowercase(Locale.ROOT)
|
||||
.toCharArray()
|
||||
.distinct()
|
||||
.filter { c -> !c.isLetter() }
|
||||
.map { c -> escape(c.toString()) }
|
||||
.fold("") { x, y -> x + y }
|
||||
val disallowedCharsRegex = "[^\\p{L}$allowedSpecialCharacters]".toRegex()
|
||||
|
||||
fun normalize(text: String): String {
|
||||
return unicodeNormalize(text).replace(disallowedCharsRegex, "")
|
||||
}
|
||||
|
||||
if (query.isEmpty()) {
|
||||
return apps
|
||||
} else {
|
||||
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))
|
||||
|
||||
if (itemLabel.startsWith(normalizedQuery)) {
|
||||
r.add(item)
|
||||
} else if (itemLabel.contains(normalizedQuery)) {
|
||||
appsSecondary.add(item)
|
||||
}
|
||||
}
|
||||
r.addAll(appsSecondary)
|
||||
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
enum class AppSetVisibility(
|
||||
val predicate: (set: Set<AbstractAppInfo>, AbstractDetailedAppInfo) -> Boolean
|
||||
) {
|
||||
VISIBLE({ _, _ -> true }),
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
29
app/src/main/java/de/jrpie/android/launcher/apps/AppInfo.kt
Normal file
29
app/src/main/java/de/jrpie/android/launcher/apps/AppInfo.kt
Normal file
|
@ -0,0 +1,29 @@
|
|||
package de.jrpie.android.launcher.apps
|
||||
|
||||
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.
|
||||
*/
|
||||
@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, userHandle)
|
||||
return activityList.firstOrNull { app -> app.name == activityName }
|
||||
?: activityList.firstOrNull()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package de.jrpie.android.launcher.apps
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.LauncherActivityInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.UserHandle
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.AppAction
|
||||
import de.jrpie.android.launcher.getUserFromId
|
||||
|
||||
/**
|
||||
* Stores information used to create [de.jrpie.android.launcher.ui.list.apps.AppsRecyclerAdapter] rows.
|
||||
*/
|
||||
class DetailedAppInfo(
|
||||
private val app: AppInfo,
|
||||
private val label: CharSequence,
|
||||
private val icon: Drawable,
|
||||
private val privateSpace: Boolean,
|
||||
private val removable: Boolean = true,
|
||||
): AbstractDetailedAppInfo {
|
||||
|
||||
constructor(activityInfo: LauncherActivityInfo, private: Boolean) : this(
|
||||
AppInfo(
|
||||
activityInfo.applicationInfo.packageName,
|
||||
activityInfo.name,
|
||||
activityInfo.user.hashCode()
|
||||
),
|
||||
activityInfo.label,
|
||||
activityInfo.getBadgedIcon(0),
|
||||
private,
|
||||
// App can be uninstalled iff it is not a system app
|
||||
activityInfo.applicationInfo.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0
|
||||
)
|
||||
|
||||
|
||||
|
||||
override fun getLabel(): String {
|
||||
return label.toString()
|
||||
}
|
||||
|
||||
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, it.user == getPrivateSpaceUser(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package de.jrpie.android.launcher.apps
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherApps
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.UserHandle
|
||||
import androidx.annotation.RequiresApi
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.ShortcutAction
|
||||
import de.jrpie.android.launcher.getUserFromId
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
class DetailedPinnedShortcutInfo(
|
||||
private val shortcutInfo: PinnedShortcutInfo,
|
||||
private val label: String,
|
||||
private val icon: Drawable,
|
||||
private val privateSpace: Boolean
|
||||
) : AbstractDetailedAppInfo {
|
||||
|
||||
constructor(context: Context, shortcut: ShortcutInfo) : this(
|
||||
PinnedShortcutInfo(shortcut),
|
||||
shortcut.longLabel.toString(),
|
||||
(context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps)
|
||||
.getShortcutBadgedIconDrawable(shortcut, 0),
|
||||
shortcut.userHandle == getPrivateSpaceUser(context)
|
||||
)
|
||||
|
||||
override fun getRawInfo(): AbstractAppInfo {
|
||||
return shortcutInfo
|
||||
}
|
||||
|
||||
override fun getLabel(): String {
|
||||
return label
|
||||
}
|
||||
|
||||
override fun getIcon(context: Context): Drawable {
|
||||
return icon
|
||||
}
|
||||
|
||||
override fun getUser(context: Context): UserHandle {
|
||||
return getUserFromId(shortcutInfo.user, context)
|
||||
}
|
||||
|
||||
override fun isPrivate(): Boolean {
|
||||
return privateSpace
|
||||
}
|
||||
|
||||
override fun isRemovable(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getAction(): Action {
|
||||
return ShortcutAction(shortcutInfo)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromPinnedShortcutInfo(shortcutInfo: PinnedShortcutInfo, context: Context): DetailedPinnedShortcutInfo? {
|
||||
return shortcutInfo.getShortcutInfo(context)?.let {
|
||||
DetailedPinnedShortcutInfo(context, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package de.jrpie.android.launcher.apps
|
||||
|
||||
import android.app.Service
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherApps
|
||||
import android.content.pm.LauncherApps.ShortcutQuery
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import de.jrpie.android.launcher.getUserFromId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
@Serializable
|
||||
@SerialName("shortcut")
|
||||
data class PinnedShortcutInfo(
|
||||
val id: String,
|
||||
val packageName: String,
|
||||
val activityName: String,
|
||||
val user: Int
|
||||
): AbstractAppInfo {
|
||||
|
||||
constructor(info: ShortcutInfo) : this(info.id, info.`package`, info.activity?.className ?: "", info.userHandle.hashCode())
|
||||
|
||||
fun getShortcutInfo(context: Context): ShortcutInfo? {
|
||||
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
|
||||
return try {
|
||||
launcherApps.getShortcuts(
|
||||
ShortcutQuery().apply {
|
||||
setQueryFlags(ShortcutQuery.FLAG_MATCH_PINNED)
|
||||
setPackage(packageName)
|
||||
setActivity(ComponentName(packageName, activityName))
|
||||
setShortcutIds(listOf(id))
|
||||
},
|
||||
getUserFromId(user, context)
|
||||
)?.firstOrNull()
|
||||
} catch(_: Exception) {
|
||||
// can throw SecurityException or IllegalStateException when profile is locked
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
140
app/src/main/java/de/jrpie/android/launcher/apps/PrivateSpace.kt
Normal file
140
app/src/main/java/de/jrpie/android/launcher/apps/PrivateSpace.kt
Normal file
|
@ -0,0 +1,140 @@
|
|||
package de.jrpie.android.launcher.apps
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.LauncherApps
|
||||
import android.os.Build
|
||||
import android.os.UserHandle
|
||||
import android.os.UserManager
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.isDefaultHomeScreen
|
||||
import de.jrpie.android.launcher.setDefaultHomeScreen
|
||||
|
||||
|
||||
/*
|
||||
* Checks whether the device supports private space.
|
||||
*/
|
||||
fun isPrivateSpaceSupported(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
|
||||
}
|
||||
|
||||
fun getPrivateSpaceUser(context: Context): UserHandle? {
|
||||
if (!isPrivateSpaceSupported()) {
|
||||
return null
|
||||
}
|
||||
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
|
||||
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
return userManager.userProfiles.firstOrNull { u ->
|
||||
launcherApps.getLauncherUserInfo(u)?.userType == UserManager.USER_TYPE_PROFILE_PRIVATE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the user has created a private space and whether µLauncher can access it.
|
||||
*/
|
||||
fun isPrivateSpaceSetUp(
|
||||
context: Context,
|
||||
showToast: Boolean = false,
|
||||
launchSettings: Boolean = false
|
||||
): Boolean {
|
||||
if (!isPrivateSpaceSupported()) {
|
||||
if (showToast) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.alert_requires_android_v),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
return false
|
||||
}
|
||||
val privateSpaceUser = getPrivateSpaceUser(context)
|
||||
if (privateSpaceUser != null) {
|
||||
return true
|
||||
}
|
||||
if (!isDefaultHomeScreen(context)) {
|
||||
if (showToast) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.toast_private_space_default_home_screen),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
if (launchSettings) {
|
||||
setDefaultHomeScreen(context)
|
||||
}
|
||||
} else {
|
||||
if (showToast) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.toast_private_space_not_available),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
if (launchSettings) {
|
||||
try {
|
||||
context.startActivity(Intent(Settings.ACTION_PRIVACY_SETTINGS))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun isPrivateSpaceLocked(context: Context): Boolean {
|
||||
if (!isPrivateSpaceSupported()) {
|
||||
return false
|
||||
}
|
||||
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
|
||||
val privateSpaceUser = getPrivateSpaceUser(context) ?: return false
|
||||
return userManager.isQuietModeEnabled(privateSpaceUser)
|
||||
}
|
||||
|
||||
fun lockPrivateSpace(context: Context, lock: Boolean) {
|
||||
if (!isPrivateSpaceSupported()) {
|
||||
return
|
||||
}
|
||||
|
||||
// silently return when trying to unlock but hide when locked is set
|
||||
if (!lock && hidePrivateSpaceWhenLocked(context)) {
|
||||
return
|
||||
}
|
||||
|
||||
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
|
||||
val privateSpaceUser = getPrivateSpaceUser(context) ?: return
|
||||
userManager.requestQuietModeEnabled(lock, privateSpaceUser)
|
||||
}
|
||||
|
||||
fun togglePrivateSpaceLock(context: Context) {
|
||||
if (!isPrivateSpaceSetUp(context, showToast = true, launchSettings = true)) {
|
||||
return
|
||||
}
|
||||
|
||||
val lock = isPrivateSpaceLocked(context)
|
||||
lockPrivateSpace(context, !lock)
|
||||
if (!lock) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.toast_private_space_locked),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("SameReturnValue")
|
||||
fun hidePrivateSpaceWhenLocked(context: Context): Boolean {
|
||||
// Trying to access the setting as a 3rd party launcher raises a security exception.
|
||||
// This is an Android bug: https://issuetracker.google.com/issues/352276244#comment5
|
||||
// The logic for this is implemented.
|
||||
// TODO: replace this once the Android bug is fixed
|
||||
return false
|
||||
|
||||
// TODO: perhaps this should be cached
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Launcher3/src/com/android/launcher3/util/SettingsCache.java;l=61;drc=56bf7ad33bc9d5ed3c18e7abefeec5c177ec75d7
|
||||
|
||||
// val key = "hide_privatespace_entry_point"
|
||||
// return Settings.Secure.getInt(context.contentResolver, key, 0) == 1
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
package de.jrpie.android.launcher.list
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
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.PREF_SCREEN_FULLSCREEN
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.REQUEST_UNINSTALL
|
||||
import de.jrpie.android.launcher.UIObject
|
||||
import de.jrpie.android.launcher.getPreferences
|
||||
import de.jrpie.android.launcher.launch
|
||||
import de.jrpie.android.launcher.list.apps.ListFragmentApps
|
||||
import de.jrpie.android.launcher.list.other.LauncherAction
|
||||
import de.jrpie.android.launcher.list.other.ListFragmentOther
|
||||
import de.jrpie.android.launcher.vibrantColor
|
||||
import kotlinx.android.synthetic.main.list.*
|
||||
|
||||
|
||||
var intendedChoosePause = false // know when to close
|
||||
|
||||
// TODO: Better solution for this intercommunication functionality (used in list-fragments)
|
||||
var intention = ListActivity.ListActivityIntention.VIEW
|
||||
var forGesture: String? = null
|
||||
|
||||
/**
|
||||
* The [ListActivity] is the most general purpose activity in Launcher:
|
||||
* - used to view all apps and edit their settings
|
||||
* - used to choose an app / intent to be launched
|
||||
*
|
||||
* The activity itself can also be chosen to be launched as an action.
|
||||
*/
|
||||
class ListActivity : AppCompatActivity(), UIObject {
|
||||
enum class ListActivityIntention(val titleResource: Int) {
|
||||
VIEW(R.string.list_title_view), /* view list of apps */
|
||||
PICK(R.string.list_title_pick) /* choose app or action to associate to a gesture */
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Initialise layout
|
||||
setContentView(R.layout.list)
|
||||
|
||||
list_settings.setOnClickListener {
|
||||
LauncherAction.SETTINGS.launch(this@ListActivity)
|
||||
}
|
||||
|
||||
// android:windowSoftInputMode="adjustResize" doesn't work in full screen.
|
||||
// workaround from https://stackoverflow.com/a/57623505
|
||||
this.window.decorView.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val r = Rect()
|
||||
window.decorView.getWindowVisibleDisplayFrame(r)
|
||||
val height: Int =
|
||||
list_container.context.resources.displayMetrics.heightPixels
|
||||
val diff = height - r.bottom
|
||||
if (diff != 0 && getPreferences(this).getBoolean(PREF_SCREEN_FULLSCREEN, false)) {
|
||||
if (list_container.paddingBottom !== diff) {
|
||||
list_container.setPadding(0, 0, 0, diff)
|
||||
}
|
||||
} else {
|
||||
if (list_container.paddingBottom !== 0) {
|
||||
list_container.setPadding(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart(){
|
||||
super<AppCompatActivity>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun applyTheme() {
|
||||
// list_close.setTextColor(vibrantColor)
|
||||
|
||||
list_tabs.setSelectedTabIndicatorColor(vibrantColor)
|
||||
}
|
||||
|
||||
override fun setOnClicks() {
|
||||
list_close.setOnClickListener { finish() }
|
||||
}
|
||||
|
||||
override fun adjustLayout() {
|
||||
// get info about which action this activity is open for
|
||||
intent.extras?.let { bundle ->
|
||||
intention = bundle.getString("intention")
|
||||
?.let { ListActivityIntention.valueOf(it) }
|
||||
?: ListActivityIntention.VIEW
|
||||
|
||||
if (intention != ListActivityIntention.VIEW)
|
||||
forGesture = bundle.getString("forGesture")
|
||||
}
|
||||
|
||||
// Hide tabs for the "view" action
|
||||
if (intention == ListActivityIntention.VIEW) {
|
||||
list_tabs.visibility = View.GONE
|
||||
}
|
||||
|
||||
list_heading.text = getString(intention.titleResource)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private val TAB_TITLES = arrayOf(
|
||||
R.string.list_tab_app,
|
||||
R.string.list_tab_other
|
||||
)
|
||||
|
||||
/**
|
||||
* The [ListSectionsPagerAdapter] returns the fragment,
|
||||
* which corresponds to the selected tab in [ListActivity].
|
||||
*/
|
||||
class ListSectionsPagerAdapter(private val context: Context, fm: FragmentManager)
|
||||
: FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
return when (position){
|
||||
0 -> ListFragmentApps()
|
||||
1 -> ListFragmentOther()
|
||||
else -> Fragment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return context.resources.getString(TAB_TITLES[position])
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return when (intention) {
|
||||
ListActivity.ListActivityIntention.VIEW -> 1
|
||||
else -> 2
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package de.jrpie.android.launcher.list.apps
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
/**
|
||||
* Stores information used to create [AppsRecyclerAdapter] rows.
|
||||
*
|
||||
* Represents an app installed on the users device.
|
||||
*/
|
||||
class AppInfo {
|
||||
var label: CharSequence? = null
|
||||
var packageName: CharSequence? = null
|
||||
var icon: Drawable? = null
|
||||
var isSystemApp: Boolean = false
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
package de.jrpie.android.launcher.list.apps
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import de.jrpie.android.launcher.PREF_SEARCH_AUTO_LAUNCH
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
|
||||
import de.jrpie.android.launcher.REQUEST_UNINSTALL
|
||||
import de.jrpie.android.launcher.appsList
|
||||
import de.jrpie.android.launcher.getPreferences
|
||||
import de.jrpie.android.launcher.getSavedTheme
|
||||
import de.jrpie.android.launcher.launch
|
||||
import de.jrpie.android.launcher.list.ListActivity
|
||||
import de.jrpie.android.launcher.list.intendedChoosePause
|
||||
import de.jrpie.android.launcher.loadApps
|
||||
import de.jrpie.android.launcher.openAppSettings
|
||||
import de.jrpie.android.launcher.transformGrayscale
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A [RecyclerView] (efficient scrollable list) containing all apps on the users device.
|
||||
* The apps details are represented by [AppInfo].
|
||||
*
|
||||
* @param activity - the activity this is in
|
||||
* @param intention - why the list is displayed ("view", "pick")
|
||||
* @param forGesture - the action which an app is chosen for (when the intention is "pick")
|
||||
*/
|
||||
class AppsRecyclerAdapter(val activity: Activity,
|
||||
val intention: ListActivity.ListActivityIntention
|
||||
= ListActivity.ListActivityIntention.VIEW,
|
||||
val forGesture: String? = ""):
|
||||
RecyclerView.Adapter<AppsRecyclerAdapter.ViewHolder>() {
|
||||
|
||||
private val appsListDisplayed: MutableList<AppInfo>
|
||||
|
||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
|
||||
View.OnClickListener {
|
||||
var textView: TextView = itemView.findViewById(R.id.list_apps_row_name)
|
||||
var img: ImageView = itemView.findViewById(R.id.list_apps_row_icon)
|
||||
var menuDots: ImageView = itemView.findViewById(R.id.list_apps_row_menu)
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val pos = adapterPosition
|
||||
val context: Context = v.context
|
||||
val appPackageName = appsListDisplayed[pos].packageName.toString()
|
||||
|
||||
when (intention){
|
||||
ListActivity.ListActivityIntention.VIEW -> {
|
||||
val launchIntent: Intent = context.packageManager
|
||||
.getLaunchIntentForPackage(appPackageName)!!
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
ListActivity.ListActivityIntention.PICK -> {
|
||||
val returnIntent = Intent()
|
||||
returnIntent.putExtra("value", appPackageName)
|
||||
returnIntent.putExtra("forGesture", forGesture)
|
||||
activity.setResult(REQUEST_CHOOSE_APP, returnIntent)
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init { itemView.setOnClickListener(this) }
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
|
||||
val appLabel = appsListDisplayed[i].label.toString()
|
||||
val appPackageName = appsListDisplayed[i].packageName.toString()
|
||||
val appIcon = appsListDisplayed[i].icon
|
||||
val isSystemApp = appsListDisplayed[i].isSystemApp
|
||||
|
||||
viewHolder.textView.text = appLabel
|
||||
viewHolder.img.setImageDrawable(appIcon)
|
||||
|
||||
if (getSavedTheme(activity) == "dark") transformGrayscale(
|
||||
viewHolder.img
|
||||
)
|
||||
|
||||
// decide when to show the options popup menu about
|
||||
if (isSystemApp || intention == ListActivity.ListActivityIntention.PICK) {
|
||||
viewHolder.menuDots.visibility = View.INVISIBLE
|
||||
}
|
||||
else {
|
||||
viewHolder.menuDots.visibility = View.VISIBLE
|
||||
|
||||
viewHolder.menuDots.setOnClickListener{ showOptionsPopup(viewHolder, appPackageName) }
|
||||
viewHolder.menuDots.setOnLongClickListener{ showOptionsPopup(viewHolder, appPackageName) }
|
||||
viewHolder.textView.setOnLongClickListener{ showOptionsPopup(viewHolder, appPackageName) }
|
||||
viewHolder.img.setOnLongClickListener{ showOptionsPopup(viewHolder, appPackageName) }
|
||||
|
||||
// ensure onClicks are actually caught
|
||||
viewHolder.textView.setOnClickListener{ viewHolder.onClick(viewHolder.textView) }
|
||||
viewHolder.img.setOnClickListener{ viewHolder.onClick(viewHolder.img) }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("SameReturnValue")
|
||||
private fun showOptionsPopup(viewHolder: ViewHolder, appPackageName: String): Boolean {
|
||||
//create the popup menu
|
||||
|
||||
val popup = PopupMenu(activity, viewHolder.menuDots)
|
||||
popup.inflate(R.menu.menu_app)
|
||||
|
||||
popup.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.app_menu_delete -> { // delete
|
||||
intendedChoosePause = true
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE)
|
||||
intent.data = Uri.parse("package:$appPackageName")
|
||||
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||
activity.startActivityForResult(intent,
|
||||
REQUEST_UNINSTALL
|
||||
)
|
||||
|
||||
true
|
||||
}
|
||||
R.id.app_menu_info -> { // open app settings
|
||||
intendedChoosePause = true
|
||||
openAppSettings(
|
||||
appPackageName,
|
||||
activity
|
||||
)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
popup.show()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int { return appsListDisplayed.size }
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view: View = inflater.inflate(R.layout.list_apps_row, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
init {
|
||||
// Load the apps
|
||||
if (appsList.size == 0)
|
||||
loadApps(activity.packageManager)
|
||||
else {
|
||||
AsyncTask.execute { loadApps(activity.packageManager) }
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
appsListDisplayed = ArrayList()
|
||||
appsListDisplayed.addAll(appsList)
|
||||
}
|
||||
|
||||
/**
|
||||
* The function [filter] is used to search elements within this [RecyclerView].
|
||||
*/
|
||||
fun filter(text: String) {
|
||||
// normalize text for search
|
||||
fun normalize(text: String): String{
|
||||
return text.lowercase(Locale.ROOT).replace("[^a-z0-9]".toRegex(), "")
|
||||
}
|
||||
appsListDisplayed.clear()
|
||||
if (text.isEmpty()) {
|
||||
appsListDisplayed.addAll(appsList)
|
||||
} else {
|
||||
val appsSecondary: MutableList<AppInfo> = ArrayList()
|
||||
val normalizedText: String = normalize(text)
|
||||
for (item in appsList) {
|
||||
val itemLabel: String = normalize(item.label.toString())
|
||||
|
||||
if (itemLabel.startsWith(normalizedText)) {
|
||||
appsListDisplayed.add(item)
|
||||
}else if(itemLabel.contains(normalizedText)){
|
||||
appsSecondary.add(item)
|
||||
}
|
||||
}
|
||||
appsListDisplayed.addAll(appsSecondary)
|
||||
}
|
||||
|
||||
// Launch apps automatically if only one result is found and the user wants it
|
||||
// Disabled at the moment. The Setting 'PREF_SEARCH_AUTO_LAUNCH' may be
|
||||
// modifiable at some later point.
|
||||
if (appsListDisplayed.size == 1 && intention == ListActivity.ListActivityIntention.VIEW
|
||||
&& getPreferences(activity).getBoolean(PREF_SEARCH_AUTO_LAUNCH, false)) {
|
||||
launch(appsListDisplayed[0].packageName.toString(), activity)
|
||||
|
||||
val inputMethodManager = activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(View(activity).windowToken, 0)
|
||||
}
|
||||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package de.jrpie.android.launcher.list.apps
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import de.jrpie.android.launcher.PREF_SEARCH_AUTO_KEYBOARD
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.UIObject
|
||||
import de.jrpie.android.launcher.getPreferences
|
||||
import de.jrpie.android.launcher.list.ListActivity
|
||||
import de.jrpie.android.launcher.list.forGesture
|
||||
import de.jrpie.android.launcher.list.intention
|
||||
import de.jrpie.android.launcher.openSoftKeyboard
|
||||
import kotlinx.android.synthetic.main.list_apps.*
|
||||
|
||||
|
||||
/**
|
||||
* The [ListFragmentApps] is used as a tab in ListActivity.
|
||||
*
|
||||
* It is a list of all installed applications that are can be launched.
|
||||
*/
|
||||
class ListFragmentApps : Fragment(), UIObject {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.list_apps, container, false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun applyTheme() {
|
||||
}
|
||||
|
||||
override fun setOnClicks() { }
|
||||
|
||||
override fun adjustLayout() {
|
||||
|
||||
val appsRViewAdapter = AppsRecyclerAdapter(activity!!, intention, forGesture)
|
||||
|
||||
// set up the list / recycler
|
||||
list_apps_rview.apply {
|
||||
// improve performance (since content changes don't change the layout size)
|
||||
setHasFixedSize(true)
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = appsRViewAdapter
|
||||
}
|
||||
|
||||
list_apps_searchview.setOnQueryTextListener(object :
|
||||
androidx.appcompat.widget.SearchView.OnQueryTextListener {
|
||||
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
appsRViewAdapter.filter(query)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
appsRViewAdapter.filter(newText)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
if (intention == ListActivity.ListActivityIntention.VIEW
|
||||
&& getPreferences(context!!)
|
||||
.getBoolean(PREF_SEARCH_AUTO_KEYBOARD, true)) {
|
||||
openSoftKeyboard(context!!, list_apps_searchview)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package de.jrpie.android.launcher.list.other
|
||||
|
||||
import android.app.Activity
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.audioNextTrack
|
||||
import de.jrpie.android.launcher.audioPreviousTrack
|
||||
import de.jrpie.android.launcher.audioVolumeDown
|
||||
import de.jrpie.android.launcher.audioVolumeUp
|
||||
import de.jrpie.android.launcher.openAppsList
|
||||
import de.jrpie.android.launcher.openSettings
|
||||
|
||||
enum class LauncherAction(val id: String, val label: Int, val icon: Int, val launch: (Activity) -> Unit) {
|
||||
SETTINGS("launcher:settings", R.string.list_other_settings, R.drawable.baseline_settings_24, ::openSettings),
|
||||
CHOOSE("launcher:choose", R.string.list_other_list, R.drawable.baseline_menu_24, ::openAppsList),
|
||||
VOLUME_UP("launcher:volumeUp",
|
||||
R.string.list_other_volume_up,
|
||||
R.drawable.baseline_volume_up_24, ::audioVolumeUp),
|
||||
VOLUME_DOWN("launcher:volumeDown",
|
||||
R.string.list_other_volume_down,
|
||||
R.drawable.baseline_volume_down_24, ::audioVolumeDown),
|
||||
TRACK_NEXT("launcher:nextTrack",
|
||||
R.string.list_other_track_next,
|
||||
R.drawable.baseline_skip_next_24, ::audioNextTrack),
|
||||
TRACK_PREV("launcher:previousTrack",
|
||||
R.string.list_other_track_previous,
|
||||
R.drawable.baseline_skip_previous_24, ::audioPreviousTrack),
|
||||
NOP("launcher:nop", R.string.list_other_nop, R.drawable.baseline_not_interested_24, {});
|
||||
|
||||
companion object {
|
||||
fun byId(id: String): LauncherAction? {
|
||||
return LauncherAction.values().singleOrNull { it.id == id }
|
||||
}
|
||||
|
||||
fun isOtherAction(id: String): Boolean {
|
||||
return id.startsWith("launcher")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
package de.jrpie.android.launcher.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.graphics.Color
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.AttributeSet
|
||||
import android.widget.EditText
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.graphics.alpha
|
||||
import androidx.core.graphics.blue
|
||||
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) {
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(context: Context) : this(context, null)
|
||||
|
||||
private var selectedColor = Color.WHITE
|
||||
|
||||
init {
|
||||
isPersistent = true
|
||||
selectedColor = getPersistedInt(selectedColor)
|
||||
summary = selectedColor.getHex()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
showDialog()
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
override fun onGetDefaultValue(a: TypedArray, index: Int): Int {
|
||||
return a.getInt(index, selectedColor)
|
||||
}
|
||||
|
||||
override fun onSetInitialValue(defaultValue: Any?) {
|
||||
selectedColor = getPersistedInt(selectedColor)
|
||||
summary = selectedColor.getHex()
|
||||
}
|
||||
|
||||
private fun showDialog() {
|
||||
var currentColor = getPersistedInt(selectedColor)
|
||||
|
||||
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
|
||||
setView(R.layout.dialog_choose_color)
|
||||
setTitle(R.string.dialog_choose_color_title)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
persistInt(currentColor)
|
||||
summary = currentColor.getHex()
|
||||
}
|
||||
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
|
||||
}.create().also { it.show() }.apply {
|
||||
val preview = findViewById<EditText>(R.id.dialog_select_color_preview)
|
||||
|
||||
val red = findViewById<SeekBar>(R.id.dialog_select_color_seekbar_red)
|
||||
val green = findViewById<SeekBar>(R.id.dialog_select_color_seekbar_green)
|
||||
val blue = findViewById<SeekBar>(R.id.dialog_select_color_seekbar_blue)
|
||||
val alpha = findViewById<SeekBar>(R.id.dialog_select_color_seekbar_alpha)
|
||||
|
||||
val updateColor = { updateText: Boolean ->
|
||||
preview?.setTextColor(currentColor.foregroundTextColor())
|
||||
preview?.setBackgroundColor(currentColor)
|
||||
if (updateText) {
|
||||
preview?.setText(currentColor.getHex(), TextView.BufferType.EDITABLE)
|
||||
}
|
||||
red?.progress = currentColor.red
|
||||
green?.progress = currentColor.green
|
||||
blue?.progress = currentColor.blue
|
||||
alpha?.progress = currentColor.alpha
|
||||
}
|
||||
updateColor(true)
|
||||
|
||||
preview?.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(text: CharSequence?, p1: Int, p2: Int, p3: Int) {}
|
||||
override fun onTextChanged(text: CharSequence?, p1: Int, p2: Int, p3: Int) {}
|
||||
override fun afterTextChanged(editable: Editable?) {
|
||||
preview.hasFocus() || return
|
||||
val newText = editable?.toString() ?: return
|
||||
newText.isBlank() && return
|
||||
try {
|
||||
val newColor = newText.toColorInt()
|
||||
currentColor = newColor
|
||||
updateColor(false)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
})
|
||||
red?.setOnSeekBarChangeListener(SeekBarChangeListener {
|
||||
currentColor = currentColor.updateRed(it)
|
||||
updateColor(true)
|
||||
})
|
||||
green?.setOnSeekBarChangeListener(SeekBarChangeListener {
|
||||
currentColor = currentColor.updateGreen(it)
|
||||
updateColor(true)
|
||||
})
|
||||
blue?.setOnSeekBarChangeListener(SeekBarChangeListener {
|
||||
currentColor = currentColor.updateBlue(it)
|
||||
updateColor(true)
|
||||
})
|
||||
alpha?.setOnSeekBarChangeListener(SeekBarChangeListener {
|
||||
currentColor = currentColor.updateAlpha(it)
|
||||
updateColor(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class SeekBarChangeListener(val update: (Int) -> Unit) :
|
||||
SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, v: Int, fromUser: Boolean) {
|
||||
fromUser || return
|
||||
update(v)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(p0: SeekBar?) {}
|
||||
override fun onStopTrackingTouch(p0: SeekBar?) {}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun @receiver:ColorInt Int.getHex(): String {
|
||||
return "#%08X".format(this)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun @receiver:ColorInt Int.updateRed(red: Int): Int {
|
||||
return Color.argb(this.alpha, red, this.green, this.blue)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun @receiver:ColorInt Int.updateGreen(green: Int): Int {
|
||||
return Color.argb(this.alpha, this.red, green, this.blue)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun @receiver:ColorInt Int.updateBlue(blue: Int): Int {
|
||||
return Color.argb(this.alpha, this.red, this.green, blue)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun @receiver:ColorInt Int.updateAlpha(alpha: Int): Int {
|
||||
return Color.argb(alpha, this.red, this.green, this.blue)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun @receiver:ColorInt Int.foregroundTextColor(): Int {
|
||||
// https://stackoverflow.com/a/3943023
|
||||
return if (
|
||||
this.red * 0.299 + this.green * 0.587 + this.blue * 0.114
|
||||
> this.alpha / 256f * 150
|
||||
) {
|
||||
Color.BLACK
|
||||
} else {
|
||||
Color.WHITE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package de.jrpie.android.launcher.preferences;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Set;
|
||||
|
||||
import de.jrpie.android.launcher.R;
|
||||
import de.jrpie.android.launcher.actions.lock.LockMethod;
|
||||
import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer;
|
||||
import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer;
|
||||
import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer;
|
||||
import de.jrpie.android.launcher.preferences.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;
|
||||
|
||||
@Preferences(
|
||||
name = "de.jrpie.android.launcher.preferences.LauncherPreferences",
|
||||
makeFile = true,
|
||||
r = R.class,
|
||||
value = {
|
||||
@PreferenceGroup(name = "internal", prefix = "settings_internal_", suffix = "_key", value = {
|
||||
// set after the user finished the tutorial
|
||||
@Preference(name = "started", type = boolean.class, defaultValue = "false"),
|
||||
@Preference(name = "started_time", type = long.class),
|
||||
// see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt
|
||||
@Preference(name = "version_code", type = int.class, defaultValue = "-1"),
|
||||
}),
|
||||
@PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = {
|
||||
@Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class),
|
||||
@Preference(name = "hidden", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class),
|
||||
@Preference(name = "pinned_shortcuts", type = Set.class, serializer = SetPinnedShortcutInfoPreferenceSerializer.class),
|
||||
@Preference(name = "custom_names", type = HashMap.class, serializer = MapAbstractAppInfoStringPreferenceSerializer.class),
|
||||
@Preference(name = "hide_bound_apps", type = boolean.class, defaultValue = "false"),
|
||||
@Preference(name = "hide_paused_apps", type = boolean.class, defaultValue = "false"),
|
||||
@Preference(name = "hide_private_space_apps", type = boolean.class, defaultValue = "false"),
|
||||
}),
|
||||
@PreferenceGroup(name = "list", prefix = "settings_list_", suffix = "_key", value = {
|
||||
@Preference(name = "layout", type = ListLayout.class, defaultValue = "DEFAULT"),
|
||||
@Preference(name = "reverse_layout", type = boolean.class, defaultValue = "false")
|
||||
}),
|
||||
@PreferenceGroup(name = "gestures", prefix = "settings_gesture_", suffix = "_key", value = {
|
||||
}),
|
||||
@PreferenceGroup(name = "general", prefix = "settings_general_", suffix = "_key", value = {
|
||||
@Preference(name = "choose_home_screen", type = void.class)
|
||||
}),
|
||||
@PreferenceGroup(name = "theme", prefix = "settings_theme_", suffix = "_key", value = {
|
||||
@Preference(name = "wallpaper", type = void.class),
|
||||
@Preference(name = "color_theme", type = ColorTheme.class, defaultValue = "DEFAULT"),
|
||||
@Preference(name = "background", type = Background.class, defaultValue = "DIM"),
|
||||
@Preference(name = "font", type = Font.class, defaultValue = "HACK"),
|
||||
@Preference(name = "text_shadow", type = boolean.class, defaultValue = "false"),
|
||||
@Preference(name = "monochrome_icons", type = boolean.class, defaultValue = "false"),
|
||||
}),
|
||||
@PreferenceGroup(name = "clock", prefix = "settings_clock_", suffix = "_key", value = {
|
||||
@Preference(name = "font", type = Font.class, defaultValue = "HACK"),
|
||||
@Preference(name = "color", type = int.class, defaultValue = "0xffffffff"),
|
||||
@Preference(name = "date_visible", type = boolean.class, defaultValue = "true"),
|
||||
@Preference(name = "time_visible", type = boolean.class, defaultValue = "true"),
|
||||
@Preference(name = "flip_date_time", type = boolean.class, defaultValue = "false"),
|
||||
@Preference(name = "localized", type = boolean.class, defaultValue = "false"),
|
||||
@Preference(name = "show_seconds", type = boolean.class, defaultValue = "true"),
|
||||
}),
|
||||
@PreferenceGroup(name = "display", prefix = "settings_display_", suffix = "_key", value = {
|
||||
@Preference(name = "screen_timeout_disabled", type = boolean.class, defaultValue = "false"),
|
||||
@Preference(name = "hide_status_bar", type = boolean.class, defaultValue = "true"),
|
||||
@Preference(name = "hide_navigation_bar", type = boolean.class, defaultValue = "false"),
|
||||
@Preference(name = "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 = {
|
||||
@Preference(name = "double_swipe", type = boolean.class, defaultValue = "true"),
|
||||
@Preference(name = "edge_swipe", type = boolean.class, defaultValue = "true"),
|
||||
@Preference(name = "edge_swipe_edge_width", type = int.class, defaultValue = "15"),
|
||||
}),
|
||||
@PreferenceGroup(name = "actions", prefix = "settings_actions_", suffix = "_key", value = {
|
||||
@Preference(name = "lock_method", type = LockMethod.class, defaultValue = "DEVICE_ADMIN"),
|
||||
}),
|
||||
})
|
||||
public final class LauncherPreferences$Config {}
|
|
@ -0,0 +1,39 @@
|
|||
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
|
||||
import de.jrpie.android.launcher.R
|
||||
|
||||
|
||||
// TODO: move this to de.jrpie.android.launcher.ui.list.apps ?
|
||||
@Suppress("unused")
|
||||
enum class ListLayout(
|
||||
val layoutManager: (context: Context) -> RecyclerView.LayoutManager,
|
||||
val layoutResource: Int,
|
||||
val useBadgedText: Boolean,
|
||||
) {
|
||||
DEFAULT(
|
||||
{ c -> LinearLayoutManager(c) },
|
||||
R.layout.list_apps_row,
|
||||
false
|
||||
),
|
||||
TEXT(
|
||||
{ c -> LinearLayoutManager(c) },
|
||||
R.layout.list_apps_row_variant_text,
|
||||
true
|
||||
),
|
||||
GRID(
|
||||
{ c ->
|
||||
val displayMetrics = c.resources.displayMetrics
|
||||
val widthColumnPx =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 90f, displayMetrics)
|
||||
val numColumns = (displayMetrics.widthPixels / widthColumnPx).toInt()
|
||||
GridLayoutManager(c, numColumns)
|
||||
},
|
||||
R.layout.list_apps_row_variant_grid,
|
||||
false
|
||||
),
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package de.jrpie.android.launcher.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import de.jrpie.android.launcher.BuildConfig
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
|
||||
import de.jrpie.android.launcher.apps.DetailedAppInfo
|
||||
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion1
|
||||
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion2
|
||||
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3
|
||||
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown
|
||||
import de.jrpie.android.launcher.ui.HomeActivity
|
||||
|
||||
/* Current version of the structure of preferences.
|
||||
* Increase when breaking changes are introduced and write an appropriate case in
|
||||
* `migratePreferencesToNewVersion`
|
||||
*/
|
||||
const val PREFERENCE_VERSION = 4
|
||||
const val UNKNOWN_PREFERENCE_VERSION = -1
|
||||
private const val TAG = "Launcher - Preferences"
|
||||
|
||||
|
||||
/*
|
||||
* Tries to detect preferences written by older versions of the app
|
||||
* and migrate them to the current format.
|
||||
*/
|
||||
fun migratePreferencesToNewVersion(context: Context) {
|
||||
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}).")
|
||||
}
|
||||
|
||||
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}).")
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Shared preferences were written by a newer version of the app (${
|
||||
LauncherPreferences.internal().versionCode()
|
||||
})!"
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to restore preferences:\n${e.stackTrace}")
|
||||
resetPreferences(context)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetPreferences(context: Context) {
|
||||
Log.i(TAG, "Resetting preferences")
|
||||
LauncherPreferences.clear()
|
||||
LauncherPreferences.internal().versionCode(PREFERENCE_VERSION)
|
||||
|
||||
|
||||
val hidden: MutableSet<AbstractAppInfo> = mutableSetOf()
|
||||
val launcher = DetailedAppInfo.fromAppInfo(
|
||||
AppInfo(
|
||||
BuildConfig.APPLICATION_ID,
|
||||
HomeActivity::class.java.name,
|
||||
INVALID_USER
|
||||
), context
|
||||
)
|
||||
launcher?.getRawInfo()?.let { hidden.add(it) }
|
||||
Log.i(TAG,"Hiding ${launcher?.getRawInfo()}")
|
||||
LauncherPreferences.apps().hidden(hidden)
|
||||
|
||||
Action.resetToDefaultActions(context)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package de.jrpie.android.launcher.preferences.legacy
|
||||
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.actions.LauncherAction
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
|
||||
|
||||
|
||||
/**
|
||||
* Migrate preferences from version 2 (used until version 0.0.21) to the current format
|
||||
* (see [PREFERENCE_VERSION])
|
||||
*/
|
||||
fun migratePreferencesFromVersion2() {
|
||||
assert(LauncherPreferences.internal().versionCode() == 2)
|
||||
// previously there was no setting for this
|
||||
Action.setActionForGesture(Gesture.BACK, LauncherAction.CHOOSE)
|
||||
LauncherPreferences.internal().versionCode(3)
|
||||
migratePreferencesFromVersion3()
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package de.jrpie.android.launcher.preferences.legacy
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.Editor
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
|
||||
import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer
|
||||
import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.HashSet
|
||||
import androidx.core.content.edit
|
||||
|
||||
/**
|
||||
* Migrate preferences from version 3 (used until version 0.0.23) to the current format
|
||||
* (see [PREFERENCE_VERSION])
|
||||
*/
|
||||
|
||||
|
||||
fun deserializeSet(value: Set<String>?): Set<AppInfo>? {
|
||||
return value?.map {
|
||||
Json.decodeFromString<AppInfo>(it)
|
||||
}?.toHashSet()
|
||||
}
|
||||
|
||||
fun deserializeMap(value: Set<String>?): HashMap<AppInfo, String>? {
|
||||
return value?.associateTo(HashMap()) {
|
||||
val entry = Json.decodeFromString<MapEntry>(it)
|
||||
Pair(entry.key, entry.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private class MapEntry(val key: AppInfo, val value: String)
|
||||
|
||||
private fun migrateSetAppInfo(key: String, preferences: SharedPreferences, editor: Editor) {
|
||||
try {
|
||||
val serializer = SetAbstractAppInfoPreferenceSerializer()
|
||||
val set = HashSet<AbstractAppInfo>()
|
||||
|
||||
deserializeSet(preferences.getStringSet(key, null))?.let {
|
||||
set.addAll(it)
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
editor.putStringSet(
|
||||
key,
|
||||
serializer.serialize(set as java.util.Set<AbstractAppInfo>) as Set<String>?
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
editor.putStringSet(key, null)
|
||||
}
|
||||
|
||||
}
|
||||
private fun migrateMapAppInfoString(key: String, preferences: SharedPreferences, editor: Editor ) {
|
||||
try {
|
||||
val serializer = MapAbstractAppInfoStringPreferenceSerializer()
|
||||
val map = HashMap<AbstractAppInfo, String>()
|
||||
|
||||
deserializeMap(preferences.getStringSet(key, null))?.let {
|
||||
map.putAll(it)
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
editor.putStringSet(key, serializer.serialize(map) as Set<String>?)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
editor.putStringSet(key, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun migratePreferencesFromVersion3() {
|
||||
assert(PREFERENCE_VERSION == 4)
|
||||
assert(LauncherPreferences.internal().versionCode() == 3)
|
||||
|
||||
val preferences = LauncherPreferences.getSharedPreferences()
|
||||
preferences.edit {
|
||||
migrateSetAppInfo(LauncherPreferences.apps().keys().favorites(), preferences, this)
|
||||
migrateSetAppInfo(LauncherPreferences.apps().keys().hidden(), preferences, this)
|
||||
migrateMapAppInfoString(LauncherPreferences.apps().keys().customNames(), preferences, this)
|
||||
}
|
||||
|
||||
LauncherPreferences.internal().versionCode(4)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package de.jrpie.android.launcher.preferences.theme
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
|
||||
@Suppress("unused")
|
||||
enum class Background(val id: Int, val dim: Boolean = false, val blur: Boolean = false) {
|
||||
TRANSPARENT(R.style.backgroundWallpaper),
|
||||
DIM(R.style.backgroundWallpaper, dim = true),
|
||||
BLUR(R.style.backgroundWallpaper, dim = true, blur = true),
|
||||
SOLID(R.style.backgroundSolid),
|
||||
;
|
||||
|
||||
fun applyToTheme(theme: Resources.Theme) {
|
||||
var background = this
|
||||
|
||||
// force a solid background when using the light theme
|
||||
if (LauncherPreferences.theme().colorTheme() == ColorTheme.LIGHT) {
|
||||
background = SOLID
|
||||
}
|
||||
theme.applyStyle(background.id, true)
|
||||
}
|
||||
|
||||
fun applyToWindow(window: Window) {
|
||||
val layoutParams: WindowManager.LayoutParams = window.attributes
|
||||
// TODO: add a setting to change this?
|
||||
var dimAmount = 0.7f
|
||||
val dim = this.dim
|
||||
var blur = this.blur
|
||||
|
||||
// replace blur by more intense dim on old devices
|
||||
if (blur && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
blur = false
|
||||
dimAmount += 0.1f
|
||||
}
|
||||
|
||||
if (LauncherPreferences.theme().colorTheme() == ColorTheme.LIGHT) {
|
||||
dimAmount = 0f
|
||||
}
|
||||
|
||||
if (dim) {
|
||||
layoutParams.dimAmount = dimAmount
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (blur) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
|
||||
layoutParams.blurBehindRadius = 10
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
|
||||
layoutParams.blurBehindRadius = 0
|
||||
}
|
||||
}
|
||||
|
||||
window.attributes = layoutParams
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package de.jrpie.android.launcher.preferences.theme
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import de.jrpie.android.launcher.R
|
||||
|
||||
enum class ColorTheme(
|
||||
private val id: Int,
|
||||
private val labelResource: Int,
|
||||
private val shadowId: Int,
|
||||
val isAvailable: () -> Boolean
|
||||
) {
|
||||
DEFAULT(
|
||||
R.style.colorThemeDefault,
|
||||
R.string.settings_theme_color_theme_item_default,
|
||||
R.style.textShadow,
|
||||
{ true }),
|
||||
DARK(
|
||||
R.style.colorThemeDark,
|
||||
R.string.settings_theme_color_theme_item_dark,
|
||||
R.style.textShadow,
|
||||
{ true }),
|
||||
LIGHT(
|
||||
R.style.colorThemeLight,
|
||||
R.string.settings_theme_color_theme_item_light,
|
||||
R.style.textShadowLight,
|
||||
{ true }),
|
||||
DYNAMIC(
|
||||
R.style.colorThemeDynamic,
|
||||
R.string.settings_theme_color_theme_item_dynamic,
|
||||
R.style.textShadow,
|
||||
{ DynamicColors.isDynamicColorAvailable() }),
|
||||
;
|
||||
|
||||
fun applyToTheme(theme: Resources.Theme, shadow: Boolean) {
|
||||
val colorTheme = if (this.isAvailable()) this else DEFAULT
|
||||
theme.applyStyle(colorTheme.id, true)
|
||||
|
||||
if (shadow) {
|
||||
theme.applyStyle(colorTheme.shadowId, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLabel(context: Context): String {
|
||||
return context.getString(labelResource)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package de.jrpie.android.launcher.preferences.theme
|
||||
|
||||
import android.content.res.Resources
|
||||
import de.jrpie.android.launcher.R
|
||||
|
||||
/**
|
||||
* Changes here must also be added to @array/settings_theme_font_values
|
||||
*/
|
||||
|
||||
@Suppress("unused")
|
||||
enum class Font(val id: Int) {
|
||||
HACK(R.style.fontHack),
|
||||
SYSTEM_DEFAULT(R.style.fontSystemDefault),
|
||||
SANS_SERIF(R.style.fontSansSerif),
|
||||
SERIF(R.style.fontSerifMonospace),
|
||||
MONOSPACE(R.style.fontMonospace),
|
||||
SERIF_MONOSPACE(R.style.fontSerifMonospace),
|
||||
;
|
||||
|
||||
fun applyToTheme(theme: Resources.Theme) {
|
||||
theme.applyStyle(id, true)
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
package de.jrpie.android.launcher.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import de.jrpie.android.launcher.*
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import kotlinx.android.synthetic.main.settings.*
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import de.jrpie.android.launcher.settings.actions.SettingsFragmentActions
|
||||
import de.jrpie.android.launcher.settings.launcher.SettingsFragmentLauncher
|
||||
import de.jrpie.android.launcher.settings.meta.SettingsFragmentMeta
|
||||
|
||||
|
||||
var intendedSettingsPause = false // know when to close
|
||||
|
||||
/**
|
||||
* The [SettingsActivity] is a tabbed activity:
|
||||
*
|
||||
* | Actions | Choose apps or intents to be launched | [SettingsFragmentActions] |
|
||||
* | Theme | Select a theme / Customize | [SettingsFragmentLauncher] |
|
||||
* | Meta | About Launcher / Contact etc. | [SettingsFragmentMeta] |
|
||||
*
|
||||
* Settings are closed automatically if the activity goes `onPause` unexpectedly.
|
||||
*/
|
||||
class SettingsActivity: AppCompatActivity(), UIObject {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Initialise layout
|
||||
setContentView(R.layout.settings)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<AppCompatActivity>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
intendedSettingsPause = false
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (!intendedSettingsPause) finish()
|
||||
}
|
||||
|
||||
override fun applyTheme() {
|
||||
//settings_system.setTextColor(vibrantColor)
|
||||
//settings_close.setTextColor(vibrantColor)
|
||||
settings_tabs.setSelectedTabIndicatorColor(vibrantColor)
|
||||
}
|
||||
|
||||
override fun setOnClicks(){
|
||||
// As older APIs somehow do not recognize the xml defined onClick
|
||||
settings_close.setOnClickListener { finish() }
|
||||
// open device settings (see https://stackoverflow.com/a/62092663/12787264)
|
||||
settings_system.setOnClickListener {
|
||||
intendedSettingsPause = true
|
||||
startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_CHOOSE_APP -> saveListActivityChoice(this, data)
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val TAB_TITLES = arrayOf(
|
||||
R.string.settings_tab_app,
|
||||
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) {
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
return when (position){
|
||||
0 -> SettingsFragmentActions()
|
||||
1 -> SettingsFragmentLauncher()
|
||||
2 -> SettingsFragmentMeta()
|
||||
else -> Fragment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return context.resources.getString(TAB_TITLES[position])
|
||||
}
|
||||
|
||||
override fun getCount(): Int { return 3 }
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package de.jrpie.android.launcher.settings.actions
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
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.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.*
|
||||
import de.jrpie.android.launcher.list.ListActivity
|
||||
import de.jrpie.android.launcher.settings.intendedSettingsPause
|
||||
import kotlinx.android.synthetic.main.settings_actions.*
|
||||
|
||||
|
||||
/**
|
||||
* The [SettingsFragmentActions] is a used as a tab in the SettingsActivity.
|
||||
*
|
||||
* It is used to change Apps / Intents to be launched when a specific action
|
||||
* is triggered.
|
||||
* It also allows the user to view all apps ([ListActivity]) or install new ones.
|
||||
*/
|
||||
|
||||
class SettingsFragmentActions : Fragment(), UIObject {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.settings_actions, container, false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun applyTheme() {
|
||||
setButtonColor(settings_actions_button_view_apps, vibrantColor)
|
||||
setButtonColor(settings_actions_button_install_apps, vibrantColor)
|
||||
}
|
||||
|
||||
override fun setOnClicks() {
|
||||
|
||||
// App management buttons
|
||||
settings_actions_button_view_apps.setOnClickListener{
|
||||
val intent = Intent(this.context, ListActivity::class.java)
|
||||
intent.putExtra("intention", ListActivity.ListActivityIntention.VIEW.toString())
|
||||
intendedSettingsPause = true
|
||||
startActivity(intent)
|
||||
}
|
||||
settings_actions_button_install_apps.setOnClickListener{
|
||||
try {
|
||||
val rateIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("https://play.google.com/store/apps/"))
|
||||
|
||||
intendedSettingsPause = true
|
||||
startActivity(rateIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(this.context, getString(R.string.settings_apps_toast_store_not_found), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
package de.jrpie.android.launcher.settings.actions
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import de.jrpie.android.launcher.*
|
||||
import de.jrpie.android.launcher.list.ListActivity
|
||||
import kotlinx.android.synthetic.main.settings_actions_recycler.*
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import de.jrpie.android.launcher.list.other.LauncherAction
|
||||
import de.jrpie.android.launcher.settings.intendedSettingsPause
|
||||
import java.lang.Exception
|
||||
|
||||
/**
|
||||
* The [SettingsFragmentActionsRecycler] is a fragment containing the [ActionsRecyclerAdapter],
|
||||
* which displays all selected actions / apps.
|
||||
*
|
||||
* It is used in the Tutorial and in Settings
|
||||
*/
|
||||
class SettingsFragmentActionsRecycler : Fragment(), UIObject {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.settings_actions_recycler, container, false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
|
||||
// set up the list / recycler
|
||||
val actionViewManager = LinearLayoutManager(context)
|
||||
val actionViewAdapter = ActionsRecyclerAdapter( activity!! )
|
||||
|
||||
settings_actions_rview.apply {
|
||||
// improve performance (since content changes don't change the layout size)
|
||||
setHasFixedSize(true)
|
||||
layoutManager = actionViewManager
|
||||
adapter = actionViewAdapter
|
||||
}
|
||||
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
}
|
||||
|
||||
class ActionsRecyclerAdapter(val activity: Activity):
|
||||
RecyclerView.Adapter<ActionsRecyclerAdapter.ViewHolder>() {
|
||||
|
||||
private val gesturesList: List<Gesture>
|
||||
|
||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
|
||||
View.OnClickListener {
|
||||
var textView: TextView = itemView.findViewById(R.id.settings_actions_row_name)
|
||||
var img: ImageView = itemView.findViewById(R.id.settings_actions_row_icon_img)
|
||||
var chooseButton: Button = itemView.findViewById(R.id.settings_actions_row_button_choose)
|
||||
var removeAction: ImageView = itemView.findViewById(R.id.settings_actions_row_remove)
|
||||
|
||||
override fun onClick(v: View) { }
|
||||
|
||||
init { itemView.setOnClickListener(this) }
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
|
||||
val gesture = gesturesList[i]
|
||||
viewHolder.textView.text = gesture.getLabel(activity)
|
||||
setButtonColor(viewHolder.chooseButton, vibrantColor)
|
||||
if (getSavedTheme(activity) == "dark") transformGrayscale(
|
||||
viewHolder.img
|
||||
)
|
||||
fun updateViewHolder() {
|
||||
val content = gesture.getApp(activity)
|
||||
if (content == ""){
|
||||
viewHolder.img.visibility = View.INVISIBLE
|
||||
viewHolder.removeAction.visibility = View.GONE
|
||||
viewHolder.chooseButton.visibility = View.VISIBLE
|
||||
}
|
||||
else if (LauncherAction.isOtherAction(content)) {
|
||||
LauncherAction.byId(content)?.let {
|
||||
viewHolder.img.setImageResource(it.icon)
|
||||
}
|
||||
} else {
|
||||
// Set image icon (by packageName)
|
||||
try {
|
||||
viewHolder.img.setImageDrawable(activity.packageManager.getApplicationIcon(content))
|
||||
} catch (e : Exception) {
|
||||
// the button is shown, user asked to select an action
|
||||
viewHolder.img.visibility = View.INVISIBLE
|
||||
viewHolder.removeAction.visibility = View.GONE
|
||||
viewHolder.chooseButton.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
updateViewHolder()
|
||||
viewHolder.img.setOnClickListener{ chooseApp(gesture) }
|
||||
viewHolder.chooseButton.setOnClickListener{ chooseApp(gesture) }
|
||||
viewHolder.removeAction.setOnClickListener{
|
||||
gesture.removeApp(activity)
|
||||
updateViewHolder()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int { return gesturesList.size }
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view: View = inflater.inflate(R.layout.settings_actions_row, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
init {
|
||||
val doubleActions = getPreferences(activity).getBoolean(PREF_DOUBLE_ACTIONS_ENABLED, false)
|
||||
val edgeActions = getPreferences(activity).getBoolean(PREF_EDGE_ACTIONS_ENABLED, false)
|
||||
gesturesList = Gesture.values().filter {
|
||||
(doubleActions || !it.isDoubleVariant())
|
||||
&& (edgeActions || !it.isEdgeVariant())}
|
||||
}
|
||||
|
||||
/* */
|
||||
private fun chooseApp(gesture: Gesture) {
|
||||
val intent = Intent(activity, ListActivity::class.java)
|
||||
intent.putExtra("intention", ListActivity.ListActivityIntention.PICK.toString())
|
||||
intent.putExtra("forGesture", gesture.id) // for which action we choose the app
|
||||
intendedSettingsPause = true
|
||||
activity.startActivityForResult(intent,
|
||||
REQUEST_CHOOSE_APP
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
package de.jrpie.android.launcher.settings.launcher
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.PorterDuff
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Switch
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.PREF_DATE_FORMAT
|
||||
import de.jrpie.android.launcher.PREF_DOUBLE_ACTIONS_ENABLED
|
||||
import de.jrpie.android.launcher.PREF_EDGE_ACTIONS_ENABLED
|
||||
import de.jrpie.android.launcher.PREF_SCREEN_FULLSCREEN
|
||||
import de.jrpie.android.launcher.PREF_SCREEN_TIMEOUT_DISABLED
|
||||
import de.jrpie.android.launcher.PREF_SEARCH_AUTO_KEYBOARD
|
||||
import de.jrpie.android.launcher.PREF_SEARCH_AUTO_LAUNCH
|
||||
import de.jrpie.android.launcher.PREF_SLIDE_SENSITIVITY
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.UIObject
|
||||
import de.jrpie.android.launcher.getPreferences
|
||||
import de.jrpie.android.launcher.getSavedTheme
|
||||
import de.jrpie.android.launcher.resetToDarkTheme
|
||||
import de.jrpie.android.launcher.resetToDefaultTheme
|
||||
import de.jrpie.android.launcher.setButtonColor
|
||||
import de.jrpie.android.launcher.setSwitchColor
|
||||
import de.jrpie.android.launcher.setWindowFlags
|
||||
import de.jrpie.android.launcher.settings.intendedSettingsPause
|
||||
import de.jrpie.android.launcher.vibrantColor
|
||||
import kotlinx.android.synthetic.main.settings_launcher.*
|
||||
|
||||
|
||||
/**
|
||||
* The [SettingsFragmentLauncher] is a used as a tab in the SettingsActivity.
|
||||
*
|
||||
* It is used to change themes, select wallpapers ... theme related stuff
|
||||
*/
|
||||
class SettingsFragmentLauncher : Fragment(), UIObject {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.settings_launcher, container, false)
|
||||
}
|
||||
|
||||
override fun onStart(){
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
|
||||
override fun applyTheme() {
|
||||
|
||||
setSwitchColor(settings_launcher_switch_screen_timeout, vibrantColor)
|
||||
setSwitchColor(settings_launcher_switch_screen_full, vibrantColor)
|
||||
setSwitchColor(settings_launcher_switch_auto_launch, vibrantColor)
|
||||
setSwitchColor(settings_launcher_switch_auto_keyboard, vibrantColor)
|
||||
setSwitchColor(settings_launcher_switch_enable_double, vibrantColor)
|
||||
setSwitchColor(settings_launcher_switch_enable_edge, vibrantColor)
|
||||
|
||||
|
||||
setButtonColor(settings_launcher_button_choose_wallpaper, vibrantColor)
|
||||
settings_seekbar_sensitivity.progressDrawable.setColorFilter(vibrantColor, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
|
||||
override fun setOnClicks() {
|
||||
|
||||
val preferences = getPreferences(activity!!)
|
||||
|
||||
fun bindSwitchToPref(switch: Switch, pref: String, default: Boolean, onChange: (Boolean) -> Unit){
|
||||
switch.isChecked = preferences.getBoolean(pref, default)
|
||||
switch.setOnCheckedChangeListener { _, isChecked -> // Toggle double actions
|
||||
preferences.edit()
|
||||
.putBoolean(pref, isChecked)
|
||||
.apply()
|
||||
onChange(isChecked);
|
||||
}
|
||||
}
|
||||
|
||||
settings_launcher_button_choose_wallpaper.setOnClickListener {
|
||||
// https://github.com/LineageOS/android_packages_apps_Trebuchet/blob/6caab89b21b2b91f0a439e1fd8c4510dcb255819/src/com/android/launcher3/views/OptionsPopupView.java#L271
|
||||
val intent = Intent(Intent.ACTION_SET_WALLPAPER)
|
||||
//.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
.putExtra("com.android.wallpaper.LAUNCH_SOURCE", "app_launched_launcher")
|
||||
.putExtra("com.android.launcher3.WALLPAPER_FLAVOR", "focus_wallpaper")
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
||||
|
||||
bindSwitchToPref(settings_launcher_switch_screen_timeout, PREF_SCREEN_TIMEOUT_DISABLED, false) {
|
||||
activity?.let{setWindowFlags(it.window)}
|
||||
}
|
||||
bindSwitchToPref(settings_launcher_switch_screen_full, PREF_SCREEN_FULLSCREEN, true) {
|
||||
activity?.let{setWindowFlags(it.window)}
|
||||
}
|
||||
bindSwitchToPref(settings_launcher_switch_auto_launch, PREF_SEARCH_AUTO_LAUNCH, false) {}
|
||||
bindSwitchToPref(settings_launcher_switch_auto_keyboard, PREF_SEARCH_AUTO_KEYBOARD, true) {}
|
||||
bindSwitchToPref(settings_launcher_switch_enable_double, PREF_DOUBLE_ACTIONS_ENABLED, false) {
|
||||
intendedSettingsPause = true
|
||||
activity?.recreate()
|
||||
}
|
||||
bindSwitchToPref(settings_launcher_switch_enable_edge, PREF_EDGE_ACTIONS_ENABLED, false) {
|
||||
intendedSettingsPause = true
|
||||
activity?.recreate()
|
||||
}
|
||||
|
||||
settings_seekbar_sensitivity.setOnSeekBarChangeListener(
|
||||
object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) {}
|
||||
override fun onStartTrackingTouch(p0: SeekBar?) {}
|
||||
override fun onStopTrackingTouch(p0: SeekBar?) {
|
||||
preferences.edit()
|
||||
.putInt(PREF_SLIDE_SENSITIVITY, p0!!.progress * 100 / 4) // scale to %
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun adjustLayout() {
|
||||
|
||||
val preferences = getPreferences(activity!!)
|
||||
// Load values into the date-format spinner
|
||||
val staticAdapter = ArrayAdapter.createFromResource(
|
||||
activity!!, R.array.settings_launcher_time_format_spinner_items,
|
||||
android.R.layout.simple_spinner_item )
|
||||
|
||||
staticAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
settings_launcher_format_spinner.adapter = staticAdapter
|
||||
|
||||
settings_launcher_format_spinner.setSelection(preferences.getInt(PREF_DATE_FORMAT, 0))
|
||||
|
||||
settings_launcher_format_spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
|
||||
preferences.edit()
|
||||
.putInt(PREF_DATE_FORMAT, position)
|
||||
.apply()
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) { }
|
||||
}
|
||||
|
||||
// Load values into the theme spinner
|
||||
val staticThemeAdapter = ArrayAdapter.createFromResource(
|
||||
activity!!, R.array.settings_launcher_theme_spinner_items,
|
||||
android.R.layout.simple_spinner_item )
|
||||
|
||||
staticThemeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
settings_launcher_theme_spinner.adapter = staticThemeAdapter
|
||||
|
||||
val themeInt = when (getSavedTheme(activity!!)) {
|
||||
"finn" -> 0
|
||||
"dark" -> 1
|
||||
else -> 0
|
||||
}
|
||||
|
||||
settings_launcher_theme_spinner.setSelection(themeInt)
|
||||
|
||||
settings_launcher_theme_spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
|
||||
when (position) {
|
||||
0 -> if (getSavedTheme(activity!!) != "finn") resetToDefaultTheme(activity!!)
|
||||
1 -> if (getSavedTheme(activity!!) != "dark") resetToDarkTheme(activity!!)
|
||||
}
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) { }
|
||||
}
|
||||
|
||||
settings_seekbar_sensitivity.progress = preferences.getInt(PREF_SLIDE_SENSITIVITY, 2) * 4 / 100
|
||||
}
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
package de.jrpie.android.launcher.settings.meta
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
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.UIObject
|
||||
import de.jrpie.android.launcher.openNewTabWindow
|
||||
import de.jrpie.android.launcher.resetSettings
|
||||
import de.jrpie.android.launcher.setButtonColor
|
||||
import de.jrpie.android.launcher.settings.intendedSettingsPause
|
||||
import de.jrpie.android.launcher.tutorial.TutorialActivity
|
||||
import de.jrpie.android.launcher.vibrantColor
|
||||
import kotlinx.android.synthetic.main.settings_meta.*
|
||||
|
||||
/**
|
||||
* The [SettingsFragmentMeta] is a used as a tab in the SettingsActivity.
|
||||
*
|
||||
* It is used to change settings and access resources about Launcher,
|
||||
* that are not directly related to the behaviour of the app itself.
|
||||
*
|
||||
* (greek `meta` = above, next level)
|
||||
*/
|
||||
class SettingsFragmentMeta : Fragment(), UIObject {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.settings_meta, container, false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
// Rate App
|
||||
// Just copied code from https://stackoverflow.com/q/10816757/12787264
|
||||
// that is how we write good software ^^
|
||||
|
||||
private fun rateIntentForUrl(url: String): Intent {
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(String.format("%s?id=%s", url, this.context!!.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 applyTheme() {
|
||||
setButtonColor(settings_meta_button_select_launcher, vibrantColor)
|
||||
setButtonColor(settings_meta_button_view_tutorial, vibrantColor)
|
||||
setButtonColor(settings_meta_button_reset_settings, vibrantColor)
|
||||
setButtonColor(settings_meta_button_report_bug, vibrantColor)
|
||||
setButtonColor(settings_meta_button_contact, vibrantColor)
|
||||
setButtonColor(settings_meta_button_fork_contact, vibrantColor)
|
||||
setButtonColor(settings_meta_button_privacy, vibrantColor)
|
||||
}
|
||||
|
||||
override fun setOnClicks() {
|
||||
|
||||
settings_meta_button_select_launcher.setOnClickListener {
|
||||
intendedSettingsPause = true
|
||||
val callHomeSettingIntent = Intent(Settings.ACTION_HOME_SETTINGS)
|
||||
startActivity(callHomeSettingIntent)
|
||||
}
|
||||
|
||||
settings_meta_button_view_tutorial.setOnClickListener {
|
||||
intendedSettingsPause = true
|
||||
startActivity(Intent(this.context, TutorialActivity::class.java))
|
||||
}
|
||||
|
||||
// prompting for settings-reset confirmation
|
||||
settings_meta_button_reset_settings.setOnClickListener {
|
||||
AlertDialog.Builder(this.context!!, R.style.AlertDialogCustom)
|
||||
.setTitle(getString(R.string.settings_meta_reset))
|
||||
.setMessage(getString(R.string.settings_meta_reset_confirm))
|
||||
.setPositiveButton(android.R.string.ok
|
||||
) { _, _ ->
|
||||
resetSettings(this.context!!)
|
||||
activity!!.finish()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.show()
|
||||
}
|
||||
|
||||
|
||||
// report a bug
|
||||
settings_meta_button_report_bug.setOnClickListener {
|
||||
intendedSettingsPause = true
|
||||
openNewTabWindow(
|
||||
getString(R.string.settings_meta_report_bug_link),
|
||||
context!!
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// contact developer
|
||||
settings_meta_button_contact.setOnClickListener {
|
||||
intendedSettingsPause = true
|
||||
openNewTabWindow(
|
||||
getString(R.string.settings_meta_contact_url),
|
||||
context!!
|
||||
)
|
||||
}
|
||||
|
||||
// contact fork developer
|
||||
settings_meta_button_fork_contact.setOnClickListener {
|
||||
intendedSettingsPause = true
|
||||
openNewTabWindow(
|
||||
getString(R.string.settings_meta_fork_contact_url),
|
||||
context!!
|
||||
)
|
||||
}
|
||||
|
||||
// privacy policy
|
||||
settings_meta_button_privacy.setOnClickListener {
|
||||
intendedSettingsPause = true
|
||||
openNewTabWindow(
|
||||
getString(R.string.settings_meta_privacy_url),
|
||||
context!!
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
package de.jrpie.android.launcher.tutorial
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
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.PREF_STARTED
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
|
||||
import de.jrpie.android.launcher.UIObject
|
||||
import de.jrpie.android.launcher.getPreferences
|
||||
import de.jrpie.android.launcher.loadSettings
|
||||
import de.jrpie.android.launcher.resetSettings
|
||||
import de.jrpie.android.launcher.saveListActivityChoice
|
||||
import de.jrpie.android.launcher.tutorial.tabs.TutorialFragmentConcept
|
||||
import de.jrpie.android.launcher.tutorial.tabs.TutorialFragmentFinish
|
||||
import de.jrpie.android.launcher.tutorial.tabs.TutorialFragmentSetup
|
||||
import de.jrpie.android.launcher.tutorial.tabs.TutorialFragmentStart
|
||||
import de.jrpie.android.launcher.tutorial.tabs.TutorialFragmentUsage
|
||||
|
||||
/**
|
||||
* The [TutorialActivity] is displayed automatically on new installations.
|
||||
* It can also be opened from Settings.
|
||||
*
|
||||
* It tells the user about the concept behind launcher
|
||||
* and helps with the setup process (on new installations)
|
||||
*/
|
||||
class TutorialActivity: AppCompatActivity(), UIObject {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Initialise layout
|
||||
setContentView(R.layout.tutorial)
|
||||
|
||||
val preferences = getPreferences(this)
|
||||
// Check if the app was started before
|
||||
if (!preferences.getBoolean(PREF_STARTED, false))
|
||||
resetSettings(this)
|
||||
|
||||
loadSettings(this)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<AppCompatActivity>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_CHOOSE_APP -> saveListActivityChoice(this,data)
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
// Default: prevent going back, allow if viewed again later
|
||||
override fun onBackPressed() {
|
||||
if (getPreferences(this).getBoolean(PREF_STARTED, false))
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The [TutorialSectionsPagerAdapter] defines which fragments are shown when,
|
||||
* in the [TutorialActivity].
|
||||
*
|
||||
* Tabs: (Start | Concept | Usage | Setup | Finish)
|
||||
*/
|
||||
class TutorialSectionsPagerAdapter(fm: FragmentManager)
|
||||
: FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
return when (position){
|
||||
0 -> TutorialFragmentStart()
|
||||
1 -> TutorialFragmentConcept()
|
||||
2 -> TutorialFragmentUsage()
|
||||
3 -> TutorialFragmentSetup()
|
||||
4 -> TutorialFragmentFinish()
|
||||
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 }
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package de.jrpie.android.launcher.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.BuildConfig
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.UIObject
|
||||
import kotlinx.android.synthetic.main.tutorial_concept.*
|
||||
import kotlinx.android.synthetic.main.tutorial_concept.view.*
|
||||
|
||||
/**
|
||||
* The [TutorialFragmentConcept] 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 {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val layout = inflater.inflate(R.layout.tutorial_concept, container, false)
|
||||
layout.tutorial_concept_badge_version.text = BuildConfig.VERSION_NAME
|
||||
return layout
|
||||
}
|
||||
|
||||
override fun onStart(){
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
package de.jrpie.android.launcher.tutorial.tabs
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import de.jrpie.android.launcher.*
|
||||
import de.jrpie.android.launcher.BuildConfig.VERSION_NAME
|
||||
import kotlinx.android.synthetic.main.tutorial_finish.*
|
||||
|
||||
/**
|
||||
* The [TutorialFragmentFinish] 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 {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.tutorial_finish, container, false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun applyTheme() {
|
||||
setButtonColor(tutorial_finish_button_start, vibrantColor)
|
||||
tutorial_finish_button_start.blink()
|
||||
}
|
||||
|
||||
override fun setOnClicks() {
|
||||
super.setOnClicks()
|
||||
tutorial_finish_button_start.setOnClickListener{ finishTutorial() }
|
||||
}
|
||||
|
||||
private fun finishTutorial() {
|
||||
context?.let { getPreferences(it) }?.let {
|
||||
if (!it.getBoolean(PREF_STARTED, false)) {
|
||||
it.edit()
|
||||
.putBoolean(PREF_STARTED, true) // never auto run this again
|
||||
.putLong(
|
||||
PREF_STARTED_TIME,
|
||||
System.currentTimeMillis() / 1000L
|
||||
) // record first startup timestamp
|
||||
.putString(PREF_VERSION, VERSION_NAME) // save current launcher version
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package de.jrpie.android.launcher.tutorial.tabs
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import de.jrpie.android.launcher.*
|
||||
import kotlinx.android.synthetic.main.tutorial_start.*
|
||||
|
||||
/**
|
||||
* The [TutorialFragmentStart] 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 {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.tutorial_start, container, false)
|
||||
}
|
||||
|
||||
override fun onStart(){
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun applyTheme() {
|
||||
|
||||
tutorial_start_icon_right.setTextColor(vibrantColor)
|
||||
tutorial_start_icon_right.blink()
|
||||
|
||||
}
|
||||
}
|
47
app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt
Normal file
47
app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt
Normal 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)
|
||||
}
|
267
app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt
Normal file
267
app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt
Normal file
|
@ -0,0 +1,267 @@
|
|||
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.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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.openTutorial
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* [HomeActivity] is the actual application Launcher,
|
||||
* what makes this application special / unique.
|
||||
*
|
||||
* In this activity we display the date and time,
|
||||
* and we listen for actions like tapping, swiping or button presses.
|
||||
*
|
||||
* As it also is the first thing that is started when someone opens Launcher,
|
||||
* it also contains some logic related to the overall application:
|
||||
* - Setting global variables (preferences etc.)
|
||||
* - Opening the [TutorialActivity] on new installations
|
||||
*/
|
||||
class HomeActivity : UIObject, AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: HomeBinding
|
||||
private var touchGestureDetector: TouchGestureDetector? = null
|
||||
|
||||
private var sharedPreferencesListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
|
||||
if (prefKey?.startsWith("clock.") == true ||
|
||||
prefKey?.startsWith("display.") == true
|
||||
) {
|
||||
recreate()
|
||||
}
|
||||
|
||||
if (prefKey?.startsWith("action.") == true) {
|
||||
updateSettingsFallbackButtonVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super<AppCompatActivity>.onCreate(savedInstanceState)
|
||||
super<UIObject>.onCreate()
|
||||
|
||||
|
||||
// Initialise layout
|
||||
binding = HomeBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
// Handle back key / gesture on Android 13+, cf. onKeyDown()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_OVERLAY
|
||||
) {
|
||||
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()
|
||||
|
||||
super<UIObject>.onStart()
|
||||
|
||||
// If the tutorial was not finished, start it
|
||||
if (!LauncherPreferences.internal().started()) {
|
||||
openTutorial(this)
|
||||
}
|
||||
|
||||
LauncherPreferences.getSharedPreferences()
|
||||
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
|
||||
if (hasFocus && LauncherPreferences.display().hideNavigationBar()) {
|
||||
hideNavigationBar()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updateSettingsFallbackButtonVisibility() {
|
||||
// If µLauncher settings can not be reached from any action bound to an enabled gesture,
|
||||
// show the fallback button.
|
||||
binding.buttonFallbackSettings.visibility = if (
|
||||
!Gesture.entries.any { g ->
|
||||
g.isEnabled() && Action.forGesture(g)?.canReachSettings() == true
|
||||
}
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun initClock() {
|
||||
val locale = Locale.getDefault()
|
||||
val dateVisible = LauncherPreferences.clock().dateVisible()
|
||||
val timeVisible = LauncherPreferences.clock().timeVisible()
|
||||
|
||||
var dateFMT = "yyyy-MM-dd"
|
||||
var timeFMT = "HH:mm"
|
||||
if (LauncherPreferences.clock().showSeconds()) {
|
||||
timeFMT += ":ss"
|
||||
}
|
||||
|
||||
if (LauncherPreferences.clock().localized()) {
|
||||
dateFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, dateFMT)
|
||||
timeFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, timeFMT)
|
||||
}
|
||||
|
||||
var upperFormat = dateFMT
|
||||
var lowerFormat = timeFMT
|
||||
var upperVisible = dateVisible
|
||||
var lowerVisible = timeVisible
|
||||
|
||||
if (LauncherPreferences.clock().flipDateTime()) {
|
||||
upperFormat = lowerFormat.also { lowerFormat = upperFormat }
|
||||
upperVisible = lowerVisible.also { lowerVisible = upperVisible }
|
||||
}
|
||||
|
||||
binding.homeUpperView.isVisible = upperVisible
|
||||
binding.homeLowerView.isVisible = lowerVisible
|
||||
|
||||
binding.homeUpperView.setTextColor(LauncherPreferences.clock().color())
|
||||
binding.homeLowerView.setTextColor(LauncherPreferences.clock().color())
|
||||
|
||||
binding.homeLowerView.format24Hour = lowerFormat
|
||||
binding.homeUpperView.format24Hour = upperFormat
|
||||
binding.homeLowerView.format12Hour = lowerFormat
|
||||
binding.homeUpperView.format12Hour = upperFormat
|
||||
}
|
||||
|
||||
override fun getTheme(): Resources.Theme {
|
||||
val mTheme = modifyTheme(super.getTheme())
|
||||
mTheme.applyStyle(R.style.backgroundWallpaper, true)
|
||||
LauncherPreferences.clock().font().applyToTheme(mTheme)
|
||||
LauncherPreferences.theme().colorTheme().applyToTheme(
|
||||
mTheme,
|
||||
LauncherPreferences.theme().textShadow()
|
||||
)
|
||||
|
||||
return mTheme
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
/* This should be initialized in onCreate()
|
||||
However on some devices there seems to be a bug where the touchGestureDetector
|
||||
is not working properly after resuming the app.
|
||||
Reinitializing the touchGestureDetector every time the app is resumed might help to fix that.
|
||||
(see issue #138)
|
||||
*/
|
||||
touchGestureDetector = TouchGestureDetector(
|
||||
this, 0, 0,
|
||||
LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
|
||||
).also {
|
||||
it.updateScreenSize(windowManager)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
initClock()
|
||||
updateSettingsFallbackButtonVisibility()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
LauncherPreferences.getSharedPreferences()
|
||||
.unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@SuppressLint("GestureBackNavigation")
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_BACK -> {
|
||||
// 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
|
||||
// and apps like Samsung Sound Assistant.
|
||||
return false
|
||||
}
|
||||
Gesture.VOLUME_UP(this)
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
if (Action.forGesture(Gesture.VOLUME_DOWN) == LauncherAction.VOLUME_DOWN) {
|
||||
// see above
|
||||
return false
|
||||
}
|
||||
Gesture.VOLUME_DOWN(this)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
touchGestureDetector?.onTouchEvent(event)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun setOnClicks() {
|
||||
|
||||
binding.homeUpperView.setOnClickListener {
|
||||
if (LauncherPreferences.clock().flipDateTime()) {
|
||||
Gesture.TIME(this)
|
||||
} else {
|
||||
Gesture.DATE(this)
|
||||
}
|
||||
}
|
||||
|
||||
binding.homeLowerView.setOnClickListener {
|
||||
if (LauncherPreferences.clock().flipDateTime()) {
|
||||
Gesture.DATE(this)
|
||||
} else {
|
||||
Gesture.TIME(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun handleBack() {
|
||||
Gesture.BACK(this)
|
||||
}
|
||||
|
||||
override fun isHomeScreen(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package de.jrpie.android.launcher.ui
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherApps
|
||||
import android.content.pm.LauncherApps.PinItemRequest
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.actions.ShortcutAction
|
||||
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
|
||||
import de.jrpie.android.launcher.databinding.ActivityPinShortcutBinding
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import androidx.core.content.edit
|
||||
|
||||
class PinShortcutActivity : AppCompatActivity(), UIObject {
|
||||
private lateinit var binding: ActivityPinShortcutBinding
|
||||
|
||||
private var isBound = false
|
||||
private var request: PinItemRequest? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super<AppCompatActivity>.onCreate(savedInstanceState)
|
||||
super<UIObject>.onCreate()
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
binding = ActivityPinShortcutBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val launcherApps = getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
|
||||
val request = launcherApps.getPinItemRequest(intent)
|
||||
this.request = request
|
||||
if (request == null || request.requestType != PinItemRequest.REQUEST_TYPE_SHORTCUT) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
binding.pinShortcutLabel.text = request.shortcutInfo!!.shortLabel ?: "?"
|
||||
binding.pinShortcutLabel.setCompoundDrawables(
|
||||
launcherApps.getShortcutBadgedIconDrawable(request.shortcutInfo, 0).also {
|
||||
val size = (40 * resources.displayMetrics.density).toInt()
|
||||
it.setBounds(0,0, size, size)
|
||||
}, null, null, null)
|
||||
|
||||
binding.pinShortcutButtonBind.setOnClickListener {
|
||||
AlertDialog.Builder(this, R.style.AlertDialogCustom)
|
||||
.setTitle(getString(R.string.pin_shortcut_button_bind))
|
||||
.setView(R.layout.dialog_select_gesture)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create().also { it.show() }.let { dialog ->
|
||||
val viewManager = LinearLayoutManager(dialog.context)
|
||||
val viewAdapter = GestureRecyclerAdapter (dialog.context) { gesture ->
|
||||
if (!isBound) {
|
||||
isBound = true
|
||||
request.accept()
|
||||
}
|
||||
LauncherPreferences.getSharedPreferences().edit {
|
||||
ShortcutAction(PinnedShortcutInfo(request.shortcutInfo!!)).bindToGesture(
|
||||
this,
|
||||
gesture.id
|
||||
)
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
dialog.findViewById<RecyclerView>(R.id.dialog_select_gesture_recycler).apply {
|
||||
setHasFixedSize(true)
|
||||
layoutManager = viewManager
|
||||
adapter = viewAdapter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.pinShortcutClose.setOnClickListener { finish() }
|
||||
binding.pinShortcutButtonOk.setOnClickListener { finish() }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<AppCompatActivity>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
super.onDestroy()
|
||||
return
|
||||
}
|
||||
if(binding.pinShortcutSwitchVisible.isChecked) {
|
||||
if(!isBound) {
|
||||
request?.accept()
|
||||
}
|
||||
request?.shortcutInfo?.let {
|
||||
val set = LauncherPreferences.apps().pinnedShortcuts() ?: mutableSetOf()
|
||||
set.add(PinnedShortcutInfo(it))
|
||||
LauncherPreferences.apps().pinnedShortcuts(set)
|
||||
}
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun getTheme(): Resources.Theme {
|
||||
return modifyTheme(super.getTheme())
|
||||
}
|
||||
|
||||
inner class GestureRecyclerAdapter(val context: Context, val onClick: (Gesture) -> Unit): RecyclerView.Adapter<GestureRecyclerAdapter.ViewHolder>() {
|
||||
private val gestures = Gesture.entries.filter { it.isEnabled() }.toList()
|
||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val label: TextView = itemView.findViewById(R.id.dialog_select_gesture_row_name)
|
||||
val description: TextView = itemView.findViewById(R.id.dialog_select_gesture_row_description)
|
||||
val icon: ImageView = itemView.findViewById(R.id.dialog_select_gesture_row_icon)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view: View = inflater.inflate(R.layout.dialog_select_gesture_row, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val gesture = gestures[position]
|
||||
holder.label.text = gesture.getLabel(context)
|
||||
holder.description.text = gesture.getDescription(context)
|
||||
holder.icon.setImageDrawable(
|
||||
Action.forGesture(gesture)?.getIcon(context)
|
||||
)
|
||||
holder.itemView.setOnClickListener {
|
||||
onClick(gesture)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return gestures.size
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,339 @@
|
|||
package de.jrpie.android.launcher.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Insets
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.tan
|
||||
|
||||
class TouchGestureDetector(
|
||||
private val context: Context,
|
||||
var width: Int,
|
||||
var height: Int,
|
||||
var edgeWidth: Float
|
||||
) {
|
||||
private val ANGULAR_THRESHOLD = tan(Math.PI / 6)
|
||||
private val TOUCH_SLOP: Int
|
||||
private val TOUCH_SLOP_SQUARE: Int
|
||||
private val DOUBLE_TAP_SLOP: Int
|
||||
private val DOUBLE_TAP_SLOP_SQUARE: Int
|
||||
private val LONG_PRESS_TIMEOUT: Int
|
||||
private val TAP_TIMEOUT: Int
|
||||
private val DOUBLE_TAP_TIMEOUT: Int
|
||||
|
||||
private val MIN_TRIANGLE_HEIGHT = 250
|
||||
|
||||
private val longPressHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private var systemGestureInsetTop = 100
|
||||
private var systemGestureInsetBottom = 0
|
||||
private var systemGestureInsetLeft = 0
|
||||
private var systemGestureInsetRight = 0
|
||||
|
||||
|
||||
data class Vector(val x: Float, val y: Float) {
|
||||
fun absSquared(): Float {
|
||||
return this.x * this.x + this.y * this.y
|
||||
}
|
||||
|
||||
fun plus(vector: Vector): Vector {
|
||||
return Vector(this.x + vector.x, this.y + vector.y)
|
||||
}
|
||||
|
||||
fun max(other: Vector): Vector {
|
||||
return Vector(max(this.x, other.x), max(this.y, other.y))
|
||||
}
|
||||
|
||||
fun min(other: Vector): Vector {
|
||||
return Vector(min(this.x, other.x), min(this.y, other.y))
|
||||
}
|
||||
|
||||
operator fun minus(vector: Vector): Vector {
|
||||
return Vector(this.x - vector.x, this.y - vector.y)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PointerPath(
|
||||
val number: Int,
|
||||
val start: Vector,
|
||||
var last: Vector = start
|
||||
) {
|
||||
var min = Vector(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
|
||||
var max = Vector(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY)
|
||||
fun sizeSquared(): Float {
|
||||
return (max - min).absSquared()
|
||||
}
|
||||
|
||||
fun getDirection(): Vector {
|
||||
return last - start
|
||||
}
|
||||
|
||||
fun update(vector: Vector) {
|
||||
min = min.min(vector)
|
||||
max = max.max(vector)
|
||||
last = vector
|
||||
}
|
||||
}
|
||||
|
||||
private fun PointerPath.startIntersectsSystemGestureInsets(): Boolean {
|
||||
// ignore x, since this makes edge swipes very hard to execute
|
||||
return start.y < systemGestureInsetTop
|
||||
|| start.y > height - systemGestureInsetBottom
|
||||
}
|
||||
|
||||
private fun PointerPath.intersectsSystemGestureInsets(): Boolean {
|
||||
return min.x < systemGestureInsetLeft
|
||||
|| min.y < systemGestureInsetTop
|
||||
|| max.x > width - systemGestureInsetRight
|
||||
|| max.y > height - systemGestureInsetBottom
|
||||
}
|
||||
|
||||
private fun PointerPath.isTap(): Boolean {
|
||||
if (intersectsSystemGestureInsets()) {
|
||||
return false
|
||||
}
|
||||
return sizeSquared() < TOUCH_SLOP_SQUARE
|
||||
}
|
||||
|
||||
init {
|
||||
val configuration = ViewConfiguration.get(context)
|
||||
TOUCH_SLOP = configuration.scaledTouchSlop
|
||||
TOUCH_SLOP_SQUARE = TOUCH_SLOP * TOUCH_SLOP
|
||||
DOUBLE_TAP_SLOP = configuration.scaledDoubleTapSlop
|
||||
DOUBLE_TAP_SLOP_SQUARE = DOUBLE_TAP_SLOP * DOUBLE_TAP_SLOP
|
||||
|
||||
LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout()
|
||||
TAP_TIMEOUT = ViewConfiguration.getTapTimeout()
|
||||
DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout()
|
||||
}
|
||||
|
||||
private var paths = HashMap<Int, PointerPath>()
|
||||
|
||||
/* Set when
|
||||
* - the longPressHandler has detected this gesture as a long press
|
||||
* - the gesture was cancelled by MotionEvent.ACTION_CANCEL
|
||||
* In any case, the current gesture should be ignored by further detection logic.
|
||||
*/
|
||||
private var cancelled = false
|
||||
|
||||
private var lastTappedTime = 0L
|
||||
private var lastTappedLocation: Vector? = null
|
||||
|
||||
fun onTouchEvent(event: MotionEvent) {
|
||||
|
||||
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||
synchronized(this@TouchGestureDetector) {
|
||||
cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
val pointerIdToIndex =
|
||||
(0..<event.pointerCount).associateBy { event.getPointerId(it) }
|
||||
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
synchronized(this@TouchGestureDetector) {
|
||||
paths = HashMap()
|
||||
cancelled = false
|
||||
}
|
||||
longPressHandler.postDelayed({
|
||||
synchronized(this@TouchGestureDetector) {
|
||||
if (cancelled) {
|
||||
return@postDelayed
|
||||
}
|
||||
if (paths.entries.size == 1 && paths.entries.firstOrNull()?.value?.isTap() == true) {
|
||||
cancelled = true
|
||||
Gesture.LONG_CLICK.invoke(context)
|
||||
}
|
||||
}
|
||||
}, LONG_PRESS_TIMEOUT.toLong())
|
||||
}
|
||||
|
||||
// add new pointers
|
||||
for (i in 0..<event.pointerCount) {
|
||||
if (paths.containsKey(event.getPointerId(i))) {
|
||||
continue
|
||||
}
|
||||
val index = pointerIdToIndex[i] ?: continue
|
||||
paths[i] = PointerPath(
|
||||
paths.entries.size,
|
||||
Vector(event.getX(index), event.getY(index))
|
||||
)
|
||||
}
|
||||
|
||||
for (i in 0..<event.pointerCount) {
|
||||
val index = pointerIdToIndex[i] ?: continue
|
||||
|
||||
for (j in 0..<event.historySize) {
|
||||
paths[i]?.update(
|
||||
Vector(
|
||||
event.getHistoricalX(index, j),
|
||||
event.getHistoricalY(index, j)
|
||||
)
|
||||
)
|
||||
}
|
||||
paths[i]?.update(Vector(event.getX(index), event.getY(index)))
|
||||
}
|
||||
|
||||
if (event.actionMasked == MotionEvent.ACTION_UP) {
|
||||
synchronized(this@TouchGestureDetector) {
|
||||
// if the long press handler is still running, kill it
|
||||
longPressHandler.removeCallbacksAndMessages(null)
|
||||
// if the gesture was already detected as a long click, there is nothing to do
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
}
|
||||
classifyPaths(paths, event.downTime, event.eventTime)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
private fun getGestureForDirection(direction: Vector): Gesture? {
|
||||
return if (ANGULAR_THRESHOLD * abs(direction.x) > abs(direction.y)) { // horizontal swipe
|
||||
if (direction.x > TOUCH_SLOP)
|
||||
Gesture.SWIPE_RIGHT
|
||||
else if (direction.x < -TOUCH_SLOP)
|
||||
Gesture.SWIPE_LEFT
|
||||
else null
|
||||
} else if (ANGULAR_THRESHOLD * abs(direction.y) > abs(direction.x)) { // vertical swipe
|
||||
if (direction.y < -TOUCH_SLOP)
|
||||
Gesture.SWIPE_UP
|
||||
else if (direction.y > TOUCH_SLOP)
|
||||
Gesture.SWIPE_DOWN
|
||||
else null
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun classifyPaths(paths: Map<Int, PointerPath>, timeStart: Long, timeEnd: Long) {
|
||||
val duration = timeEnd - timeStart
|
||||
val pointerCount = paths.entries.size
|
||||
if (paths.entries.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val mainPointerPath = paths.entries.firstOrNull { it.value.number == 0 }?.value ?: return
|
||||
|
||||
// Ignore swipes starting at the very top and the very bottom
|
||||
if (paths.entries.any { it.value.startIntersectsSystemGestureInsets() }) {
|
||||
return
|
||||
}
|
||||
|
||||
if (pointerCount == 1 && mainPointerPath.isTap()) {
|
||||
// detect taps
|
||||
|
||||
if (duration in 0..TAP_TIMEOUT) {
|
||||
if (timeStart - lastTappedTime < DOUBLE_TAP_TIMEOUT &&
|
||||
lastTappedLocation?.let {
|
||||
(mainPointerPath.last - it).absSquared() < DOUBLE_TAP_SLOP_SQUARE
|
||||
} == true
|
||||
) {
|
||||
Gesture.DOUBLE_CLICK.invoke(context)
|
||||
} else {
|
||||
lastTappedTime = timeEnd
|
||||
lastTappedLocation = mainPointerPath.last
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// detect swipes
|
||||
|
||||
val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe()
|
||||
val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe()
|
||||
|
||||
var gesture = getGestureForDirection(mainPointerPath.getDirection())
|
||||
|
||||
if (doubleActions && pointerCount > 1) {
|
||||
if (paths.entries.any { getGestureForDirection(it.value.getDirection()) != gesture }) {
|
||||
// the directions of the pointers don't match
|
||||
return
|
||||
}
|
||||
gesture = gesture?.let(Gesture::getDoubleVariant)
|
||||
}
|
||||
|
||||
// detect triangles
|
||||
val startEndMin = mainPointerPath.start.min(mainPointerPath.last)
|
||||
val startEndMax = mainPointerPath.start.max(mainPointerPath.last)
|
||||
when (gesture) {
|
||||
Gesture.SWIPE_DOWN -> {
|
||||
if (startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
|
||||
gesture = Gesture.SWIPE_LARGER
|
||||
} else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) {
|
||||
gesture = Gesture.SWIPE_SMALLER
|
||||
}
|
||||
}
|
||||
|
||||
Gesture.SWIPE_UP -> {
|
||||
if (startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
|
||||
gesture = Gesture.SWIPE_LARGER_REVERSE
|
||||
} else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) {
|
||||
gesture = Gesture.SWIPE_SMALLER_REVERSE
|
||||
}
|
||||
}
|
||||
|
||||
Gesture.SWIPE_RIGHT -> {
|
||||
if (startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
|
||||
gesture = Gesture.SWIPE_V
|
||||
} else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) {
|
||||
gesture = Gesture.SWIPE_LAMBDA
|
||||
}
|
||||
}
|
||||
|
||||
Gesture.SWIPE_LEFT -> {
|
||||
if (startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
|
||||
gesture = Gesture.SWIPE_V_REVERSE
|
||||
} else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) {
|
||||
gesture = Gesture.SWIPE_LAMBDA_REVERSE
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
if (edgeActions) {
|
||||
if (mainPointerPath.max.x < edgeWidth * width) {
|
||||
gesture = gesture?.getEdgeVariant(Gesture.Edge.LEFT)
|
||||
} else if (mainPointerPath.min.x > (1 - edgeWidth) * width) {
|
||||
gesture = gesture?.getEdgeVariant(Gesture.Edge.RIGHT)
|
||||
}
|
||||
|
||||
if (mainPointerPath.max.y < edgeWidth * height) {
|
||||
gesture = gesture?.getEdgeVariant(Gesture.Edge.TOP)
|
||||
} else if (mainPointerPath.min.y > (1 - edgeWidth) * height) {
|
||||
gesture = gesture?.getEdgeVariant(Gesture.Edge.BOTTOM)
|
||||
}
|
||||
}
|
||||
|
||||
if (timeStart - lastTappedTime < 2 * DOUBLE_TAP_TIMEOUT) {
|
||||
gesture = gesture?.getTapComboVariant()
|
||||
}
|
||||
gesture?.invoke(context)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateScreenSize(windowManager: WindowManager) {
|
||||
val displayMetrics = DisplayMetrics()
|
||||
@Suppress("deprecation") // required to support API < 30
|
||||
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
width = displayMetrics.widthPixels
|
||||
height = displayMetrics.heightPixels
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
fun setSystemGestureInsets(insets: Insets) {
|
||||
systemGestureInsetTop = insets.top
|
||||
systemGestureInsetBottom = insets.bottom
|
||||
systemGestureInsetLeft = insets.left
|
||||
systemGestureInsetRight = insets.right
|
||||
}
|
||||
}
|
103
app/src/main/java/de/jrpie/android/launcher/ui/UIObject.kt
Normal file
103
app/src/main/java/de/jrpie/android/launcher/ui/UIObject.kt
Normal file
|
@ -0,0 +1,103 @@
|
|||
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
|
||||
|
||||
/**
|
||||
* An interface implemented by every [Activity], Fragment etc. in Launcher.
|
||||
* It handles themes and window flags - a useful abstraction as it is the same everywhere.
|
||||
*/
|
||||
@Suppress("deprecation") // FLAG_FULLSCREEN is required to support API level < 30
|
||||
fun setWindowFlags(window: Window, homeScreen: Boolean) {
|
||||
window.setFlags(0, 0) // clear flags
|
||||
|
||||
// Display notification bar
|
||||
if (LauncherPreferences.display().hideStatusBar())
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
else window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
|
||||
// Screen Timeout
|
||||
if (LauncherPreferences.display().screenTimeoutDisabled())
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
)
|
||||
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
if (!homeScreen) {
|
||||
LauncherPreferences.theme().background().applyToWindow(window)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
interface UIObject {
|
||||
fun onCreate() {
|
||||
if (this !is Activity) {
|
||||
return
|
||||
}
|
||||
setWindowFlags(window, isHomeScreen())
|
||||
|
||||
if (!LauncherPreferences.display().rotateScreen()) {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||
}
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
setOnClicks()
|
||||
adjustLayout()
|
||||
}
|
||||
|
||||
fun modifyTheme(theme: Resources.Theme): Resources.Theme {
|
||||
LauncherPreferences.theme().colorTheme().applyToTheme(
|
||||
theme,
|
||||
LauncherPreferences.theme().textShadow()
|
||||
)
|
||||
LauncherPreferences.theme().background().applyToTheme(theme)
|
||||
LauncherPreferences.theme().font().applyToTheme(theme)
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
// fun applyTheme() { }
|
||||
fun setOnClicks() {}
|
||||
fun adjustLayout() {}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,270 @@
|
|||
package de.jrpie.android.launcher.ui.list
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import de.jrpie.android.launcher.Application
|
||||
import de.jrpie.android.launcher.R
|
||||
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
|
||||
import de.jrpie.android.launcher.ui.list.apps.ListFragmentApps
|
||||
import de.jrpie.android.launcher.ui.list.other.ListFragmentOther
|
||||
|
||||
|
||||
/**
|
||||
* The [ListActivity] is the most general purpose activity in Launcher:
|
||||
* - used to view all apps and edit their settings
|
||||
* - used to choose an app / intent to be launched
|
||||
*
|
||||
* The activity itself can also be chosen to be launched as an action.
|
||||
*/
|
||||
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) {
|
||||
VIEW(R.string.list_title_view), /* view list of apps */
|
||||
PICK(R.string.list_title_pick) /* choose app or action to associate to a gesture */
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super<AppCompatActivity>.onCreate(savedInstanceState)
|
||||
super<UIObject>.onCreate()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && intention == ListActivityIntention.VIEW) {
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_OVERLAY
|
||||
) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
// get info about which action this activity is open for
|
||||
intent.extras?.let { bundle ->
|
||||
intention = bundle.getString("intention")
|
||||
?.let { ListActivityIntention.valueOf(it) }
|
||||
?: ListActivityIntention.VIEW
|
||||
|
||||
@Suppress("deprecation") // required to support API level < 33
|
||||
favoritesVisibility = bundle.getSerializable("favoritesVisibility")
|
||||
as? AppFilter.Companion.AppSetVisibility ?: favoritesVisibility
|
||||
@Suppress("deprecation") // required to support API level < 33
|
||||
privateSpaceVisibility = bundle.getSerializable("privateSpaceVisibility")
|
||||
as? AppFilter.Companion.AppSetVisibility ?: privateSpaceVisibility
|
||||
@Suppress("deprecation") // required to support API level < 33
|
||||
hiddenVisibility = bundle.getSerializable("hiddenVisibility")
|
||||
as? AppFilter.Companion.AppSetVisibility ?: hiddenVisibility
|
||||
|
||||
if (intention != ListActivityIntention.VIEW)
|
||||
forGesture = bundle.getString("forGesture")
|
||||
}
|
||||
|
||||
|
||||
// Initialise layout
|
||||
binding = ListBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.listSettings.setOnClickListener {
|
||||
LauncherAction.SETTINGS.launch(this@ListActivity)
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
this.window.decorView.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val r = Rect()
|
||||
window.decorView.getWindowVisibleDisplayFrame(r)
|
||||
val height: Int =
|
||||
binding.listContainer.context.resources.displayMetrics.heightPixels
|
||||
val diff = height - r.bottom
|
||||
if (diff != 0 &&
|
||||
LauncherPreferences.display().hideStatusBar()
|
||||
) {
|
||||
if (binding.listContainer.paddingBottom != diff) {
|
||||
binding.listContainer.setPadding(0, 0, 0, diff)
|
||||
}
|
||||
} else {
|
||||
if (binding.listContainer.paddingBottom != 0) {
|
||||
binding.listContainer.setPadding(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<AppCompatActivity>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
// ensure that the activity closes then an app is launched
|
||||
// and when the user navigates to recent apps
|
||||
finish()
|
||||
}
|
||||
|
||||
fun updateTitle() {
|
||||
var titleResource = intention.titleResource
|
||||
if (intention == ListActivityIntention.VIEW) {
|
||||
titleResource =
|
||||
if (hiddenVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
|
||||
R.string.list_title_hidden
|
||||
} else if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
|
||||
R.string.list_title_private_space
|
||||
} else if (favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
|
||||
R.string.list_title_favorite
|
||||
} else {
|
||||
R.string.list_title_view
|
||||
}
|
||||
}
|
||||
|
||||
binding.listHeading.text = getString(titleResource)
|
||||
}
|
||||
|
||||
|
||||
override fun getTheme(): Resources.Theme {
|
||||
return modifyTheme(super.getTheme())
|
||||
}
|
||||
|
||||
override fun setOnClicks() {
|
||||
binding.listClose.setOnClickListener { finish() }
|
||||
binding.listLock.setOnClickListener {
|
||||
togglePrivateSpaceLock(this)
|
||||
if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun adjustLayout() {
|
||||
|
||||
// Hide tabs for the "view" action
|
||||
if (intention == ListActivityIntention.VIEW) {
|
||||
binding.listTabs.visibility = View.GONE
|
||||
}
|
||||
|
||||
updateTitle()
|
||||
|
||||
val sectionsPagerAdapter = ListSectionsPagerAdapter(this)
|
||||
binding.listViewpager.let {
|
||||
it.adapter = sectionsPagerAdapter
|
||||
binding.listTabs.setupWithViewPager(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val TAB_TITLES = arrayOf(
|
||||
R.string.list_tab_app,
|
||||
R.string.list_tab_other
|
||||
)
|
||||
|
||||
/**
|
||||
* The [ListSectionsPagerAdapter] returns the fragment,
|
||||
* which corresponds to the selected tab in [ListActivity].
|
||||
*
|
||||
* This should eventually be replaced by a [FragmentStateAdapter]
|
||||
* However this keyboard does not open when using [ViewPager2]
|
||||
* so currently [ViewPager] is used here.
|
||||
* https://github.com/jrpie/launcher/issues/130
|
||||
*/
|
||||
@Suppress("deprecation")
|
||||
class ListSectionsPagerAdapter(private val activity: ListActivity) :
|
||||
FragmentPagerAdapter(activity.supportFragmentManager) {
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> ListFragmentApps()
|
||||
1 -> ListFragmentOther()
|
||||
else -> Fragment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return activity.resources.getString(TAB_TITLES[position])
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return when (activity.intention) {
|
||||
ListActivity.ListActivityIntention.VIEW -> 1
|
||||
else -> 2
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,245 @@
|
|||
package de.jrpie.android.launcher.ui.list.apps
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.graphics.Rect
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import de.jrpie.android.launcher.Application
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.AppFilter
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.DetailedAppInfo
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.preferences.ListLayout
|
||||
import de.jrpie.android.launcher.ui.list.ListActivity
|
||||
import de.jrpie.android.launcher.ui.transformGrayscale
|
||||
|
||||
/**
|
||||
* A [RecyclerView] (efficient scrollable list) containing all apps on the users device.
|
||||
* The apps details are represented by [AppInfo].
|
||||
*
|
||||
* @param activity - the activity this is in
|
||||
* @param intention - why the list is displayed ("view", "pick")
|
||||
* @param forGesture - the action which an app is chosen for (when the intention is "pick")
|
||||
*/
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
class AppsRecyclerAdapter(
|
||||
val activity: Activity,
|
||||
val root: View,
|
||||
private val intention: ListActivity.ListActivityIntention
|
||||
= ListActivity.ListActivityIntention.VIEW,
|
||||
private val forGesture: String? = "",
|
||||
private var appFilter: AppFilter = AppFilter(activity, ""),
|
||||
private val layout: ListLayout
|
||||
) :
|
||||
RecyclerView.Adapter<AppsRecyclerAdapter.ViewHolder>() {
|
||||
|
||||
private val apps = (activity.applicationContext as Application).apps
|
||||
private val appsListDisplayed: MutableList<AbstractDetailedAppInfo> = mutableListOf()
|
||||
private val grayscale = LauncherPreferences.theme().monochromeIcons()
|
||||
|
||||
// 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),
|
||||
View.OnClickListener {
|
||||
var textView: TextView = itemView.findViewById(R.id.list_apps_row_name)
|
||||
var img: ImageView = itemView.findViewById(R.id.list_apps_row_icon)
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val rect = Rect()
|
||||
img.getGlobalVisibleRect(rect)
|
||||
selectItem(bindingAdapterPosition, rect)
|
||||
}
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
|
||||
var appLabel = appsListDisplayed[i].getCustomLabel(activity)
|
||||
|
||||
val appIcon = appsListDisplayed[i].getIcon(activity)
|
||||
|
||||
viewHolder.img.transformGrayscale(grayscale)
|
||||
viewHolder.img.setImageDrawable(appIcon.constantState?.newDrawable() ?: appIcon)
|
||||
|
||||
if (layout.useBadgedText) {
|
||||
appLabel = activity.packageManager.getUserBadgedLabel(
|
||||
appLabel,
|
||||
appsListDisplayed[i].getUser(activity)
|
||||
).toString()
|
||||
}
|
||||
viewHolder.textView.text = appLabel
|
||||
|
||||
|
||||
// decide when to show the options popup menu about
|
||||
if (intention == ListActivity.ListActivityIntention.VIEW) {
|
||||
viewHolder.textView.setOnLongClickListener {
|
||||
showOptionsPopup(
|
||||
viewHolder,
|
||||
appsListDisplayed[i]
|
||||
)
|
||||
}
|
||||
viewHolder.img.setOnLongClickListener {
|
||||
showOptionsPopup(
|
||||
viewHolder,
|
||||
appsListDisplayed[i]
|
||||
)
|
||||
}
|
||||
// ensure onClicks are actually caught
|
||||
viewHolder.textView.setOnClickListener { viewHolder.onClick(viewHolder.textView) }
|
||||
viewHolder.img.setOnClickListener { viewHolder.onClick(viewHolder.img) }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("SameReturnValue")
|
||||
private fun showOptionsPopup(
|
||||
viewHolder: ViewHolder,
|
||||
appInfo: AbstractDetailedAppInfo
|
||||
): Boolean {
|
||||
//create the popup menu
|
||||
|
||||
val popup = PopupMenu(activity, viewHolder.img)
|
||||
popup.inflate(R.menu.menu_app)
|
||||
|
||||
if (!appInfo.isRemovable()) {
|
||||
popup.menu.findItem(R.id.app_menu_delete).setVisible(false)
|
||||
}
|
||||
|
||||
if (appInfo !is DetailedAppInfo) {
|
||||
popup.menu.findItem(R.id.app_menu_info).setVisible(false)
|
||||
}
|
||||
|
||||
if (LauncherPreferences.apps().hidden()?.contains(appInfo.getRawInfo()) == true) {
|
||||
popup.menu.findItem(R.id.app_menu_hidden).setTitle(R.string.list_app_hidden_remove)
|
||||
}
|
||||
|
||||
if (LauncherPreferences.apps().favorites()?.contains(appInfo.getRawInfo()) == true) {
|
||||
popup.menu.findItem(R.id.app_menu_favorite).setTitle(R.string.list_app_favorite_remove)
|
||||
}
|
||||
|
||||
|
||||
popup.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.app_menu_delete -> {
|
||||
appInfo.getRawInfo().uninstall(activity); true
|
||||
}
|
||||
|
||||
R.id.app_menu_info -> {
|
||||
(appInfo.getRawInfo() as? AppInfo)?.openSettings(activity); true
|
||||
}
|
||||
|
||||
R.id.app_menu_favorite -> {
|
||||
appInfo.getRawInfo().toggleFavorite(); true
|
||||
}
|
||||
|
||||
R.id.app_menu_hidden -> {
|
||||
appInfo.getRawInfo().toggleHidden(root); true
|
||||
}
|
||||
|
||||
R.id.app_menu_rename -> {
|
||||
appInfo.showRenameDialog(activity); true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
popup.show()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return appsListDisplayed.size
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
|
||||
val layout = LauncherPreferences.list().layout()
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view: View = inflater.inflate(layout.layoutResource, parent, false)
|
||||
val viewHolder = ViewHolder(view)
|
||||
return viewHolder
|
||||
}
|
||||
|
||||
fun selectItem(pos: Int, rect: Rect = Rect()) {
|
||||
if (pos >= appsListDisplayed.size) {
|
||||
return
|
||||
}
|
||||
val appInfo = appsListDisplayed[pos]
|
||||
when (intention) {
|
||||
ListActivity.ListActivityIntention.VIEW -> {
|
||||
appInfo.getAction().invoke(activity, rect)
|
||||
}
|
||||
|
||||
ListActivity.ListActivityIntention.PICK -> {
|
||||
activity.finish()
|
||||
forGesture ?: return
|
||||
val gesture = Gesture.byId(forGesture) ?: return
|
||||
Action.setActionForGesture(gesture, appInfo.getAction())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAppsList(triggerAutoLaunch: Boolean = false) {
|
||||
appsListDisplayed.clear()
|
||||
apps.value?.let { appsListDisplayed.addAll(appFilter(it)) }
|
||||
|
||||
if (triggerAutoLaunch &&
|
||||
appsListDisplayed.size == 1
|
||||
&& intention == ListActivity.ListActivityIntention.VIEW
|
||||
&& !disableAutoLaunch
|
||||
&& LauncherPreferences.functionality().searchAutoLaunch()
|
||||
) {
|
||||
val app = appsListDisplayed[0]
|
||||
app.getAction().invoke(activity)
|
||||
|
||||
val inputMethodManager =
|
||||
activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(View(activity).windowToken, 0)
|
||||
}
|
||||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* The function [setSearchString] is used to search elements within this [RecyclerView].
|
||||
*/
|
||||
fun setSearchString(search: String) {
|
||||
appFilter.query = search
|
||||
updateAppsList(true)
|
||||
|
||||
}
|
||||
|
||||
fun setFavoritesVisibility(v: AppFilter.Companion.AppSetVisibility) {
|
||||
appFilter.favoritesVisibility = v
|
||||
updateAppsList()
|
||||
}
|
||||
|
||||
fun setHiddenAppsVisibility(v: AppFilter.Companion.AppSetVisibility) {
|
||||
appFilter.hiddenVisibility = v
|
||||
updateAppsList()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
package de.jrpie.android.launcher.ui.list.apps
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.apps.AppFilter
|
||||
import de.jrpie.android.launcher.databinding.ListAppsBinding
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
import de.jrpie.android.launcher.ui.list.ListActivity
|
||||
import de.jrpie.android.launcher.ui.openSoftKeyboard
|
||||
|
||||
|
||||
/**
|
||||
* The [ListFragmentApps] is used as a tab in ListActivity.
|
||||
*
|
||||
* It is a list of all installed applications that are can be launched.
|
||||
*/
|
||||
class ListFragmentApps : Fragment(), UIObject {
|
||||
private lateinit var binding: ListAppsBinding
|
||||
private lateinit var appsRecyclerAdapter: AppsRecyclerAdapter
|
||||
|
||||
private var sharedPreferencesListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
|
||||
appsRecyclerAdapter.updateAppsList()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = ListAppsBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
LauncherPreferences.getSharedPreferences()
|
||||
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
|
||||
binding.listAppsCheckBoxFavorites.isChecked =
|
||||
((activity as? ListActivity)?.favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
LauncherPreferences.getSharedPreferences()
|
||||
.unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
}
|
||||
|
||||
|
||||
override fun setOnClicks() {}
|
||||
|
||||
override fun adjustLayout() {
|
||||
val listActivity = activity as? ListActivity ?: return
|
||||
|
||||
appsRecyclerAdapter =
|
||||
AppsRecyclerAdapter(
|
||||
listActivity, binding.root, listActivity.intention, listActivity.forGesture,
|
||||
appFilter = AppFilter(
|
||||
requireContext(),
|
||||
"",
|
||||
favoritesVisibility = listActivity.favoritesVisibility,
|
||||
privateSpaceVisibility = listActivity.privateSpaceVisibility,
|
||||
hiddenVisibility = listActivity.hiddenVisibility
|
||||
),
|
||||
layout = LauncherPreferences.list().layout()
|
||||
)
|
||||
|
||||
|
||||
// set up the list / recycler
|
||||
binding.listAppsRview.apply {
|
||||
// improve performance (since content changes don't change the layout size)
|
||||
setHasFixedSize(true)
|
||||
layoutManager = LauncherPreferences.list().layout().layoutManager(context)
|
||||
.also {
|
||||
if (LauncherPreferences.list().reverseLayout()) {
|
||||
(it as? LinearLayoutManager)?.reverseLayout = true
|
||||
(it as? GridLayoutManager)?.reverseLayout = true
|
||||
}
|
||||
}
|
||||
adapter = appsRecyclerAdapter
|
||||
}
|
||||
|
||||
binding.listAppsSearchview.setOnQueryTextListener(object :
|
||||
androidx.appcompat.widget.SearchView.OnQueryTextListener {
|
||||
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
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 {
|
||||
|
||||
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 {
|
||||
listActivity.favoritesVisibility =
|
||||
if (binding.listAppsCheckBoxFavorites.isChecked) {
|
||||
AppFilter.Companion.AppSetVisibility.EXCLUSIVE
|
||||
} else {
|
||||
AppFilter.Companion.AppSetVisibility.VISIBLE
|
||||
}
|
||||
appsRecyclerAdapter.setFavoritesVisibility(listActivity.favoritesVisibility)
|
||||
(activity as? ListActivity)?.updateTitle()
|
||||
}
|
||||
|
||||
if (listActivity.intention == ListActivity.ListActivityIntention.VIEW
|
||||
&& LauncherPreferences.functionality().searchAutoOpenKeyboard()
|
||||
) {
|
||||
binding.listAppsSearchview.openSoftKeyboard(requireContext())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package de.jrpie.android.launcher.list.other
|
||||
package de.jrpie.android.launcher.ui.list.other
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
|
@ -6,8 +6,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import de.jrpie.android.launcher.R
|
||||
import kotlinx.android.synthetic.main.list_other.*
|
||||
import de.jrpie.android.launcher.databinding.ListOtherBinding
|
||||
|
||||
/**
|
||||
* The [ListFragmentOther] is used as a tab in ListActivity,
|
||||
|
@ -17,19 +16,22 @@ import kotlinx.android.synthetic.main.list_other.*
|
|||
*/
|
||||
class ListFragmentOther : Fragment() {
|
||||
|
||||
private lateinit var binding: ListOtherBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.list_other, container, false)
|
||||
): View {
|
||||
binding = ListOtherBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
// set up the list / recycler
|
||||
val viewManager = LinearLayoutManager(context)
|
||||
val viewAdapter = OtherRecyclerAdapter(activity!!)
|
||||
val viewAdapter = OtherRecyclerAdapter(requireActivity())
|
||||
|
||||
list_other_rview.apply {
|
||||
binding.listOtherRview.apply {
|
||||
// improve performance (since content changes don't change the layout size)
|
||||
setHasFixedSize(true)
|
||||
layoutManager = viewManager
|
|
@ -1,7 +1,6 @@
|
|||
package de.jrpie.android.launcher.list.other
|
||||
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,8 +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.list.forGesture
|
||||
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.ListActivity
|
||||
|
||||
/**
|
||||
* The [OtherRecyclerAdapter] will only be displayed in the ListActivity,
|
||||
|
@ -19,10 +20,11 @@ import de.jrpie.android.launcher.list.forGesture
|
|||
* It lists `other` things to be launched that are not really represented by a URI,
|
||||
* rather by Launcher- internal conventions.
|
||||
*/
|
||||
class OtherRecyclerAdapter(val activity: Activity):
|
||||
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 {
|
||||
|
@ -31,13 +33,18 @@ 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.id) }
|
||||
activity.finish()
|
||||
val gestureId = (activity as? ListActivity)?.forGesture ?: return
|
||||
val gesture = Gesture.byId(gestureId) ?: return
|
||||
Action.setActionForGesture(gesture, content)
|
||||
}
|
||||
|
||||
init { itemView.setOnClickListener(this) }
|
||||
init {
|
||||
itemView.setOnClickListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
|
||||
|
@ -48,19 +55,13 @@ class OtherRecyclerAdapter(val activity: Activity):
|
|||
viewHolder.iconView.setImageResource(icon)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int { return othersList.size }
|
||||
override fun getItemCount(): Int {
|
||||
return othersList.size
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view: View = inflater.inflate(R.layout.list_other_row, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
private fun returnChoiceIntent(forApp: String, value: String) {
|
||||
val returnIntent = Intent()
|
||||
returnIntent.putExtra("value", value)
|
||||
returnIntent.putExtra("forGesture", forApp)
|
||||
activity.setResult(REQUEST_CHOOSE_APP, returnIntent)
|
||||
activity.finish()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package de.jrpie.android.launcher.ui.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
|
||||
/*
|
||||
* An overlay to indicate the areas where edge-gestures are detected
|
||||
*/
|
||||
class GestureAreaIndicatorOverlayView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
|
||||
|
||||
private var horizontalWidth = 0.1f
|
||||
private var verticalWidth = 0.1f
|
||||
|
||||
private lateinit var edgeLeft: Rect
|
||||
private lateinit var edgeRight: Rect
|
||||
private lateinit var edgeTop: Rect
|
||||
private lateinit var edgeBottom: Rect
|
||||
|
||||
private val hideTask = Runnable {
|
||||
visibility = INVISIBLE
|
||||
}
|
||||
|
||||
private var sharedPreferencesListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
|
||||
if (prefKey == LauncherPreferences.enabled_gestures().keys().edgeSwipeEdgeWidth()) {
|
||||
|
||||
this.removeCallbacks(hideTask)
|
||||
visibility = VISIBLE
|
||||
|
||||
update()
|
||||
|
||||
requestLayout()
|
||||
invalidate()
|
||||
|
||||
this.postDelayed(hideTask, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
|
||||
private val overlayPaint = Paint()
|
||||
init {
|
||||
overlayPaint.setARGB(50,255,0,0)
|
||||
overlayPaint.strokeWidth = 10f
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
horizontalWidth = LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
|
||||
verticalWidth = horizontalWidth
|
||||
|
||||
edgeTop = Rect(0,0,(width * horizontalWidth).toInt(), height)
|
||||
edgeBottom = Rect((width * (1 - horizontalWidth)).toInt(),0,width, height)
|
||||
edgeLeft = Rect(0,0, width, (height * verticalWidth).toInt())
|
||||
edgeRight = Rect(0,(height * (1-verticalWidth)).toInt(), width, height)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
LauncherPreferences.getSharedPreferences().registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
LauncherPreferences.getSharedPreferences().unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
|
||||
arrayOf(edgeLeft,
|
||||
edgeRight, edgeTop, edgeBottom).forEach { e ->
|
||||
canvas.drawRect(e, overlayPaint)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
package de.jrpie.android.launcher.ui.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.databinding.SettingsBinding
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.preferences.theme.Background
|
||||
import de.jrpie.android.launcher.preferences.theme.ColorTheme
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
import de.jrpie.android.launcher.ui.settings.actions.SettingsFragmentActions
|
||||
import de.jrpie.android.launcher.ui.settings.launcher.SettingsFragmentLauncher
|
||||
import de.jrpie.android.launcher.ui.settings.meta.SettingsFragmentMeta
|
||||
|
||||
/**
|
||||
* The [SettingsActivity] is a tabbed activity:
|
||||
*
|
||||
* | Actions | Choose apps or intents to be launched | [SettingsFragmentActions] |
|
||||
* | Theme | Select a theme / Customize | [SettingsFragmentLauncher] |
|
||||
* | Meta | About Launcher / Contact etc. | [SettingsFragmentMeta] |
|
||||
*
|
||||
* Settings are closed automatically if the activity goes `onPause` unexpectedly.
|
||||
*/
|
||||
class SettingsActivity : AppCompatActivity(), UIObject {
|
||||
|
||||
private val solidBackground = LauncherPreferences.theme().background() == Background.SOLID
|
||||
|| LauncherPreferences.theme().colorTheme() == ColorTheme.LIGHT
|
||||
|
||||
private val sharedPreferencesListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
|
||||
if (solidBackground &&
|
||||
(prefKey == LauncherPreferences.theme().keys().background() ||
|
||||
prefKey == LauncherPreferences.theme().keys().colorTheme())
|
||||
) {
|
||||
// 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
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super<AppCompatActivity>.onCreate(savedInstanceState)
|
||||
super<UIObject>.onCreate()
|
||||
|
||||
|
||||
// Initialise layout
|
||||
binding = SettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
// set up tabs and swiping in settings
|
||||
val sectionsPagerAdapter = SettingsSectionsPagerAdapter(this)
|
||||
binding.settingsViewpager.apply {
|
||||
adapter = sectionsPagerAdapter
|
||||
setCurrentItem(intent.getIntExtra(EXTRA_TAB, 0), false)
|
||||
}
|
||||
TabLayoutMediator(binding.settingsTabs, binding.settingsViewpager) { tab, position ->
|
||||
tab.text = sectionsPagerAdapter.getPageTitle(position)
|
||||
}.attach()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<AppCompatActivity>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
LauncherPreferences.getSharedPreferences()
|
||||
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
LauncherPreferences.getSharedPreferences()
|
||||
.unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun getTheme(): Resources.Theme {
|
||||
return modifyTheme(super.getTheme())
|
||||
}
|
||||
|
||||
override fun setOnClicks() {
|
||||
// As older APIs somehow do not recognize the xml defined onClick
|
||||
binding.settingsClose.setOnClickListener { finish() }
|
||||
// open device settings (see https://stackoverflow.com/a/62092663/12787264)
|
||||
binding.settingsSystem.setOnClickListener {
|
||||
startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_TAB = "tab"
|
||||
}
|
||||
}
|
||||
|
||||
private val TAB_TITLES = arrayOf(
|
||||
R.string.settings_tab_actions,
|
||||
R.string.settings_tab_launcher,
|
||||
R.string.settings_tab_meta
|
||||
)
|
||||
|
||||
class SettingsSectionsPagerAdapter(private val activity: FragmentActivity) :
|
||||
FragmentStateAdapter(activity) {
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> SettingsFragmentActions()
|
||||
1 -> SettingsFragmentLauncher()
|
||||
2 -> SettingsFragmentMeta()
|
||||
else -> Fragment()
|
||||
}
|
||||
}
|
||||
|
||||
fun getPageTitle(position: Int): CharSequence {
|
||||
return activity.resources.getString(TAB_TITLES[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return 3
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package de.jrpie.android.launcher.ui.settings.actions
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
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 de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.databinding.SettingsActionsBinding
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
import de.jrpie.android.launcher.ui.list.ListActivity
|
||||
|
||||
|
||||
/**
|
||||
* The [SettingsFragmentActions] is a used as a tab in the SettingsActivity.
|
||||
*
|
||||
* It is used to change Apps / Intents to be launched when a specific action
|
||||
* is triggered.
|
||||
* It also allows the user to view all apps ([ListActivity]) or install new ones.
|
||||
*/
|
||||
|
||||
class
|
||||
SettingsFragmentActions : Fragment(), UIObject {
|
||||
|
||||
private var binding: SettingsActionsBinding? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = SettingsActionsBinding.inflate(inflater, container, false)
|
||||
|
||||
binding?.root?.viewTreeObserver?.addOnGlobalLayoutListener {
|
||||
val buttonHeight =
|
||||
binding?.settingsActionsButtons?.height ?: return@addOnGlobalLayoutListener
|
||||
val height = binding?.root?.height ?: return@addOnGlobalLayoutListener
|
||||
|
||||
if (buttonHeight > 0.2 * height) {
|
||||
binding?.settingsActionsButtons?.visibility = View.GONE
|
||||
} else {
|
||||
binding?.settingsActionsButtons?.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
return binding!!.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun setOnClicks() {
|
||||
|
||||
binding!!.settingsActionsButtonInstallApps.setOnClickListener {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_MAIN)
|
||||
intent.addCategory(Intent.CATEGORY_APP_MARKET)
|
||||
startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
getString(R.string.settings_apps_toast_store_not_found),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
package de.jrpie.android.launcher.ui.settings.actions
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
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.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.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.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],
|
||||
* which displays all selected actions / apps.
|
||||
*
|
||||
* It is used in the Tutorial and in Settings
|
||||
*/
|
||||
class SettingsFragmentActionsRecycler : Fragment(), UIObject {
|
||||
|
||||
private var savedScrollPosition = 0
|
||||
|
||||
private var sharedPreferencesListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
|
||||
actionViewAdapter?.updateActions()
|
||||
}
|
||||
private lateinit var binding: SettingsActionsRecyclerBinding
|
||||
private var actionViewAdapter: ActionsRecyclerAdapter? = null
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = SettingsActionsRecyclerBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
|
||||
// set up the list / recycler
|
||||
val actionViewManager = LinearLayoutManager(context)
|
||||
actionViewAdapter = ActionsRecyclerAdapter(requireActivity())
|
||||
|
||||
binding.settingsActionsRview.apply {
|
||||
// improve performance (since content changes don't change the layout size)
|
||||
setHasFixedSize(true)
|
||||
layoutManager = actionViewManager
|
||||
adapter = actionViewAdapter
|
||||
|
||||
}
|
||||
LauncherPreferences.getSharedPreferences()
|
||||
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
LauncherPreferences.getSharedPreferences()
|
||||
.unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
override fun onPause() {
|
||||
savedScrollPosition =
|
||||
(binding.settingsActionsRview.layoutManager as LinearLayoutManager)
|
||||
.findFirstCompletelyVisibleItemPosition()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
(binding.settingsActionsRview.layoutManager)?.scrollToPosition(savedScrollPosition)
|
||||
}
|
||||
}
|
||||
|
||||
class ActionsRecyclerAdapter(val activity: Activity) :
|
||||
RecyclerView.Adapter<ActionsRecyclerAdapter.ViewHolder>() {
|
||||
|
||||
private val drawableUnknown = AppCompatResources.getDrawable(activity, R.drawable.baseline_question_mark_24)
|
||||
|
||||
private val gesturesList: ArrayList<Gesture> =
|
||||
Gesture.entries.filter(Gesture::isEnabled) as ArrayList<Gesture>
|
||||
|
||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
|
||||
View.OnClickListener {
|
||||
var textView: TextView = itemView.findViewById(R.id.settings_actions_row_name)
|
||||
var descriptionTextView: TextView =
|
||||
itemView.findViewById(R.id.settings_actions_row_description)
|
||||
var img: ImageView = itemView.findViewById(R.id.settings_actions_row_icon_img)
|
||||
var chooseButton: Button = itemView.findViewById(R.id.settings_actions_row_button_choose)
|
||||
var removeAction: ImageView = itemView.findViewById(R.id.settings_actions_row_remove)
|
||||
|
||||
override fun onClick(v: View) {}
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateViewHolder(gesture: Gesture, viewHolder: ViewHolder) {
|
||||
val action = Action.forGesture(gesture)
|
||||
|
||||
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
|
||||
viewHolder.img.setImageDrawable(drawable)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
|
||||
val gesture = gesturesList[i]
|
||||
viewHolder.textView.text = gesture.getLabel(activity)
|
||||
|
||||
val description = gesture.getDescription(activity)
|
||||
viewHolder.descriptionTextView.text = description
|
||||
|
||||
viewHolder.img.transformGrayscale(LauncherPreferences.theme().monochromeIcons())
|
||||
|
||||
updateViewHolder(gesture, viewHolder)
|
||||
viewHolder.img.setOnClickListener { chooseApp(gesture) }
|
||||
viewHolder.chooseButton.setOnClickListener { chooseApp(gesture) }
|
||||
viewHolder.removeAction.setOnClickListener { Action.clearActionForGesture(gesture) }
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return gesturesList.size
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view: View = inflater.inflate(R.layout.settings_actions_row, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateActions() {
|
||||
val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe()
|
||||
val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe()
|
||||
this.gesturesList.clear()
|
||||
gesturesList.addAll(Gesture.entries.filter {
|
||||
(doubleActions || !it.isDoubleVariant())
|
||||
&& (edgeActions || !it.isEdgeVariant())
|
||||
})
|
||||
|
||||
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.startActivity(intent)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package de.jrpie.android.launcher.ui.settings.launcher
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.actions.lock.LockMethod
|
||||
import de.jrpie.android.launcher.actions.openAppsList
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.preferences.theme.ColorTheme
|
||||
import de.jrpie.android.launcher.setDefaultHomeScreen
|
||||
|
||||
|
||||
/**
|
||||
* The [SettingsFragmentLauncher] is a used as a tab in the SettingsActivity.
|
||||
*
|
||||
* It is used to change themes, select wallpapers ... theme related stuff
|
||||
*/
|
||||
class SettingsFragmentLauncher : PreferenceFragmentCompat() {
|
||||
|
||||
|
||||
private var sharedPreferencesListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
|
||||
if (prefKey?.startsWith("clock.") == true) {
|
||||
updateVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateVisibility() {
|
||||
val showSeconds = findPreference<androidx.preference.Preference>(
|
||||
LauncherPreferences.clock().keys().showSeconds()
|
||||
)
|
||||
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() {
|
||||
super.onStart()
|
||||
LauncherPreferences.getSharedPreferences()
|
||||
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
LauncherPreferences.getSharedPreferences()
|
||||
.unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
|
||||
val selectWallpaper = findPreference<androidx.preference.Preference>(
|
||||
LauncherPreferences.theme().keys().wallpaper()
|
||||
)
|
||||
selectWallpaper?.setOnPreferenceClickListener {
|
||||
// https://github.com/LineageOS/android_packages_apps_Trebuchet/blob/6caab89b21b2b91f0a439e1fd8c4510dcb255819/src/com/android/launcher3/views/OptionsPopupView.java#L271
|
||||
val intent = Intent(Intent.ACTION_SET_WALLPAPER)
|
||||
.putExtra("com.android.wallpaper.LAUNCH_SOURCE", "app_launched_launcher")
|
||||
.putExtra("com.android.launcher3.WALLPAPER_FLAVOR", "focus_wallpaper")
|
||||
startActivity(intent)
|
||||
true
|
||||
}
|
||||
val chooseHomeScreen = findPreference<androidx.preference.Preference>(
|
||||
LauncherPreferences.general().keys().chooseHomeScreen()
|
||||
)
|
||||
chooseHomeScreen?.setOnPreferenceClickListener {
|
||||
setDefaultHomeScreen(requireContext(), checkDefault = false)
|
||||
true
|
||||
}
|
||||
|
||||
val hiddenApps = findPreference<androidx.preference.Preference>(
|
||||
LauncherPreferences.apps().keys().hidden()
|
||||
)
|
||||
hiddenApps?.setOnPreferenceClickListener {
|
||||
openAppsList(requireContext(), favorite = false, hidden = true)
|
||||
true
|
||||
}
|
||||
|
||||
val lockMethod = findPreference<androidx.preference.Preference>(
|
||||
LauncherPreferences.actions().keys().lockMethod()
|
||||
)
|
||||
|
||||
lockMethod?.setOnPreferenceClickListener {
|
||||
LockMethod.chooseMethod(requireContext())
|
||||
true
|
||||
}
|
||||
|
||||
findPreference<androidx.preference.DropDownPreference>(
|
||||
LauncherPreferences.theme().keys().colorTheme()
|
||||
)?.apply {
|
||||
entries = ColorTheme.entries.filter { x -> x.isAvailable() }
|
||||
.map { x -> x.getLabel(requireContext()) }.toTypedArray()
|
||||
entryValues = ColorTheme.entries.filter { x -> x.isAvailable() }
|
||||
.map { x -> x.name }.toTypedArray()
|
||||
}
|
||||
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
lockMethod?.isVisible = false
|
||||
}
|
||||
|
||||
updateVisibility()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
package de.jrpie.android.launcher.ui.settings.meta
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.BuildConfig
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.copyToClipboard
|
||||
import de.jrpie.android.launcher.databinding.SettingsMetaBinding
|
||||
import de.jrpie.android.launcher.getDeviceInfo
|
||||
import de.jrpie.android.launcher.openInBrowser
|
||||
import de.jrpie.android.launcher.openTutorial
|
||||
import de.jrpie.android.launcher.preferences.resetPreferences
|
||||
import de.jrpie.android.launcher.ui.LegalInfoActivity
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
||||
/**
|
||||
* The [SettingsFragmentMeta] is a used as a tab in the SettingsActivity.
|
||||
*
|
||||
* It is used to change settings and access resources about Launcher,
|
||||
* that are not directly related to the behaviour of the app itself.
|
||||
*
|
||||
* (greek `meta` = above, next level)
|
||||
*/
|
||||
class SettingsFragmentMeta : Fragment(), UIObject {
|
||||
|
||||
private lateinit var binding: SettingsMetaBinding
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = SettingsMetaBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun setOnClicks() {
|
||||
|
||||
fun bindURL(view: View, urlRes: Int) {
|
||||
view.setOnClickListener {
|
||||
openInBrowser(
|
||||
getString(urlRes),
|
||||
requireContext()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
binding.settingsMetaButtonViewTutorial.setOnClickListener {
|
||||
openTutorial(requireContext())
|
||||
}
|
||||
|
||||
// prompting for settings-reset confirmation
|
||||
binding.settingsMetaButtonResetSettings.setOnClickListener {
|
||||
AlertDialog.Builder(this.requireContext(), R.style.AlertDialogCustom)
|
||||
.setTitle(getString(R.string.settings_meta_reset))
|
||||
.setMessage(getString(R.string.settings_meta_reset_confirm))
|
||||
.setPositiveButton(
|
||||
android.R.string.ok
|
||||
) { _, _ ->
|
||||
resetPreferences(this.requireContext())
|
||||
requireActivity().finish()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.show()
|
||||
}
|
||||
|
||||
|
||||
// view code
|
||||
bindURL(binding.settingsMetaButtonViewCode, R.string.settings_meta_link_github)
|
||||
|
||||
// report a bug
|
||||
binding.settingsMetaButtonReportBug.setOnClickListener {
|
||||
val deviceInfo = getDeviceInfo()
|
||||
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
|
||||
setView(R.layout.dialog_report_bug)
|
||||
setTitle(R.string.dialog_report_bug_title)
|
||||
setPositiveButton(R.string.dialog_report_bug_create_report) { _, _ ->
|
||||
openInBrowser(
|
||||
getString(R.string.settings_meta_report_bug_link),
|
||||
requireContext()
|
||||
)
|
||||
}
|
||||
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
|
||||
}.create().also { it.show() }.apply {
|
||||
val info = findViewById<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
|
||||
bindURL(binding.settingsMetaButtonJoinChat, R.string.settings_meta_chat_url)
|
||||
|
||||
// contact developer
|
||||
// bindURL(binding.settingsMetaButtonContact, R.string.settings_meta_contact_url)
|
||||
|
||||
// contact fork developer
|
||||
bindURL(binding.settingsMetaButtonForkContact, R.string.settings_meta_fork_contact_url)
|
||||
|
||||
// donate
|
||||
bindURL(binding.settingsMetaButtonDonate, R.string.settings_meta_donate_url)
|
||||
|
||||
// privacy policy
|
||||
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
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
package de.jrpie.android.launcher.ui.tutorial
|
||||
|
||||
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.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.ui.UIObject
|
||||
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.
|
||||
* It can also be opened from Settings.
|
||||
*
|
||||
* It tells the user about the concept behind launcher
|
||||
* and helps with the setup process (on new installations)
|
||||
*/
|
||||
class TutorialActivity : AppCompatActivity(), UIObject {
|
||||
|
||||
private lateinit var binding: TutorialBinding
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super<AppCompatActivity>.onCreate(savedInstanceState)
|
||||
super<UIObject>.onCreate()
|
||||
|
||||
// Initialise layout
|
||||
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(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 {
|
||||
return modifyTheme(super.getTheme())
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<AppCompatActivity>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The [TutorialSectionsPagerAdapter] defines which fragments are shown when,
|
||||
* in the [TutorialActivity].
|
||||
*
|
||||
* Tabs: (Start | Concept | Usage | Setup | Finish)
|
||||
*/
|
||||
class TutorialSectionsPagerAdapter(activity: FragmentActivity) :
|
||||
FragmentStateAdapter(activity) {
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return 6
|
||||
}
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> TutorialFragment0Start()
|
||||
1 -> TutorialFragment1Concept()
|
||||
2 -> TutorialFragment2Usage()
|
||||
3 -> TutorialFragment3AppList()
|
||||
4 -> TutorialFragment4Setup()
|
||||
5 -> TutorialFragment5Finish()
|
||||
else -> Fragment()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
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.databinding.Tutorial0StartBinding
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
||||
/**
|
||||
* 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 TutorialFragment0Start : Fragment(), UIObject {
|
||||
|
||||
private lateinit var binding: Tutorial0StartBinding
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = Tutorial0StartBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
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.BuildConfig
|
||||
import de.jrpie.android.launcher.databinding.Tutorial1ConceptBinding
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
||||
/**
|
||||
* The [TutorialFragment1Concept] is a used as a tab in the TutorialActivity.
|
||||
*
|
||||
* It is used to display info about Launchers concept (open source, efficiency ...)
|
||||
*/
|
||||
class TutorialFragment1Concept : Fragment(), UIObject {
|
||||
private lateinit var binding: Tutorial1ConceptBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = Tutorial1ConceptBinding.inflate(inflater, container, false)
|
||||
binding.tutorialConceptBadgeVersion.text = BuildConfig.VERSION_NAME
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package de.jrpie.android.launcher.tutorial.tabs
|
||||
package de.jrpie.android.launcher.ui.tutorial.tabs
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
|
@ -6,23 +6,23 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.UIObject
|
||||
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(){
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package de.jrpie.android.launcher.ui.tutorial.tabs
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
||||
/**
|
||||
* The [TutorialFragment3AppList] is a used as a tab in the TutorialActivity.
|
||||
*
|
||||
* Tells the user how his screen will look and how the app can be used
|
||||
*/
|
||||
class TutorialFragment3AppList : Fragment(), UIObject {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.tutorial_3_app_list, container, false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package de.jrpie.android.launcher.tutorial.tabs
|
||||
package de.jrpie.android.launcher.ui.tutorial.tabs
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
|
@ -6,23 +6,23 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.UIObject
|
||||
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(){
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
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.BuildConfig.VERSION_CODE
|
||||
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 [TutorialFragment5Finish] is a used as a tab in the TutorialActivity.
|
||||
*
|
||||
* It is used to display further resources and let the user start Launcher
|
||||
*/
|
||||
class TutorialFragment5Finish : Fragment(), UIObject {
|
||||
|
||||
private lateinit var binding: Tutorial5FinishBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = Tutorial5FinishBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun setOnClicks() {
|
||||
super.setOnClicks()
|
||||
binding.tutorialFinishButtonStart.setOnClickListener { finishTutorial() }
|
||||
}
|
||||
|
||||
private fun finishTutorial() {
|
||||
if (!LauncherPreferences.internal().started()) {
|
||||
LauncherPreferences.internal().started(true)
|
||||
LauncherPreferences.internal().startedTime(System.currentTimeMillis() / 1000L)
|
||||
LauncherPreferences.internal().versionCode(VERSION_CODE)
|
||||
}
|
||||
context?.let { setDefaultHomeScreen(it, checkDefault = true) }
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package de.jrpie.android.launcher.ui.util
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
|
||||
class HtmlTextView(context: Context, attr: AttributeSet?, int: Int) :
|
||||
AppCompatTextView(context, attr, int) {
|
||||
constructor(context: Context, attr: AttributeSet?) : this(context, attr, 0)
|
||||
constructor(context: Context) : this(context, null, 0)
|
||||
|
||||
init {
|
||||
@Suppress("deprecation") // required to support API level < 24
|
||||
text = Html.fromHtml(text.toString())
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
5
app/src/main/res/color/text_color_toggle.xml
Normal file
5
app/src/main/res/color/text_color_toggle.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_enabled="false" android:alpha="0.2" android:color="?android:color" />
|
||||
<item android:color="?android:color" />
|
||||
</selector>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue