mirror of
https://github.com/jrpie/Launcher.git
synced 2025-04-27 14:20:50 +02:00
Compare commits
84 commits
Author | SHA1 | Date | |
---|---|---|---|
a4fcdf60c7 | |||
ffaaba7abb | |||
e7a06c443d | |||
077bd1ce44 | |||
22633bdac3 | |||
4f795289d5 | |||
2774b74d9d | |||
3d49ec16a7 | |||
![]() |
a0b2417363 | ||
![]() |
20a01e9f03 | ||
![]() |
24250ad345 | ||
![]() |
7cce425339 | ||
![]() |
0877ca6772 | ||
![]() |
03a9833b51 | ||
![]() |
ce65741717 | ||
![]() |
c085087e1e | ||
![]() |
cbd23159da | ||
![]() |
940e5785dc | ||
![]() |
14ffbd1f6c | ||
![]() |
bfc84b57ca | ||
![]() |
8b1963f3e1 | ||
![]() |
4f801427a4 | ||
![]() |
8a487eb4c7 | ||
![]() |
e6dd2634ae | ||
0441b3fd3d | |||
e7c1d28576 | |||
653d16b269 | |||
5d695ec0ea | |||
b4608ef153 | |||
8e140e2e69 | |||
7fc58fe384 | |||
54409b6312 | |||
865cd47583 | |||
58ddd3c8cc | |||
0baa889de5 | |||
fa34cbae90 | |||
7ac09bd465 | |||
![]() |
c8d7a1cc3e | ||
7094d55484 | |||
b3e4d8834a | |||
008d0242ee | |||
![]() |
65034bf2fb | ||
![]() |
3cec2c36c6 | ||
00350d4c3a | |||
0941062270 | |||
c783a51658 | |||
da115bb2d9 | |||
90434617e7 | |||
47940811b4 | |||
![]() |
232046e986 | ||
![]() |
ff108ee323 | ||
![]() |
943867d938 | ||
![]() |
59f4a29044 | ||
![]() |
bd70b822cf | ||
72f9c0595f | |||
75b22400c5 | |||
c1511cd475 | |||
3597baee1f | |||
e02ca4091f | |||
541e60356c | |||
492749a340 | |||
55af392706 | |||
077ee4381a | |||
e250a58ef4 | |||
c7af387a94 | |||
6cd17343fc | |||
b156b68d53 | |||
c9ee2c6304 | |||
bf45b6602e | |||
d7dd1aa71a | |||
3664159782 | |||
8df9aae029 | |||
![]() |
f776fbb88e | ||
![]() |
a5ec8bb796 | ||
3b2dca9af9 | |||
1b12032750 | |||
55a54fb9a5 | |||
![]() |
e39ff62613 | ||
9fe1a37ed6 | |||
1f825d6f00 | |||
5ea03d39fa | |||
ae119ac4ce | |||
941b06b258 | |||
![]() |
9935386ad8 |
150 changed files with 5375 additions and 1455 deletions
.github
.scripts
app
build.gradle
src/main
AndroidManifest.xml
java/de/jrpie/android/launcher
Application.ktFunctions.kt
actions
apps
AbstractAppInfo.ktAbstractDetailedAppInfo.ktAppFilter.ktAppInfo.ktDetailedAppInfo.ktDetailedPinnedShortcutInfo.ktPinnedShortcutInfo.ktPrivateSpace.kt
preferences
ColorPreference.ktLauncherPreferences$Config.javaListLayout.ktPreferences.kt
legacy
serialization
theme
ui
Helper.ktHomeActivity.ktPinShortcutActivity.ktTouchGestureDetector.ktUIObject.kt
list
settings
tutorial
util
widgets
widgets
res
drawable-mdpi
drawable
baseline_add_24.xmlbaseline_apps_24.xmlbaseline_clock_24.xmlbaseline_home_24.xmlbaseline_more_horiz_24.xmlbaseline_navigate_before_24.xmlbaseline_navigate_next_24.xmlbaseline_question_mark_24.xmlbaseline_volume_adjust_24.xmlbaseline_widgets_24.xmlhome_round_screen.pngic_launcher_background.xml
layout
activity_manage_widget_panels.xmlactivity_manage_widgets.xmlactivity_pin_shortcut.xmlactivity_select_widget.xmlactivity_widget_panel.xmlclock.xmldialog_create_widget_panel.xmldialog_rename_widget_panel.xmldialog_select_widget_panel.xmlhome.xmllist.xmllist_apps_row.xmllist_apps_row_variant_grid.xmllist_apps_row_variant_text.xml
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
|
@ -1,3 +1,3 @@
|
|||
# How you can support finnmglas/Launcher
|
||||
# How you can support jrpie/Launcher
|
||||
|
||||
custom: sponsor.finnmglas.com
|
||||
custom: https://s.jrpie.de/launcher-donate
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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()
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -22,26 +22,44 @@ class LauncherAccessibilityService : AccessibilityService() {
|
|||
|
||||
companion object {
|
||||
private const val TAG = "Launcher Accessibility"
|
||||
private const val ACTION_REQUEST_ENABLE = "ACTION_REQUEST_ENABLE"
|
||||
const val ACTION_LOCK_SCREEN = "ACTION_LOCK_SCREEN"
|
||||
const val ACTION_RECENT_APPS = "ACTION_RECENT_APPS"
|
||||
|
||||
fun lockScreen(context: Context) {
|
||||
private fun invoke(context: Context, action: String, failureMessageRes: Int) {
|
||||
try {
|
||||
context.startService(
|
||||
Intent(
|
||||
context,
|
||||
LauncherAccessibilityService::class.java
|
||||
).apply {
|
||||
action = ACTION_LOCK_SCREEN
|
||||
this.action = action
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.alert_lock_screen_failed),
|
||||
context.getString(failureMessageRes),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun lockScreen(context: Context) {
|
||||
if (!isEnabled(context)) {
|
||||
showEnableDialog(context)
|
||||
} else {
|
||||
invoke(context, ACTION_LOCK_SCREEN, R.string.alert_lock_screen_failed)
|
||||
}
|
||||
}
|
||||
|
||||
fun openRecentApps(context: Context) {
|
||||
if (!isEnabled(context)) {
|
||||
showEnableDialog(context)
|
||||
} else {
|
||||
invoke(context, ACTION_RECENT_APPS, R.string.alert_recent_apps_failed)
|
||||
}
|
||||
}
|
||||
|
||||
fun isEnabled(context: Context): Boolean {
|
||||
val enabledServices = Settings.Secure.getString(
|
||||
context.contentResolver,
|
||||
|
@ -58,7 +76,7 @@ class LauncherAccessibilityService : AccessibilityService() {
|
|||
setView(R.layout.dialog_consent_accessibility)
|
||||
setTitle(R.string.dialog_consent_accessibility_title)
|
||||
setPositiveButton(R.string.dialog_consent_accessibility_ok) { _, _ ->
|
||||
lockScreen(context)
|
||||
invoke(context, ACTION_REQUEST_ENABLE, R.string.alert_enable_accessibility_failed)
|
||||
}
|
||||
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
|
||||
}.create().also { it.show() }.apply {
|
||||
|
@ -94,7 +112,9 @@ class LauncherAccessibilityService : AccessibilityService() {
|
|||
}
|
||||
|
||||
when (action) {
|
||||
ACTION_REQUEST_ENABLE -> {} // do nothing
|
||||
ACTION_LOCK_SCREEN -> handleLockScreen()
|
||||
ACTION_RECENT_APPS -> performGlobalAction(GLOBAL_ACTION_RECENTS)
|
||||
}
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
|
|
|
@ -6,10 +6,10 @@ import android.widget.Button
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import de.jrpie.android.launcher.BuildConfig
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
|
||||
|
||||
@Suppress("unused")
|
||||
enum class LockMethod(
|
||||
private val lock: (Context) -> Unit,
|
||||
private val isEnabled: (Context) -> Boolean,
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package de.jrpie.android.launcher.apps
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* This interface is implemented by [AppInfo] and [PinnedShortcutInfo].
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface AbstractAppInfo {
|
||||
fun serialize(): String {
|
||||
return Json.encodeToString(this)
|
||||
}
|
||||
companion object {
|
||||
const val INVALID_USER = -1
|
||||
|
||||
fun deserialize(serialized: String): AbstractAppInfo {
|
||||
return Json.decodeFromString(serialized)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package de.jrpie.android.launcher.apps
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.UserHandle
|
||||
import android.util.Log
|
||||
import de.jrpie.android.launcher.Application
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
|
||||
/**
|
||||
* This interface is implemented by [DetailedAppInfo] and [DetailedPinnedShortcutInfo]
|
||||
*/
|
||||
sealed interface AbstractDetailedAppInfo {
|
||||
fun getRawInfo(): AbstractAppInfo
|
||||
fun getLabel(): String
|
||||
fun getIcon(context: Context): Drawable
|
||||
fun getUser(context: Context): UserHandle
|
||||
fun isPrivate(): Boolean
|
||||
fun isRemovable(): Boolean
|
||||
fun getAction(): Action
|
||||
|
||||
|
||||
fun getCustomLabel(context: Context): String {
|
||||
val map = (context.applicationContext as? Application)?.getCustomAppNames()
|
||||
return map?.get(getRawInfo()) ?: getLabel()
|
||||
}
|
||||
|
||||
|
||||
fun setCustomLabel(label: CharSequence?) {
|
||||
Log.i("Launcher", "Setting custom label for ${this.getRawInfo()} to ${label}.")
|
||||
val map = LauncherPreferences.apps().customNames() ?: HashMap<AbstractAppInfo, String>()
|
||||
|
||||
if (label.isNullOrEmpty()) {
|
||||
map.remove(getRawInfo())
|
||||
} else {
|
||||
map[getRawInfo()] = label.toString()
|
||||
}
|
||||
LauncherPreferences.apps().customNames(map)
|
||||
}
|
||||
|
||||
}
|
|
@ -6,6 +6,7 @@ import android.os.Build
|
|||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.AppAction
|
||||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.actions.ShortcutAction
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import java.util.Locale
|
||||
import kotlin.text.Regex.Companion.escape
|
||||
|
@ -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()) }),
|
||||
;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,33 +4,18 @@ import android.app.Service
|
|||
import android.content.Context
|
||||
import android.content.pm.LauncherActivityInfo
|
||||
import android.content.pm.LauncherApps
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
|
||||
import de.jrpie.android.launcher.getUserFromId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* Represents an app installed on the users device.
|
||||
* Contains the minimal amount of data required to identify the app.
|
||||
*/
|
||||
@Serializable
|
||||
class AppInfo(val packageName: String, val activityName: String?, val user: Int = INVALID_USER) {
|
||||
|
||||
fun serialize(): String {
|
||||
return Json.encodeToString(this)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if(other is AppInfo) {
|
||||
return other.user == user && other.packageName == packageName
|
||||
&& other.activityName == activityName
|
||||
}
|
||||
return super.equals(other)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return packageName.hashCode()
|
||||
}
|
||||
@SerialName("app")
|
||||
data class AppInfo(val packageName: String, val activityName: String?, val user: Int = INVALID_USER): AbstractAppInfo {
|
||||
|
||||
fun getLauncherActivityInfo(
|
||||
context: Context
|
||||
|
@ -41,17 +26,4 @@ class AppInfo(val packageName: String, val activityName: String?, val user: Int
|
|||
return activityList.firstOrNull { app -> app.name == activityName }
|
||||
?: activityList.firstOrNull()
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "AppInfo {package=$packageName, activity=$activityName, user=$user}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val INVALID_USER = -1
|
||||
|
||||
fun deserialize(serialized: String): AppInfo {
|
||||
return Json.decodeFromString(serialized)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,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 {
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package de.jrpie.android.launcher.apps
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherApps
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.UserHandle
|
||||
import androidx.annotation.RequiresApi
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.ShortcutAction
|
||||
import de.jrpie.android.launcher.getUserFromId
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
class DetailedPinnedShortcutInfo(
|
||||
private val shortcutInfo: PinnedShortcutInfo,
|
||||
private val label: String,
|
||||
private val icon: Drawable,
|
||||
private val privateSpace: Boolean
|
||||
) : AbstractDetailedAppInfo {
|
||||
|
||||
constructor(context: Context, shortcut: ShortcutInfo) : this(
|
||||
PinnedShortcutInfo(shortcut),
|
||||
shortcut.longLabel.toString(),
|
||||
(context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps)
|
||||
.getShortcutBadgedIconDrawable(shortcut, 0),
|
||||
shortcut.userHandle == getPrivateSpaceUser(context)
|
||||
)
|
||||
|
||||
override fun getRawInfo(): AbstractAppInfo {
|
||||
return shortcutInfo
|
||||
}
|
||||
|
||||
override fun getLabel(): String {
|
||||
return label
|
||||
}
|
||||
|
||||
override fun getIcon(context: Context): Drawable {
|
||||
return icon
|
||||
}
|
||||
|
||||
override fun getUser(context: Context): UserHandle {
|
||||
return getUserFromId(shortcutInfo.user, context)
|
||||
}
|
||||
|
||||
override fun isPrivate(): Boolean {
|
||||
return privateSpace
|
||||
}
|
||||
|
||||
override fun isRemovable(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getAction(): Action {
|
||||
return ShortcutAction(shortcutInfo)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromPinnedShortcutInfo(shortcutInfo: PinnedShortcutInfo, context: Context): DetailedPinnedShortcutInfo? {
|
||||
return shortcutInfo.getShortcutInfo(context)?.let {
|
||||
DetailedPinnedShortcutInfo(context, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package de.jrpie.android.launcher.apps
|
||||
|
||||
import android.app.Service
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherApps
|
||||
import android.content.pm.LauncherApps.ShortcutQuery
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import de.jrpie.android.launcher.getUserFromId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
@Serializable
|
||||
@SerialName("shortcut")
|
||||
data class PinnedShortcutInfo(
|
||||
val id: String,
|
||||
val packageName: String,
|
||||
val activityName: String,
|
||||
val user: Int
|
||||
): AbstractAppInfo {
|
||||
|
||||
constructor(info: ShortcutInfo) : this(info.id, info.`package`, info.activity?.className ?: "", info.userHandle.hashCode())
|
||||
|
||||
fun getShortcutInfo(context: Context): ShortcutInfo? {
|
||||
val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
|
||||
return try {
|
||||
launcherApps.getShortcuts(
|
||||
ShortcutQuery().apply {
|
||||
setQueryFlags(ShortcutQuery.FLAG_MATCH_PINNED)
|
||||
setPackage(packageName)
|
||||
setActivity(ComponentName(packageName, activityName))
|
||||
setShortcutIds(listOf(id))
|
||||
},
|
||||
getUserFromId(user, context)
|
||||
)?.firstOrNull()
|
||||
} catch(_: Exception) {
|
||||
// can throw SecurityException or IllegalStateException when profile is locked
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import androidx.core.graphics.green
|
|||
import androidx.core.graphics.red
|
||||
import androidx.preference.Preference
|
||||
import de.jrpie.android.launcher.R
|
||||
import androidx.core.graphics.toColorInt
|
||||
|
||||
class ColorPreference(context: Context, attrs: AttributeSet?) :
|
||||
Preference(context, attrs) {
|
||||
|
@ -52,7 +53,7 @@ class ColorPreference(context: Context, attrs: AttributeSet?) :
|
|||
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
|
||||
setView(R.layout.dialog_choose_color)
|
||||
setTitle(R.string.dialog_choose_color_title)
|
||||
setPositiveButton(R.string.dialog_select_color_ok) { _, _ ->
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
persistInt(currentColor)
|
||||
summary = currentColor.getHex()
|
||||
}
|
||||
|
@ -83,10 +84,10 @@ class ColorPreference(context: Context, attrs: AttributeSet?) :
|
|||
override fun onTextChanged(text: CharSequence?, p1: Int, p2: Int, p3: Int) {}
|
||||
override fun afterTextChanged(editable: Editable?) {
|
||||
preview.hasFocus() || return
|
||||
val newText = editable?.toString()
|
||||
newText.isNullOrBlank() && return
|
||||
val newText = editable?.toString() ?: return
|
||||
newText.isBlank() && return
|
||||
try {
|
||||
val newColor = Color.parseColor(newText.toString())
|
||||
val newColor = newText.toColorInt()
|
||||
currentColor = newColor
|
||||
updateColor(false)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
|
|
|
@ -5,8 +5,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 {}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 )
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -3,7 +3,11 @@ package de.jrpie.android.launcher.ui
|
|||
import android.app.Activity
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
|
||||
|
@ -11,10 +15,12 @@ import de.jrpie.android.launcher.preferences.LauncherPreferences
|
|||
* An interface implemented by every [Activity], Fragment etc. in Launcher.
|
||||
* It handles themes and window flags - a useful abstraction as it is the same everywhere.
|
||||
*/
|
||||
@Suppress("deprecation") // FLAG_FULLSCREEN is required to support API level < 30
|
||||
fun setWindowFlags(window: Window, homeScreen: Boolean) {
|
||||
window.setFlags(0, 0) // clear flags
|
||||
|
||||
// Display notification bar
|
||||
if (LauncherPreferences.display().fullScreen())
|
||||
if (LauncherPreferences.display().hideStatusBar())
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
|
@ -36,17 +42,19 @@ fun setWindowFlags(window: Window, homeScreen: Boolean) {
|
|||
|
||||
}
|
||||
|
||||
|
||||
interface UIObject {
|
||||
fun onCreate() {
|
||||
if (this is Activity) {
|
||||
setWindowFlags(window, isHomeScreen())
|
||||
|
||||
if (!LauncherPreferences.display().rotateScreen()) {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||
}
|
||||
if (this !is Activity) {
|
||||
return
|
||||
}
|
||||
setWindowFlags(window, isHomeScreen())
|
||||
|
||||
if (!LauncherPreferences.display().rotateScreen()) {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||
}
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
setOnClicks()
|
||||
adjustLayout()
|
||||
|
@ -70,4 +78,26 @@ interface UIObject {
|
|||
fun isHomeScreen(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun hideNavigationBar() {
|
||||
if (this !is Activity) {
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.insetsController?.apply {
|
||||
hide(WindowInsets.Type.navigationBars())
|
||||
systemBarsBehavior =
|
||||
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
} else {
|
||||
// Try to hide the navigation bar but do not hide the status bar
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
or View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,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
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package de.jrpie.android.launcher.ui.list.apps
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -15,12 +14,12 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import de.jrpie.android.launcher.Application
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
|
||||
import de.jrpie.android.launcher.actions.AppAction
|
||||
import de.jrpie.android.launcher.actions.Action
|
||||
import de.jrpie.android.launcher.actions.Gesture
|
||||
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.AppFilter
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.DetailedAppInfo
|
||||
import de.jrpie.android.launcher.getUserFromId
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.preferences.ListLayout
|
||||
import de.jrpie.android.launcher.ui.list.ListActivity
|
||||
|
@ -47,7 +46,8 @@ class AppsRecyclerAdapter(
|
|||
RecyclerView.Adapter<AppsRecyclerAdapter.ViewHolder>() {
|
||||
|
||||
private val apps = (activity.applicationContext as Application).apps
|
||||
private val appsListDisplayed: MutableList<DetailedAppInfo> = mutableListOf()
|
||||
private val appsListDisplayed: MutableList<AbstractDetailedAppInfo> = mutableListOf()
|
||||
private val grayscale = LauncherPreferences.theme().monochromeIcons()
|
||||
|
||||
// temporarily disable auto launch
|
||||
var disableAutoLaunch: Boolean = false
|
||||
|
@ -68,7 +68,7 @@ class AppsRecyclerAdapter(
|
|||
override fun onClick(v: View) {
|
||||
val rect = Rect()
|
||||
img.getGlobalVisibleRect(rect)
|
||||
selectItem(adapterPosition, rect)
|
||||
selectItem(bindingAdapterPosition, rect)
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -80,20 +80,19 @@ class AppsRecyclerAdapter(
|
|||
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
|
||||
var appLabel = appsListDisplayed[i].getCustomLabel(activity)
|
||||
|
||||
val appIcon = appsListDisplayed[i].getIcon(activity)
|
||||
|
||||
viewHolder.img.transformGrayscale(grayscale)
|
||||
viewHolder.img.setImageDrawable(appIcon.constantState?.newDrawable() ?: appIcon)
|
||||
|
||||
if (layout.useBadgedText) {
|
||||
appLabel = activity.packageManager.getUserBadgedLabel(
|
||||
appLabel,
|
||||
getUserFromId(appsListDisplayed[i].app.user, activity)
|
||||
appsListDisplayed[i].getUser(activity)
|
||||
).toString()
|
||||
}
|
||||
|
||||
val appIcon = appsListDisplayed[i].icon
|
||||
|
||||
viewHolder.textView.text = appLabel
|
||||
viewHolder.img.setImageDrawable(appIcon)
|
||||
|
||||
if (LauncherPreferences.theme().monochromeIcons())
|
||||
viewHolder.img.transformGrayscale()
|
||||
|
||||
// decide when to show the options popup menu about
|
||||
if (intention == ListActivity.ListActivityIntention.VIEW) {
|
||||
|
@ -118,22 +117,26 @@ class AppsRecyclerAdapter(
|
|||
@Suppress("SameReturnValue")
|
||||
private fun showOptionsPopup(
|
||||
viewHolder: ViewHolder,
|
||||
appInfo: DetailedAppInfo
|
||||
appInfo: AbstractDetailedAppInfo
|
||||
): Boolean {
|
||||
//create the popup menu
|
||||
|
||||
val popup = PopupMenu(activity, viewHolder.img)
|
||||
popup.inflate(R.menu.menu_app)
|
||||
|
||||
if (appInfo.isSystemApp) {
|
||||
if (!appInfo.isRemovable()) {
|
||||
popup.menu.findItem(R.id.app_menu_delete).setVisible(false)
|
||||
}
|
||||
|
||||
if (LauncherPreferences.apps().hidden()?.contains(appInfo.app) == true) {
|
||||
if (appInfo !is DetailedAppInfo) {
|
||||
popup.menu.findItem(R.id.app_menu_info).setVisible(false)
|
||||
}
|
||||
|
||||
if (LauncherPreferences.apps().hidden()?.contains(appInfo.getRawInfo()) == true) {
|
||||
popup.menu.findItem(R.id.app_menu_hidden).setTitle(R.string.list_app_hidden_remove)
|
||||
}
|
||||
|
||||
if (LauncherPreferences.apps().favorites()?.contains(appInfo.app) == true) {
|
||||
if (LauncherPreferences.apps().favorites()?.contains(appInfo.getRawInfo()) == true) {
|
||||
popup.menu.findItem(R.id.app_menu_favorite).setTitle(R.string.list_app_favorite_remove)
|
||||
}
|
||||
|
||||
|
@ -141,19 +144,19 @@ class AppsRecyclerAdapter(
|
|||
popup.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.app_menu_delete -> {
|
||||
appInfo.app.uninstall(activity); true
|
||||
appInfo.getRawInfo().uninstall(activity); true
|
||||
}
|
||||
|
||||
R.id.app_menu_info -> {
|
||||
appInfo.app.openSettings(activity); true
|
||||
(appInfo.getRawInfo() as? AppInfo)?.openSettings(activity); true
|
||||
}
|
||||
|
||||
R.id.app_menu_favorite -> {
|
||||
appInfo.app.toggleFavorite(); true
|
||||
appInfo.getRawInfo().toggleFavorite(); true
|
||||
}
|
||||
|
||||
R.id.app_menu_hidden -> {
|
||||
appInfo.app.toggleHidden(root); true
|
||||
appInfo.getRawInfo().toggleHidden(root); true
|
||||
}
|
||||
|
||||
R.id.app_menu_rename -> {
|
||||
|
@ -188,15 +191,14 @@ class AppsRecyclerAdapter(
|
|||
val appInfo = appsListDisplayed[pos]
|
||||
when (intention) {
|
||||
ListActivity.ListActivityIntention.VIEW -> {
|
||||
AppAction(appInfo.app).invoke(activity, rect)
|
||||
appInfo.getAction().invoke(activity, rect)
|
||||
}
|
||||
|
||||
ListActivity.ListActivityIntention.PICK -> {
|
||||
val returnIntent = Intent()
|
||||
AppAction(appInfo.app).writeToIntent(returnIntent)
|
||||
returnIntent.putExtra("forGesture", forGesture)
|
||||
activity.setResult(REQUEST_CHOOSE_APP, returnIntent)
|
||||
activity.finish()
|
||||
forGesture ?: return
|
||||
val gesture = Gesture.byId(forGesture) ?: return
|
||||
Action.setActionForGesture(gesture, appInfo.getAction())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -211,8 +213,8 @@ class AppsRecyclerAdapter(
|
|||
&& !disableAutoLaunch
|
||||
&& LauncherPreferences.functionality().searchAutoLaunch()
|
||||
) {
|
||||
val info = appsListDisplayed[0]
|
||||
AppAction(info.app).invoke(activity)
|
||||
val app = appsListDisplayed[0]
|
||||
app.getAction().invoke(activity)
|
||||
|
||||
val inputMethodManager =
|
||||
activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package de.jrpie.android.launcher.ui.list.apps
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.LauncherApps
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
|
@ -13,11 +13,13 @@ import android.widget.EditText
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.REQUEST_UNINSTALL
|
||||
import de.jrpie.android.launcher.apps.AppInfo
|
||||
import de.jrpie.android.launcher.apps.DetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractAppInfo
|
||||
import de.jrpie.android.launcher.apps.AbstractDetailedAppInfo
|
||||
import de.jrpie.android.launcher.apps.PinnedShortcutInfo
|
||||
import de.jrpie.android.launcher.getUserFromId
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import androidx.core.net.toUri
|
||||
|
||||
private const val LOG_TAG = "AppContextMenu"
|
||||
|
||||
|
@ -32,27 +34,29 @@ fun AppInfo.openSettings(
|
|||
}
|
||||
}
|
||||
|
||||
fun AppInfo.uninstall(activity: android.app.Activity) {
|
||||
val packageName = this.packageName
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
package de.jrpie.android.launcher.ui.tutorial
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.window.OnBackInvokedDispatcher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.REQUEST_CHOOSE_APP
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import de.jrpie.android.launcher.databinding.TutorialBinding
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.saveListActivityChoice
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentConcept
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentFinish
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentSetup
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentStart
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentUsage
|
||||
import de.jrpie.android.launcher.ui.blink
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment0Start
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment1Concept
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment2Usage
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment3AppList
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment4Setup
|
||||
import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragment5Finish
|
||||
|
||||
/**
|
||||
* The [TutorialActivity] is displayed automatically on new installations.
|
||||
|
@ -29,19 +31,75 @@ import de.jrpie.android.launcher.ui.tutorial.tabs.TutorialFragmentUsage
|
|||
*/
|
||||
class TutorialActivity : AppCompatActivity(), UIObject {
|
||||
|
||||
private lateinit var binding: TutorialBinding
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super<AppCompatActivity>.onCreate(savedInstanceState)
|
||||
super<UIObject>.onCreate()
|
||||
|
||||
// Initialise layout
|
||||
setContentView(R.layout.tutorial)
|
||||
binding = TutorialBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
// Handle back key / gesture on Android 13+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||
OnBackInvokedDispatcher.PRIORITY_OVERLAY
|
||||
) {
|
||||
// prevent going back when the tutorial is shown for the first time
|
||||
if (!LauncherPreferences.internal().started()) {
|
||||
return@registerOnBackInvokedCallback
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// set up tabs and swiping in settings
|
||||
val sectionsPagerAdapter = TutorialSectionsPagerAdapter(supportFragmentManager)
|
||||
val viewPager: ViewPager = findViewById(R.id.tutorial_viewpager)
|
||||
viewPager.adapter = sectionsPagerAdapter
|
||||
val tabs: TabLayout = findViewById(R.id.tutorial_tabs)
|
||||
tabs.setupWithViewPager(viewPager)
|
||||
val sectionsPagerAdapter = TutorialSectionsPagerAdapter(this)
|
||||
binding.tutorialViewpager.apply {
|
||||
adapter = sectionsPagerAdapter
|
||||
currentItem = 0
|
||||
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
binding.tutorialButtonNext.apply {
|
||||
val lastItem = sectionsPagerAdapter.itemCount - 1
|
||||
visibility = if (position == lastItem) {
|
||||
View.INVISIBLE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
if (position == 0) {
|
||||
blink()
|
||||
} else {
|
||||
clearAnimation()
|
||||
}
|
||||
}
|
||||
binding.tutorialButtonBack.apply {
|
||||
visibility = if (position == 0) {
|
||||
View.INVISIBLE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
TabLayoutMediator(binding.tutorialTabs, binding.tutorialViewpager) { _, _ -> }.attach()
|
||||
binding.tutorialButtonNext.setOnClickListener {
|
||||
binding.tutorialViewpager.apply {
|
||||
setCurrentItem(
|
||||
(currentItem + 1).coerceAtMost(sectionsPagerAdapter.itemCount - 1),
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
binding.tutorialButtonBack.setOnClickListener {
|
||||
binding.tutorialViewpager.apply {
|
||||
setCurrentItem((currentItem - 1).coerceAtLeast(0), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTheme(): Resources.Theme {
|
||||
|
@ -53,14 +111,9 @@ class TutorialActivity : AppCompatActivity(), UIObject {
|
|||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_CHOOSE_APP -> saveListActivityChoice(data)
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
// Default: prevent going back, allow if viewed again later
|
||||
// prevent going back when the tutorial is shown for the first time
|
||||
@Deprecated("Deprecated in Java", ReplaceWith("use anyway"))
|
||||
@Suppress("deprecation") // support API level < 33
|
||||
override fun onBackPressed() {
|
||||
if (LauncherPreferences.internal().started())
|
||||
super.onBackPressed()
|
||||
|
@ -74,26 +127,22 @@ class TutorialActivity : AppCompatActivity(), UIObject {
|
|||
*
|
||||
* Tabs: (Start | Concept | Usage | Setup | Finish)
|
||||
*/
|
||||
class TutorialSectionsPagerAdapter(fm: FragmentManager) :
|
||||
FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||
class TutorialSectionsPagerAdapter(activity: FragmentActivity) :
|
||||
FragmentStateAdapter(activity) {
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
override fun getItemCount(): Int {
|
||||
return 6
|
||||
}
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> TutorialFragmentStart()
|
||||
1 -> TutorialFragmentConcept()
|
||||
2 -> TutorialFragmentUsage()
|
||||
3 -> TutorialFragmentSetup()
|
||||
4 -> TutorialFragmentFinish()
|
||||
0 -> TutorialFragment0Start()
|
||||
1 -> TutorialFragment1Concept()
|
||||
2 -> TutorialFragment2Usage()
|
||||
3 -> TutorialFragment3AppList()
|
||||
4 -> TutorialFragment4Setup()
|
||||
5 -> TutorialFragment5Finish()
|
||||
else -> Fragment()
|
||||
}
|
||||
}
|
||||
|
||||
/* We don't use titles here, as we have the dots */
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return ""
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,24 +5,22 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.databinding.TutorialStartBinding
|
||||
import de.jrpie.android.launcher.databinding.Tutorial0StartBinding
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
import de.jrpie.android.launcher.ui.blink
|
||||
|
||||
/**
|
||||
* The [TutorialFragmentStart] is a used as a tab in the TutorialActivity.
|
||||
* The [TutorialFragment0Start] is a used as a tab in the TutorialActivity.
|
||||
*
|
||||
* It displays info about the app and gets the user into the tutorial
|
||||
*/
|
||||
class TutorialFragmentStart : Fragment(), UIObject {
|
||||
class TutorialFragment0Start : Fragment(), UIObject {
|
||||
|
||||
private lateinit var binding: TutorialStartBinding
|
||||
private lateinit var binding: Tutorial0StartBinding
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = TutorialStartBinding.inflate(inflater, container, false)
|
||||
binding.tutorialStartIconRight.blink()
|
||||
binding = Tutorial0StartBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
|
@ -6,22 +6,22 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.BuildConfig
|
||||
import de.jrpie.android.launcher.databinding.TutorialConceptBinding
|
||||
import de.jrpie.android.launcher.databinding.Tutorial1ConceptBinding
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
||||
/**
|
||||
* The [TutorialFragmentConcept] is a used as a tab in the TutorialActivity.
|
||||
* The [TutorialFragment1Concept] is a used as a tab in the TutorialActivity.
|
||||
*
|
||||
* It is used to display info about Launchers concept (open source, efficiency ...)
|
||||
*/
|
||||
class TutorialFragmentConcept : Fragment(), UIObject {
|
||||
private lateinit var binding: TutorialConceptBinding
|
||||
class TutorialFragment1Concept : Fragment(), UIObject {
|
||||
private lateinit var binding: Tutorial1ConceptBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = TutorialConceptBinding.inflate(inflater, container, false)
|
||||
binding = Tutorial1ConceptBinding.inflate(inflater, container, false)
|
||||
binding.tutorialConceptBadgeVersion.text = BuildConfig.VERSION_NAME
|
||||
return binding.root
|
||||
}
|
|
@ -9,17 +9,17 @@ import de.jrpie.android.launcher.R
|
|||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
||||
/**
|
||||
* The [TutorialFragmentUsage] is a used as a tab in the TutorialActivity.
|
||||
* The [TutorialFragment2Usage] is a used as a tab in the TutorialActivity.
|
||||
*
|
||||
* Tells the user how his screen will look and how the app can be used
|
||||
*/
|
||||
class TutorialFragmentUsage : Fragment(), UIObject {
|
||||
class TutorialFragment2Usage : Fragment(), UIObject {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.tutorial_usage, container, false)
|
||||
return inflater.inflate(R.layout.tutorial_2_usage, container, false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
|
@ -0,0 +1,30 @@
|
|||
package de.jrpie.android.launcher.ui.tutorial.tabs
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.R
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
||||
/**
|
||||
* The [TutorialFragment3AppList] is a used as a tab in the TutorialActivity.
|
||||
*
|
||||
* Tells the user how his screen will look and how the app can be used
|
||||
*/
|
||||
class TutorialFragment3AppList : Fragment(), UIObject {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.tutorial_3_app_list, container, false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super<Fragment>.onStart()
|
||||
super<UIObject>.onStart()
|
||||
}
|
||||
|
||||
}
|
|
@ -9,17 +9,17 @@ import de.jrpie.android.launcher.R
|
|||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
||||
/**
|
||||
* The [TutorialFragmentSetup] is a used as a tab in the TutorialActivity.
|
||||
* The [TutorialFragment4Setup] is a used as a tab in the TutorialActivity.
|
||||
*
|
||||
* It is used to display info in the tutorial
|
||||
*/
|
||||
class TutorialFragmentSetup : Fragment(), UIObject {
|
||||
class TutorialFragment4Setup : Fragment(), UIObject {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.tutorial_setup, container, false)
|
||||
return inflater.inflate(R.layout.tutorial_4_setup, container, false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
|
@ -6,25 +6,25 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.jrpie.android.launcher.BuildConfig.VERSION_CODE
|
||||
import de.jrpie.android.launcher.databinding.TutorialFinishBinding
|
||||
import de.jrpie.android.launcher.databinding.Tutorial5FinishBinding
|
||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||
import de.jrpie.android.launcher.setDefaultHomeScreen
|
||||
import de.jrpie.android.launcher.ui.UIObject
|
||||
|
||||
/**
|
||||
* The [TutorialFragmentFinish] is a used as a tab in the TutorialActivity.
|
||||
* The [TutorialFragment5Finish] is a used as a tab in the TutorialActivity.
|
||||
*
|
||||
* It is used to display further resources and let the user start Launcher
|
||||
*/
|
||||
class TutorialFragmentFinish : Fragment(), UIObject {
|
||||
class TutorialFragment5Finish : Fragment(), UIObject {
|
||||
|
||||
private lateinit var binding: TutorialFinishBinding
|
||||
private lateinit var binding: Tutorial5FinishBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = TutorialFinishBinding.inflate(inflater, container, false)
|
||||
binding = Tutorial5FinishBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ class HtmlTextView(context: Context, attr: AttributeSet?, int: Int) :
|
|||
constructor(context: Context) : this(context, null, 0)
|
||||
|
||||
init {
|
||||
@Suppress("deprecation") // required to support API level < 24
|
||||
text = Html.fromHtml(text.toString())
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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() }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
126
app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt
Normal file
126
app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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) { }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
BIN
app/src/main/res/drawable-mdpi/tutorial_app_list.png
Normal file
BIN
app/src/main/res/drawable-mdpi/tutorial_app_list.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 124 KiB |
BIN
app/src/main/res/drawable-mdpi/tutorial_home_screen.png
Normal file
BIN
app/src/main/res/drawable-mdpi/tutorial_home_screen.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 23 KiB |
11
app/src/main/res/drawable/baseline_add_24.xml
Normal file
11
app/src/main/res/drawable/baseline_add_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="?android:textColor"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||
|
||||
</vector>
|
11
app/src/main/res/drawable/baseline_apps_24.xml
Normal file
11
app/src/main/res/drawable/baseline_apps_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="?android:textColor"
|
||||
android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z" />
|
||||
|
||||
</vector>
|
15
app/src/main/res/drawable/baseline_clock_24.xml
Normal file
15
app/src/main/res/drawable/baseline_clock_24.xml
Normal 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>
|
11
app/src/main/res/drawable/baseline_home_24.xml
Normal file
11
app/src/main/res/drawable/baseline_home_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="?android:textColor"
|
||||
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
|
||||
|
||||
</vector>
|
|
@ -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>
|
11
app/src/main/res/drawable/baseline_navigate_before_24.xml
Normal file
11
app/src/main/res/drawable/baseline_navigate_before_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="?android:textColor"
|
||||
android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z" />
|
||||
|
||||
</vector>
|
11
app/src/main/res/drawable/baseline_navigate_next_24.xml
Normal file
11
app/src/main/res/drawable/baseline_navigate_next_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="?android:textColor"
|
||||
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z" />
|
||||
|
||||
</vector>
|
12
app/src/main/res/drawable/baseline_question_mark_24.xml
Normal file
12
app/src/main/res/drawable/baseline_question_mark_24.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11.07,12.85c0.77,-1.39 2.25,-2.21 3.11,-3.44c0.91,-1.29 0.4,-3.7 -2.18,-3.7c-1.69,0 -2.52,1.28 -2.87,2.34L6.54,6.96C7.25,4.83 9.18,3 11.99,3c2.35,0 3.96,1.07 4.78,2.41c0.7,1.15 1.11,3.3 0.03,4.9c-1.2,1.77 -2.35,2.31 -2.97,3.45c-0.25,0.46 -0.35,0.76 -0.35,2.24h-2.89C10.58,15.22 10.46,13.95 11.07,12.85zM14,20c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2S14,18.9 14,20z" />
|
||||
|
||||
</vector>
|
16
app/src/main/res/drawable/baseline_volume_adjust_24.xml
Normal file
16
app/src/main/res/drawable/baseline_volume_adjust_24.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="?android:textColor"
|
||||
android:pathData="m 3,9 v 6 h 4 l 5,5 V 4 L 7,9 Z m 13.5,3 C 16.5,10.23 15.48,8.71 14,7.97 v 8.05 c 1.48,-0.73 2.5,-2.25 2.5,-4.02 z" />
|
||||
<path
|
||||
android:fillAlpha="0.5"
|
||||
android:fillColor="?android:textColor"
|
||||
android:pathData="m 14,3.23 v 2.06 c 2.89,0.86 5,3.54 5,6.71 0,3.17 -2.11,5.85 -5,6.71 v 2.06 C 18.01,19.86 21,16.28 21,12 21,7.72 18.01,4.14 14,3.23 Z"
|
||||
android:strokeAlpha="0.5" />
|
||||
|
||||
</vector>
|
11
app/src/main/res/drawable/baseline_widgets_24.xml
Normal file
11
app/src/main/res/drawable/baseline_widgets_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="?android:textColor"
|
||||
android:pathData="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 |
|
@ -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>
|
73
app/src/main/res/layout/activity_manage_widget_panels.xml
Normal file
73
app/src/main/res/layout/activity_manage_widget_panels.xml
Normal 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>
|
25
app/src/main/res/layout/activity_manage_widgets.xml
Normal file
25
app/src/main/res/layout/activity_manage_widgets.xml
Normal 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>
|
|
@ -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>
|
72
app/src/main/res/layout/activity_select_widget.xml
Normal file
72
app/src/main/res/layout/activity_select_widget.xml
Normal 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>
|
15
app/src/main/res/layout/activity_widget_panel.xml
Normal file
15
app/src/main/res/layout/activity_widget_panel.xml
Normal 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>
|
35
app/src/main/res/layout/clock.xml
Normal file
35
app/src/main/res/layout/clock.xml
Normal 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>
|
18
app/src/main/res/layout/dialog_create_widget_panel.xml
Normal file
18
app/src/main/res/layout/dialog_create_widget_panel.xml
Normal 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>
|
18
app/src/main/res/layout/dialog_rename_widget_panel.xml
Normal file
18
app/src/main/res/layout/dialog_rename_widget_panel.xml
Normal 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>
|
26
app/src/main/res/layout/dialog_select_widget_panel.xml
Normal file
26
app/src/main/res/layout/dialog_select_widget_panel.xml
Normal 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>
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -15,18 +15,21 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@mipmap/ic_launcher_round"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/list_apps_row_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="60sp"
|
||||
android:layout_marginStart="20sp"
|
||||
android:gravity="start"
|
||||
android:text=""
|
||||
android:textSize="20sp"
|
||||
tools:text="@string/app_name"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/list_apps_row_icon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -15,6 +15,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:src="@mipmap/ic_launcher_round"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
|
@ -25,7 +26,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"
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue