Compare commits

..

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

311 changed files with 4198 additions and 11809 deletions

4
.github/FUNDING.yml vendored
View file

@ -1,3 +1,3 @@
# How you can support jrpie/Launcher
# How you can support finnmglas/Launcher
custom: https://s.jrpie.de/launcher-donate
custom: sponsor.finnmglas.com

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

@ -0,0 +1,31 @@
---
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.

View file

@ -1,50 +0,0 @@
name: Bug report
description: Create a report to help improve this app
title: '[bug] '
labels: bug
body:
- type: markdown
attributes:
value: |
Thank you for helping to improve µLauncher!
- type: textarea
id: bug
attributes:
label: Describe the Bug
description: What happened?
placeholder: A clear and concise description of what the bug is.
render: markdown
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What did you expect to happen instead?
render: markdown
validations:
required: false
- type: textarea
id: reproduce
attributes:
label: To Reproduce
description: What steps are required to reproduce the bug?
render: markdown
placeholder: |
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: false
- type: textarea
id: device
attributes:
label: Your Device
description: |
What device are you using? Adding this information helps to reproduce the bug.
You can copy this from µLauncher > Settings > Meta > Report Bug.
render: markdown
validations:
required: false

View file

@ -1,24 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: '[feature] '
title: ''
labels: enhancement
assignees: ''
---
# <!--MANDATORY--> Please describe the problem to be solved
**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 [...]
<!-- 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 the solution you'd like**
A clear and concise description of what you want to happen.
# <!--OPTIONAL--> Describe the solution you would suggest
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
<!-- 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. -->
**Additional info**
Add any other info, comments or screenshots about the feature request here.

View file

@ -1,32 +0,0 @@
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
View file

@ -15,9 +15,7 @@ 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/
app/release/
.kotlin/
# release/
# Gradle files
.gradle/
@ -40,15 +38,12 @@ 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 Normal file
View file

@ -0,0 +1,138 @@
<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 Normal file
View file

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

6
.idea/compiler.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

10
.idea/deploymentTargetSelector.xml generated Normal file
View file

@ -0,0 +1,10 @@
<?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 Normal file
View file

@ -0,0 +1,25 @@
<?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 Normal file
View file

@ -0,0 +1,6 @@
<?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 Normal file
View file

@ -0,0 +1,17 @@
<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 Normal file
View file

@ -0,0 +1,263 @@
<?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>

View file

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

View file

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

134
README.md
View file

@ -1,112 +1,38 @@
<!-- Shields from shields.io -->
[![][shield-release]][latest-release]
[![Android CI](https://github.com/jrpie/Launcher/actions/workflows/android.yml/badge.svg)](https://github.com/jrpie/Launcher/actions/workflows/android.yml)
<!--[![][shield-release]][latest-release] -->
[![][shield-license]][license]
[![Chat on Matrix](https://matrix.to/img/matrix-badge.svg)][matrix]
[![Chat on Discord](https://img.shields.io/badge/discord-join%20chat-007ec6.svg?style=flat)][discord]
<!-- 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.
### 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.
# μ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.
### Technical
* Small improvements to the gesture detection.
* Different apps set as default.
* Package name was changed to `de.jrpie.android.launcher` to avoid clashing with the original app.
* Dropped support for API < 21 (i.e. pre Lollypop)
* Some refactoring
---
---
[hack-font]: https://sourcefoundry.org/hack/
[original-repo]: https://github.com/finnmglas/Launcher
[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 -->
@ -118,16 +44,14 @@ Note that those are not signed.
<!-- 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 -->

View file

@ -1,5 +0,0 @@
# 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.

View file

@ -1,115 +1,45 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlinx-serialization'
apply plugin: 'kotlin-android-extensions'
android {
dataBinding {
enabled = true
}
packaging {
resources.excludes.addAll(
[
"META-INF/LICENSE.md",
"META-INF/NOTICE.md",
"META-INF/LICENSE-notice.md"
]
)
}
compileSdkVersion 34
buildToolsVersion "34.0.0"
defaultConfig {
applicationId "de.jrpie.android.launcher"
minSdkVersion 21
targetSdkVersion 35
compileSdk 35
versionCode 44
versionName "0.1.4"
versionCode 16
versionName "j-0.0.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.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.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.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'
}

View file

@ -1,6 +1,4 @@
# Add project specific ProGuard rules here.
-dontobfuscate
-dontoptimize
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
@ -21,10 +19,3 @@
# 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

View file

@ -0,0 +1,21 @@
{
"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
}

View file

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

View file

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

View file

@ -3,98 +3,43 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<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"
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:theme="@style/launcherBaseTheme">
<activity android:name=".HomeActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/launcherHomeTheme">
android:screenOrientation="portrait"
tools:ignore="LockedOrientationActivity">
<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=".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 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>
<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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -1,160 +0,0 @@
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))
}
}
}

View file

@ -1,226 +1,418 @@
package de.jrpie.android.launcher
import android.app.Activity
import android.app.Service
import android.app.role.RoleManager
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.content.pm.LauncherApps.ShortcutQuery
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo
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.os.Build
import android.os.Bundle
import android.os.UserHandle
import android.os.UserManager
import android.os.SystemClock
import android.provider.Settings
import android.util.Log
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.widget.Toast
import androidx.annotation.RequiresApi
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.ShortcutAction
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.DetailedAppInfo
import de.jrpie.android.launcher.apps.DetailedPinnedShortcutInfo
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
import de.jrpie.android.launcher.apps.getPrivateSpaceUser
import de.jrpie.android.launcher.apps.isPrivateSpaceSupported
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
import androidx.core.net.toUri
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
const val LOG_TAG = "Launcher"
/* Preference Key Constants */
const val REQUEST_SET_DEFAULT_HOME = 42
const val PREF_DOMINANT = "custom_dominant"
const val PREF_VIBRANT = "custom_vibrant"
const val PREF_THEME = "theme"
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)
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)
} else {
val testIntent = Intent(Intent.ACTION_MAIN)
testIntent.addCategory(Intent.CATEGORY_HOME)
val defaultHome = testIntent.resolveActivity(context.packageManager)?.packageName
return defaultHome == context.packageName
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()
}
}
}
fun setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) {
val isDefault = isDefaultHomeScreen(context)
if (checkDefault && isDefault) {
// Launcher is already the default home app
return
}
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)
}
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
}
/* Settings related functions */
val intent = Intent(Settings.ACTION_HOME_SETTINGS)
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")
context.startActivity(intent)
}
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 openSettings(activity: Activity) {
activity.startActivity(Intent(activity, SettingsActivity::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 openTutorial(activity: Activity){
activity.startActivity(Intent(activity, TutorialActivity::class.java))
}
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 openAppsList(activity: Activity){
val intent = Intent(activity, ListActivity::class.java)
intent.putExtra("intention", ListActivity.ListActivityIntention.VIEW.toString())
intendedSettingsPause = true
activity.startActivity(intent)
}
fun openTutorial(context: Context) {
context.startActivity(Intent(context, TutorialActivity::class.java))
}
/**
* Load all apps.
* [loadApps] is used to speed up the [AppsRecyclerAdapter] loading time,
* as it caches all the apps and allows for fast access to the data.
*/
fun getApps(
packageManager: PackageManager,
context: Context
): MutableList<AbstractDetailedAppInfo> {
var start = System.currentTimeMillis()
val loadList = mutableListOf<AbstractDetailedAppInfo>()
fun loadApps(packageManager: PackageManager) {
val loadList = mutableListOf<AppInfo>()
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))
}
}
// 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
val app = AppInfo()
app.label = ri.loadLabel(packageManager)
app.packageName = ri.activityInfo.packageName
app.icon = ri.activityInfo.loadIcon(packageManager)
loadList.add(app)
}
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
)
loadList.add(detailedAppInfo)
}
}
loadList.sortBy { it.getCustomLabel(context) }
else window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
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
// 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 for the bug report button
fun getDeviceInfo(): String {
return """
µLauncher version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})
Android version: ${Build.VERSION.RELEASE} (sdk ${Build.VERSION.SDK_INT})
Model: ${Build.MODEL}
Device: ${Build.DEVICE}
Brand: ${Build.BRAND}
Manufacturer: ${Build.MANUFACTURER}
""".trimIndent()
// Used in Tutorial and Settings `ActivityOnResult`
fun saveListActivityChoice(context: Context, data: Intent?) {
val value = data?.getStringExtra("value")
val forGesture = data?.getStringExtra("forGesture") ?: return
Gesture.byId(forGesture)?.setApp(context, value.toString())
loadSettings(context)
}
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)
// 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)
}
// not setting it in any other case (yet), unable to find a good solution
}
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)
}
}
// 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
}

View file

@ -0,0 +1,144 @@
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 }
}
}
}

View file

@ -0,0 +1,249 @@
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 }
}

View file

@ -0,0 +1,22 @@
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() { }
}

View file

@ -1,100 +0,0 @@
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()
}
}
}
}

View file

@ -1,81 +0,0 @@
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
}
}

View file

@ -1,369 +0,0 @@
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 }
}
}
}

View file

@ -1,341 +0,0 @@
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)
}
}
}

View file

@ -1,57 +0,0 @@
package de.jrpie.android.launcher.actions
import android.app.Service
import android.content.Context
import android.content.pm.LauncherApps
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.os.Build
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("action:shortcut")
class ShortcutAction(val shortcut: PinnedShortcutInfo) : Action {
override fun invoke(context: Context, rect: Rect?): Boolean {
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
// TODO
return false
}
shortcut.getShortcutInfo(context)?.let {
launcherApps.startShortcut(it, rect, null)
}
// TODO: handle null
return true
}
override fun label(context: Context): String {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return "?"
}
return shortcut.getShortcutInfo(context)?.longLabel?.toString() ?: "?"
}
override fun getIcon(context: Context): Drawable? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return null
}
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
return shortcut.getShortcutInfo(context)?.let { launcherApps.getShortcutBadgedIconDrawable(it, 0) }
}
override fun isAvailable(context: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return false
}
return shortcut.getShortcutInfo(context) != null
}
override fun canReachSettings(): Boolean {
return false
}
}

View file

@ -1,90 +0,0 @@
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()
}
}
}
}

View file

@ -1,151 +0,0 @@
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()
}
}
}

View file

@ -1,56 +0,0 @@
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()
}
}
}

View file

@ -1,71 +0,0 @@
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)
}
}
}

View file

@ -1,22 +0,0 @@
package de.jrpie.android.launcher.apps
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
/**
* This interface is implemented by [AppInfo] and [PinnedShortcutInfo].
*/
@Serializable
sealed interface AbstractAppInfo {
fun serialize(): String {
return Json.encodeToString(this)
}
companion object {
const val INVALID_USER = -1
fun deserialize(serialized: String): AbstractAppInfo {
return Json.decodeFromString(serialized)
}
}
}

View file

@ -1,42 +0,0 @@
package de.jrpie.android.launcher.apps
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.UserHandle
import android.util.Log
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.preferences.LauncherPreferences
/**
* This interface is implemented by [DetailedAppInfo] and [DetailedPinnedShortcutInfo]
*/
sealed interface AbstractDetailedAppInfo {
fun getRawInfo(): AbstractAppInfo
fun getLabel(): String
fun getIcon(context: Context): Drawable
fun getUser(context: Context): UserHandle
fun isPrivate(): Boolean
fun isRemovable(): Boolean
fun getAction(): Action
fun getCustomLabel(context: Context): String {
val map = (context.applicationContext as? Application)?.getCustomAppNames()
return map?.get(getRawInfo()) ?: getLabel()
}
fun setCustomLabel(label: CharSequence?) {
Log.i("Launcher", "Setting custom label for ${this.getRawInfo()} to ${label}.")
val map = LauncherPreferences.apps().customNames() ?: HashMap<AbstractAppInfo, String>()
if (label.isNullOrEmpty()) {
map.remove(getRawInfo())
} else {
map[getRawInfo()] = label.toString()
}
LauncherPreferences.apps().customNames(map)
}
}

View file

@ -1,102 +0,0 @@
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)
}
}
}

View file

@ -1,29 +0,0 @@
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()
}
}

View file

@ -1,74 +0,0 @@
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))
}
}
}
}

View file

@ -1,66 +0,0 @@
package de.jrpie.android.launcher.apps
import android.app.Service
import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.UserHandle
import androidx.annotation.RequiresApi
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.ShortcutAction
import de.jrpie.android.launcher.getUserFromId
@RequiresApi(Build.VERSION_CODES.N_MR1)
class DetailedPinnedShortcutInfo(
private val shortcutInfo: PinnedShortcutInfo,
private val label: String,
private val icon: Drawable,
private val privateSpace: Boolean
) : AbstractDetailedAppInfo {
constructor(context: Context, shortcut: ShortcutInfo) : this(
PinnedShortcutInfo(shortcut),
shortcut.longLabel.toString(),
(context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps)
.getShortcutBadgedIconDrawable(shortcut, 0),
shortcut.userHandle == getPrivateSpaceUser(context)
)
override fun getRawInfo(): AbstractAppInfo {
return shortcutInfo
}
override fun getLabel(): String {
return label
}
override fun getIcon(context: Context): Drawable {
return icon
}
override fun getUser(context: Context): UserHandle {
return getUserFromId(shortcutInfo.user, context)
}
override fun isPrivate(): Boolean {
return privateSpace
}
override fun isRemovable(): Boolean {
return true
}
override fun getAction(): Action {
return ShortcutAction(shortcutInfo)
}
companion object {
fun fromPinnedShortcutInfo(shortcutInfo: PinnedShortcutInfo, context: Context): DetailedPinnedShortcutInfo? {
return shortcutInfo.getShortcutInfo(context)?.let {
DetailedPinnedShortcutInfo(context, it)
}
}
}
}

View file

@ -1,46 +0,0 @@
package de.jrpie.android.launcher.apps
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.LauncherApps.ShortcutQuery
import android.content.pm.ShortcutInfo
import android.os.Build
import androidx.annotation.RequiresApi
import de.jrpie.android.launcher.getUserFromId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@RequiresApi(Build.VERSION_CODES.N_MR1)
@Serializable
@SerialName("shortcut")
data class PinnedShortcutInfo(
val id: String,
val packageName: String,
val activityName: String,
val user: Int
): AbstractAppInfo {
constructor(info: ShortcutInfo) : this(info.id, info.`package`, info.activity?.className ?: "", info.userHandle.hashCode())
fun getShortcutInfo(context: Context): ShortcutInfo? {
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
return try {
launcherApps.getShortcuts(
ShortcutQuery().apply {
setQueryFlags(ShortcutQuery.FLAG_MATCH_PINNED)
setPackage(packageName)
setActivity(ComponentName(packageName, activityName))
setShortcutIds(listOf(id))
},
getUserFromId(user, context)
)?.firstOrNull()
} catch(_: Exception) {
// can throw SecurityException or IllegalStateException when profile is locked
null
}
}
}

View file

@ -1,140 +0,0 @@
package de.jrpie.android.launcher.apps
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.os.Build
import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import android.widget.Toast
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.isDefaultHomeScreen
import de.jrpie.android.launcher.setDefaultHomeScreen
/*
* Checks whether the device supports private space.
*/
fun isPrivateSpaceSupported(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
}
fun getPrivateSpaceUser(context: Context): UserHandle? {
if (!isPrivateSpaceSupported()) {
return null
}
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
return userManager.userProfiles.firstOrNull { u ->
launcherApps.getLauncherUserInfo(u)?.userType == UserManager.USER_TYPE_PROFILE_PRIVATE
}
}
/**
* Check whether the user has created a private space and whether µLauncher can access it.
*/
fun isPrivateSpaceSetUp(
context: Context,
showToast: Boolean = false,
launchSettings: Boolean = false
): Boolean {
if (!isPrivateSpaceSupported()) {
if (showToast) {
Toast.makeText(
context,
context.getString(R.string.alert_requires_android_v),
Toast.LENGTH_LONG
).show()
}
return false
}
val privateSpaceUser = getPrivateSpaceUser(context)
if (privateSpaceUser != null) {
return true
}
if (!isDefaultHomeScreen(context)) {
if (showToast) {
Toast.makeText(
context,
context.getString(R.string.toast_private_space_default_home_screen),
Toast.LENGTH_LONG
).show()
}
if (launchSettings) {
setDefaultHomeScreen(context)
}
} else {
if (showToast) {
Toast.makeText(
context,
context.getString(R.string.toast_private_space_not_available),
Toast.LENGTH_LONG
).show()
}
if (launchSettings) {
try {
context.startActivity(Intent(Settings.ACTION_PRIVACY_SETTINGS))
} catch (_: ActivityNotFoundException) {
}
}
}
return false
}
fun isPrivateSpaceLocked(context: Context): Boolean {
if (!isPrivateSpaceSupported()) {
return false
}
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
val privateSpaceUser = getPrivateSpaceUser(context) ?: return false
return userManager.isQuietModeEnabled(privateSpaceUser)
}
fun lockPrivateSpace(context: Context, lock: Boolean) {
if (!isPrivateSpaceSupported()) {
return
}
// silently return when trying to unlock but hide when locked is set
if (!lock && hidePrivateSpaceWhenLocked(context)) {
return
}
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
val privateSpaceUser = getPrivateSpaceUser(context) ?: return
userManager.requestQuietModeEnabled(lock, privateSpaceUser)
}
fun togglePrivateSpaceLock(context: Context) {
if (!isPrivateSpaceSetUp(context, showToast = true, launchSettings = true)) {
return
}
val lock = isPrivateSpaceLocked(context)
lockPrivateSpace(context, !lock)
if (!lock) {
Toast.makeText(
context,
context.getString(R.string.toast_private_space_locked),
Toast.LENGTH_LONG
).show()
}
}
@Suppress("SameReturnValue")
fun hidePrivateSpaceWhenLocked(context: Context): Boolean {
// Trying to access the setting as a 3rd party launcher raises a security exception.
// This is an Android bug: https://issuetracker.google.com/issues/352276244#comment5
// The logic for this is implemented.
// TODO: replace this once the Android bug is fixed
return false
// TODO: perhaps this should be cached
// https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Launcher3/src/com/android/launcher3/util/SettingsCache.java;l=61;drc=56bf7ad33bc9d5ed3c18e7abefeec5c177ec75d7
// val key = "hide_privatespace_entry_point"
// return Settings.Secure.getInt(context.contentResolver, key, 0) == 1
}

