Compare commits

..

84 commits

Author SHA1 Message Date
a4fcdf60c7
add widget panels (see ) 2025-04-26 21:52:21 +02:00
ffaaba7abb
fix 2025-04-25 11:25:00 +02:00
e7a06c443d
add support for app widgets (see ) 2025-04-24 14:37:44 +02:00
077bd1ce44
add option to hide keyboard when scrolling (cf. ) 2025-04-23 01:51:58 +02:00
22633bdac3
try to fix 2025-04-15 19:24:23 +02:00
4f795289d5
improve English translation 2025-04-15 18:55:13 +02:00
2774b74d9d
0.1.4 2025-04-15 18:38:00 +02:00
3d49ec16a7
Merge pull request from toolatebot/weblate-jrpie-launcher-launcher
Translations update from Toolate
2025-04-13 16:27:29 +02:00
letterhaven
a0b2417363 Translated using Weblate (Arabic)
Currently translated at 99.6% (257 of 258 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/ar/
2025-04-13 14:07:19 +00:00
letterhaven
20a01e9f03 Added translation using Weblate (Arabic) 2025-04-13 14:07:19 +00:00
Lukas Hamm
24250ad345 Translated using Weblate (Lithuanian)
Currently translated at 5.4% (14 of 258 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/lt/
2025-04-13 14:07:18 +00:00
Lukas Hamm
7cce425339 Added translation using Weblate (Lithuanian) 2025-04-13 14:07:18 +00:00
class0068
0877ca6772 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 14.2% (3 of 21 strings)

Translation: jrpie-Launcher/metadata
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/zh_Hans/
2025-04-13 14:07:18 +00:00
class0068
03a9833b51 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (258 of 258 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/
2025-04-13 14:07:18 +00:00
Vossa Excelencia
ce65741717 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (258 of 258 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-04-13 14:07:18 +00:00
class0068
c085087e1e Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (257 of 257 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/
2025-04-13 14:07:18 +00:00
class0068
cbd23159da Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 98.8% (254 of 257 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/
2025-04-13 14:07:18 +00:00
Vossa Excelencia
940e5785dc Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (254 of 254 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-04-13 14:07:18 +00:00
class0068
14ffbd1f6c Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (254 of 254 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/
2025-04-13 14:07:18 +00:00
Vossa Excelencia
bfc84b57ca Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (254 of 254 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-04-13 14:07:18 +00:00
T
8b1963f3e1 Translated using Weblate (Spanish)
Currently translated at 98.0% (249 of 254 strings)

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/es/
2025-04-13 14:07:18 +00:00
toolatebot
4f801427a4 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/
2025-04-13 14:07:18 +00:00
class0068
8a487eb4c7 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (254 of 254 strings)

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

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/
2025-04-13 14:07:18 +00:00
0441b3fd3d
set max width for choose app button; change label from scegliere l'applicazione to scegliere in Italian 2025-04-13 14:54:11 +02:00
e7c1d28576
upgrade AGP 2025-04-13 14:40:57 +02:00
653d16b269
new action: launch other launchers 2025-03-29 21:09:15 +01:00
5d695ec0ea
fix 2025-03-29 18:45:53 +01:00
b4608ef153
add new action: show recent apps 2025-03-24 13:21:58 +01:00
8e140e2e69
rename tab "Apps" to "Actions" and "Volume Up/Down" to "Volume Up/Down Key" 2025-03-20 16:23:01 +01:00
7fc58fe384
0.1.3 2025-03-20 15:52:12 +01:00
54409b6312
fix 2025-03-20 14:55:22 +01:00
865cd47583
0.1.2 2025-03-20 14:16:29 +01:00
58ddd3c8cc
fix - revert part of 9043461 as ViewPager2 causes an issue with opening the keyboard 2025-03-20 14:09:00 +01:00
0baa889de5
0.1.1 2025-03-19 17:55:35 +01:00
fa34cbae90
Merge pull request from toolatebot/weblate-jrpie-launcher-launcher
Translations update from Toolate
2025-03-19 17:05:33 +01:00
7ac09bd465 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (254 of 254 strings)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Translation: jrpie-Launcher/Launcher
Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/
2025-03-05 00:07:17 +00:00
9fe1a37ed6
implement : show pinned shortcuts in app list 2025-03-05 00:31:43 +01:00
1f825d6f00
implement : option to reverse app list 2025-03-04 16:49:29 +01:00
5ea03d39fa
fix blurred text in dialogs 2025-03-03 21:26:39 +01:00
ae119ac4ce
remove unused code 2025-03-03 21:23:16 +01:00
941b06b258
minor reformatting 2025-02-22 03:03:52 +01:00
Hendika N
9935386ad8 Add option to hide navigation bar on home screen 2025-02-22 07:35:28 +07:00
150 changed files with 5375 additions and 1455 deletions
.github
.scripts
app
build.gradle
src/main
AndroidManifest.xml
java/de/jrpie/android/launcher
res

4
.github/FUNDING.yml vendored
View file

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

View file

@ -1,5 +1,5 @@
#!/bin/bash
export JAVA_HOME="/usr/lib/jvm/java-23-openjdk/"
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"

View file

@ -23,8 +23,8 @@ android {
minSdkVersion 21
targetSdkVersion 35
compileSdk 35
versionCode 39
versionName "0.0.23"
versionCode 44
versionName "0.1.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -85,17 +85,17 @@ android {
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
lintOptions {
lint {
abortOnError false
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.activity:activity:1.8.0'
implementation 'androidx.activity:activity-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
@ -106,6 +106,7 @@ dependencies {
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"
implementation 'androidx.activity:activity:1.10.1'
annotationProcessor "eu.jonahbauer:android-preference-annotations:1.1.2"
annotationProcessor "com.android.databinding:compiler:$android_plugin_version"
testImplementation 'junit:junit:4.13.2'

View file

@ -8,6 +8,7 @@
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_HIDDEN_PROFILES" />
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
<uses-permission android:name="android.permission.BIND_APPWIDGET" />
<application
android:name=".Application"
@ -19,6 +20,19 @@
android:supportsRtl="true"
android:theme="@style/launcherBaseTheme"
tools:ignore="UnusedAttribute">
<activity
android:name=".ui.widgets.manage.ManageWidgetPanelsActivity"
android:exported="false" />
<activity
android:name=".ui.widgets.WidgetPanelActivity"
android:exported="false" />
<activity
android:name=".ui.widgets.manage.ManageWidgetsActivity"
android:exported="false"
android:theme="@style/launcherHomeTheme" />
<activity
android:name=".ui.widgets.manage.SelectWidgetActivity"
android:exported="false" />
<activity
android:name=".ui.PinShortcutActivity"
android:autoRemoveFromRecents="true"
@ -85,7 +99,7 @@
<service
android:name=".actions.lock.LauncherAccessibilityService"
android:exported="true"
android:label="@string/accessibility_service_name"
android:label="@string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />

View file

@ -7,24 +7,37 @@ import android.content.IntentFilter
import android.content.SharedPreferences
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
import android.os.AsyncTask
import android.os.Build
import android.os.Build.VERSION_CODES
import android.os.UserHandle
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import androidx.preference.PreferenceManager
import de.jrpie.android.launcher.actions.TorchManager
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.DetailedAppInfo
import de.jrpie.android.launcher.apps.AbstractAppInfo
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
import de.jrpie.android.launcher.apps.isPrivateSpaceLocked
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion
import de.jrpie.android.launcher.preferences.resetPreferences
import de.jrpie.android.launcher.widgets.LauncherWidgetProvider
import de.jrpie.android.launcher.widgets.Widget
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
const val APP_WIDGET_HOST_ID = 42;
class Application : android.app.Application() {
val apps = MutableLiveData<List<DetailedAppInfo>>()
val apps = MutableLiveData<List<AbstractDetailedAppInfo>>()
val widgets = MutableLiveData<Set<Widget>>()
val privateSpaceLocked = MutableLiveData<Boolean>()
lateinit var appWidgetHost: AppWidgetHost
lateinit var appWidgetManager: AppWidgetManager
private val profileAvailabilityBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
@ -82,10 +95,14 @@ class Application : android.app.Application() {
}
var torchManager: TorchManager? = null
private var customAppNames: HashMap<AppInfo, String>? = null
private var customAppNames: HashMap<AbstractAppInfo, String>? = null
private val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, pref ->
if (pref == getString(R.string.settings_apps_custom_names_key)) {
customAppNames = LauncherPreferences.apps().customNames()
} else if (pref == LauncherPreferences.apps().keys().pinnedShortcuts()) {
loadApps()
} else if (pref == LauncherPreferences.widgets().keys().widgets()) {
widgets.postValue(LauncherPreferences.widgets().widgets() ?: setOf())
}
}
@ -99,19 +116,22 @@ class Application : android.app.Application() {
torchManager = TorchManager(this)
}
appWidgetHost = AppWidgetHost(this.applicationContext, APP_WIDGET_HOST_ID)
appWidgetManager = AppWidgetManager.getInstance(this.applicationContext)
appWidgetHost.startListening()
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
LauncherPreferences.init(preferences, this.resources)
// Try to restore old preferences
migratePreferencesToNewVersion(this)
// First time opening the app: set defaults and start tutorial
// First time opening the app: set defaults
// The tutorial is started from HomeActivity#onStart, as starting it here is blocked by android
if (!LauncherPreferences.internal().started()) {
resetPreferences(this)
LauncherPreferences.internal().started(true)
openTutorial(this)
}
@ -132,7 +152,8 @@ class Application : android.app.Application() {
it.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
}
}
ContextCompat.registerReceiver(this, profileAvailabilityBroadcastReceiver, filter,
ContextCompat.registerReceiver(
this, profileAvailabilityBroadcastReceiver, filter,
ContextCompat.RECEIVER_EXPORTED
)
}
@ -143,13 +164,21 @@ class Application : android.app.Application() {
loadApps()
}
fun getCustomAppNames(): HashMap<AppInfo, String> {
fun getCustomAppNames(): HashMap<AbstractAppInfo, String> {
return (customAppNames ?: LauncherPreferences.apps().customNames() ?: HashMap())
.also { customAppNames = it }
}
private fun loadApps() {
privateSpaceLocked.postValue(isPrivateSpaceLocked(this))
AsyncTask.execute { apps.postValue(getApps(packageManager, applicationContext)) }
CoroutineScope(Dispatchers.Default).launch {
apps.postValue(getApps(packageManager, applicationContext))
}
}
override fun onTerminate() {
appWidgetHost.stopListening()
super.onTerminate()
}
}

View file

@ -6,13 +6,15 @@ import android.app.role.RoleManager
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.content.pm.LauncherApps.ShortcutQuery
import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.UserHandle
@ -24,24 +26,23 @@ 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.actions.shortcuts.PinnedShortcutInfo
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.DetailedAppInfo
import de.jrpie.android.launcher.apps.DetailedPinnedShortcutInfo
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
import de.jrpie.android.launcher.apps.getPrivateSpaceUser
import de.jrpie.android.launcher.apps.isPrivateSpaceSupported
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
import androidx.core.net.toUri
/* REQUEST CODES */
const val REQUEST_CHOOSE_APP = 1
const val REQUEST_UNINSTALL = 2
const val REQUEST_SET_DEFAULT_HOME = 42
const val LOG_TAG = "Launcher"
const val REQUEST_SET_DEFAULT_HOME = 42
fun isDefaultHomeScreen(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val roleManager = context.getSystemService(RoleManager::class.java)
@ -63,7 +64,7 @@ fun setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
&& context is Activity
&& !isDefault // using role manager only works when µLauncher is not already the default.
&& checkDefault // using role manager only works when µLauncher is not already the default.
) {
val roleManager = context.getSystemService(RoleManager::class.java)
context.startActivityForResult(
@ -90,18 +91,24 @@ fun getUserFromId(userId: Int?, context: Context): UserHandle {
fun removeUnusedShortcuts(context: Context) {
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
fun getShortcuts(profile: UserHandle): List<ShortcutInfo>? {
return launcherApps.getShortcuts(
ShortcutQuery().apply {
setQueryFlags(ShortcutQuery.FLAG_MATCH_PINNED)
},
profile
)
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: Set<PinnedShortcutInfo> =
val boundActions: MutableSet<PinnedShortcutInfo> =
Gesture.entries.mapNotNull { Action.forGesture(it) as? ShortcutAction }.map { it.shortcut }
.toSet()
.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) ->
@ -116,7 +123,7 @@ fun removeUnusedShortcuts(context: Context) {
}
fun openInBrowser(url: String, context: Context) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
intent.putExtras(Bundle().apply { putBoolean("new_window", true) })
try {
context.startActivity(intent)
@ -126,18 +133,19 @@ fun openInBrowser(url: String, context: Context) {
}
fun openTutorial(context: Context) {
context.startActivity(Intent(context, TutorialActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
context.startActivity(Intent(context, TutorialActivity::class.java))
}
/**
* Load all apps.
*/
fun getApps(packageManager: PackageManager, context: Context): MutableList<DetailedAppInfo> {
val start = System.currentTimeMillis()
val loadList = mutableListOf<DetailedAppInfo>()
fun getApps(
packageManager: PackageManager,
context: Context
): MutableList<AbstractDetailedAppInfo> {
var start = System.currentTimeMillis()
val loadList = mutableListOf<AbstractDetailedAppInfo>()
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
val userManager = context.getSystemService(Service.USER_SERVICE) as UserManager
@ -174,7 +182,7 @@ fun getApps(packageManager: PackageManager, context: Context): MutableList<Detai
i.addCategory(Intent.CATEGORY_LAUNCHER)
val allApps = packageManager.queryIntentActivities(i, 0)
for (ri in allApps) {
val app = AppInfo(ri.activityInfo.packageName, null, AppInfo.INVALID_USER)
val app = AppInfo(ri.activityInfo.packageName, null, INVALID_USER)
val detailedAppInfo = DetailedAppInfo(
app,
ri.loadLabel(packageManager),
@ -184,22 +192,24 @@ fun getApps(packageManager: PackageManager, context: Context): MutableList<Detai
loadList.add(detailedAppInfo)
}
}
loadList.sortBy { it.getCustomLabel(context).toString() }
loadList.sortBy { it.getCustomLabel(context) }
val end = System.currentTimeMillis()
var end = System.currentTimeMillis()
Log.i(LOG_TAG, "${loadList.size} apps loaded (${end - start}ms)")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
start = System.currentTimeMillis()
LauncherPreferences.apps().pinnedShortcuts()
?.mapNotNull { DetailedPinnedShortcutInfo.fromPinnedShortcutInfo(it, context) }
?.let {
end = System.currentTimeMillis()
Log.i(LOG_TAG, "${it.size} shortcuts loaded (${end - start}ms)")
loadList.addAll(it)
}
}
return loadList
}
// Used in Tutorial and Settings `ActivityOnResult`
fun saveListActivityChoice(data: Intent?) {
val forGesture = data?.getStringExtra("forGesture") ?: return
Gesture.byId(forGesture)?.let { Action.setActionForGesture(it, Action.fromIntent(data)) }
}
// used for the bug report button
fun getDeviceInfo(): String {
return """
@ -216,4 +226,4 @@ fun copyToClipboard(context: Context, text: String) {
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("Debug Info", text)
clipboardManager.setPrimaryClip(clipData)
}
}

View file

@ -2,11 +2,11 @@ package de.jrpie.android.launcher.actions
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences.Editor
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.widget.Toast
import androidx.core.content.edit
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.preferences.LauncherPreferences
import kotlinx.serialization.Serializable
@ -14,6 +14,10 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
/**
* Represents an action that can be bound to a [Gesture].
* There are four types of actions: [AppAction], [ShortcutAction], [LauncherAction] and [WidgetPanelAction]
*/
@Serializable
sealed interface Action {
fun invoke(context: Context, rect: Rect? = null): Boolean
@ -21,6 +25,10 @@ sealed interface Action {
fun getIcon(context: Context): Drawable?
fun isAvailable(context: Context): Boolean
fun showConfigurationDialog(context: Context, onSuccess: (Action) -> Unit) {
onSuccess(this)
}
// Can the action be used to reach µLauncher settings?
fun canReachSettings(): Boolean
@ -29,10 +37,6 @@ sealed interface Action {
prefEditor.putString(id, Json.encodeToString(this))
}
fun writeToIntent(intent: Intent) {
intent.putExtra("action", Json.encodeToString(this))
}
companion object {
fun forGesture(gesture: Gesture): Action? {
@ -44,23 +48,23 @@ sealed interface Action {
}
fun resetToDefaultActions(context: Context) {
val editor = LauncherPreferences.getSharedPreferences().edit()
val boundActions = HashSet<String>()
Gesture.entries.forEach { gesture ->
context.resources
.getStringArray(gesture.defaultsResource)
.filterNot { boundActions.contains(it) }
.map { Pair(it, Json.decodeFromString<Action>(it)) }
.firstOrNull { it.second.isAvailable(context) }
?.apply {
// allow to bind CHOOSE to multiple gestures
if (second != LauncherAction.CHOOSE) {
boundActions.add(first)
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)
}
second.bindToGesture(editor, gesture.id)
}
}
}
editor.apply()
}
fun setActionForGesture(gesture: Gesture, action: Action?) {
@ -68,15 +72,15 @@ sealed interface Action {
clearActionForGesture(gesture)
return
}
val editor = LauncherPreferences.getSharedPreferences().edit()
action.bindToGesture(editor, gesture.id)
editor.apply()
LauncherPreferences.getSharedPreferences().edit {
action.bindToGesture(this, gesture.id)
}
}
fun clearActionForGesture(gesture: Gesture) {
LauncherPreferences.getSharedPreferences().edit()
.remove(gesture.id)
.apply()
LauncherPreferences.getSharedPreferences().edit {
remove(gesture.id)
}
}
fun launch(
@ -87,6 +91,9 @@ sealed interface Action {
) {
if (action != null && action.invoke(context)) {
if (context is Activity) {
// There does not seem to be a good alternative to overridePendingTransition.
// Note that we can't use overrideActivityTransition here.
@Suppress("deprecation")
context.overridePendingTransition(animationIn, animationOut)
}
} else {
@ -97,10 +104,5 @@ sealed interface Action {
).show()
}
}
fun fromIntent(data: Intent): Action? {
val json = data.getStringExtra("action") ?: return null
return Json.decodeFromString(json)
}
}
}

View file

@ -11,7 +11,7 @@ import android.graphics.drawable.Drawable
import android.util.Log
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.AppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.apps.DetailedAppInfo
import de.jrpie.android.launcher.ui.list.apps.openSettings
import kotlinx.serialization.SerialName
@ -67,7 +67,7 @@ class AppAction(val app: AppInfo) : Action {
}
override fun getIcon(context: Context): Drawable? {
return DetailedAppInfo.fromAppInfo(app, context)?.icon
return DetailedAppInfo.fromAppInfo(app, context)?.getIcon(context)
}
override fun isAvailable(context: Context): Boolean {

View file

@ -250,7 +250,7 @@ enum class Gesture(
"action.back",
R.string.settings_gesture_back,
R.string.settings_gesture_description_back,
R.array.default_up
R.array.default_back
);
enum class Edge {

View file

@ -11,8 +11,11 @@ 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
@ -66,7 +69,11 @@ enum class LauncherAction(
R.string.list_other_list_private_space,
R.drawable.baseline_security_24,
{ context ->
openAppsList(context, private = true)
if ((context.applicationContext as Application).privateSpaceLocked.value != true
|| !hidePrivateSpaceWhenLocked(context)
) {
openAppsList(context, private = true)
}
},
available = { _ ->
isPrivateSpaceSupported()
@ -83,31 +90,37 @@ enum class LauncherAction(
"volume_up",
R.string.list_other_volume_up,
R.drawable.baseline_volume_up_24,
{ context -> audioVolumeAdjust(context, true)}
{ 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, false)}
{ 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)}
{ 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)}
{ 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)}
{ context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PREVIOUS) }
),
EXPAND_NOTIFICATIONS_PANEL(
"expand_notifications_panel",
@ -121,6 +134,14 @@ enum class LauncherAction(
R.drawable.baseline_settings_applications_24,
::expandSettingsPanel
),
RECENT_APPS(
"recent_apps",
R.string.list_other_recent_apps,
R.drawable.baseline_apps_24,
LauncherAccessibilityService::openRecentApps,
false,
{ _ -> BuildConfig.USE_ACCESSIBILITY_SERVICE }
),
LOCK_SCREEN(
"lock_screen",
R.string.list_other_lock_screen,
@ -131,7 +152,13 @@ enum class LauncherAction(
"toggle_torch",
R.string.list_other_torch,
R.drawable.baseline_flashlight_on_24,
::toggleTorch
::toggleTorch,
),
LAUNCH_OTHER_LAUNCHER(
"launcher_other_launcher",
R.string.list_other_launch_other_launcher,
R.drawable.baseline_home_24,
::launchOtherLauncher
),
NOP("nop", R.string.list_other_nop, R.drawable.baseline_not_interested_24, {});
@ -176,17 +203,13 @@ private fun audioManagerPressKey(context: Context, key: Int) {
}
private fun audioVolumeAdjust(context: Context, louder: Boolean) {
private fun audioVolumeAdjust(context: Context, direction: Int) {
val audioManager =
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
audioManager.adjustStreamVolume(
AudioManager.STREAM_MUSIC,
if (louder) {
AudioManager.ADJUST_RAISE
} else {
AudioManager.ADJUST_LOWER
},
direction,
AudioManager.FLAG_SHOW_UI
)
}
@ -241,6 +264,15 @@ private fun expandSettingsPanel(context: Context) {
}
}
private fun launchOtherLauncher(context: Context) {
context.startActivity(
Intent.createChooser(
Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) },
context.getString(R.string.list_other_launch_other_launcher)
)
)
}
private fun openSettings(context: Context) {
context.startActivity(Intent(context, SettingsActivity::class.java))
}

View file

@ -6,7 +6,7 @@ import android.content.pm.LauncherApps
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.os.Build
import de.jrpie.android.launcher.actions.shortcuts.PinnedShortcutInfo
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View file

@ -0,0 +1,83 @@
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.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.ui.widgets.WidgetPanelActivity
import de.jrpie.android.launcher.ui.widgets.manage.EXTRA_PANEL_ID
import de.jrpie.android.launcher.ui.widgets.manage.WidgetPanelsRecyclerAdapter
import de.jrpie.android.launcher.widgets.WidgetPanel
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("action:panel")
class WidgetPanelAction(val widgetPanelId: Int) : Action {
override fun invoke(context: Context, rect: Rect?): Boolean {
if (WidgetPanel.byId(widgetPanelId) == null) {
Toast.makeText(context, R.string.alert_widget_panel_not_found, Toast.LENGTH_LONG).show()
} else {
context.startActivity(Intent(context, WidgetPanelActivity::class.java).also {
it.putExtra(EXTRA_PANEL_ID, widgetPanelId)
})
}
return true
}
override fun label(context: Context): String {
return WidgetPanel.byId(widgetPanelId)?.label
?: context.getString(R.string.list_other_open_widget_panel)
}
override fun isAvailable(context: Context): Boolean {
return true
}
override fun canReachSettings(): Boolean {
return false
}
override fun getIcon(context: Context): Drawable? {
return ResourcesCompat.getDrawable(
context.resources,
R.drawable.baseline_widgets_24,
context.theme
)
}
override fun showConfigurationDialog(context: Context, onSuccess: (Action) -> Unit) {
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
setTitle(R.string.dialog_select_widget_panel_title)
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
setView(R.layout.dialog_select_widget_panel)
}.create().also { it.show() }.also { alertDialog ->
val infoTextView =
alertDialog.findViewById<TextView>(R.id.dialog_select_widget_panel_info)
alertDialog.findViewById<RecyclerView>(R.id.dialog_select_widget_panel_recycler)
?.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(alertDialog.context)
adapter =
WidgetPanelsRecyclerAdapter(alertDialog.context, false) { widgetPanel ->
onSuccess(WidgetPanelAction(widgetPanel.id))
alertDialog.dismiss()
}
if (adapter?.itemCount == 0) {
infoTextView?.visibility = View.VISIBLE
}
}
}
true
}
}

View file

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

View file

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

View file

@ -1,60 +0,0 @@
package de.jrpie.android.launcher.actions.shortcuts
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.Serializable
@RequiresApi(Build.VERSION_CODES.N_MR1)
@Serializable
class PinnedShortcutInfo(
val id: String,
val packageName: String,
val activityName: String,
val user: Int
) {
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 launcherApps.getShortcuts(
ShortcutQuery().apply {
setQueryFlags(ShortcutQuery.FLAG_MATCH_PINNED)
setPackage(packageName)
setActivity(ComponentName(packageName, activityName))
setShortcutIds(listOf(id))
},
getUserFromId(user, context)
)?.firstOrNull()
}
override fun equals(other: Any?): Boolean {
return (other as? PinnedShortcutInfo)?.let {
packageName == this.packageName &&
activityName == this.activityName &&
id == this.id &&
user == this.user
} ?: false
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + packageName.hashCode()
result = 31 * result + activityName.hashCode()
result = 31 * result + user
return result
}
override fun toString(): String {
return "PinnedShortcutInfo { package=$packageName, activity=$activityName, user=$user, id=$id}"
}
}

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import android.os.Build
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.AppAction
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.ShortcutAction
import de.jrpie.android.launcher.preferences.LauncherPreferences
import java.util.Locale
import kotlin.text.Regex.Companion.escape
@ -18,13 +19,14 @@ class AppFilter(
var privateSpaceVisibility: AppSetVisibility = AppSetVisibility.VISIBLE
) {
operator fun invoke(apps: List<DetailedAppInfo>): List<DetailedAppInfo> {
operator fun invoke(apps: List<AbstractDetailedAppInfo>): List<AbstractDetailedAppInfo> {
var apps =
apps.sortedBy { app -> app.getCustomLabel(context).toString().lowercase(Locale.ROOT) }
apps.sortedBy { app -> app.getCustomLabel(context).lowercase(Locale.ROOT) }
val hidden = LauncherPreferences.apps().hidden() ?: setOf()
val favorites = LauncherPreferences.apps().favorites() ?: setOf()
val private = apps.filter { it.isPrivateSpaceApp }.map { it.app }.toSet()
val private = apps.filter { it.isPrivate() }
.map { it.getRawInfo() }.toSet()
apps = apps.filter { info ->
favoritesVisibility.predicate(favorites, info)
@ -35,9 +37,13 @@ class AppFilter(
if (LauncherPreferences.apps().hideBoundApps()) {
val boundApps = Gesture.entries
.filter(Gesture::isEnabled)
.mapNotNull { g -> (Action.forGesture(g) as? AppAction)?.app }
.mapNotNull { g -> Action.forGesture(g) }
.mapNotNull {
(it as? AppAction)?.app
?: (it as? ShortcutAction)?.shortcut
}
.toSet()
apps = apps.filterNot { info -> boundApps.contains(info.app) }
apps = apps.filterNot { info -> boundApps.contains(info.getRawInfo()) }
}
// normalize text for search
@ -57,11 +63,11 @@ class AppFilter(
if (query.isEmpty()) {
return apps
} else {
val r: MutableList<DetailedAppInfo> = ArrayList()
val appsSecondary: MutableList<DetailedAppInfo> = ArrayList()
val r: MutableList<AbstractDetailedAppInfo> = ArrayList()
val appsSecondary: MutableList<AbstractDetailedAppInfo> = ArrayList()
val normalizedQuery: String = normalize(query)
for (item in apps) {
val itemLabel: String = normalize(item.getCustomLabel(context).toString())
val itemLabel: String = normalize(item.getCustomLabel(context))
if (itemLabel.startsWith(normalizedQuery)) {
r.add(item)
@ -77,11 +83,11 @@ class AppFilter(
companion object {
enum class AppSetVisibility(
val predicate: (set: Set<AppInfo>, DetailedAppInfo) -> Boolean
val predicate: (set: Set<AbstractAppInfo>, AbstractDetailedAppInfo) -> Boolean
) {
VISIBLE({ _, _ -> true }),
HIDDEN({ set, appInfo -> !set.contains(appInfo.app) }),
EXCLUSIVE({ set, appInfo -> set.contains(appInfo.app) }),
HIDDEN({ set, appInfo -> !set.contains(appInfo.getRawInfo()) }),
EXCLUSIVE({ set, appInfo -> set.contains(appInfo.getRawInfo()) }),
;
}

View file

@ -4,33 +4,18 @@ import android.app.Service
import android.content.Context
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.getUserFromId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
/**
* Represents an app installed on the users device.
* Contains the minimal amount of data required to identify the app.
*/
@Serializable
class AppInfo(val packageName: String, val activityName: String?, val user: Int = INVALID_USER) {
fun serialize(): String {
return Json.encodeToString(this)
}
override fun equals(other: Any?): Boolean {
if(other is AppInfo) {
return other.user == user && other.packageName == packageName
&& other.activityName == activityName
}
return super.equals(other)
}
override fun hashCode(): Int {
return packageName.hashCode()
}
@SerialName("app")
data class AppInfo(val packageName: String, val activityName: String?, val user: Int = INVALID_USER): AbstractAppInfo {
fun getLauncherActivityInfo(
context: Context
@ -41,17 +26,4 @@ class AppInfo(val packageName: String, val activityName: String?, val user: Int
return activityList.firstOrNull { app -> app.name == activityName }
?: activityList.firstOrNull()
}
override fun toString(): String {
return "AppInfo {package=$packageName, activity=$activityName, user=$user}"
}
companion object {
const val INVALID_USER = -1
fun deserialize(serialized: String): AppInfo {
return Json.decodeFromString(serialized)
}
}
}

View file

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

View file

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

View file

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

View file

@ -91,10 +91,17 @@ fun isPrivateSpaceLocked(context: Context): Boolean {
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)
@ -116,3 +123,18 @@ fun togglePrivateSpaceLock(context: Context) {
}
}
@Suppress("SameReturnValue")
fun hidePrivateSpaceWhenLocked(context: Context): Boolean {
// Trying to access the setting as a 3rd party launcher raises a security exception.
// This is an Android bug: https://issuetracker.google.com/issues/352276244#comment5
// The logic for this is implemented.
// TODO: replace this once the Android bug is fixed
return false
// TODO: perhaps this should be cached
// https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Launcher3/src/com/android/launcher3/util/SettingsCache.java;l=61;drc=56bf7ad33bc9d5ed3c18e7abefeec5c177ec75d7
// val key = "hide_privatespace_entry_point"
// return Settings.Secure.getInt(context.contentResolver, key, 0) == 1
}

View file

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

View file

@ -5,8 +5,11 @@ 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.MapAppInfoStringPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.SetAppInfoPreferenceSerializer;
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.serialization.SetWidgetPanelSerializer;
import de.jrpie.android.launcher.preferences.serialization.SetWidgetSerializer;
import de.jrpie.android.launcher.preferences.theme.Background;
import de.jrpie.android.launcher.preferences.theme.ColorTheme;
import de.jrpie.android.launcher.preferences.theme.Font;
@ -20,20 +23,24 @@ import eu.jonahbauer.android.preference.annotations.Preferences;
r = R.class,
value = {
@PreferenceGroup(name = "internal", prefix = "settings_internal_", suffix = "_key", value = {
// set after the user finished the tutorial
@Preference(name = "started", type = boolean.class, defaultValue = "false"),
@Preference(name = "started_time", type = long.class),
// see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt
@Preference(name = "version_code", type = int.class, defaultValue = "-1"),
}),
@PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = {
@Preference(name = "favorites", type = Set.class, serializer = SetAppInfoPreferenceSerializer.class),
@Preference(name = "hidden", type = Set.class, serializer = SetAppInfoPreferenceSerializer.class),
@Preference(name = "custom_names", type = HashMap.class, serializer = MapAppInfoStringPreferenceSerializer.class),
@Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class),
@Preference(name = "hidden", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class),
@Preference(name = "pinned_shortcuts", type = Set.class, serializer = SetPinnedShortcutInfoPreferenceSerializer.class),
@Preference(name = "custom_names", type = HashMap.class, serializer = MapAbstractAppInfoStringPreferenceSerializer.class),
@Preference(name = "hide_bound_apps", type = boolean.class, defaultValue = "false"),
@Preference(name = "hide_paused_apps", type = boolean.class, defaultValue = "false"),
@Preference(name = "hide_private_space_apps", type = boolean.class, defaultValue = "false"),
}),
@PreferenceGroup(name = "list", prefix = "settings_list_", suffix = "_key", value = {
@Preference(name = "layout", type = ListLayout.class, defaultValue = "DEFAULT")
@Preference(name = "layout", type = ListLayout.class, defaultValue = "DEFAULT"),
@Preference(name = "reverse_layout", type = boolean.class, defaultValue = "false")
}),
@PreferenceGroup(name = "gestures", prefix = "settings_gesture_", suffix = "_key", value = {
}),
@ -59,13 +66,15 @@ import eu.jonahbauer.android.preference.annotations.Preferences;
}),
@PreferenceGroup(name = "display", prefix = "settings_display_", suffix = "_key", value = {
@Preference(name = "screen_timeout_disabled", type = boolean.class, defaultValue = "false"),
@Preference(name = "full_screen", type = boolean.class, defaultValue = "true"),
@Preference(name = "hide_status_bar", type = boolean.class, defaultValue = "true"),
@Preference(name = "hide_navigation_bar", type = boolean.class, defaultValue = "false"),
@Preference(name = "rotate_screen", type = boolean.class, defaultValue = "true"),
}),
@PreferenceGroup(name = "functionality", prefix = "settings_functionality_", suffix = "_key", value = {
@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"),
@Preference(name = "search_auto_close_keyboard", type = boolean.class, defaultValue = "false"),
}),
@PreferenceGroup(name = "enabled_gestures", prefix = "settings_enabled_gestures_", suffix = "_key", value = {
@Preference(name = "double_swipe", type = boolean.class, defaultValue = "true"),
@ -75,5 +84,9 @@ import eu.jonahbauer.android.preference.annotations.Preferences;
@PreferenceGroup(name = "actions", prefix = "settings_actions_", suffix = "_key", value = {
@Preference(name = "lock_method", type = LockMethod.class, defaultValue = "DEVICE_ADMIN"),
}),
@PreferenceGroup(name = "widgets", prefix = "settings_widgets_", suffix= "_key", value = {
@Preference(name = "widgets", type = Set.class, serializer = SetWidgetSerializer.class),
@Preference(name = "custom_panels", type = Set.class, serializer = SetWidgetPanelSerializer.class)
}),
})
public final class LauncherPreferences$Config {}

View file

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

View file

@ -2,20 +2,29 @@ package de.jrpie.android.launcher.preferences
import android.content.Context
import android.util.Log
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.apps.AbstractAppInfo
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.apps.AppInfo
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.migratePreferencesFromVersion4
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown
import de.jrpie.android.launcher.ui.HomeActivity
import de.jrpie.android.launcher.widgets.ClockWidget
import de.jrpie.android.launcher.widgets.WidgetPanel
import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.deleteAllWidgets
/* Current version of the structure of preferences.
* Increase when breaking changes are introduced and write an appropriate case in
* `migratePreferencesToNewVersion`
*/
const val PREFERENCE_VERSION = 3
const val PREFERENCE_VERSION = 5
const val UNKNOWN_PREFERENCE_VERSION = -1
private const val TAG = "Launcher - Preferences"
@ -37,13 +46,22 @@ fun migratePreferencesToNewVersion(context: Context) {
}
1 -> {
migratePreferencesFromVersion1()
migratePreferencesFromVersion1(context)
Log.i(TAG, "migration of preferences complete (1 -> ${PREFERENCE_VERSION}).")
}
2 -> {
migratePreferencesFromVersion2()
migratePreferencesFromVersion2(context)
Log.i(TAG, "migration of preferences complete (2 -> ${PREFERENCE_VERSION}).")
}
3 -> {
migratePreferencesFromVersion3(context)
Log.i(TAG, "migration of preferences complete (3 -> ${PREFERENCE_VERSION}).")
}
4 -> {
migratePreferencesFromVersion4(context)
Log.i(TAG, "migration of preferences complete (4 -> ${PREFERENCE_VERSION}).")
}
else -> {
Log.w(
@ -64,18 +82,29 @@ fun resetPreferences(context: Context) {
Log.i(TAG, "Resetting preferences")
LauncherPreferences.clear()
LauncherPreferences.internal().versionCode(PREFERENCE_VERSION)
deleteAllWidgets(context)
LauncherPreferences.widgets().widgets(
setOf(
ClockWidget(
(context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(),
WidgetPosition(1, 3, 10, 4),
WidgetPanel.HOME.id
)
)
)
val hidden: MutableSet<AppInfo> = mutableSetOf()
val hidden: MutableSet<AbstractAppInfo> = mutableSetOf()
val launcher = DetailedAppInfo.fromAppInfo(
AppInfo(
BuildConfig.APPLICATION_ID,
HomeActivity::class.java.name,
AppInfo.INVALID_USER
INVALID_USER
), context
)
launcher?.app?.let { hidden.add(it) }
Log.i(TAG,"Hiding ${launcher?.app}")
launcher?.getRawInfo()?.let { hidden.add(it) }
Log.i(TAG,"Hiding ${launcher?.getRawInfo()}")
LauncherPreferences.apps().hidden(hidden)
Action.resetToDefaultActions(context)

View file

@ -1,19 +1,33 @@
package de.jrpie.android.launcher.preferences.legacy
import android.content.Context
import androidx.core.content.edit
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.AbstractAppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.AppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
import de.jrpie.android.launcher.preferences.serialization.MapAppInfoStringPreferenceSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.json.JSONException
import org.json.JSONObject
@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),
@ -77,7 +91,7 @@ private fun Action.Companion.legacyFromPreference(id: String): Action? {
private fun migrateAppInfoStringMap(key: String) {
val preferences = LauncherPreferences.getSharedPreferences()
MapAppInfoStringPreferenceSerializer().serialize(
serializeMapAppInfo(
preferences.getStringSet(key, setOf())?.mapNotNull { entry ->
try {
val obj = JSONObject(entry)
@ -89,7 +103,7 @@ private fun migrateAppInfoStringMap(key: String) {
}
}?.toMap(HashMap())
)?.let {
preferences.edit().putStringSet(key, it as Set<String>).apply()
preferences.edit { putStringSet(key, it) }
}
}
@ -98,16 +112,16 @@ private fun migrateAppInfoSet(key: String) {
.map(AppInfo.Companion::legacyDeserialize)
.map(AppInfo::serialize)
.toSet()
.let { LauncherPreferences.getSharedPreferences().edit().putStringSet(key, it).apply() }
.let { LauncherPreferences.getSharedPreferences().edit { putStringSet(key, it) } }
}
private fun migrateAction(key: String) {
Action.legacyFromPreference(key)?.let { action ->
LauncherPreferences.getSharedPreferences().edit()
.putString(key, Json.encodeToString(action))
.remove("$key.app")
.remove("$key.user")
.apply()
LauncherPreferences.getSharedPreferences().edit {
putString(key, Json.encodeToString(action))
.remove("$key.app")
.remove("$key.user")
}
}
}
@ -116,7 +130,7 @@ private fun migrateAction(key: String) {
* Migrate preferences from version 1 (used until version j-0.0.18) to the current format
* (see [PREFERENCE_VERSION])
*/
fun migratePreferencesFromVersion1() {
fun migratePreferencesFromVersion1(context: Context) {
assert(LauncherPreferences.internal().versionCode() == 1)
Gesture.entries.forEach { g -> migrateAction(g.id) }
migrateAppInfoSet(LauncherPreferences.apps().keys().hidden())
@ -124,5 +138,5 @@ fun migratePreferencesFromVersion1() {
migrateAppInfoStringMap(LauncherPreferences.apps().keys().customNames())
LauncherPreferences.internal().versionCode(2)
migratePreferencesFromVersion2()
migratePreferencesFromVersion2(context)
}

View file

@ -1,5 +1,6 @@
package de.jrpie.android.launcher.preferences.legacy
import android.content.Context
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction
@ -11,10 +12,10 @@ 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(PREFERENCE_VERSION == 3)
fun migratePreferencesFromVersion2(context: Context) {
assert(LauncherPreferences.internal().versionCode() == 2)
// previously there was no setting for this
Action.setActionForGesture(Gesture.BACK, LauncherAction.CHOOSE)
LauncherPreferences.internal().versionCode(3)
migratePreferencesFromVersion3(context)
}

View file

@ -0,0 +1,85 @@
package de.jrpie.android.launcher.preferences.legacy
import android.content.Context
import android.content.SharedPreferences
import android.content.SharedPreferences.Editor
import androidx.core.content.edit
import de.jrpie.android.launcher.apps.AbstractAppInfo
import de.jrpie.android.launcher.apps.AppInfo
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
/**
* 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(context: Context) {
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)
migratePreferencesFromVersion4(context)
}

View file

@ -0,0 +1,27 @@
package de.jrpie.android.launcher.preferences.legacy
import android.content.Context
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
import de.jrpie.android.launcher.widgets.ClockWidget
import de.jrpie.android.launcher.widgets.WidgetPanel
import de.jrpie.android.launcher.widgets.WidgetPosition
fun migratePreferencesFromVersion4(context: Context) {
assert(PREFERENCE_VERSION == 5)
assert(LauncherPreferences.internal().versionCode() == 4)
LauncherPreferences.widgets().widgets(
setOf(
ClockWidget(
(context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(),
WidgetPosition(1, 3, 10, 4),
WidgetPanel.HOME.id
)
)
)
LauncherPreferences.internal().versionCode(5)
}

View file

@ -3,12 +3,12 @@ package de.jrpie.android.launcher.preferences.legacy
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.core.content.edit
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.theme.Background
import de.jrpie.android.launcher.preferences.theme.ColorTheme
private fun migrateStringPreference(
oldPrefs: SharedPreferences,
newPreferences: SharedPreferences.Editor,
@ -64,318 +64,317 @@ fun migratePreferencesFromVersionUnknown(context: Context) {
return
}
val newPrefs = LauncherPreferences.getSharedPreferences().edit()
LauncherPreferences.getSharedPreferences().edit {
migrateBooleanPreference(
oldPrefs,
newPrefs,
"startedBefore",
"internal.started_before",
false
)
migrateBooleanPreference(
oldPrefs,
this,
"startedBefore",
"internal.started_before",
false
)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_volumeUpApp",
"action.volume_up.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_volumeUpApp_user",
"action.volume_up.user",
-1
)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_volumeDownApp",
"action.volume_down.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_volumeDownApp_user",
"action.volume_down.user",
-1
)
migrateStringPreference(oldPrefs, newPrefs, "action_timeApp", "action.time.app", "")
migrateIntPreference(oldPrefs, newPrefs, "action_timeApp_user", "action.time.user", -1)
migrateStringPreference(oldPrefs, newPrefs, "action_dateApp", "action.date.app", "")
migrateIntPreference(oldPrefs, newPrefs, "action_dateApp_user", "action.date.user", -1)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_longClickApp",
"action.long_click.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_longClickApp_user",
"action.long_click.user",
-1
)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_doubleClickApp",
"action.double_click.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_doubleClickApp_user",
"action.double_click.user",
-1
)
migrateStringPreference(oldPrefs, newPrefs, "action_upApp", "action.up.app", "")
migrateIntPreference(oldPrefs, newPrefs, "action_upApp_user", "action.up.user", -1)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_up_leftApp",
"action.up_left.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_up_leftApp_user",
"action.up_left.user",
-1
)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_up_rightApp",
"action.up_right.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_up_rightApp_user",
"action.up_right.user",
-1
)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_doubleUpApp",
"action.double_up.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_doubleUpApp_user",
"action.double_up.user",
-1
)
migrateStringPreference(oldPrefs, newPrefs, "action_downApp", "action.down.app", "")
migrateIntPreference(oldPrefs, newPrefs, "action_downApp_user", "action.down.user", -1)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_down_leftApp",
"action.down_left.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_down_leftApp_user",
"action.down_left.user",
-1
)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_down_rightApp",
"action.down_right.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_down_rightApp_user",
"action.down_right.user",
-1
)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_doubleDownApp",
"action.double_down.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_doubleDownApp_user",
"action.double_down.user",
-1
)
migrateStringPreference(oldPrefs, newPrefs, "action_leftApp", "action.left.app", "")
migrateIntPreference(oldPrefs, newPrefs, "action_leftApp_user", "action.left.user", -1)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_left_topApp",
"action.left_top.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_left_topApp_user",
"action.left_top.user",
-1
)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_left_bottomApp",
"action.left_bottom.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_left_bottomApp_user",
"action.left_bottom.user",
-1
)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_doubleLeftApp",
"action.double_left.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_doubleLeftApp_user",
"action.double_left.user",
-1
)
migrateStringPreference(oldPrefs, newPrefs, "action_rightApp", "action.right.app", "")
migrateIntPreference(
oldPrefs,
newPrefs,
"action_rightApp_user",
"action.right.user",
-1
)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_right_topApp",
"action.right_top.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_right_topApp_user",
"action.right_top.user",
-1
)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_right_bottomApp",
"action.right_bottom.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_right_bottomApp_user",
"action.right_bottom.user",
-1
)
migrateStringPreference(
oldPrefs,
newPrefs,
"action_doubleRightApp",
"action.double_right.app",
""
)
migrateIntPreference(
oldPrefs,
newPrefs,
"action_doubleRightApp_user",
"action.double_right.user",
-1
)
migrateBooleanPreference(oldPrefs, newPrefs, "timeVisible", "clock.time_visible", true)
migrateBooleanPreference(oldPrefs, newPrefs, "dateVisible", "clock.date_visible", true)
migrateBooleanPreference(
oldPrefs,
newPrefs,
"dateLocalized",
"clock.date_localized",
false
)
migrateBooleanPreference(
oldPrefs,
newPrefs,
"dateTimeFlip",
"clock.date_time_flip",
false
)
migrateBooleanPreference(
oldPrefs,
newPrefs,
"disableTimeout",
"display.disable_timeout",
false
)
migrateBooleanPreference(
oldPrefs,
newPrefs,
"useFullScreen",
"display.use_full_screen",
true
)
migrateBooleanPreference(
oldPrefs,
newPrefs,
"enableDoubleActions",
"enabled_gestures.double_actions",
true
)
migrateBooleanPreference(
oldPrefs,
newPrefs,
"enableEdgeActions",
"enabled_gestures.edge_actions",
true
)
migrateBooleanPreference(
oldPrefs,
newPrefs,
"searchAutoLaunch",
"functionality.search_auto_launch",
true
)
migrateBooleanPreference(
oldPrefs,
newPrefs,
"searchAutoKeyboard",
"functionality.search_auto_keyboard",
true
)
newPrefs.apply()
migrateStringPreference(
oldPrefs,
this,
"action_volumeUpApp",
"action.volume_up.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_volumeUpApp_user",
"action.volume_up.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_volumeDownApp",
"action.volume_down.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_volumeDownApp_user",
"action.volume_down.user",
-1
)
migrateStringPreference(oldPrefs, this, "action_timeApp", "action.time.app", "")
migrateIntPreference(oldPrefs, this, "action_timeApp_user", "action.time.user", -1)
migrateStringPreference(oldPrefs, this, "action_dateApp", "action.date.app", "")
migrateIntPreference(oldPrefs, this, "action_dateApp_user", "action.date.user", -1)
migrateStringPreference(
oldPrefs,
this,
"action_longClickApp",
"action.long_click.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_longClickApp_user",
"action.long_click.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_doubleClickApp",
"action.double_click.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_doubleClickApp_user",
"action.double_click.user",
-1
)
migrateStringPreference(oldPrefs, this, "action_upApp", "action.up.app", "")
migrateIntPreference(oldPrefs, this, "action_upApp_user", "action.up.user", -1)
migrateStringPreference(
oldPrefs,
this,
"action_up_leftApp",
"action.up_left.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_up_leftApp_user",
"action.up_left.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_up_rightApp",
"action.up_right.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_up_rightApp_user",
"action.up_right.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_doubleUpApp",
"action.double_up.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_doubleUpApp_user",
"action.double_up.user",
-1
)
migrateStringPreference(oldPrefs, this, "action_downApp", "action.down.app", "")
migrateIntPreference(oldPrefs, this, "action_downApp_user", "action.down.user", -1)
migrateStringPreference(
oldPrefs,
this,
"action_down_leftApp",
"action.down_left.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_down_leftApp_user",
"action.down_left.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_down_rightApp",
"action.down_right.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_down_rightApp_user",
"action.down_right.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_doubleDownApp",
"action.double_down.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_doubleDownApp_user",
"action.double_down.user",
-1
)
migrateStringPreference(oldPrefs, this, "action_leftApp", "action.left.app", "")
migrateIntPreference(oldPrefs, this, "action_leftApp_user", "action.left.user", -1)
migrateStringPreference(
oldPrefs,
this,
"action_left_topApp",
"action.left_top.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_left_topApp_user",
"action.left_top.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_left_bottomApp",
"action.left_bottom.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_left_bottomApp_user",
"action.left_bottom.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_doubleLeftApp",
"action.double_left.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_doubleLeftApp_user",
"action.double_left.user",
-1
)
migrateStringPreference(oldPrefs, this, "action_rightApp", "action.right.app", "")
migrateIntPreference(
oldPrefs,
this,
"action_rightApp_user",
"action.right.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_right_topApp",
"action.right_top.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_right_topApp_user",
"action.right_top.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_right_bottomApp",
"action.right_bottom.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_right_bottomApp_user",
"action.right_bottom.user",
-1
)
migrateStringPreference(
oldPrefs,
this,
"action_doubleRightApp",
"action.double_right.app",
""
)
migrateIntPreference(
oldPrefs,
this,
"action_doubleRightApp_user",
"action.double_right.user",
-1
)
migrateBooleanPreference(oldPrefs, this, "timeVisible", "clock.time_visible", true)
migrateBooleanPreference(oldPrefs, this, "dateVisible", "clock.date_visible", true)
migrateBooleanPreference(
oldPrefs,
this,
"dateLocalized",
"clock.date_localized",
false
)
migrateBooleanPreference(
oldPrefs,
this,
"dateTimeFlip",
"clock.date_time_flip",
false
)
migrateBooleanPreference(
oldPrefs,
this,
"disableTimeout",
"display.disable_timeout",
false
)
migrateBooleanPreference(
oldPrefs,
this,
"useFullScreen",
"display.use_full_screen",
true
)
migrateBooleanPreference(
oldPrefs,
this,
"enableDoubleActions",
"enabled_gestures.double_actions",
true
)
migrateBooleanPreference(
oldPrefs,
this,
"enableEdgeActions",
"enabled_gestures.edge_actions",
true
)
migrateBooleanPreference(
oldPrefs,
this,
"searchAutoLaunch",
"functionality.search_auto_launch",
true
)
migrateBooleanPreference(
oldPrefs,
this,
"searchAutoKeyboard",
"functionality.search_auto_keyboard",
true
)
}
when (oldPrefs.getString("theme", "finn")) {
"finn" -> {
@ -393,5 +392,5 @@ fun migratePreferencesFromVersionUnknown(context: Context) {
LauncherPreferences.internal().versionCode(1)
Log.i(TAG, "migrated preferences to version 1.")
migratePreferencesFromVersion1()
migratePreferencesFromVersion1(context)
}

View file

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

View file

@ -5,7 +5,6 @@ import android.content.res.Resources
import com.google.android.material.color.DynamicColors
import de.jrpie.android.launcher.R
@Suppress("unused")
enum class ColorTheme(
private val id: Int,
private val labelResource: Int,

View file

@ -1,5 +1,6 @@
package de.jrpie.android.launcher.ui
import android.app.Activity
import android.content.Context
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
@ -27,17 +28,28 @@ fun View.blink(
}
// Taken from: https://stackoverflow.com/a/30340794/12787264
fun ImageView.transformGrayscale() {
this.colorFilter = ColorMatrixColorFilter(ColorMatrix().apply {
setSaturation(0f)
})
fun ImageView.transformGrayscale(grayscale: Boolean) {
this.colorFilter = if (grayscale) {
ColorMatrixColorFilter(ColorMatrix().apply {
setSaturation(0f)
})
} else {
null
}
}
// Taken from https://stackoverflow.com/a/50743764/12787264
// Taken from https://stackoverflow.com/a/50743764
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)
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
}
// https://stackoverflow.com/a/17789187
fun closeSoftKeyboard(activity: Activity) {
activity.currentFocus?.let { focus ->
(activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
.hideSoftInputFromWindow( focus.windowToken, 0 )
}
}

View file

@ -1,25 +1,25 @@
package de.jrpie.android.launcher.ui
import android.annotation.SuppressLint
import android.app.Activity
import android.content.SharedPreferences
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
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.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.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,
@ -33,10 +33,10 @@ import java.util.Locale
* - Setting global variables (preferences etc.)
* - Opening the [TutorialActivity] on new installations
*/
class HomeActivity : UIObject, AppCompatActivity() {
class HomeActivity : UIObject, Activity() {
private lateinit var binding: HomeBinding
private lateinit var touchGestureDetector: TouchGestureDetector
private var touchGestureDetector: TouchGestureDetector? = null
private var sharedPreferencesListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
@ -44,35 +44,25 @@ class HomeActivity : UIObject, AppCompatActivity() {
prefKey?.startsWith("display.") == true
) {
recreate()
} else if (prefKey?.startsWith("action.") == true) {
updateSettingsFallbackButtonVisibility()
} else if (prefKey == LauncherPreferences.widgets().keys().widgets()) {
binding.homeWidgetContainer.updateWidgets(this@HomeActivity,
LauncherPreferences.widgets().widgets()
)
}
if (prefKey?.startsWith("action.") == true) {
updateSettingsFallbackButtonVisibility()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super<AppCompatActivity>.onCreate(savedInstanceState)
super<Activity>.onCreate(savedInstanceState)
super<UIObject>.onCreate()
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
val width = displayMetrics.widthPixels
val height = displayMetrics.heightPixels
touchGestureDetector = TouchGestureDetector(
this,
width,
height,
LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
)
// Initialise layout
binding = HomeBinding.inflate(layoutInflater)
setContentView(binding.root)
setContentView(binding.root)
// Handle back key / gesture on Android 13+, cf. onKeyDown()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -85,20 +75,45 @@ class HomeActivity : UIObject, AppCompatActivity() {
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<Activity>.onStart()
super<UIObject>.onStart()
// If the tutorial was not finished, start it
if (!LauncherPreferences.internal().started()) {
openTutorial(this)
}
LauncherPreferences.getSharedPreferences()
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
(application as Application).appWidgetHost.startListening()
}
override fun onStop() {
(application as Application).appWidgetHost.stopListening()
super.onStop()
}
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.
@ -113,44 +128,6 @@ class HomeActivity : UIObject, AppCompatActivity() {
}
}
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)
@ -166,11 +143,33 @@ class HomeActivity : UIObject, AppCompatActivity() {
override fun onResume() {
super.onResume()
touchGestureDetector.edgeWidth =
/* 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)
}
initClock()
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
}
}
updateSettingsFallbackButtonVisibility()
binding.homeWidgetContainer.updateWidgets(this@HomeActivity,
LauncherPreferences.widgets().widgets()
)
}
override fun onDestroy() {
@ -186,6 +185,7 @@ class HomeActivity : UIObject, AppCompatActivity() {
// Only used pre Android 13, cf. onBackInvokedDispatcher
handleBack()
}
KeyEvent.KEYCODE_VOLUME_UP -> {
if (Action.forGesture(Gesture.VOLUME_UP) == LauncherAction.VOLUME_UP) {
// Let the OS handle the key event. This works better with some custom ROMs
@ -207,29 +207,10 @@ class HomeActivity : UIObject, AppCompatActivity() {
}
override fun onTouchEvent(event: MotionEvent): Boolean {
return touchGestureDetector.onTouchEvent(event) || super.onTouchEvent(event)
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)
}

View file

@ -21,14 +21,16 @@ 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.actions.shortcuts.PinnedShortcutInfo
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)
@ -46,7 +48,22 @@ class PinShortcutActivity : AppCompatActivity(), UIObject {
val launcherApps = getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
val request = launcherApps.getPinItemRequest(intent)
if (request == null || request.requestType != PinItemRequest.REQUEST_TYPE_SHORTCUT) {
this.request = request
if (request == null) {
finish()
return
}
if (request.requestType == PinItemRequest.REQUEST_TYPE_APPWIDGET) {
// TODO
request.getAppWidgetProviderInfo(this)
// startActivity()
finish()
return
}
if (request.requestType != PinItemRequest.REQUEST_TYPE_SHORTCUT) {
finish()
return
}
@ -70,9 +87,12 @@ class PinShortcutActivity : AppCompatActivity(), UIObject {
isBound = true
request.accept()
}
val editor = LauncherPreferences.getSharedPreferences().edit()
ShortcutAction(PinnedShortcutInfo(request.shortcutInfo!!)).bindToGesture(editor, gesture.id)
editor.apply()
LauncherPreferences.getSharedPreferences().edit {
ShortcutAction(PinnedShortcutInfo(request.shortcutInfo!!)).bindToGesture(
this,
gesture.id
)
}
dialog.dismiss()
}
dialog.findViewById<RecyclerView>(R.id.dialog_select_gesture_recycler).apply {
@ -84,6 +104,7 @@ class PinShortcutActivity : AppCompatActivity(), UIObject {
}
binding.pinShortcutClose.setOnClickListener { finish() }
binding.pinShortcutButtonOk.setOnClickListener { finish() }
}
override fun onStart() {
@ -91,16 +112,34 @@ class PinShortcutActivity : AppCompatActivity(), UIObject {
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>() {
val gestures = Gesture.entries.filter { it.isEnabled() }.toList()
private val gestures = Gesture.entries.filter { it.isEnabled() }.toList()
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val label = itemView.findViewById<TextView>(R.id.dialog_select_gesture_row_name)
val description = itemView.findViewById<TextView>(R.id.dialog_select_gesture_row_description)
val icon = itemView.findViewById<ImageView>(R.id.dialog_select_gesture_row_icon)
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 {
@ -124,5 +163,6 @@ class PinShortcutActivity : AppCompatActivity(), UIObject {
override fun getItemCount(): Int {
return gestures.size
}
}
}

View file

@ -1,8 +1,15 @@
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
@ -12,8 +19,8 @@ import kotlin.math.tan
class TouchGestureDetector(
private val context: Context,
val width: Int,
val height: Int,
var width: Int,
var height: Int,
var edgeWidth: Float
) {
private val ANGULAR_THRESHOLD = tan(Math.PI / 6)
@ -27,20 +34,31 @@ class TouchGestureDetector(
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)
}
@ -57,16 +75,35 @@ class TouchGestureDetector(
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
}
@ -84,20 +121,48 @@ class TouchGestureDetector(
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): Boolean {
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) {
paths = HashMap()
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))) {
for (i in 0..<event.pointerCount) {
if (paths.containsKey(event.getPointerId(i))) {
continue
}
val index = pointerIdToIndex[i] ?: continue
@ -122,9 +187,17 @@ class TouchGestureDetector(
}
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 true
return
}
private fun getGestureForDirection(direction: Vector): Gesture? {
@ -152,9 +225,8 @@ class TouchGestureDetector(
val mainPointerPath = paths.entries.firstOrNull { it.value.number == 0 }?.value ?: return
// Ignore swipes at the very top, since this interferes with the status bar.
// TODO: replace 100px by sensible dp value (e.g. twice the height of the status bar)
if (paths.entries.any { it.value.start.y < 100 }) {
// Ignore swipes starting at the very top and the very bottom
if (paths.entries.any { it.value.startIntersectsSystemGestureInsets() }) {
return
}
@ -164,17 +236,14 @@ class TouchGestureDetector(
if (duration in 0..TAP_TIMEOUT) {
if (timeStart - lastTappedTime < DOUBLE_TAP_TIMEOUT &&
lastTappedLocation?.let {
(mainPointerPath.last - it).absSquared() < DOUBLE_TAP_SLOP_SQUARE} == true
(mainPointerPath.last - it).absSquared() < DOUBLE_TAP_SLOP_SQUARE
} == true
) {
Gesture.DOUBLE_CLICK.invoke(context)
} else {
lastTappedTime = timeEnd
lastTappedLocation = mainPointerPath.last
}
} else if (duration > LONG_PRESS_TIMEOUT) {
// TODO: Don't wait until the finger is lifted.
// Instead set a timer to start long click as soon as LONG_PRESS_TIMEOUT is reached
Gesture.LONG_CLICK.invoke(context)
}
} else {
// detect swipes
@ -197,34 +266,38 @@ class TouchGestureDetector(
val startEndMax = mainPointerPath.start.max(mainPointerPath.last)
when (gesture) {
Gesture.SWIPE_DOWN -> {
if(startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
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) {
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) {
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) {
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 -> { }
else -> {}
}
if (edgeActions) {
@ -247,4 +320,20 @@ class TouchGestureDetector(
gesture?.invoke(context)
}
}
fun updateScreenSize(windowManager: WindowManager) {
val displayMetrics = DisplayMetrics()
@Suppress("deprecation") // required to support API < 30
windowManager.defaultDisplay.getMetrics(displayMetrics)
width = displayMetrics.widthPixels
height = displayMetrics.heightPixels
}
@RequiresApi(Build.VERSION_CODES.Q)
fun setSystemGestureInsets(insets: Insets) {
systemGestureInsetTop = insets.top
systemGestureInsetBottom = insets.bottom
systemGestureInsetLeft = insets.left
systemGestureInsetRight = insets.right
}
}

View file

@ -3,7 +3,11 @@ package de.jrpie.android.launcher.ui
import android.app.Activity
import android.content.pm.ActivityInfo
import android.content.res.Resources
import android.os.Build
import android.view.View
import android.view.Window
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import de.jrpie.android.launcher.preferences.LauncherPreferences
@ -11,10 +15,12 @@ import de.jrpie.android.launcher.preferences.LauncherPreferences
* An interface implemented by every [Activity], Fragment etc. in Launcher.
* It handles themes and window flags - a useful abstraction as it is the same everywhere.
*/
@Suppress("deprecation") // FLAG_FULLSCREEN is required to support API level < 30
fun setWindowFlags(window: Window, homeScreen: Boolean) {
window.setFlags(0, 0) // clear flags
// Display notification bar
if (LauncherPreferences.display().fullScreen())
if (LauncherPreferences.display().hideStatusBar())
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
@ -36,17 +42,19 @@ fun setWindowFlags(window: Window, homeScreen: Boolean) {
}
interface UIObject {
fun onCreate() {
if (this is Activity) {
setWindowFlags(window, isHomeScreen())
if (!LauncherPreferences.display().rotateScreen()) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
}
if (this !is Activity) {
return
}
setWindowFlags(window, isHomeScreen())
if (!LauncherPreferences.display().rotateScreen()) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
}
}
fun onStart() {
setOnClicks()
adjustLayout()
@ -70,4 +78,26 @@ interface UIObject {
fun isHomeScreen(): Boolean {
return false
}
@Suppress("DEPRECATION")
fun hideNavigationBar() {
if (this !is Activity) {
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.apply {
hide(WindowInsets.Type.navigationBars())
systemBarsBehavior =
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
} else {
// Try to hide the navigation bar but do not hide the status bar
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_IMMERSIVE
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
}
}
}

View file

@ -1,27 +1,20 @@
package de.jrpie.android.launcher.ui.list
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import android.window.OnBackInvokedDispatcher
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager
import com.google.android.material.tabs.TabLayout
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.REQUEST_UNINSTALL
import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.apps.AppFilter
import de.jrpie.android.launcher.apps.hidePrivateSpaceWhenLocked
import de.jrpie.android.launcher.apps.isPrivateSpaceLocked
import de.jrpie.android.launcher.apps.isPrivateSpaceSetUp
import de.jrpie.android.launcher.apps.togglePrivateSpaceLock
@ -32,14 +25,6 @@ import de.jrpie.android.launcher.ui.list.apps.ListFragmentApps
import de.jrpie.android.launcher.ui.list.other.ListFragmentOther
// TODO: Better solution for this intercommunication functionality (used in list-fragments)
var intention = ListActivity.ListActivityIntention.VIEW
var favoritesVisibility: AppFilter.Companion.AppSetVisibility = AppFilter.Companion.AppSetVisibility.VISIBLE
var privateSpaceVisibility: AppFilter.Companion.AppSetVisibility =
AppFilter.Companion.AppSetVisibility.VISIBLE
var hiddenVisibility: AppFilter.Companion.AppSetVisibility = AppFilter.Companion.AppSetVisibility.HIDDEN
var forGesture: String? = null
/**
* The [ListActivity] is the most general purpose activity in Launcher:
* - used to view all apps and edit their settings
@ -49,9 +34,34 @@ var forGesture: String? = null
*/
class ListActivity : AppCompatActivity(), UIObject {
private lateinit var binding: ListBinding
var intention = ListActivityIntention.VIEW
var favoritesVisibility: AppFilter.Companion.AppSetVisibility =
AppFilter.Companion.AppSetVisibility.VISIBLE
var privateSpaceVisibility: AppFilter.Companion.AppSetVisibility =
AppFilter.Companion.AppSetVisibility.VISIBLE
var hiddenVisibility: AppFilter.Companion.AppSetVisibility =
AppFilter.Companion.AppSetVisibility.HIDDEN
var forGesture: String? = null
private fun updateLockIcon(locked: Boolean) {
if (
// only show lock for VIEW intention
(intention != ListActivityIntention.VIEW)
// hide lock when private space does not exist
|| !isPrivateSpaceSetUp(this)
// hide lock when private space apps are hidden from the main list and we are not in the private space list
|| (LauncherPreferences.apps().hidePrivateSpaceApps()
&& privateSpaceVisibility != AppFilter.Companion.AppSetVisibility.EXCLUSIVE)
// hide lock when private space is locked and the hidden when locked setting is set
|| (locked && hidePrivateSpaceWhenLocked(this))
) {
binding.listLock.visibility = View.GONE
return
}
binding.listLock.visibility = View.VISIBLE
binding.listLock.setImageDrawable(
AppCompatResources.getDrawable(
this,
@ -74,7 +84,6 @@ 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 */
@ -99,10 +108,13 @@ class ListActivity : AppCompatActivity(), UIObject {
?.let { ListActivityIntention.valueOf(it) }
?: ListActivityIntention.VIEW
@Suppress("deprecation") // required to support API level < 33
favoritesVisibility = bundle.getSerializable("favoritesVisibility")
as? AppFilter.Companion.AppSetVisibility ?: favoritesVisibility
@Suppress("deprecation") // required to support API level < 33
privateSpaceVisibility = bundle.getSerializable("privateSpaceVisibility")
as? AppFilter.Companion.AppSetVisibility ?: privateSpaceVisibility
@Suppress("deprecation") // required to support API level < 33
hiddenVisibility = bundle.getSerializable("hiddenVisibility")
as? AppFilter.Companion.AppSetVisibility ?: hiddenVisibility
@ -119,20 +131,6 @@ class ListActivity : AppCompatActivity(), UIObject {
LauncherAction.SETTINGS.launch(this@ListActivity)
}
binding.listLock.visibility =
if (intention != ListActivityIntention.VIEW) {
View.GONE
} else if (!isPrivateSpaceSetUp(this)) {
View.GONE
} else if (LauncherPreferences.apps().hidePrivateSpaceApps()) {
if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
View.VISIBLE
} else {
View.GONE
}
} else {
View.VISIBLE
}
if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
isPrivateSpaceSetUp(this, showToast = true, launchSettings = true)
@ -155,7 +153,7 @@ class ListActivity : AppCompatActivity(), UIObject {
binding.listContainer.context.resources.displayMetrics.heightPixels
val diff = height - r.bottom
if (diff != 0 &&
LauncherPreferences.display().fullScreen()
LauncherPreferences.display().hideStatusBar()
) {
if (binding.listContainer.paddingBottom != diff) {
binding.listContainer.setPadding(0, 0, 0, diff)
@ -183,32 +181,19 @@ class ListActivity : AppCompatActivity(), UIObject {
finish()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_UNINSTALL) {
if (resultCode == Activity.RESULT_OK) {
Toast.makeText(this, getString(R.string.list_removed), Toast.LENGTH_LONG).show()
finish()
} else if (resultCode == Activity.RESULT_FIRST_USER) {
Toast.makeText(this, getString(R.string.list_not_removed), Toast.LENGTH_LONG).show()
finish()
}
}
}
fun updateTitle() {
var titleResource = intention.titleResource
if (intention == ListActivityIntention.VIEW) {
titleResource = if (hiddenVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
R.string.list_title_hidden
} else if (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
}
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)
@ -238,11 +223,11 @@ class ListActivity : AppCompatActivity(), UIObject {
updateTitle()
val sectionsPagerAdapter = ListSectionsPagerAdapter(this, supportFragmentManager)
val viewPager: ViewPager = findViewById(R.id.list_viewpager)
viewPager.adapter = sectionsPagerAdapter
val tabs: TabLayout = findViewById(R.id.list_tabs)
tabs.setupWithViewPager(viewPager)
val sectionsPagerAdapter = ListSectionsPagerAdapter(this)
binding.listViewpager.let {
it.adapter = sectionsPagerAdapter
binding.listTabs.setupWithViewPager(it)
}
}
}
@ -254,9 +239,15 @@ private val TAB_TITLES = arrayOf(
/**
* The [ListSectionsPagerAdapter] returns the fragment,
* which corresponds to the selected tab in [ListActivity].
*
* This should eventually be replaced by a [FragmentStateAdapter]
* However this keyboard does not open when using [ViewPager2]
* so currently [ViewPager] is used here.
* https://github.com/jrpie/launcher/issues/130
*/
class ListSectionsPagerAdapter(private val context: Context, fm: FragmentManager) :
FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
@Suppress("deprecation")
class ListSectionsPagerAdapter(private val activity: ListActivity) :
FragmentPagerAdapter(activity.supportFragmentManager) {
override fun getItem(position: Int): Fragment {
return when (position) {
@ -267,11 +258,11 @@ class ListSectionsPagerAdapter(private val context: Context, fm: FragmentManager
}
override fun getPageTitle(position: Int): CharSequence {
return context.resources.getString(TAB_TITLES[position])
return activity.resources.getString(TAB_TITLES[position])
}
override fun getCount(): Int {
return when (intention) {
return when (activity.intention) {
ListActivity.ListActivityIntention.VIEW -> 1
else -> 2
}

View file

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

View file

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

View file

@ -9,18 +9,18 @@ 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 androidx.recyclerview.widget.RecyclerView
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.closeSoftKeyboard
import de.jrpie.android.launcher.ui.list.ListActivity
import de.jrpie.android.launcher.ui.list.favoritesVisibility
import de.jrpie.android.launcher.ui.list.forGesture
import de.jrpie.android.launcher.ui.list.hiddenVisibility
import de.jrpie.android.launcher.ui.list.intention
import de.jrpie.android.launcher.ui.list.privateSpaceVisibility
import de.jrpie.android.launcher.ui.openSoftKeyboard
import kotlin.math.absoluteValue
/**
@ -52,7 +52,7 @@ class ListFragmentApps : Fragment(), UIObject {
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
binding.listAppsCheckBoxFavorites.isChecked =
(favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE)
((activity as? ListActivity)?.favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE)
}
override fun onStop() {
@ -65,26 +65,48 @@ class ListFragmentApps : Fragment(), UIObject {
override fun setOnClicks() {}
override fun adjustLayout() {
val listActivity = activity as? ListActivity ?: return
appsRecyclerAdapter =
AppsRecyclerAdapter(
requireActivity(), binding.root, intention, forGesture,
listActivity, binding.root, listActivity.intention, listActivity.forGesture,
appFilter = AppFilter(
requireContext(),
"",
favoritesVisibility = favoritesVisibility,
privateSpaceVisibility = privateSpaceVisibility,
hiddenVisibility = hiddenVisibility
favoritesVisibility = listActivity.favoritesVisibility,
privateSpaceVisibility = listActivity.privateSpaceVisibility,
hiddenVisibility = listActivity.hiddenVisibility
),
layout = LauncherPreferences.list().layout()
)
// set up the list / recycler
binding.listAppsRview.apply {
// improve performance (since content changes don't change the layout size)
setHasFixedSize(true)
layoutManager = LauncherPreferences.list().layout().layoutManager(context)
.also {
if (LauncherPreferences.list().reverseLayout()) {
(it as? LinearLayoutManager)?.reverseLayout = true
(it as? GridLayoutManager)?.reverseLayout = true
}
}
adapter = appsRecyclerAdapter
if (LauncherPreferences.functionality().searchAutoCloseKeyboard()) {
addOnScrollListener(object : RecyclerView.OnScrollListener() {
var totalDy: Int = 0
var threshold = (resources.displayMetrics.density * 100).toInt()
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
totalDy += dy
if (totalDy.absoluteValue > 100) {
totalDy = 0
closeSoftKeyboard(requireActivity())
}
}
})
}
}
binding.listAppsSearchview.setOnQueryTextListener(object :
@ -115,7 +137,8 @@ class ListFragmentApps : Fragment(), UIObject {
if (newText == " " &&
!appsRecyclerAdapter.disableAutoLaunch &&
intention == ListActivity.ListActivityIntention.VIEW &&
(activity as? ListActivity)?.intention
== ListActivity.ListActivityIntention.VIEW &&
LauncherPreferences.functionality().searchAutoLaunch()
) {
appsRecyclerAdapter.disableAutoLaunch = true
@ -132,17 +155,17 @@ class ListFragmentApps : Fragment(), UIObject {
})
binding.listAppsCheckBoxFavorites.setOnClickListener {
favoritesVisibility =
listActivity.favoritesVisibility =
if (binding.listAppsCheckBoxFavorites.isChecked) {
AppFilter.Companion.AppSetVisibility.EXCLUSIVE
} else {
AppFilter.Companion.AppSetVisibility.VISIBLE
}
appsRecyclerAdapter.setFavoritesVisibility(favoritesVisibility)
appsRecyclerAdapter.setFavoritesVisibility(listActivity.favoritesVisibility)
(activity as? ListActivity)?.updateTitle()
}
if (intention == ListActivity.ListActivityIntention.VIEW
if (listActivity.intention == ListActivity.ListActivityIntention.VIEW
&& LauncherPreferences.functionality().searchAutoOpenKeyboard()
) {
binding.listAppsSearchview.openSoftKeyboard(requireContext())

View file

@ -1,7 +1,6 @@
package de.jrpie.android.launcher.ui.list.other
import android.app.Activity
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -9,9 +8,11 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.ui.list.forGesture
import de.jrpie.android.launcher.actions.WidgetPanelAction
import de.jrpie.android.launcher.ui.list.ListActivity
/**
* The [OtherRecyclerAdapter] will only be displayed in the ListActivity,
@ -23,8 +24,10 @@ import de.jrpie.android.launcher.ui.list.forGesture
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<Action> =
LauncherAction.entries.filter { it.isAvailable(activity) }
.plus(WidgetPanelAction(-1))
.toTypedArray()
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener {
@ -33,10 +36,15 @@ class OtherRecyclerAdapter(val activity: Activity) :
override fun onClick(v: View) {
val pos = adapterPosition
val pos = bindingAdapterPosition
val content = othersList[pos]
forGesture?.let { returnChoiceIntent(it, content) }
val gestureId = (activity as? ListActivity)?.forGesture ?: return
val gesture = Gesture.byId(gestureId) ?: return
content.showConfigurationDialog(activity) { configuredAction ->
Action.setActionForGesture(gesture, configuredAction)
activity.finish()
}
}
init {
@ -45,11 +53,11 @@ class OtherRecyclerAdapter(val activity: Activity) :
}
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
val otherLabel = activity.getString(othersList[i].label)
val icon = othersList[i].icon
val otherLabel = othersList[i].label(activity)
val icon = othersList[i].getIcon(activity)
viewHolder.textView.text = otherLabel
viewHolder.iconView.setImageResource(icon)
viewHolder.iconView.setImageDrawable(icon)
}
override fun getItemCount(): Int {
@ -61,12 +69,4 @@ class OtherRecyclerAdapter(val activity: Activity) :
val view: View = inflater.inflate(R.layout.list_other_row, parent, false)
return ViewHolder(view)
}
private fun returnChoiceIntent(forGesture: String, action: LauncherAction) {
val returnIntent = Intent()
returnIntent.putExtra("forGesture", forGesture)
action.writeToIntent(returnIntent)
activity.setResult(REQUEST_CHOOSE_APP, returnIntent)
activity.finish()
}
}

View file

@ -1,6 +1,5 @@
package de.jrpie.android.launcher.ui.settings
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources
@ -8,17 +7,14 @@ import android.os.Bundle
import android.provider.Settings
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager
import com.google.android.material.tabs.TabLayout
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
import de.jrpie.android.launcher.databinding.SettingsBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.theme.Background
import de.jrpie.android.launcher.preferences.theme.ColorTheme
import de.jrpie.android.launcher.saveListActivityChoice
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
@ -49,15 +45,15 @@ class SettingsActivity : AppCompatActivity(), UIObject {
// 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("tab", 1) }
.also { it.putExtra(EXTRA_TAB, 1) }
finish()
startActivity(i)
} else
if (prefKey?.startsWith("theme.") == true ||
prefKey?.startsWith("display.") == true
) {
recreate()
}
if (prefKey?.startsWith("theme.") == true ||
prefKey?.startsWith("display.") == true
) {
recreate()
}
}
private lateinit var binding: SettingsBinding
@ -71,15 +67,14 @@ class SettingsActivity : AppCompatActivity(), UIObject {
setContentView(binding.root)
// set up tabs and swiping in settings
val sectionsPagerAdapter = SettingsSectionsPagerAdapter(this, supportFragmentManager)
val viewPager: ViewPager = findViewById(R.id.settings_viewpager)
viewPager.adapter = sectionsPagerAdapter
val tabs: TabLayout = findViewById(R.id.settings_tabs)
tabs.setupWithViewPager(viewPager)
if (intent.hasExtra("tab")) {
tabs.getTabAt(intent.getIntExtra("tab", 0))?.select()
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() {
@ -108,24 +103,21 @@ class SettingsActivity : AppCompatActivity(), UIObject {
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_CHOOSE_APP -> saveListActivityChoice(data)
else -> super.onActivityResult(requestCode, resultCode, data)
}
companion object {
private const val EXTRA_TAB = "tab"
}
}
private val TAB_TITLES = arrayOf(
R.string.settings_tab_app,
R.string.settings_tab_actions,
R.string.settings_tab_launcher,
R.string.settings_tab_meta
)
class SettingsSectionsPagerAdapter(private val context: Context, fm: FragmentManager) :
FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
class SettingsSectionsPagerAdapter(private val activity: FragmentActivity) :
FragmentStateAdapter(activity) {
override fun getItem(position: Int): Fragment {
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> SettingsFragmentActions()
1 -> SettingsFragmentLauncher()
@ -134,11 +126,11 @@ class SettingsSectionsPagerAdapter(private val context: Context, fm: FragmentMan
}
}
override fun getPageTitle(position: Int): CharSequence {
return context.resources.getString(TAB_TITLES[position])
fun getPageTitle(position: Int): CharSequence {
return activity.resources.getString(TAB_TITLES[position])
}
override fun getCount(): Int {
override fun getItemCount(): Int {
return 3
}
}

View file

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

View file

@ -11,6 +11,8 @@ 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
import de.jrpie.android.launcher.ui.widgets.manage.ManageWidgetPanelsActivity
import de.jrpie.android.launcher.ui.widgets.manage.ManageWidgetsActivity
/**
@ -81,6 +83,22 @@ class SettingsFragmentLauncher : PreferenceFragmentCompat() {
true
}
val manageWidgets = findPreference<androidx.preference.Preference>(
LauncherPreferences.widgets().keys().widgets()
)
manageWidgets?.setOnPreferenceClickListener {
startActivity(Intent(requireActivity(), ManageWidgetsActivity::class.java))
true
}
val manageWidgetPanels = findPreference<androidx.preference.Preference>(
LauncherPreferences.widgets().keys().customPanels()
)
manageWidgetPanels?.setOnPreferenceClickListener {
startActivity(Intent(requireActivity(), ManageWidgetPanelsActivity::class.java))
true
}
val hiddenApps = findPreference<androidx.preference.Preference>(
LauncherPreferences.apps().keys().hidden()
)

View file

@ -2,7 +2,6 @@ package de.jrpie.android.launcher.ui.settings.meta
import android.app.AlertDialog
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -16,10 +15,10 @@ import de.jrpie.android.launcher.copyToClipboard
import de.jrpie.android.launcher.databinding.SettingsMetaBinding
import de.jrpie.android.launcher.getDeviceInfo
import de.jrpie.android.launcher.openInBrowser
import de.jrpie.android.launcher.openTutorial
import de.jrpie.android.launcher.preferences.resetPreferences
import de.jrpie.android.launcher.ui.LegalInfoActivity
import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
/**
* The [SettingsFragmentMeta] is a used as a tab in the SettingsActivity.
@ -47,8 +46,17 @@ class SettingsFragmentMeta : Fragment(), UIObject {
override fun setOnClicks() {
fun bindURL(view: View, urlRes: Int) {
view.setOnClickListener {
openInBrowser(
getString(urlRes),
requireContext()
)
}
}
binding.settingsMetaButtonViewTutorial.setOnClickListener {
startActivity(Intent(this.context, TutorialActivity::class.java))
openTutorial(requireContext())
}
// prompting for settings-reset confirmation
@ -69,12 +77,7 @@ class SettingsFragmentMeta : Fragment(), UIObject {
// view code
binding.settingsMetaButtonViewCode.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_link_github),
requireContext()
)
}
bindURL(binding.settingsMetaButtonViewCode, R.string.settings_meta_link_github)
// report a bug
binding.settingsMetaButtonReportBug.setOnClickListener {
@ -110,37 +113,19 @@ class SettingsFragmentMeta : Fragment(), UIObject {
}
// join chat
binding.settingsMetaButtonJoinChat.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_chat_url),
requireContext()
)
}
bindURL(binding.settingsMetaButtonJoinChat, R.string.settings_meta_chat_url)
// contact developer
binding.settingsMetaButtonContact.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_contact_url),
requireContext()
)
}
// bindURL(binding.settingsMetaButtonContact, R.string.settings_meta_contact_url)
// contact fork developer
binding.settingsMetaButtonForkContact.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_fork_contact_url),
requireContext()
)
}
bindURL(binding.settingsMetaButtonForkContact, R.string.settings_meta_fork_contact_url)
// donate
bindURL(binding.settingsMetaButtonDonate, R.string.settings_meta_donate_url)
// privacy policy
binding.settingsMetaButtonPrivacy.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_privacy_url),
requireContext()
)
}
bindURL(binding.settingsMetaButtonPrivacy, R.string.settings_meta_privacy_url)
// legal info
binding.settingsMetaButtonLicenses.setOnClickListener {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,80 @@
package de.jrpie.android.launcher.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.databinding.ClockBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import java.util.Locale
class ClockView(context: Context, attrs: AttributeSet? = null, val appWidgetId: Int): ConstraintLayout(context, attrs) {
val binding: ClockBinding = ClockBinding.inflate(LayoutInflater.from(context), this, true)
init {
initClock()
setOnClicks()
}
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.clockUpperView.isVisible = upperVisible
binding.clockLowerView.isVisible = lowerVisible
binding.clockUpperView.setTextColor(LauncherPreferences.clock().color())
binding.clockLowerView.setTextColor(LauncherPreferences.clock().color())
binding.clockLowerView.format24Hour = lowerFormat
binding.clockUpperView.format24Hour = upperFormat
binding.clockLowerView.format12Hour = lowerFormat
binding.clockUpperView.format12Hour = upperFormat
}
fun setOnClicks() {
binding.clockUpperView.setOnClickListener {
if (LauncherPreferences.clock().flipDateTime()) {
Gesture.TIME(context)
} else {
Gesture.DATE(context)
}
}
binding.clockLowerView.setOnClickListener {
if (LauncherPreferences.clock().flipDateTime()) {
Gesture.DATE(context)
} else {
Gesture.TIME(context)
}
}
}
}

View file

@ -0,0 +1,144 @@
package de.jrpie.android.launcher.ui.widgets
import android.app.Activity
import android.content.Context
import android.graphics.PointF
import android.graphics.RectF
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.View.MeasureSpec.makeMeasureSpec
import android.view.ViewGroup
import androidx.core.graphics.contains
import androidx.core.view.size
import de.jrpie.android.launcher.widgets.Widget
import de.jrpie.android.launcher.widgets.WidgetPanel
import de.jrpie.android.launcher.widgets.WidgetPosition
import kotlin.math.max
/**
* This only works in an Activity, not AppCompatActivity
*/
open class WidgetContainerView(
var widgetPanelId: Int,
context: Context,
attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {
constructor(context: Context, attrs: AttributeSet) : this(WidgetPanel.HOME.id, context, attrs)
var widgetViewById = HashMap<Int, View>()
open fun updateWidgets(activity: Activity, widgets: Collection<Widget>?) {
synchronized(widgetViewById) {
if (widgets == null) {
return
}
Log.i("WidgetContainer", "updating ${activity.localClassName}")
widgetViewById.forEach { removeView(it.value) }
widgetViewById.clear()
widgets.filter { it.panelId == widgetPanelId }.forEach { widget ->
widget.createView(activity)?.let {
addView(it, LayoutParams(widget.position))
widgetViewById.put(widget.id, it)
}
}
}
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
if (ev == null) {
return false
}
val position = PointF(ev.x, ev.y)
return widgetViewById.filter {
RectF(
it.value.x,
it.value.y,
it.value.x + it.value.width,
it.value.y + it.value.height
).contains(position) == true
}.any {
Widget.byId(context, it.key)?.allowInteraction == false
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var maxHeight = suggestedMinimumHeight
var maxWidth = suggestedMinimumWidth
val mWidth = MeasureSpec.getSize(widthMeasureSpec)
val mHeight = MeasureSpec.getSize(heightMeasureSpec)
(0..<size).map { getChildAt(it) }.forEach {
val position = (it.layoutParams as LayoutParams).position.getAbsoluteRect(mWidth, mHeight)
it.measure(makeMeasureSpec(position.width(), MeasureSpec.EXACTLY), makeMeasureSpec(position.height(), MeasureSpec.EXACTLY))
}
// Find rightmost and bottom-most child
(0..<size).map { getChildAt(it) }.filter { it.visibility != GONE }.forEach {
val position = (it.layoutParams as LayoutParams).position.getAbsoluteRect(mWidth, mHeight)
maxWidth = max(maxWidth, position.left + it.measuredWidth)
maxHeight = max(maxHeight, position.top + it.measuredHeight)
}
setMeasuredDimension(
resolveSizeAndState(maxWidth.toInt(), widthMeasureSpec, 0),
resolveSizeAndState(maxHeight.toInt(), heightMeasureSpec, 0)
)
}
/**
* Returns a set of layout parameters with a width of
* [ViewGroup.LayoutParams.WRAP_CONTENT],
* a height of [ViewGroup.LayoutParams.WRAP_CONTENT]
* and with the coordinates (0, 0).
*/
override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams {
return LayoutParams(WidgetPosition(0,0,1,1))
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
for (i in 0..<size) {
val child = getChildAt(i)
val lp = child.layoutParams as LayoutParams
val position = lp.position.getAbsoluteRect(r - l, b - t)
child.layout(position.left, position.top, position.right, position.bottom)
child.layoutParams.width = position.width()
child.layoutParams.height = position.height()
}
}
override fun generateLayoutParams(attrs: AttributeSet?): ViewGroup.LayoutParams {
return LayoutParams(context, attrs)
}
// Override to allow type-checking of LayoutParams.
override fun checkLayoutParams(p: ViewGroup.LayoutParams?): Boolean {
return p is LayoutParams
}
override fun generateLayoutParams(p: ViewGroup.LayoutParams?): ViewGroup.LayoutParams {
return LayoutParams(p)
}
override fun shouldDelayChildPressedState(): Boolean {
return false
}
companion object {
class LayoutParams : ViewGroup.LayoutParams {
var position = WidgetPosition(0,0,4,4)
constructor(position: WidgetPosition) : super(WRAP_CONTENT, WRAP_CONTENT) {
this.position = position
}
constructor(c: Context, attrs: AttributeSet?) : super(c, attrs)
constructor(source: ViewGroup.LayoutParams?) : super(source)
}
}
}

View file

@ -0,0 +1,50 @@
package de.jrpie.android.launcher.ui.widgets
import android.app.Activity
import android.content.res.Resources
import android.os.Bundle
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.databinding.ActivityWidgetPanelBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.ui.widgets.manage.EXTRA_PANEL_ID
import de.jrpie.android.launcher.widgets.WidgetPanel
class WidgetPanelActivity : Activity(), UIObject {
lateinit var binding: ActivityWidgetPanelBinding
var widgetPanelId: Int = WidgetPanel.Companion.HOME.id
override fun onCreate(savedInstanceState: Bundle?) {
super<Activity>.onCreate(savedInstanceState)
super<UIObject>.onCreate()
widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.Companion.HOME.id)
val binding = ActivityWidgetPanelBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.widgetPanelWidgetContainer.widgetPanelId = widgetPanelId
binding.widgetPanelWidgetContainer.updateWidgets(
this,
LauncherPreferences.widgets().widgets()
)
}
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 onStart() {
super<Activity>.onStart()
super<UIObject>.onStart()
}
override fun isHomeScreen(): Boolean {
return true
}
}

View file

@ -0,0 +1,104 @@
package de.jrpie.android.launcher.ui.widgets.manage
import android.annotation.SuppressLint
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources
import android.os.Bundle
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.databinding.ActivityManageWidgetPanelsBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.widgets.WidgetPanel
import de.jrpie.android.launcher.widgets.updateWidgetPanel
class ManageWidgetPanelsActivity : AppCompatActivity(), UIObject {
private val sharedPreferencesListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
if (prefKey == LauncherPreferences.widgets().keys().customPanels()) {
viewAdapter.widgetPanels =
(LauncherPreferences.widgets().customPanels() ?: setOf()).toTypedArray()
@SuppressLint("NotifyDataSetChanged")
viewAdapter.notifyDataSetChanged()
}
}
private lateinit var binding: ActivityManageWidgetPanelsBinding
private lateinit var viewAdapter: WidgetPanelsRecyclerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super<AppCompatActivity>.onCreate(savedInstanceState)
super<UIObject>.onCreate()
binding = ActivityManageWidgetPanelsBinding.inflate(layoutInflater)
setContentView(binding.main)
val viewManager = LinearLayoutManager(this)
viewAdapter = WidgetPanelsRecyclerAdapter(this, true) { widgetPanel ->
startActivity(
Intent(
this@ManageWidgetPanelsActivity,
ManageWidgetsActivity::class.java
).also {
it.putExtra(EXTRA_PANEL_ID, widgetPanel.id)
})
}
binding.manageWidgetPanelsRecycler.apply {
// improve performance (since content changes don't change the layout size)
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
binding.manageWidgetPanelsClose.setOnClickListener { finish() }
binding.manageWidgetPanelsAddPanel.setOnClickListener {
AlertDialog.Builder(this@ManageWidgetPanelsActivity, R.style.AlertDialogCustom).apply {
setTitle(R.string.dialog_create_widget_panel_title)
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
setPositiveButton(R.string.dialog_ok) { dialogInterface, _ ->
val panelId = WidgetPanel.allocateId()
val label = (dialogInterface as? AlertDialog)
?.findViewById<EditText>(R.id.dialog_create_widget_panel_edit_text)?.text?.toString()
?: (getString(R.string.widget_panel_default_name, panelId))
updateWidgetPanel(WidgetPanel(panelId, label))
}
setView(R.layout.dialog_create_widget_panel)
}.create().also { it.show() }.apply {
findViewById<EditText>(R.id.dialog_create_widget_panel_edit_text)
?.setText(
getString(
R.string.widget_panel_default_name,
WidgetPanel.allocateId()
)
)
}
true
}
}
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() {
binding.manageWidgetPanelsClose.setOnClickListener { finish() }
}
}

View file

@ -0,0 +1,185 @@
package de.jrpie.android.launcher.ui.widgets.manage
import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources
import android.graphics.Rect
import android.os.Bundle
import android.util.Log
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.ui.widgets.WidgetContainerView
import de.jrpie.android.launcher.widgets.AppWidget
import de.jrpie.android.launcher.widgets.WidgetPanel
import de.jrpie.android.launcher.widgets.WidgetPosition
import kotlin.math.min
// http://coderender.blogspot.com/2012/01/hosting-android-widgets-my.html
const val REQUEST_CREATE_APPWIDGET = 1
const val REQUEST_PICK_APPWIDGET = 2
const val EXTRA_PANEL_ID = "widgetPanelId"
// We can't use AppCompatActivity, since some AppWidgets don't work there.
class ManageWidgetsActivity : Activity(), UIObject {
var panelId: Int = WidgetPanel.HOME.id
private var sharedPreferencesListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
if (prefKey == LauncherPreferences.widgets().keys().widgets()) {
// We can't observe the livedata because this is not an AppCompatActivity
findViewById<WidgetContainerView>(R.id.manage_widgets_container).updateWidgets(this,
LauncherPreferences.widgets().widgets()
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super<Activity>.onCreate(savedInstanceState)
super<UIObject>.onCreate()
setContentView(R.layout.activity_manage_widgets)
panelId = intent.extras?.getInt(EXTRA_PANEL_ID, WidgetPanel.HOME.id) ?: WidgetPanel.HOME.id
findViewById<FloatingActionButton>(R.id.manage_widgets_button_add).setOnClickListener {
selectWidget()
}
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
findViewById<WidgetContainerView>(R.id.manage_widgets_container).let {
it.widgetPanelId = panelId
it.updateWidgets(this, (application as Application).widgets.value)
}
}
override fun onStart() {
super<Activity>.onStart()
super<UIObject>.onStart()
LauncherPreferences.getSharedPreferences()
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
}
override fun onResume() {
super.onResume()
findViewById<WidgetContainerView>(R.id.manage_widgets_container).updateWidgets(this,
LauncherPreferences.widgets().widgets()
)
}
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 onDestroy() {
LauncherPreferences.getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener)
super.onDestroy()
}
fun selectWidget() {
val appWidgetHost = (application as Application).appWidgetHost
startActivityForResult(
Intent(this, SelectWidgetActivity::class.java).also {
it.putExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
appWidgetHost.allocateAppWidgetId()
)
it.putExtra(
EXTRA_PANEL_ID,
panelId
)
}, REQUEST_PICK_APPWIDGET
)
}
fun createWidget(data: Intent) {
Log.i("Launcher", "creating widget")
val appWidgetManager = (application as Application).appWidgetManager
val appWidgetId = data.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return
val provider = appWidgetManager.getAppWidgetInfo(appWidgetId)
val display = windowManager.defaultDisplay
val position = WidgetPosition.fromAbsoluteRect(
Rect(0,0,
min(400, appWidgetManager.getAppWidgetInfo(appWidgetId).minWidth),
min(400, appWidgetManager.getAppWidgetInfo(appWidgetId).minHeight)
),
display.width,
display.height
)
val widget = AppWidget(appWidgetId, position, panelId, provider)
LauncherPreferences.widgets().widgets(
(LauncherPreferences.widgets().widgets() ?: HashSet()).also {
it.add(widget)
}
)
}
private fun configureWidget(data: Intent) {
val extras = data.extras
val appWidgetId = extras!!.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
val widget = AppWidget(appWidgetId, panelId = panelId)
if (widget.isConfigurable(this)) {
widget.configure(this, REQUEST_CREATE_APPWIDGET)
} else {
createWidget(data)
}
}
override fun onActivityResult(
requestCode: Int, resultCode: Int,
data: Intent?
) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_PICK_APPWIDGET) {
configureWidget(data!!)
} else if (requestCode == REQUEST_CREATE_APPWIDGET) {
createWidget(data!!)
}
} else if (resultCode == RESULT_CANCELED && data != null) {
val appWidgetId =
data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (appWidgetId != -1) {
AppWidget(appWidgetId).delete(this)
}
}
}
/**
* For a better preview, [ManageWidgetsActivity] should behave exactly like [HomeActivity]
*/
override fun isHomeScreen(): Boolean {
return true
}
}

View file

@ -0,0 +1,172 @@
package de.jrpie.android.launcher.ui.widgets.manage
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Intent
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.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.databinding.ActivitySelectWidgetBinding
import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.widgets.ClockWidget
import de.jrpie.android.launcher.widgets.LauncherAppWidgetProvider
import de.jrpie.android.launcher.widgets.LauncherClockWidgetProvider
import de.jrpie.android.launcher.widgets.LauncherWidgetProvider
import de.jrpie.android.launcher.widgets.WidgetPanel
import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission
import de.jrpie.android.launcher.widgets.getAppWidgetHost
import de.jrpie.android.launcher.widgets.getAppWidgetProviders
import de.jrpie.android.launcher.widgets.updateWidget
private const val REQUEST_WIDGET_PERMISSION = 29
/**
* This activity lets the user pick an app widget to add.
* It provides an interface similar to [android.appwidget.AppWidgetManager.ACTION_APPWIDGET_PICK],
* but shows more information and also shows widgets from other user profiles.
*/
class SelectWidgetActivity : AppCompatActivity(), UIObject {
lateinit var binding: ActivitySelectWidgetBinding
var widgetId: Int = -1
var widgetPanelId: Int = WidgetPanel.HOME.id
private fun tryBindWidget(info: LauncherWidgetProvider) {
when (info) {
is LauncherAppWidgetProvider -> {
if (bindAppWidgetOrRequestPermission(
this,
info.info,
widgetId,
REQUEST_WIDGET_PERMISSION
)
) {
setResult(
RESULT_OK,
Intent().also {
it.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
it.putExtra(EXTRA_PANEL_ID, widgetPanelId)
}
)
finish()
}
}
is LauncherClockWidgetProvider -> {
updateWidget(ClockWidget(widgetId, WidgetPosition(0, 4, 12, 3), widgetPanelId))
finish()
}
}
}
override fun onStart() {
super<AppCompatActivity>.onStart()
super<UIObject>.onStart()
}
override fun onCreate(savedInstanceState: Bundle?) {
super<AppCompatActivity>.onCreate(savedInstanceState)
super<UIObject>.onCreate()
binding = ActivitySelectWidgetBinding.inflate(layoutInflater)
setContentView(binding.root)
widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.HOME.id)
if (widgetId == -1) {
widgetId = getAppWidgetHost().allocateAppWidgetId()
}
val viewManager = LinearLayoutManager(this)
val viewAdapter = SelectWidgetRecyclerAdapter()
binding.selectWidgetRecycler.apply {
setHasFixedSize(false)
layoutManager = viewManager
adapter = viewAdapter
}
}
override fun getTheme(): Resources.Theme {
return modifyTheme(super.getTheme())
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_WIDGET_PERMISSION && resultCode == RESULT_OK) {
data ?: return
val provider = (data.getSerializableExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER) as? AppWidgetProviderInfo) ?: return
tryBindWidget(LauncherAppWidgetProvider(provider))
}
}
inner class SelectWidgetRecyclerAdapter() :
RecyclerView.Adapter<SelectWidgetRecyclerAdapter.ViewHolder>() {
private val widgets = getAppWidgetProviders(this@SelectWidgetActivity).toTypedArray()
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener {
var textView: TextView = itemView.findViewById(R.id.list_widgets_row_name)
var descriptionView: TextView = itemView.findViewById(R.id.list_widgets_row_description)
var iconView: ImageView = itemView.findViewById(R.id.list_widgets_row_icon)
var previewView: ImageView = itemView.findViewById(R.id.list_widgets_row_preview)
override fun onClick(v: View) {
tryBindWidget(widgets[bindingAdapterPosition])
}
init {
itemView.setOnClickListener(this)
}
}
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
val label = widgets[i].loadLabel(this@SelectWidgetActivity)
val description = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
widgets[i].loadDescription(this@SelectWidgetActivity)
} else {
""
}
val preview =
widgets[i].loadPreviewImage(this@SelectWidgetActivity)
val icon =
widgets[i].loadIcon(this@SelectWidgetActivity)
viewHolder.textView.text = label
viewHolder.descriptionView.text = description
viewHolder.descriptionView.visibility =
if (description?.isEmpty() == false) { View.VISIBLE } else { View.GONE }
viewHolder.iconView.setImageDrawable(icon)
viewHolder.previewView.setImageDrawable(preview)
viewHolder.previewView.visibility =
if (preview != null) { View.VISIBLE } else { View.GONE }
viewHolder.previewView.requestLayout()
}
override fun getItemCount(): Int {
return widgets.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view: View = inflater.inflate(R.layout.list_widgets_row, parent, false)
return ViewHolder(view)
}
}
}

View file

@ -0,0 +1,175 @@
package de.jrpie.android.launcher.ui.widgets.manage
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.core.graphics.contains
import androidx.core.graphics.minus
import androidx.core.graphics.toRect
import androidx.core.view.children
import de.jrpie.android.launcher.ui.widgets.WidgetContainerView
import de.jrpie.android.launcher.widgets.Widget
import de.jrpie.android.launcher.widgets.WidgetPanel
import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.updateWidget
import kotlin.math.max
import kotlin.math.min
/**
* A variant of the [WidgetContainerView] which allows to manage widgets.
*/
class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSet? = null) :
WidgetContainerView(widgetPanelId, context, attrs) {
constructor(context: Context, attrs: AttributeSet?) : this(WidgetPanel.HOME.id, context, attrs)
val TOUCH_SLOP: Int
val TOUCH_SLOP_SQUARE: Int
val LONG_PRESS_TIMEOUT: Long
init {
val configuration = ViewConfiguration.get(context)
TOUCH_SLOP = configuration.scaledTouchSlop
TOUCH_SLOP_SQUARE = TOUCH_SLOP * TOUCH_SLOP
LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong()
}
enum class EditMode(val resize: (dx: Int, dy: Int, rect: Rect) -> Rect) {
MOVE({ dx, dy, rect ->
Rect(rect.left + dx, rect.top + dy, rect.right + dx, rect.bottom + dy)
}),
TOP({ dx, dy, rect ->
Rect(rect.left, min(rect.top + dy, rect.bottom - 200), rect.right, rect.bottom)
}),
BOTTOM({ dx, dy, rect ->
Rect(rect.left, rect.top, rect.right, max(rect.top + 200, rect.bottom + dy))
}),
LEFT({ dx, dy, rect ->
Rect(min(rect.left + dx, rect.right - 200), rect.top, rect.right, rect.bottom)
}),
RIGHT({ dx, dy, rect ->
Rect(rect.left, rect.top, max(rect.left + 200, rect.right + dx), rect.bottom)
}),
}
var selectedWidgetOverlayView: WidgetOverlayView? = null
var selectedWidgetView: View? = null
var currentGestureStart: Point? = null
var startWidgetPosition: Rect? = null
var lastPosition = Rect()
private val longPressHandler = Handler(Looper.getMainLooper())
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
return true
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event == null) {
return false
}
synchronized(this) {
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
val start = Point(event.x.toInt(), event.y.toInt())
currentGestureStart = start
val view = children.mapNotNull { it as? WidgetOverlayView }.firstOrNull {
RectF(it.x, it.y, it.x + it.width, it.y + it.height).toRect().contains(start) == true
} ?: return false
val position = (view.layoutParams as Companion.LayoutParams).position.getAbsoluteRect(width, height)
selectedWidgetOverlayView = view
selectedWidgetView = widgetViewById.get(view.widgetId) ?: return true
startWidgetPosition = position
val positionInView = start.minus(Point(position.left, position.top))
view.mode = view.getHandles().firstOrNull { it.position.contains(positionInView) }?.mode ?: EditMode.MOVE
longPressHandler.postDelayed({
synchronized(this@WidgetManagerView) {
view.showPopupMenu()
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
endInteraction()
}
}, LONG_PRESS_TIMEOUT)
}
if (event.actionMasked == MotionEvent.ACTION_MOVE ||
event.actionMasked == MotionEvent.ACTION_UP
) {
val distanceX = event.x - (currentGestureStart?.x ?: return true)
val distanceY = event.y - (currentGestureStart?.y ?: return true)
if (distanceX * distanceX + distanceY * distanceY > TOUCH_SLOP_SQUARE) {
longPressHandler.removeCallbacksAndMessages(null)
}
val view = selectedWidgetOverlayView ?: return true
val start = startWidgetPosition ?: return true
val absoluteNewPosition = view.mode?.resize(
distanceX.toInt(),
distanceY.toInt(),
start
) ?: return true
val newPosition = WidgetPosition.fromAbsoluteRect(
absoluteNewPosition, width, height
)
if (newPosition != lastPosition) {
lastPosition = absoluteNewPosition
(view.layoutParams as Companion.LayoutParams).position = newPosition
(selectedWidgetView?.layoutParams as? Companion.LayoutParams)?.position = newPosition
requestLayout()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_PRESS)
}
}
if (event.actionMasked == MotionEvent.ACTION_UP) {
longPressHandler.removeCallbacksAndMessages(null)
val id = selectedWidgetOverlayView?.widgetId ?: return true
val widget = Widget.byId(context, id) ?: return true
widget.position = newPosition
endInteraction()
updateWidget(widget)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END)
}
}
}
}
return true
}
private fun endInteraction() {
startWidgetPosition = null
selectedWidgetOverlayView?.mode = null
}
override fun updateWidgets(activity: Activity, widgets: Collection<Widget>?) {
super.updateWidgets(activity, widgets)
if (widgets == null) {
return
}
children.mapNotNull { it as? WidgetOverlayView }.forEach { removeView(it) }
widgets.filter { it.panelId == widgetPanelId }.forEach { widget ->
WidgetOverlayView(activity).let {
addView(it)
it.widgetId = widget.id
(it.layoutParams as Companion.LayoutParams).position = widget.position
}
}
}
}

View file

@ -0,0 +1,132 @@
package de.jrpie.android.launcher.ui.widgets.manage
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import android.widget.PopupMenu
import androidx.core.graphics.toRectF
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.widgets.Widget
import de.jrpie.android.launcher.widgets.updateWidget
private const val HANDLE_SIZE = 100
private const val HANDLE_EDGE_SIZE = (1.2 * HANDLE_SIZE).toInt()
/**
* An overlay to show configuration options for a widget in [WidgetManagerView]
*/
class WidgetOverlayView : View {
val paint = Paint()
val handlePaint = Paint()
val selectedHandlePaint = Paint()
var mode: WidgetManagerView.EditMode? = null
class Handle(val mode: WidgetManagerView.EditMode, val position: Rect)
init {
handlePaint.style = Paint.Style.STROKE
handlePaint.setARGB(255, 255, 255, 255)
selectedHandlePaint.style = Paint.Style.FILL_AND_STROKE
selectedHandlePaint.setARGB(100, 255, 255, 255)
paint.style = Paint.Style.STROKE
paint.setARGB(255, 255, 255, 255)
}
private var preview: Drawable? = null
var widgetId: Int = -1
set(newId) {
field = newId
preview = Widget.byId(context, widgetId)?.getPreview(context)
}
constructor(context: Context) : super(context) {
init(null, 0)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(attrs, 0)
}
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
context,
attrs,
defStyle
) {
init(attrs, defStyle)
}
private fun init(attrs: AttributeSet?, defStyle: Int) { }
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
getHandles().forEach {
if (it.mode == mode) {
canvas.drawRoundRect(it.position.toRectF(), 5f, 5f, selectedHandlePaint)
} else {
canvas.drawRoundRect(it.position.toRectF(), 5f, 5f, handlePaint)
}
}
val bounds = getBounds()
canvas.drawRoundRect(bounds.toRectF(), 5f, 5f, paint)
if (mode == null) {
return
}
//preview?.bounds = bounds
//preview?.draw(canvas)
}
fun showPopupMenu() {
val widget = Widget.byId(context, widgetId)?: return
val menu = PopupMenu(context, this)
menu.menu.let {
it.add(
context.getString(R.string.widget_menu_remove)
).setOnMenuItemClickListener { _ ->
Widget.byId(context, widgetId)?.delete(context)
return@setOnMenuItemClickListener true
}
it.add(
if (widget.allowInteraction) {
context.getString(R.string.widget_menu_disable_interaction)
} else {
context.getString(R.string.widget_menu_enable_interaction)
}
).setOnMenuItemClickListener { _ ->
widget.allowInteraction = !widget.allowInteraction
updateWidget(widget)
return@setOnMenuItemClickListener true
}
}
menu.show()
}
fun getHandles(): List<Handle> {
return listOf<Handle>(
Handle(WidgetManagerView.EditMode.TOP,
Rect(HANDLE_EDGE_SIZE, 0, width - HANDLE_EDGE_SIZE, HANDLE_SIZE)),
Handle(WidgetManagerView.EditMode.BOTTOM,
Rect(HANDLE_EDGE_SIZE, height - HANDLE_SIZE, width - HANDLE_EDGE_SIZE, height)),
Handle(WidgetManagerView.EditMode.LEFT,
Rect(0, HANDLE_EDGE_SIZE, HANDLE_SIZE, height - HANDLE_EDGE_SIZE)),
Handle(WidgetManagerView.EditMode.RIGHT,
Rect(width - HANDLE_SIZE, HANDLE_EDGE_SIZE, width, height - HANDLE_EDGE_SIZE))
)
}
private fun getBounds(): Rect {
return Rect(0,0, width, height)
}
}

View file

@ -0,0 +1,98 @@
package de.jrpie.android.launcher.ui.widgets.manage
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.PopupMenu
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.widgets.WidgetPanel
import de.jrpie.android.launcher.widgets.updateWidgetPanel
class WidgetPanelsRecyclerAdapter(
val context: Context,
val showMenu: Boolean = false,
val onSelectWidgetPanel: (WidgetPanel) -> Unit
) :
RecyclerView.Adapter<WidgetPanelsRecyclerAdapter.ViewHolder>() {
var widgetPanels = (LauncherPreferences.widgets().customPanels() ?: setOf()).toTypedArray()
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var labelView: TextView = itemView.findViewById(R.id.list_widget_panels_label)
}
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
viewHolder.labelView.text = widgetPanels[i].label
viewHolder.itemView.setOnClickListener {
onSelectWidgetPanel(widgetPanels[i])
}
if (showMenu) {
viewHolder.itemView.setOnLongClickListener {
showOptionsPopup(
viewHolder,
widgetPanels[i]
)
}
}
}
@Suppress("SameReturnValue")
private fun showOptionsPopup(
viewHolder: ViewHolder,
widgetPanel: WidgetPanel
): Boolean {
//create the popup menu
val popup = PopupMenu(context, viewHolder.labelView)
popup.menu.add(R.string.manage_widget_panels_delete).setOnMenuItemClickListener { _ ->
widgetPanel.delete(context)
true
}
popup.menu.add(R.string.manage_widget_panels_rename).setOnMenuItemClickListener { _ ->
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
setPositiveButton(R.string.dialog_ok) { dialogInterface, _ ->
var newLabel = (dialogInterface as? AlertDialog)
?.findViewById<EditText>(R.id.dialog_rename_widget_panel_edit_text)
?.text?.toString()
if (newLabel == null || newLabel.isEmpty()) {
newLabel =
(context.getString(R.string.widget_panel_default_name, widgetPanel.id))
}
widgetPanel.label = newLabel
updateWidgetPanel(widgetPanel)
}
setView(R.layout.dialog_rename_widget_panel)
}.create().also { it.show() }.apply {
findViewById<EditText>(R.id.dialog_rename_widget_panel_edit_text)?.let {
it.setText(widgetPanel.label)
it.hint = context.getString(R.string.widget_panel_default_name, widgetPanel.id)
}
}
true
}
popup.show()
return true
}
override fun getItemCount(): Int {
return widgetPanels.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view: View =
LayoutInflater.from(context).inflate(R.layout.list_widget_panels_row, parent, false)
val viewHolder = ViewHolder(view)
return viewHolder
}
}

View file

@ -0,0 +1,126 @@
package de.jrpie.android.launcher.widgets
import android.app.Activity
import android.appwidget.AppWidgetHostView
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.SizeF
import android.view.View
import de.jrpie.android.launcher.Application
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("widget:app")
class AppWidget(
override val id: Int,
override var position: WidgetPosition = WidgetPosition(0,0,1,1),
override var panelId: Int = WidgetPanel.HOME.id,
override var allowInteraction: Boolean = false,
// We keep track of packageName, className and user to make it possible to restore the widget
// on a new device when restoring settings (currently not implemented)
// In normal operation only id and position are used.
val packageName: String? = null,
val className: String? = null,
val user: Int? = null
): Widget() {
constructor(
id: Int,
position: WidgetPosition,
panelId: Int,
widgetProviderInfo: AppWidgetProviderInfo
) :
this(
id,
position,
panelId,
false,
widgetProviderInfo.provider.packageName,
widgetProviderInfo.provider.className,
widgetProviderInfo.profile.hashCode()
)
/**
* Get the [AppWidgetProviderInfo] by [id].
* If the widget is not installed, use [restoreAppWidgetProviderInfo] instead.
*/
fun getAppWidgetProviderInfo(context: Context): AppWidgetProviderInfo? {
if (id < 0) {
return null
}
return (context.applicationContext as Application).appWidgetManager
.getAppWidgetInfo(id)
}
/**
* Restore the AppWidgetProviderInfo from [user], [packageName] and [className].
* Only use this when the widget is not installed,
* in normal operation use [getAppWidgetProviderInfo] instead.
*/
/*fun restoreAppWidgetProviderInfo(context: Context): AppWidgetProviderInfo? {
return getAppWidgetProviders(context).firstOrNull {
it.profile.hashCode() == user
&& it.provider.packageName == packageName
&& it.provider.className == className
}
}*/
override fun toString(): String {
return "WidgetInfo(id=$id, position=$position, packageName=$packageName, className=$className, user=$user)"
}
override fun createView(activity: Activity): AppWidgetHostView? {
val providerInfo = activity.getAppWidgetManager().getAppWidgetInfo(id) ?: return null
val view = activity.getAppWidgetHost()
.createView(activity, this.id, providerInfo)
val dp = activity.resources.displayMetrics.density
val screenWidth = activity.resources.displayMetrics.widthPixels
val screenHeight = activity.resources.displayMetrics.heightPixels
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val absolutePosition = position.getAbsoluteRect(screenWidth, screenHeight)
view.updateAppWidgetSize(Bundle.EMPTY,
listOf(SizeF(
absolutePosition.width() / dp,
absolutePosition.height() / dp
)))
}
view.setPadding(0,0,0,0)
return view
}
override fun findView(views: Sequence<View>): AppWidgetHostView? {
return views.mapNotNull { it as? AppWidgetHostView }.firstOrNull { it.appWidgetId == id }
}
override fun getIcon(context: Context): Drawable? {
return context.getAppWidgetManager().getAppWidgetInfo(id)?.loadIcon(context, DisplayMetrics.DENSITY_HIGH)
}
override fun getPreview(context: Context): Drawable? {
return context.getAppWidgetManager().getAppWidgetInfo(id)?.loadPreviewImage(context, DisplayMetrics.DENSITY_HIGH)
}
override fun isConfigurable(context: Context): Boolean {
return context.getAppWidgetManager().getAppWidgetInfo(id)?.configure != null
}
override fun configure(activity: Activity, requestCode: Int) {
if (!isConfigurable(activity)) {
return
}
activity.getAppWidgetHost().startAppWidgetConfigureActivityForResult(
activity,
id,
0,
requestCode,
null
)
}
}

View file

@ -0,0 +1,42 @@
package de.jrpie.android.launcher.widgets
import android.app.Activity
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import de.jrpie.android.launcher.ui.widgets.ClockView
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("widget:clock")
class ClockWidget(
override val id: Int,
override var position: WidgetPosition,
override val panelId: Int,
override var allowInteraction: Boolean = true
) : Widget() {
override fun createView(activity: Activity): View? {
return ClockView(activity, null, id)
}
override fun findView(views: Sequence<View>): ClockView? {
return views.mapNotNull { it as? ClockView }.firstOrNull { it.appWidgetId == id }
}
override fun getPreview(context: Context): Drawable? {
return null
}
override fun getIcon(context: Context): Drawable? {
return null
}
override fun isConfigurable(context: Context): Boolean {
return false
}
override fun configure(activity: Activity, requestCode: Int) { }
}

View file

@ -0,0 +1,58 @@
package de.jrpie.android.launcher.widgets
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.DisplayMetrics
import androidx.appcompat.content.res.AppCompatResources
import de.jrpie.android.launcher.R
sealed class LauncherWidgetProvider {
abstract fun loadLabel(context: Context): CharSequence?
abstract fun loadPreviewImage(context: Context): Drawable?
abstract fun loadIcon(context: Context): Drawable?
abstract fun loadDescription(context: Context): CharSequence?
}
class LauncherAppWidgetProvider(val info: AppWidgetProviderInfo) : LauncherWidgetProvider() {
override fun loadLabel(context: Context): CharSequence? {
return info.loadLabel(context.packageManager)
}
override fun loadPreviewImage(context: Context): Drawable? {
return info.loadPreviewImage(context, DisplayMetrics.DENSITY_DEFAULT)
}
override fun loadIcon(context: Context): Drawable? {
return info.loadIcon(context, DisplayMetrics.DENSITY_DEFAULT)
}
override fun loadDescription(context: Context): CharSequence? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
info.loadDescription(context)
} else {
null
}
}
}
class LauncherClockWidgetProvider : LauncherWidgetProvider() {
override fun loadLabel(context: Context): CharSequence? {
return context.getString(R.string.widget_clock_label)
}
override fun loadDescription(context: Context): CharSequence? {
return context.getString(R.string.widget_clock_description)
}
override fun loadPreviewImage(context: Context): Drawable? {
return null
}
override fun loadIcon(context: Context): Drawable? {
return AppCompatResources.getDrawable(context, R.drawable.baseline_clock_24)
}
}

View file

@ -0,0 +1,65 @@
package de.jrpie.android.launcher.widgets
import android.app.Activity
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.preferences.LauncherPreferences
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
sealed class Widget {
abstract val id: Int
abstract var position: WidgetPosition
abstract val panelId: Int
abstract var allowInteraction: Boolean
/**
* @param activity The activity where the view will be used. Must not be an AppCompatActivity.
*/
abstract fun createView(activity: Activity): View?
abstract fun findView(views: Sequence<View>): View?
abstract fun getPreview(context: Context): Drawable?
abstract fun getIcon(context: Context): Drawable?
abstract fun isConfigurable(context: Context): Boolean
abstract fun configure(activity: Activity, requestCode: Int)
fun delete(context: Context) {
context.getAppWidgetHost().deleteAppWidgetId(id)
LauncherPreferences.widgets().widgets(
LauncherPreferences.widgets().widgets()?.also {
it.remove(this)
}
)
}
fun getPanel(): WidgetPanel? {
return WidgetPanel.byId(panelId)
}
override fun hashCode(): Int {
return id
}
override fun equals(other: Any?): Boolean {
return (other as? Widget)?.id == id
}
fun serialize(): String {
return Json.encodeToString(serializer(), this)
}
companion object {
fun deserialize(serialized: String): Widget {
return Json.decodeFromString(serialized)
}
fun byId(context: Context, id: Int): Widget? {
return (context.applicationContext as Application).widgets.value?.firstOrNull {
it.id == id
}
}
}
}

View file

@ -0,0 +1,58 @@
package de.jrpie.android.launcher.widgets
import android.content.Context
import de.jrpie.android.launcher.preferences.LauncherPreferences
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
@SerialName("panel")
class WidgetPanel(val id: Int, var label: String) {
override fun equals(other: Any?): Boolean {
return (other as? WidgetPanel)?.id == id
}
override fun hashCode(): Int {
return id
}
fun serialize(): String {
return Json.encodeToString(this)
}
fun delete(context: Context) {
LauncherPreferences.widgets().customPanels(
(LauncherPreferences.widgets().customPanels() ?: setOf()).minus(this)
)
(LauncherPreferences.widgets().widgets() ?: return)
.filter { it.panelId == this.id }.forEach { it.delete(context) }
}
companion object {
val HOME = WidgetPanel(0, "home")
fun byId(id: Int): WidgetPanel? {
if (id == 0) {
return HOME
}
return LauncherPreferences.widgets().customPanels()?.firstOrNull { it.id == id }
}
fun allocateId(): Int {
return (
(LauncherPreferences.widgets().customPanels() ?: setOf())
.plus(HOME)
.maxOfOrNull { it.id } ?: 0
) + 1
}
fun deserialize(serialized: String): WidgetPanel {
return Json.decodeFromString(serialized)
}
}
}

View file

@ -0,0 +1,58 @@
package de.jrpie.android.launcher.widgets
import android.graphics.Rect
import kotlinx.serialization.Serializable
import kotlin.math.ceil
import kotlin.math.roundToInt
import kotlin.math.max
const val GRID_SIZE: Short = 12
@Serializable
data class WidgetPosition(var x: Short, var y: Short, var width: Short, var height: Short) {
fun getAbsoluteRect(screenWidth: Int, screenHeight: Int): Rect {
val gridWidth = screenWidth / GRID_SIZE.toFloat()
val gridHeight= screenHeight / GRID_SIZE.toFloat()
return Rect(
(x * gridWidth).toInt(),
(y * gridHeight).toInt(),
((x + width) * gridWidth).toInt(),
((y + height) * gridHeight).toInt()
)
}
companion object {
fun fromAbsoluteRect(absolute: Rect, screenWidth: Int, screenHeight: Int): WidgetPosition {
val gridWidth = screenWidth / GRID_SIZE.toFloat()
val gridHeight= screenHeight / GRID_SIZE.toFloat()
val x = (absolute.left / gridWidth).roundToInt().toShort().coerceIn(0, (GRID_SIZE-1).toShort())
val y = (absolute.top / gridHeight).roundToInt().toShort().coerceIn(0, (GRID_SIZE-1).toShort())
val w = max(2, ((absolute.right - absolute.left) / gridWidth).roundToInt()).toShort()
val h = max(2, ((absolute.bottom - absolute.top) / gridHeight).roundToInt()).toShort()
return WidgetPosition(x,y,w,h)
}
fun center(minWidth: Int, minHeight: Int, screenWidth: Int, screenHeight: Int): WidgetPosition {
val gridWidth = screenWidth / GRID_SIZE.toFloat()
val gridHeight= screenHeight / GRID_SIZE.toFloat()
val cellsWidth = ceil(minWidth / gridWidth).toInt().toShort()
val cellsHeight = ceil(minHeight / gridHeight).toInt().toShort()
return WidgetPosition(
((GRID_SIZE - cellsWidth) / 2).toShort(),
((GRID_SIZE - cellsHeight) / 2).toShort(),
cellsWidth,
cellsHeight
)
}
}
}

View file

@ -0,0 +1,97 @@
package de.jrpie.android.launcher.widgets
import android.app.Activity
import android.app.Service
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.os.Build
import android.os.UserManager
import android.util.Log
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.preferences.LauncherPreferences
fun deleteAllWidgets(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.getAppWidgetHost().appWidgetIds.forEach { AppWidget(it).delete(context) }
}
}
/**
* Tries to bind [providerInfo] to the id [id].
* @param providerInfo The widget to be bound.
* @param id The id to bind the widget to. If -1 is provided, a new id is allocated.
* @param
* @param requestCode Used to start an activity to request permission to bind the widget.
*
* @return true iff the app widget was bound successfully.
*/
fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidgetProviderInfo, id: Int, requestCode: Int? = null): Boolean {
val appWidgetId = if(id == -1) {
activity.getAppWidgetHost().allocateAppWidgetId()
} else { id }
Log.i("Launcher", "Binding new widget ${appWidgetId}")
if (!activity.getAppWidgetManager().bindAppWidgetIdIfAllowed(
appWidgetId,
providerInfo.provider
)
) {
Log.i("Widgets", "requesting permission for widget")
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,appWidgetId)
putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, providerInfo.provider)
}
activity.startActivityForResult(intent, requestCode ?: 0)
return false
}
return true
}
fun getAppWidgetProviders( context: Context ): List<LauncherWidgetProvider> {
val list = mutableListOf<LauncherWidgetProvider>(LauncherClockWidgetProvider())
val appWidgetManager = context.getAppWidgetManager()
val profiles =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
(context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps).profiles
} else {
(context.getSystemService(Service.USER_SERVICE) as UserManager).userProfiles
}
list.addAll(
profiles.map {
appWidgetManager.getInstalledProvidersForProfile(it)
.map { LauncherAppWidgetProvider(it) }
}.flatten()
)
return list
}
fun updateWidget(widget: Widget) {
LauncherPreferences.widgets().widgets(
(LauncherPreferences.widgets().widgets() ?: setOf())
.minus(widget)
.plus(widget)
)
}
fun updateWidgetPanel(widgetPanel: WidgetPanel) {
LauncherPreferences.widgets().customPanels(
(LauncherPreferences.widgets().customPanels() ?: setOf())
.minus(widgetPanel)
.plus(widgetPanel)
)
}
fun Context.getAppWidgetHost(): AppWidgetHost {
return (this.applicationContext as Application).appWidgetHost
}
fun Context.getAppWidgetManager(): AppWidgetManager {
return (this.applicationContext as Application).appWidgetManager
}

Binary file not shown.

After

(image error) Size: 124 KiB

Binary file not shown.

After

(image error) Size: 23 KiB

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

View file

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

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
<path
android:fillColor="?android:textColor"
android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z" />
</vector>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="M13,13v8h8v-8h-8zM3,21h8v-8L3,13v8zM3,3v8h8L11,3L3,3zM16.66,1.69L11,7.34 16.66,13l5.66,-5.66 -5.66,-5.65z" />
</vector>

Binary file not shown.

Before

(image error) Size: 159 KiB

View file

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

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".ui.widgets.manage.ManageWidgetPanelsActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/manage_widget_panels_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
app:elevation="0dp"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/manage_widget_panels_heading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:minHeight="?actionBarSize"
android:padding="@dimen/appbar_padding"
android:text="@string/widget_panels_title"
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/manage_widget_panels_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="center"
android:includeFontPadding="true"
android:paddingLeft="16sp"
android:paddingRight="16sp"
android:src="@drawable/baseline_close_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/manage_widget_panels_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/manage_widget_panels_appbar" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/manage_widget_panels_add_panel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/baseline_add_24"
app:layout_anchor="@+id/manage_widget_panels_recycler"
app:layout_anchorGravity="end|bottom"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".ui.widgets.manage.ManageWidgetsActivity">
<de.jrpie.android.launcher.ui.widgets.manage.WidgetManagerView
android:id="@+id/manage_widgets_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/manage_widgets_button_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/baseline_add_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -80,7 +80,6 @@
android:minHeight="40dp"
tools:drawableLeft="@drawable/baseline_settings_24"
tools:text="Shortcut name" />
<!--
<Space
android:layout_width="match_parent"
android:layout_height="10dp" />
@ -90,8 +89,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:textColor"
android:checked="true"
android:text="@string/pin_shortcut_switch_visible" />
-->
<Space
android:layout_width="match_parent"
@ -103,8 +102,21 @@
android:layout_height="wrap_content"
android:text="@string/pin_shortcut_button_bind" />
<Space
android:layout_width="match_parent"
android:layout_height="10dp" />
</LinearLayout>
</ScrollView>
<Button
android:id="@+id/pin_shortcut_button_ok"
android:layout_margin="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@android:string/ok"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/select_widget_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".ui.widgets.manage.SelectWidgetActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/select_widget_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:background="@null"
app:elevation="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/select_widget_heading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:minHeight="?actionBarSize"
android:padding="@dimen/appbar_padding"
android:text="@string/select_widget_title"
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
android:textSize="30sp"
app:layout_constraintEnd_toStartOf="@id/select_widget_close"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/select_widget_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:gravity="center"
android:includeFontPadding="true"
android:paddingLeft="16sp"
android:paddingRight="16sp"
android:src="@drawable/baseline_close_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/select_widget_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/select_widget_appbar" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/home_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:longClickable="false"
tools:context=".ui.widgets.WidgetPanelActivity">
<de.jrpie.android.launcher.ui.widgets.WidgetContainerView
android:id="@+id/widget_panel_widget_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:longClickable="false"
android:fitsSystemWindows="true"
tools:context=".ui.widgets.ClockView">
<TextClock
android:id="@+id/clock_upper_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="2024-12-24" />
<TextClock
android:id="@+id/clock_lower_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:textSize="18sp"
tools:text="18:00:00"
app:layout_constraintTop_toBottomOf="@+id/clock_upper_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/AlertDialogCustom"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/dialog_create_widget_panel_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:importantForAutofill="no"
android:inputType="text"
tools:ignore="LabelFor" />
</LinearLayout>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/AlertDialogCustom"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/dialog_rename_widget_panel_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:importantForAutofill="no"
android:inputType="text"
tools:ignore="LabelFor" />
</LinearLayout>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/dialog_select_widget_panel_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/dialog_select_widget_panel_info_no_panels"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/dialog_select_widget_panel_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_weight="1" />
</LinearLayout>

View file

@ -10,29 +10,10 @@
android:fitsSystemWindows="true"
tools:context=".ui.HomeActivity">
<TextClock
android:id="@+id/home_upper_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.45"
tools:text="2024-12-24" />
<TextClock
android:id="@+id/home_lower_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:textSize="18sp"
tools:text="18:00:00"
app:layout_constraintTop_toBottomOf="@+id/home_upper_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<de.jrpie.android.launcher.ui.widgets.WidgetContainerView
android:id="@+id/home_widget_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- only shown when µLauncher settings can't be reached by a gesture -->
<ImageView

View file

@ -40,8 +40,7 @@
android:src="@drawable/baseline_settings_24"
custom:layout_constraintBottom_toBottomOf="parent"
custom:layout_constraintStart_toStartOf="parent"
custom:layout_constraintTop_toTopOf="parent"
custom:type="solid" />
custom:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/list_heading"
@ -70,8 +69,7 @@
android:src="@drawable/baseline_close_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
custom:type="solid" />
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/list_lock"
android:visibility="gone"
@ -85,8 +83,7 @@
android:src="@drawable/baseline_lock_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/list_close"
app:layout_constraintTop_toTopOf="parent"
custom:type="solid" />
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.tabs.TabLayout
@ -98,6 +95,11 @@
</com.google.android.material.appbar.AppBarLayout>
<!--
Should be replaced by androidx.viewpager2.widget.ViewPager2
but there is an issue with opening the keyboard:
https://github.com/jrpie/launcher/issues/130
-->
<androidx.viewpager.widget.ViewPager
android:id="@+id/list_viewpager"
android:layout_width="0dp"

View file

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

View file

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

View file

@ -32,6 +32,6 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="some app" />
tools:text="@string/app_name" />
</androidx.constraintlayout.widget.ConstraintLayout>

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