View file

@ -0,0 +1,166 @@
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
}
}
}

View file

@ -0,0 +1,15 @@
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
}

View file

@ -0,0 +1,204 @@
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()
}
}

View file

@ -0,0 +1,76 @@
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)
}
}
}

View file

@ -0,0 +1,38 @@
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")
}
}
}

View file

@ -1,4 +1,4 @@
package de.jrpie.android.launcher.ui.list.other
package de.jrpie.android.launcher.list.other
import android.os.Bundle
import android.view.LayoutInflater
@ -6,7 +6,8 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import de.jrpie.android.launcher.databinding.ListOtherBinding
import de.jrpie.android.launcher.R
import kotlinx.android.synthetic.main.list_other.*
/**
* The [ListFragmentOther] is used as a tab in ListActivity,
@ -16,22 +17,19 @@ import de.jrpie.android.launcher.databinding.ListOtherBinding
*/
class ListFragmentOther : Fragment() {
private lateinit var binding: ListOtherBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = ListOtherBinding.inflate(inflater)
return binding.root
): View? {
return inflater.inflate(R.layout.list_other, container, false)
}
override fun onStart() {
// set up the list / recycler
val viewManager = LinearLayoutManager(context)
val viewAdapter = OtherRecyclerAdapter(requireActivity())
val viewAdapter = OtherRecyclerAdapter(activity!!)
binding.listOtherRview.apply {
list_other_rview.apply {
// improve performance (since content changes don't change the layout size)
setHasFixedSize(true)
layoutManager = viewManager

View file

@ -1,6 +1,7 @@
package de.jrpie.android.launcher.ui.list.other
package de.jrpie.android.launcher.list.other
import android.app.Activity
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -8,10 +9,8 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.ui.list.ListActivity
import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
import de.jrpie.android.launcher.list.forGesture
/**
* The [OtherRecyclerAdapter] will only be displayed in the ListActivity,
@ -20,11 +19,10 @@ import de.jrpie.android.launcher.ui.list.ListActivity
* 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.entries.filter { it.isAvailable(activity) }.toTypedArray()
private val othersList: Array<LauncherAction> = LauncherAction.values()
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener {
@ -33,18 +31,13 @@ class OtherRecyclerAdapter(val activity: Activity) :
override fun onClick(v: View) {
val pos = bindingAdapterPosition
val pos = adapterPosition
val content = othersList[pos]
activity.finish()
val gestureId = (activity as? ListActivity)?.forGesture ?: return
val gesture = Gesture.byId(gestureId) ?: return
Action.setActionForGesture(gesture, content)
forGesture?.let { returnChoiceIntent(it, content.id) }
}
init {
itemView.setOnClickListener(this)
}
init { itemView.setOnClickListener(this) }
}
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
@ -55,13 +48,19 @@ 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()
}
}

View file

@ -1,166 +0,0 @@
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
}
}
}
}

View file

@ -1,85 +0,0 @@
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 {}

View file

@ -1,39 +0,0 @@
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
),
}

View file

@ -1,89 +0,0 @@
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)
}

View file

@ -1,141 +0,0 @@
package de.jrpie.android.launcher.preferences.legacy
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.AppAction
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.json.JSONException
import org.json.JSONObject
import androidx.core.content.edit
@Serializable
@Suppress("unused")
private class LegacyMapEntry(val key: AppInfo, val value: String)
private fun serializeMapAppInfo(value: Map<AppInfo, String>?): Set<String>? {
return value?.map { (key, value) ->
Json.encodeToString(LegacyMapEntry(key, value))
}?.toSet()
}
val oldLauncherActionIds: Map<String, LauncherAction> =
mapOf(
Pair("launcher:settings", LauncherAction.SETTINGS),
Pair("launcher:choose", LauncherAction.CHOOSE),
Pair("launcher:chooseFromFavorites", LauncherAction.CHOOSE_FROM_FAVORITES),
Pair("launcher:volumeUp", LauncherAction.VOLUME_UP),
Pair("launcher:volumeDown", LauncherAction.VOLUME_DOWN),
Pair("launcher:nextTrack", LauncherAction.TRACK_NEXT),
Pair("launcher:previousTrack", LauncherAction.TRACK_PREV),
Pair("launcher:expandNotificationsPanel", LauncherAction.EXPAND_NOTIFICATIONS_PANEL),
Pair("launcher:expandSettingsPanel", LauncherAction.EXPAND_SETTINGS_PANEL),
Pair("launcher:lockScreen", LauncherAction.LOCK_SCREEN),
Pair("launcher:toggleTorch", LauncherAction.TORCH),
Pair("launcher:nop", LauncherAction.NOP),
)
private fun AppInfo.Companion.legacyDeserialize(serialized: String): AppInfo {
val values = serialized.split(";")
val packageName = values[0]
val user = Integer.valueOf(values[1])
val activityName = values.getOrNull(2) ?: "" // TODO
return AppInfo(packageName, activityName, user)
}
/**
* Get an action for a specific id.
* An id is of the form:
* - "launcher:${launcher_action_name}", see [LauncherAction]
* - "${package_name}", see [AppAction]
* - "${package_name}:${activity_name}", see [AppAction]
*
* @param id
* @param user a user id, ignored if the action is a [LauncherAction].
*/
private fun Action.Companion.legacyFromId(id: String, user: Int?): Action? {
if (id.isEmpty()) {
return null
}
oldLauncherActionIds[id]?.let { return it }
val values = id.split(";")
return AppAction(
AppInfo(
values[0], values.getOrNull(1) ?: "", user ?: INVALID_USER
)
)
}
private fun Action.Companion.legacyFromPreference(id: String): Action? {
val preferences = LauncherPreferences.getSharedPreferences()
val actionId = preferences.getString("$id.app", "")!!
var u: Int? = preferences.getInt(
"$id.user",
INVALID_USER
)
u = if (u == INVALID_USER) null else u
return Action.legacyFromId(actionId, u)
}
private fun migrateAppInfoStringMap(key: String) {
val preferences = LauncherPreferences.getSharedPreferences()
serializeMapAppInfo(
preferences.getStringSet(key, setOf())?.mapNotNull { entry ->
try {
val obj = JSONObject(entry)
val info = AppInfo.legacyDeserialize(obj.getString("key"))
val value = obj.getString("value")
Pair(info, value)
} catch (_: JSONException) {
null
}
}?.toMap(HashMap())
)?.let {
preferences.edit { putStringSet(key, it) }
}
}
private fun migrateAppInfoSet(key: String) {
(LauncherPreferences.getSharedPreferences().getStringSet(key, setOf()) ?: return)
.map(AppInfo.Companion::legacyDeserialize)
.map(AppInfo::serialize)
.toSet()
.let { LauncherPreferences.getSharedPreferences().edit { putStringSet(key, it) } }
}
private fun migrateAction(key: String) {
Action.legacyFromPreference(key)?.let { action ->
LauncherPreferences.getSharedPreferences().edit {
putString(key, Json.encodeToString(action))
.remove("$key.app")
.remove("$key.user")
}
}
}
/**
* Migrate preferences from version 1 (used until version j-0.0.18) to the current format
* (see [PREFERENCE_VERSION])
*/
fun migratePreferencesFromVersion1() {
assert(LauncherPreferences.internal().versionCode() == 1)
Gesture.entries.forEach { g -> migrateAction(g.id) }
migrateAppInfoSet(LauncherPreferences.apps().keys().hidden())
migrateAppInfoSet(LauncherPreferences.apps().keys().favorites())
migrateAppInfoStringMap(LauncherPreferences.apps().keys().customNames())
LauncherPreferences.internal().versionCode(2)
migratePreferencesFromVersion2()
}

View file

@ -1,20 +0,0 @@
package de.jrpie.android.launcher.preferences.legacy
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
/**
* Migrate preferences from version 2 (used until version 0.0.21) to the current format
* (see [PREFERENCE_VERSION])
*/
fun migratePreferencesFromVersion2() {
assert(LauncherPreferences.internal().versionCode() == 2)
// previously there was no setting for this
Action.setActionForGesture(Gesture.BACK, LauncherAction.CHOOSE)
LauncherPreferences.internal().versionCode(3)
migratePreferencesFromVersion3()
}

View file

@ -1,85 +0,0 @@
package de.jrpie.android.launcher.preferences.legacy
import android.content.SharedPreferences
import android.content.SharedPreferences.Editor
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.AbstractAppInfo
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer
import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.util.HashSet
import androidx.core.content.edit
/**
* Migrate preferences from version 3 (used until version 0.0.23) to the current format
* (see [PREFERENCE_VERSION])
*/
fun deserializeSet(value: Set<String>?): Set<AppInfo>? {
return value?.map {
Json.decodeFromString<AppInfo>(it)
}?.toHashSet()
}
fun deserializeMap(value: Set<String>?): HashMap<AppInfo, String>? {
return value?.associateTo(HashMap()) {
val entry = Json.decodeFromString<MapEntry>(it)
Pair(entry.key, entry.value)
}
}
@Serializable
private class MapEntry(val key: AppInfo, val value: String)
private fun migrateSetAppInfo(key: String, preferences: SharedPreferences, editor: Editor) {
try {
val serializer = SetAbstractAppInfoPreferenceSerializer()
val set = HashSet<AbstractAppInfo>()
deserializeSet(preferences.getStringSet(key, null))?.let {
set.addAll(it)
}
@Suppress("UNCHECKED_CAST")
editor.putStringSet(
key,
serializer.serialize(set as java.util.Set<AbstractAppInfo>) as Set<String>?
)
} catch (e: Exception) {
e.printStackTrace()
editor.putStringSet(key, null)
}
}
private fun migrateMapAppInfoString(key: String, preferences: SharedPreferences, editor: Editor ) {
try {
val serializer = MapAbstractAppInfoStringPreferenceSerializer()
val map = HashMap<AbstractAppInfo, String>()
deserializeMap(preferences.getStringSet(key, null))?.let {
map.putAll(it)
}
@Suppress("UNCHECKED_CAST")
editor.putStringSet(key, serializer.serialize(map) as Set<String>?)
} catch (e: Exception) {
e.printStackTrace()
editor.putStringSet(key, null)
}
}
fun migratePreferencesFromVersion3() {
assert(PREFERENCE_VERSION == 4)
assert(LauncherPreferences.internal().versionCode() == 3)
val preferences = LauncherPreferences.getSharedPreferences()
preferences.edit {
migrateSetAppInfo(LauncherPreferences.apps().keys().favorites(), preferences, this)
migrateSetAppInfo(LauncherPreferences.apps().keys().hidden(), preferences, this)
migrateMapAppInfoString(LauncherPreferences.apps().keys().customNames(), preferences, this)
}
LauncherPreferences.internal().versionCode(4)
}

View file

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

View file

@ -1,71 +0,0 @@
@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
package de.jrpie.android.launcher.preferences.serialization
import de.jrpie.android.launcher.apps.AbstractAppInfo
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Suppress("UNCHECKED_CAST")
class SetAbstractAppInfoPreferenceSerializer :
PreferenceSerializer<java.util.Set<AbstractAppInfo>?, java.util.Set<java.lang.String>?> {
@Throws(PreferenceSerializationException::class)
override fun serialize(value: java.util.Set<AbstractAppInfo>?): java.util.Set<java.lang.String> {
return value?.map(AbstractAppInfo::serialize)
?.toHashSet() as java.util.Set<java.lang.String>
}
@Throws(PreferenceSerializationException::class)
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.Set<AbstractAppInfo>? {
return value?.map(java.lang.String::toString)?.map(AbstractAppInfo::deserialize)
?.toHashSet() as? java.util.Set<AbstractAppInfo>
}
}
@Suppress("UNCHECKED_CAST")
class SetPinnedShortcutInfoPreferenceSerializer :
PreferenceSerializer<java.util.Set<PinnedShortcutInfo>?, java.util.Set<java.lang.String>?> {
@Throws(PreferenceSerializationException::class)
override fun serialize(value: java.util.Set<PinnedShortcutInfo>?): java.util.Set<java.lang.String> {
return value?.map { Json.encodeToString<PinnedShortcutInfo>(it) }
?.toHashSet() as java.util.Set<java.lang.String>
}
@Throws(PreferenceSerializationException::class)
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.Set<PinnedShortcutInfo>? {
return value?.map(java.lang.String::toString)
?.map { Json.decodeFromString<PinnedShortcutInfo>(it) }
?.toHashSet() as? java.util.Set<PinnedShortcutInfo>
}
}
@Suppress("UNCHECKED_CAST")
class MapAbstractAppInfoStringPreferenceSerializer :
PreferenceSerializer<java.util.HashMap<AbstractAppInfo, String>?, java.util.Set<java.lang.String>?> {
@Serializable
private class MapEntry(val key: AbstractAppInfo, val value: String)
@Throws(PreferenceSerializationException::class)
override fun serialize(value: java.util.HashMap<AbstractAppInfo, String>?): java.util.Set<java.lang.String>? {
return value?.map { (key, value) ->
Json.encodeToString(MapEntry(key, value))
}?.toHashSet() as? java.util.Set<java.lang.String>
}
@Throws(PreferenceSerializationException::class)
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.HashMap<AbstractAppInfo, String>? {
return value?.associateTo(HashMap()) {
val entry = Json.decodeFromString<MapEntry>(it.toString())
Pair(entry.key, entry.value)
}
}
}

View file

@ -1,64 +0,0 @@
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
}
}

View file

@ -1,48 +0,0 @@
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)
}
}

View file

@ -1,23 +0,0 @@
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)
}
}

View file

@ -0,0 +1,109 @@
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 }
}

View file

@ -0,0 +1,68 @@
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()
}
}
}
}

View file

@ -0,0 +1,137 @@
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
)
}
}

View file

@ -0,0 +1,176 @@
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
}
}

View file

@ -0,0 +1,137 @@
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!!
)
}
}
}

View file

@ -0,0 +1,98 @@
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 }
}

View file

@ -0,0 +1,35 @@
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()
}
}

View file

@ -0,0 +1,56 @@
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()
}
}

View file

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

View file

@ -0,0 +1,36 @@
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()
}
}

View file

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

View file

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

View file

@ -1,267 +0,0 @@
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
}
}

View file

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

View file

@ -1,154 +0,0 @@
package de.jrpie.android.launcher.ui
import android.app.AlertDialog
import android.app.Service
import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.LauncherApps.PinItemRequest
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.ShortcutAction
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
import de.jrpie.android.launcher.databinding.ActivityPinShortcutBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import androidx.core.content.edit
class PinShortcutActivity : AppCompatActivity(), UIObject {
private lateinit var binding: ActivityPinShortcutBinding
private var isBound = false
private var request: PinItemRequest? = null
override fun onCreate(savedInstanceState: Bundle?) {
super<AppCompatActivity>.onCreate(savedInstanceState)
super<UIObject>.onCreate()
enableEdgeToEdge()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
finish()
return
}
binding = ActivityPinShortcutBinding.inflate(layoutInflater)
setContentView(binding.root)
val launcherApps = getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
val request = launcherApps.getPinItemRequest(intent)
this.request = request
if (request == null || request.requestType != PinItemRequest.REQUEST_TYPE_SHORTCUT) {
finish()
return
}
binding.pinShortcutLabel.text = request.shortcutInfo!!.shortLabel ?: "?"
binding.pinShortcutLabel.setCompoundDrawables(
launcherApps.getShortcutBadgedIconDrawable(request.shortcutInfo, 0).also {
val size = (40 * resources.displayMetrics.density).toInt()
it.setBounds(0,0, size, size)
}, null, null, null)
binding.pinShortcutButtonBind.setOnClickListener {
AlertDialog.Builder(this, R.style.AlertDialogCustom)
.setTitle(getString(R.string.pin_shortcut_button_bind))
.setView(R.layout.dialog_select_gesture)
.setNegativeButton(android.R.string.cancel, null)
.create().also { it.show() }.let { dialog ->
val viewManager = LinearLayoutManager(dialog.context)
val viewAdapter = GestureRecyclerAdapter (dialog.context) { gesture ->
if (!isBound) {
isBound = true
request.accept()
}
LauncherPreferences.getSharedPreferences().edit {
ShortcutAction(PinnedShortcutInfo(request.shortcutInfo!!)).bindToGesture(
this,
gesture.id
)
}
dialog.dismiss()
}
dialog.findViewById<RecyclerView>(R.id.dialog_select_gesture_recycler).apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
}
}
binding.pinShortcutClose.setOnClickListener { finish() }
binding.pinShortcutButtonOk.setOnClickListener { finish() }
}
override fun onStart() {
super<AppCompatActivity>.onStart()
super<UIObject>.onStart()
}
override fun onDestroy() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
super.onDestroy()
return
}
if(binding.pinShortcutSwitchVisible.isChecked) {
if(!isBound) {
request?.accept()
}
request?.shortcutInfo?.let {
val set = LauncherPreferences.apps().pinnedShortcuts() ?: mutableSetOf()
set.add(PinnedShortcutInfo(it))
LauncherPreferences.apps().pinnedShortcuts(set)
}
}
super.onDestroy()
}
override fun getTheme(): Resources.Theme {
return modifyTheme(super.getTheme())
}
inner class GestureRecyclerAdapter(val context: Context, val onClick: (Gesture) -> Unit): RecyclerView.Adapter<GestureRecyclerAdapter.ViewHolder>() {
private val gestures = Gesture.entries.filter { it.isEnabled() }.toList()
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val label: TextView = itemView.findViewById(R.id.dialog_select_gesture_row_name)
val description: TextView = itemView.findViewById(R.id.dialog_select_gesture_row_description)
val icon: ImageView = itemView.findViewById(R.id.dialog_select_gesture_row_icon)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view: View = inflater.inflate(R.layout.dialog_select_gesture_row, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val gesture = gestures[position]
holder.label.text = gesture.getLabel(context)
holder.description.text = gesture.getDescription(context)
holder.icon.setImageDrawable(
Action.forGesture(gesture)?.getIcon(context)
)
holder.itemView.setOnClickListener {
onClick(gesture)
}
}
override fun getItemCount(): Int {
return gestures.size
}
}
}

View file

@ -1,339 +0,0 @@
package de.jrpie.android.launcher.ui
import android.content.Context
import android.graphics.Insets
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.DisplayMetrics
import android.view.MotionEvent
import android.view.ViewConfiguration
import android.view.WindowManager
import androidx.annotation.RequiresApi
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.preferences.LauncherPreferences
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.tan
class TouchGestureDetector(
private val context: Context,
var width: Int,
var height: Int,
var edgeWidth: Float
) {
private val ANGULAR_THRESHOLD = tan(Math.PI / 6)
private val TOUCH_SLOP: Int
private val TOUCH_SLOP_SQUARE: Int
private val DOUBLE_TAP_SLOP: Int
private val DOUBLE_TAP_SLOP_SQUARE: Int
private val LONG_PRESS_TIMEOUT: Int
private val TAP_TIMEOUT: Int
private val DOUBLE_TAP_TIMEOUT: Int
private val MIN_TRIANGLE_HEIGHT = 250
private val longPressHandler = Handler(Looper.getMainLooper())
private var systemGestureInsetTop = 100
private var systemGestureInsetBottom = 0
private var systemGestureInsetLeft = 0
private var systemGestureInsetRight = 0
data class Vector(val x: Float, val y: Float) {
fun absSquared(): Float {
return this.x * this.x + this.y * this.y
}
fun plus(vector: Vector): Vector {
return Vector(this.x + vector.x, this.y + vector.y)
}
fun max(other: Vector): Vector {
return Vector(max(this.x, other.x), max(this.y, other.y))
}
fun min(other: Vector): Vector {
return Vector(min(this.x, other.x), min(this.y, other.y))
}
operator fun minus(vector: Vector): Vector {
return Vector(this.x - vector.x, this.y - vector.y)
}
}
class PointerPath(
val number: Int,
val start: Vector,
var last: Vector = start
) {
var min = Vector(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
var max = Vector(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY)
fun sizeSquared(): Float {
return (max - min).absSquared()
}
fun getDirection(): Vector {
return last - start
}
fun update(vector: Vector) {
min = min.min(vector)
max = max.max(vector)
last = vector
}
}
private fun PointerPath.startIntersectsSystemGestureInsets(): Boolean {
// ignore x, since this makes edge swipes very hard to execute
return start.y < systemGestureInsetTop
|| start.y > height - systemGestureInsetBottom
}
private fun PointerPath.intersectsSystemGestureInsets(): Boolean {
return min.x < systemGestureInsetLeft
|| min.y < systemGestureInsetTop
|| max.x > width - systemGestureInsetRight
|| max.y > height - systemGestureInsetBottom
}
private fun PointerPath.isTap(): Boolean {
if (intersectsSystemGestureInsets()) {
return false
}
return sizeSquared() < TOUCH_SLOP_SQUARE
}
init {
val configuration = ViewConfiguration.get(context)
TOUCH_SLOP = configuration.scaledTouchSlop
TOUCH_SLOP_SQUARE = TOUCH_SLOP * TOUCH_SLOP
DOUBLE_TAP_SLOP = configuration.scaledDoubleTapSlop
DOUBLE_TAP_SLOP_SQUARE = DOUBLE_TAP_SLOP * DOUBLE_TAP_SLOP
LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout()
TAP_TIMEOUT = ViewConfiguration.getTapTimeout()
DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout()
}
private var paths = HashMap<Int, PointerPath>()
/* Set when
* - the longPressHandler has detected this gesture as a long press
* - the gesture was cancelled by MotionEvent.ACTION_CANCEL
* In any case, the current gesture should be ignored by further detection logic.
*/
private var cancelled = false
private var lastTappedTime = 0L
private var lastTappedLocation: Vector? = null
fun onTouchEvent(event: MotionEvent) {
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
synchronized(this@TouchGestureDetector) {
cancelled = true
}
}
val pointerIdToIndex =
(0..<event.pointerCount).associateBy { event.getPointerId(it) }
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
synchronized(this@TouchGestureDetector) {
paths = HashMap()
cancelled = false
}
longPressHandler.postDelayed({
synchronized(this@TouchGestureDetector) {
if (cancelled) {
return@postDelayed
}
if (paths.entries.size == 1 && paths.entries.firstOrNull()?.value?.isTap() == true) {
cancelled = true
Gesture.LONG_CLICK.invoke(context)
}
}
}, LONG_PRESS_TIMEOUT.toLong())
}
// add new pointers
for (i in 0..<event.pointerCount) {
if (paths.containsKey(event.getPointerId(i))) {
continue
}
val index = pointerIdToIndex[i] ?: continue
paths[i] = PointerPath(
paths.entries.size,
Vector(event.getX(index), event.getY(index))
)
}
for (i in 0..<event.pointerCount) {
val index = pointerIdToIndex[i] ?: continue
for (j in 0..<event.historySize) {
paths[i]?.update(
Vector(
event.getHistoricalX(index, j),
event.getHistoricalY(index, j)
)
)
}
paths[i]?.update(Vector(event.getX(index), event.getY(index)))
}
if (event.actionMasked == MotionEvent.ACTION_UP) {
synchronized(this@TouchGestureDetector) {
// if the long press handler is still running, kill it
longPressHandler.removeCallbacksAndMessages(null)
// if the gesture was already detected as a long click, there is nothing to do
if (cancelled) {
return
}
}
classifyPaths(paths, event.downTime, event.eventTime)
}
return
}
private fun getGestureForDirection(direction: Vector): Gesture? {
return if (ANGULAR_THRESHOLD * abs(direction.x) > abs(direction.y)) { // horizontal swipe
if (direction.x > TOUCH_SLOP)
Gesture.SWIPE_RIGHT
else if (direction.x < -TOUCH_SLOP)
Gesture.SWIPE_LEFT
else null
} else if (ANGULAR_THRESHOLD * abs(direction.y) > abs(direction.x)) { // vertical swipe
if (direction.y < -TOUCH_SLOP)
Gesture.SWIPE_UP
else if (direction.y > TOUCH_SLOP)
Gesture.SWIPE_DOWN
else null
} else null
}
private fun classifyPaths(paths: Map<Int, PointerPath>, timeStart: Long, timeEnd: Long) {
val duration = timeEnd - timeStart
val pointerCount = paths.entries.size
if (paths.entries.isEmpty()) {
return
}
val mainPointerPath = paths.entries.firstOrNull { it.value.number == 0 }?.value ?: return
// Ignore swipes starting at the very top and the very bottom
if (paths.entries.any { it.value.startIntersectsSystemGestureInsets() }) {
return
}
if (pointerCount == 1 && mainPointerPath.isTap()) {
// detect taps
if (duration in 0..TAP_TIMEOUT) {
if (timeStart - lastTappedTime < DOUBLE_TAP_TIMEOUT &&
lastTappedLocation?.let {
(mainPointerPath.last - it).absSquared() < DOUBLE_TAP_SLOP_SQUARE
} == true
) {
Gesture.DOUBLE_CLICK.invoke(context)
} else {
lastTappedTime = timeEnd
lastTappedLocation = mainPointerPath.last
}
}
} else {
// detect swipes
val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe()
val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe()
var gesture = getGestureForDirection(mainPointerPath.getDirection())
if (doubleActions && pointerCount > 1) {
if (paths.entries.any { getGestureForDirection(it.value.getDirection()) != gesture }) {
// the directions of the pointers don't match
return
}
gesture = gesture?.let(Gesture::getDoubleVariant)
}
// detect triangles
val startEndMin = mainPointerPath.start.min(mainPointerPath.last)
val startEndMax = mainPointerPath.start.max(mainPointerPath.last)
when (gesture) {
Gesture.SWIPE_DOWN -> {
if (startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
gesture = Gesture.SWIPE_LARGER
} else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) {
gesture = Gesture.SWIPE_SMALLER
}
}
Gesture.SWIPE_UP -> {
if (startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
gesture = Gesture.SWIPE_LARGER_REVERSE
} else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) {
gesture = Gesture.SWIPE_SMALLER_REVERSE
}
}
Gesture.SWIPE_RIGHT -> {
if (startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
gesture = Gesture.SWIPE_V
} else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) {
gesture = Gesture.SWIPE_LAMBDA
}
}
Gesture.SWIPE_LEFT -> {
if (startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
gesture = Gesture.SWIPE_V_REVERSE
} else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) {
gesture = Gesture.SWIPE_LAMBDA_REVERSE
}
}
else -> {}
}
if (edgeActions) {
if (mainPointerPath.max.x < edgeWidth * width) {
gesture = gesture?.getEdgeVariant(Gesture.Edge.LEFT)
} else if (mainPointerPath.min.x > (1 - edgeWidth) * width) {
gesture = gesture?.getEdgeVariant(Gesture.Edge.RIGHT)
}
if (mainPointerPath.max.y < edgeWidth * height) {
gesture = gesture?.getEdgeVariant(Gesture.Edge.TOP)
} else if (mainPointerPath.min.y > (1 - edgeWidth) * height) {
gesture = gesture?.getEdgeVariant(Gesture.Edge.BOTTOM)
}
}
if (timeStart - lastTappedTime < 2 * DOUBLE_TAP_TIMEOUT) {
gesture = gesture?.getTapComboVariant()
}
gesture?.invoke(context)
}
}
fun updateScreenSize(windowManager: WindowManager) {
val displayMetrics = DisplayMetrics()
@Suppress("deprecation") // required to support API < 30
windowManager.defaultDisplay.getMetrics(displayMetrics)
width = displayMetrics.widthPixels
height = displayMetrics.heightPixels
}
@RequiresApi(Build.VERSION_CODES.Q)
fun setSystemGestureInsets(insets: Insets) {
systemGestureInsetTop = insets.top
systemGestureInsetBottom = insets.bottom
systemGestureInsetLeft = insets.left
systemGestureInsetRight = insets.right
}
}

View file

@ -1,103 +0,0 @@
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)
}
}
}

View file

@ -1,270 +0,0 @@
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
}
}
}

View file

@ -1,245 +0,0 @@
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()
}
}

View file

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

View file

@ -1,157 +0,0 @@
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())
}
}
}

View file

@ -1,83 +0,0 @@
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)
}
}
}

View file

@ -1,136 +0,0 @@
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
}
}

View file

@ -1,73 +0,0 @@
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()
}
}
}
}

View file

@ -1,183 +0,0 @@
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)
}
}

View file

@ -1,117 +0,0 @@
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()
}
}

View file

@ -1,138 +0,0 @@
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
}
}

View file

@ -1,148 +0,0 @@
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()
}
}
}

View file

@ -1,31 +0,0 @@
package de.jrpie.android.launcher.ui.tutorial.tabs
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.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()
}
}

View file

@ -1,34 +0,0 @@
package de.jrpie.android.launcher.ui.tutorial.tabs
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.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()
}
}

View file

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

View file

@ -1,50 +0,0 @@
package de.jrpie.android.launcher.ui.tutorial.tabs
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.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()
}
}

View file

@ -1,19 +0,0 @@
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()
}
}

View file

@ -1,5 +0,0 @@
<?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