diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index fa112ae..63740a7 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -13,7 +13,6 @@ body:
label: Describe the Bug
description: What happened?
placeholder: A clear and concise description of what the bug is.
- render: markdown
validations:
required: true
- type: textarea
@@ -21,7 +20,6 @@ body:
attributes:
label: Expected Behavior
description: What did you expect to happen instead?
- render: markdown
validations:
required: false
- type: textarea
@@ -29,7 +27,6 @@ body:
attributes:
label: To Reproduce
description: What steps are required to reproduce the bug?
- render: markdown
placeholder: |
Steps to reproduce the behavior:
1. Go to '...'
@@ -45,6 +42,5 @@ body:
description: |
What device are you using? Adding this information helps to reproduce the bug.
You can copy this from µLauncher > Settings > Meta > Report Bug.
- render: markdown
validations:
required: false
diff --git a/.scripts/release.sh b/.scripts/release.sh
index f207c87..dc6959d 100755
--- a/.scripts/release.sh
+++ b/.scripts/release.sh
@@ -1,9 +1,25 @@
#!/bin/bash
+
+# This script builds all variants of µLauncher to create a release, namely:
+# - app-release.apk (GitHub release; used by F-Droid for reproducible builds)
+# - launcher-accrescent.apks (Accrescent)
+# - app-release.aab (Play Store)
+
+# This is only intended to work on my (@jrpie) computer.
+# To use this script for building a fork you need to:
+# - install bundletool.jar and
+# - create a keystore and modify the variables below accordingly
+
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 for the default release
KEYSTORE="$HOME/data/keys/launcher_jrpie.jks"
+# keystore for the default accrescent release
KEYSTORE_ACCRESCENT="$HOME/data/keys/launcher_jrpie_accrescent.jks"
+
+# keepassxc-password is a custom script to fetch passwords from my password manager
KEYSTORE_PASS=$(keepassxc-password "android_keys/launcher")
KEYSTORE_ACCRESCENT_PASS=$(keepassxc-password "android_keys/launcher-accrescent")
@@ -11,12 +27,11 @@ if [[ $(git status --porcelain) ]]; then
echo "There are uncommitted changes."
read -p "Continue anyway? (y/n) " -n 1 -r
- echo # (optional) move to a new line
+ echo
if ! [[ $REPLY =~ ^[Yy]$ ]]
then
exit 1
fi
-
fi
rm -rf "$OUTPUT_DIR"
diff --git a/app/build.gradle b/app/build.gradle
index 1a0a6fb..7cc9422 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -23,8 +23,8 @@ android {
minSdkVersion 21
targetSdkVersion 35
compileSdk 35
- versionCode 44
- versionName "0.1.4"
+ versionCode 48
+ versionName "0.2.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -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'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a5f8831..087ec28 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
tools:ignore="QueryAllPackagesPermission" />
+
+
+
+
+
+
+
-
-
+
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/Application.kt b/app/src/main/java/de/jrpie/android/launcher/Application.kt
index e6cce23..ba47942 100644
--- a/app/src/main/java/de/jrpie/android/launcher/Application.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt
@@ -12,6 +12,8 @@ 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.AbstractAppInfo
@@ -23,10 +25,17 @@ import de.jrpie.android.launcher.preferences.resetPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlin.system.exitProcess
+
+
+const val APP_WIDGET_HOST_ID = 42
+
class Application : android.app.Application() {
val apps = MutableLiveData>()
val privateSpaceLocked = MutableLiveData()
+ lateinit var appWidgetHost: AppWidgetHost
+ lateinit var appWidgetManager: AppWidgetManager
private val profileAvailabilityBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
@@ -98,15 +107,23 @@ class Application : android.app.Application() {
// TODO Error: Invalid resource ID 0x00000000.
// DynamicColors.applyToActivitiesIfAvailable(this)
+ Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
+ sendCrashNotification(this@Application, throwable)
+ exitProcess(1)
+ }
+
if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
torchManager = TorchManager(this)
}
+ appWidgetHost = AppWidgetHost(this.applicationContext, APP_WIDGET_HOST_ID)
+ appWidgetManager = AppWidgetManager.getInstance(this.applicationContext)
+
+
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
LauncherPreferences.init(preferences, this.resources)
-
// Try to restore old preferences
migratePreferencesToNewVersion(this)
@@ -144,6 +161,8 @@ class Application : android.app.Application() {
removeUnusedShortcuts(this)
}
loadApps()
+
+ createNotificationChannels(this)
}
fun getCustomAppNames(): HashMap {
diff --git a/app/src/main/java/de/jrpie/android/launcher/Functions.kt b/app/src/main/java/de/jrpie/android/launcher/Functions.kt
index afc2c31..b6df30b 100644
--- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt
@@ -223,4 +223,14 @@ 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)
-}
\ No newline at end of file
+}
+
+fun writeEmail(context: Context, to: String, subject: String, text: String) {
+ val intent = Intent(Intent.ACTION_SENDTO)
+ intent.setData("mailto:".toUri())
+ intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(to))
+ intent.putExtra(Intent.EXTRA_SUBJECT, subject)
+ intent.putExtra(Intent.EXTRA_TEXT, text)
+ context.startActivity(Intent.createChooser(intent, context.getString(R.string.send_email)))
+}
+
diff --git a/app/src/main/java/de/jrpie/android/launcher/Notifications.kt b/app/src/main/java/de/jrpie/android/launcher/Notifications.kt
new file mode 100644
index 0000000..0cf0efb
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/Notifications.kt
@@ -0,0 +1,87 @@
+package de.jrpie.android.launcher
+
+import android.app.Activity
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.util.Log
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import de.jrpie.android.launcher.ui.EXTRA_CRASH_LOG
+import de.jrpie.android.launcher.ui.ReportCrashActivity
+import java.io.PrintWriter
+import java.io.StringWriter
+import kotlin.random.Random
+
+private val NOTIFICATION_CHANNEL_CRASH = "launcher:crash"
+
+fun createNotificationChannels(context: Context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationChannel = NotificationChannel(
+ NOTIFICATION_CHANNEL_CRASH,
+ context.getString(R.string.notification_channel_crash),
+ NotificationManager.IMPORTANCE_HIGH
+ )
+
+ val notificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(notificationChannel)
+ }
+}
+
+fun requestNotificationPermission(activity: Activity) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ return
+ }
+
+ val permission =
+ (activity.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED)
+
+ if (!permission) {
+ ActivityCompat.requestPermissions(
+ activity,
+ arrayOf( android.Manifest.permission.POST_NOTIFICATIONS ),
+ 1
+ )
+ }
+}
+
+fun sendCrashNotification(context: Context, throwable: Throwable) {
+ val stringWriter = StringWriter()
+ val printWriter = PrintWriter(stringWriter)
+ throwable.printStackTrace(printWriter)
+
+ val intent = Intent(context, ReportCrashActivity::class.java)
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ intent.putExtra(EXTRA_CRASH_LOG, stringWriter.toString())
+
+ val pendingIntent = PendingIntent.getActivity(
+ context,
+ Random.nextInt(),
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_CRASH)
+ .setSmallIcon(R.drawable.baseline_bug_report_24)
+ .setContentTitle(context.getString(R.string.notification_crash_title))
+ .setContentText(context.getString(R.string.notification_crash_explanation))
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(false)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+
+ val notificationManager = NotificationManagerCompat.from(context)
+ try {
+ notificationManager.notify(
+ 0,
+ builder.build()
+ )
+ } catch (e: SecurityException) {
+ Log.e("Crash Notification", "Could not send notification")
+ }
+}
diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt b/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt
index 9a2dc62..a883922 100644
--- a/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/actions/Action.kt
@@ -6,14 +6,18 @@ 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
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
-import androidx.core.content.edit
+/**
+ * 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
diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/TorchManager.kt b/app/src/main/java/de/jrpie/android/launcher/actions/TorchManager.kt
index 7e694c6..a2ea801 100644
--- a/app/src/main/java/de/jrpie/android/launcher/actions/TorchManager.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/actions/TorchManager.kt
@@ -1,7 +1,6 @@
package de.jrpie.android.launcher.actions
import android.content.Context
-import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.os.Build
@@ -78,7 +77,8 @@ class TorchManager(context: Context) {
cameraManager.setTorchMode(camera, !torchEnabled)
}
- } catch (e: CameraAccessException) {
+ } catch (e: Exception) {
+ // CameraAccessException, IllegalArgumentException
Toast.makeText(
context,
context.getString(R.string.alert_torch_access_exception),
diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt
new file mode 100644
index 0000000..84c4179
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt
@@ -0,0 +1,91 @@
+package de.jrpie.android.launcher.actions
+
+import android.app.Activity
+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 (context is WidgetPanelActivity) {
+ if (context.widgetPanelId == widgetPanelId) {
+ context.finish()
+ return true
+ }
+ }
+
+ if (WidgetPanel.byId(this.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, this.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(R.id.dialog_select_widget_panel_info)
+ alertDialog.findViewById(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
+ }
+}
diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt
index 93b4cbf..541510a 100644
--- a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt
@@ -6,7 +6,6 @@ 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
diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java
index 85979fe..d509ef2 100644
--- a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java
+++ b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java
@@ -8,6 +8,8 @@ import de.jrpie.android.launcher.actions.lock.LockMethod;
import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer;
+import de.jrpie.android.launcher.preferences.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;
@@ -72,6 +74,7 @@ import eu.jonahbauer.android.preference.annotations.Preferences;
@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"),
@@ -81,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 {}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt
index 59ecc7a..a4b1f43 100644
--- a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt
@@ -4,21 +4,30 @@ import android.content.Context
import android.util.Log
import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.actions.Action
-import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.AbstractAppInfo
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
+import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.DetailedAppInfo
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion1
+import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion100
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.sendCrashNotification
import de.jrpie.android.launcher.ui.HomeActivity
+import de.jrpie.android.launcher.widgets.ClockWidget
+import de.jrpie.android.launcher.widgets.DebugInfoWidget
+import de.jrpie.android.launcher.widgets.WidgetPanel
+import de.jrpie.android.launcher.widgets.WidgetPosition
+import de.jrpie.android.launcher.widgets.generateInternalId
+import de.jrpie.android.launcher.widgets.getAppWidgetHost
/* Current version of the structure of preferences.
* Increase when breaking changes are introduced and write an appropriate case in
* `migratePreferencesToNewVersion`
*/
-const val PREFERENCE_VERSION = 4
+const val PREFERENCE_VERSION = 101
const val UNKNOWN_PREFERENCE_VERSION = -1
private const val TAG = "Launcher - Preferences"
@@ -40,18 +49,28 @@ 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()
+ migratePreferencesFromVersion3(context)
Log.i(TAG, "migration of preferences complete (3 -> ${PREFERENCE_VERSION}).")
}
+ // There was a bug where instead of the preference version the app version was written.
+ in 4..99 -> {
+ migratePreferencesFromVersion4(context)
+ Log.i(TAG, "migration of preferences complete (4 -> ${PREFERENCE_VERSION}).")
+ }
+ 100 -> {
+ migratePreferencesFromVersion100(context)
+ Log.i(TAG, "migration of preferences complete (100 -> ${PREFERENCE_VERSION}).")
+ }
+
else -> {
Log.w(
TAG,
@@ -63,6 +82,7 @@ fun migratePreferencesToNewVersion(context: Context) {
}
} catch (e: Exception) {
Log.e(TAG, "Unable to restore preferences:\n${e.stackTrace}")
+ sendCrashNotification(context, e)
resetPreferences(context)
}
}
@@ -71,18 +91,45 @@ fun resetPreferences(context: Context) {
Log.i(TAG, "Resetting preferences")
LauncherPreferences.clear()
LauncherPreferences.internal().versionCode(PREFERENCE_VERSION)
+ context.getAppWidgetHost().deleteHost()
+ LauncherPreferences.widgets().widgets(
+ setOf(
+ ClockWidget(
+ generateInternalId(),
+ WidgetPosition(1, 3, 10, 4),
+ WidgetPanel.HOME.id
+ )
+ )
+ )
+
+ if (BuildConfig.DEBUG) {
+ LauncherPreferences.widgets().widgets(
+ LauncherPreferences.widgets().widgets().also {
+ it.add(
+ DebugInfoWidget(
+ generateInternalId(),
+ WidgetPosition(1, 1, 10, 4),
+ WidgetPanel.HOME.id
+ )
+ )
+ }
+ )
+ }
val hidden: MutableSet = mutableSetOf()
- val launcher = DetailedAppInfo.fromAppInfo(
- AppInfo(
- BuildConfig.APPLICATION_ID,
- HomeActivity::class.java.name,
- INVALID_USER
- ), context
- )
- launcher?.getRawInfo()?.let { hidden.add(it) }
- Log.i(TAG,"Hiding ${launcher?.getRawInfo()}")
+
+ if (!BuildConfig.DEBUG) {
+ val launcher = DetailedAppInfo.fromAppInfo(
+ AppInfo(
+ BuildConfig.APPLICATION_ID,
+ HomeActivity::class.java.name,
+ INVALID_USER
+ ), context
+ )
+ launcher?.getRawInfo()?.let { hidden.add(it) }
+ Log.i(TAG, "Hiding ${launcher?.getRawInfo()}")
+ }
LauncherPreferences.apps().hidden(hidden)
Action.resetToDefaultActions(context)
diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt
index a1cb022..6cd9819 100644
--- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt
@@ -1,11 +1,13 @@
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.AppInfo
import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
+import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
import kotlinx.serialization.Serializable
@@ -13,7 +15,6 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.json.JSONException
import org.json.JSONObject
-import androidx.core.content.edit
@Serializable
@@ -129,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())
@@ -137,5 +138,5 @@ fun migratePreferencesFromVersion1() {
migrateAppInfoStringMap(LauncherPreferences.apps().keys().customNames())
LauncherPreferences.internal().versionCode(2)
- migratePreferencesFromVersion2()
+ migratePreferencesFromVersion2(context)
}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt
new file mode 100644
index 0000000..43e4bc7
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt
@@ -0,0 +1,39 @@
+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.DebugInfoWidget
+import de.jrpie.android.launcher.widgets.generateInternalId
+import de.jrpie.android.launcher.widgets.updateWidget
+
+fun migratePreferencesFromVersion100(context: Context) {
+ assert(PREFERENCE_VERSION == 101)
+ assert(LauncherPreferences.internal().versionCode() == 100)
+
+ val widgets = LauncherPreferences.widgets().widgets() ?: setOf()
+ widgets.forEach { widget ->
+ when (widget) {
+ is ClockWidget -> {
+ val id = widget.id
+ val newId = generateInternalId()
+ (context.applicationContext as Application).appWidgetHost.deleteAppWidgetId(id)
+ widget.delete(context)
+ widget.id = newId
+ updateWidget(widget)
+ }
+ is DebugInfoWidget -> {
+ val id = widget.id
+ val newId = generateInternalId()
+ (context.applicationContext as Application).appWidgetHost.deleteAppWidgetId(id)
+ widget.delete(context)
+ widget.id = newId
+ updateWidget(widget)
+ }
+ else -> {}
+ }
+ }
+ LauncherPreferences.internal().versionCode(101)
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt
index 4e6eae1..9714359 100644
--- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt
@@ -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() {
+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()
+ migratePreferencesFromVersion3(context)
}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt
index 4a9241f..e0a8447 100644
--- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version3.kt
@@ -1,17 +1,17 @@
package de.jrpie.android.launcher.preferences.legacy
+import android.content.Context
import android.content.SharedPreferences
import android.content.SharedPreferences.Editor
-import de.jrpie.android.launcher.apps.AppInfo
+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
-import java.util.HashSet
-import androidx.core.content.edit
/**
* Migrate preferences from version 3 (used until version 0.0.23) to the current format
@@ -70,8 +70,7 @@ private fun migrateMapAppInfoString(key: String, preferences: SharedPreferences,
}
}
-fun migratePreferencesFromVersion3() {
- assert(PREFERENCE_VERSION == 4)
+fun migratePreferencesFromVersion3(context: Context) {
assert(LauncherPreferences.internal().versionCode() == 3)
val preferences = LauncherPreferences.getSharedPreferences()
@@ -82,4 +81,5 @@ fun migratePreferencesFromVersion3() {
}
LauncherPreferences.internal().versionCode(4)
+ migratePreferencesFromVersion4(context)
}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt
new file mode 100644
index 0000000..fb353d5
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt
@@ -0,0 +1,24 @@
+package de.jrpie.android.launcher.preferences.legacy
+
+import android.content.Context
+import de.jrpie.android.launcher.preferences.LauncherPreferences
+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.generateInternalId
+
+fun migratePreferencesFromVersion4(context: Context) {
+ assert(LauncherPreferences.internal().versionCode() < 100)
+
+ LauncherPreferences.widgets().widgets(
+ setOf(
+ ClockWidget(
+ generateInternalId(),
+ WidgetPosition(1, 3, 10, 4),
+ WidgetPanel.HOME.id
+ )
+ )
+ )
+ LauncherPreferences.internal().versionCode(100)
+ migratePreferencesFromVersion100(context)
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt
index 2d1152d..f954b31 100644
--- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt
@@ -3,10 +3,10 @@ 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
-import androidx.core.content.edit
private fun migrateStringPreference(
@@ -392,5 +392,5 @@ fun migratePreferencesFromVersionUnknown(context: Context) {
LauncherPreferences.internal().versionCode(1)
Log.i(TAG, "migrated preferences to version 1.")
- migratePreferencesFromVersion1()
+ migratePreferencesFromVersion1(context)
}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt
index 3e19daf..7b5d794 100644
--- a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt
@@ -4,6 +4,8 @@ package de.jrpie.android.launcher.preferences.serialization
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
@@ -28,6 +30,40 @@ class SetAbstractAppInfoPreferenceSerializer :
}
}
+
+@Suppress("UNCHECKED_CAST")
+class SetWidgetSerializer :
+ PreferenceSerializer?, java.util.Set?> {
+ @Throws(PreferenceSerializationException::class)
+ override fun serialize(value: java.util.Set?): java.util.Set? {
+ return value?.map(Widget::serialize)
+ ?.toHashSet() as? java.util.Set
+ }
+
+ @Throws(PreferenceSerializationException::class)
+ override fun deserialize(value: java.util.Set?): java.util.Set? {
+ return value?.map(java.lang.String::toString)?.map(Widget::deserialize)
+ ?.toHashSet() as? java.util.Set
+ }
+}
+
+@Suppress("UNCHECKED_CAST")
+class SetWidgetPanelSerializer :
+ PreferenceSerializer?, java.util.Set?> {
+ @Throws(PreferenceSerializationException::class)
+ override fun serialize(value: java.util.Set?): java.util.Set? {
+ return value?.map(WidgetPanel::serialize)
+ ?.toHashSet() as? java.util.Set
+ }
+
+ @Throws(PreferenceSerializationException::class)
+ override fun deserialize(value: java.util.Set?): java.util.Set? {
+ return value?.map(java.lang.String::toString)?.map(WidgetPanel::deserialize)
+ ?.toHashSet() as? java.util.Set
+ }
+}
+
+
@Suppress("UNCHECKED_CAST")
class SetPinnedShortcutInfoPreferenceSerializer :
PreferenceSerializer?, java.util.Set?> {
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt b/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt
index 1ca4d2b..a863c67 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/Helper.kt
@@ -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
@@ -38,10 +39,17 @@ fun ImageView.transformGrayscale(grayscale: Boolean) {
}
-// 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 )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt
index 53a0876..76bf443 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt
@@ -1,26 +1,18 @@
package de.jrpie.android.launcher.ui
-import android.annotation.SuppressLint
import android.content.SharedPreferences
-import android.content.res.Configuration
import android.content.res.Resources
-import android.os.Build
import android.os.Bundle
-import android.view.KeyEvent
-import android.view.MotionEvent
import android.view.View
-import android.window.OnBackInvokedDispatcher
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.isVisible
-import de.jrpie.android.launcher.R
+import de.jrpie.android.launcher.Application
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.databinding.ActivityHomeBinding
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
+import de.jrpie.android.launcher.ui.util.LauncherGestureActivity
/**
* [HomeActivity] is the actual application Launcher,
@@ -34,10 +26,9 @@ import java.util.Locale
* - Setting global variables (preferences etc.)
* - Opening the [TutorialActivity] on new installations
*/
-class HomeActivity : UIObject, AppCompatActivity() {
+class HomeActivity : UIObject, LauncherGestureActivity() {
- private lateinit var binding: HomeBinding
- private lateinit var touchGestureDetector: TouchGestureDetector
+ private lateinit var binding: ActivityHomeBinding
private var sharedPreferencesListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
@@ -45,61 +36,32 @@ 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.onCreate(savedInstanceState)
+ super.onCreate(savedInstanceState)
super.onCreate()
- touchGestureDetector = TouchGestureDetector(
- this, 0, 0,
- LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
- )
- touchGestureDetector.updateScreenSize(windowManager)
-
// Initialise layout
- binding = HomeBinding.inflate(layoutInflater)
+ binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- binding.root.setOnApplyWindowInsetsListener { _, windowInsets ->
- @Suppress("deprecation") // required to support API 29
- val insets = windowInsets.systemGestureInsets
- touchGestureDetector.setSystemGestureInsets(insets)
-
- windowInsets
- }
- }
-
-
-
- // Handle back key / gesture on Android 13+, cf. onKeyDown()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_OVERLAY
- ) {
- handleBack()
- }
- }
binding.buttonFallbackSettings.setOnClickListener {
LauncherAction.SETTINGS.invoke(this)
}
}
- override fun onConfigurationChanged(newConfig: Configuration) {
- super.onConfigurationChanged(newConfig)
- touchGestureDetector.updateScreenSize(windowManager)
- }
-
override fun onStart() {
- super.onStart()
-
+ super.onStart()
super.onStart()
// If the tutorial was not finished, start it
@@ -120,7 +82,6 @@ class HomeActivity : UIObject, AppCompatActivity() {
}
}
-
private fun updateSettingsFallbackButtonVisibility() {
// If µLauncher settings can not be reached from any action bound to an enabled gesture,
// show the fallback button.
@@ -135,129 +96,46 @@ 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 {
+ return modifyTheme(super.getTheme())
}
- 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 onPause() {
+ try {
+ (application as Application).appWidgetHost.stopListening()
+ } catch (e: Exception) {
+ // Throws a NullPointerException on Android 12 an earlier, see #172
+ e.printStackTrace()
+ }
+ super.onPause()
}
override fun onResume() {
super.onResume()
-
- touchGestureDetector.edgeWidth =
- LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
-
- initClock()
updateSettingsFallbackButtonVisibility()
+
+ binding.homeWidgetContainer.updateWidgets(this@HomeActivity,
+ LauncherPreferences.widgets().widgets()
+ )
+
+ (application as Application).appWidgetHost.startListening()
}
+
override fun onDestroy() {
LauncherPreferences.getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener)
super.onDestroy()
}
- @SuppressLint("GestureBackNavigation")
- override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
- when (keyCode) {
- KeyEvent.KEYCODE_BACK -> {
- // Only used pre Android 13, cf. onBackInvokedDispatcher
- handleBack()
- }
-
- KeyEvent.KEYCODE_VOLUME_UP -> {
- if (Action.forGesture(Gesture.VOLUME_UP) == LauncherAction.VOLUME_UP) {
- // Let the OS handle the key event. This works better with some custom ROMs
- // and apps like Samsung Sound Assistant.
- return false
- }
- Gesture.VOLUME_UP(this)
- }
-
- KeyEvent.KEYCODE_VOLUME_DOWN -> {
- if (Action.forGesture(Gesture.VOLUME_DOWN) == LauncherAction.VOLUME_DOWN) {
- // see above
- return false
- }
- Gesture.VOLUME_DOWN(this)
- }
- }
- return true
- }
-
- override fun onTouchEvent(event: MotionEvent): Boolean {
- touchGestureDetector.onTouchEvent(event)
- return true
- }
-
- override fun setOnClicks() {
-
- binding.homeUpperView.setOnClickListener {
- if (LauncherPreferences.clock().flipDateTime()) {
- Gesture.TIME(this)
- } else {
- Gesture.DATE(this)
- }
- }
-
- binding.homeLowerView.setOnClickListener {
- if (LauncherPreferences.clock().flipDateTime()) {
- Gesture.DATE(this)
- } else {
- Gesture.TIME(this)
- }
- }
- }
-
-
- private fun handleBack() {
+ override fun handleBack() {
Gesture.BACK(this)
}
+ override fun getRootView(): View {
+ return binding.root
+ }
+
override fun isHomeScreen(): Boolean {
return true
}
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt
index 71908ba..70f737f 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt
@@ -1,7 +1,6 @@
package de.jrpie.android.launcher.ui
import android.app.AlertDialog
-import android.app.Service
import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.LauncherApps.PinItemRequest
@@ -45,11 +44,25 @@ class PinShortcutActivity : AppCompatActivity(), UIObject {
binding = ActivityPinShortcutBinding.inflate(layoutInflater)
setContentView(binding.root)
- val launcherApps = getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps
+ val launcherApps = getSystemService(LAUNCHER_APPS_SERVICE) as LauncherApps
val request = launcherApps.getPinItemRequest(intent)
this.request = request
- if (request == null || request.requestType != PinItemRequest.REQUEST_TYPE_SHORTCUT) {
+ if (request == null) {
+ finish()
+ return
+ }
+
+ if (request.requestType == PinItemRequest.REQUEST_TYPE_APPWIDGET) {
+
+ // TODO handle app widgets
+ request.getAppWidgetProviderInfo(this)
+ // startActivity()
+ finish()
+ return
+ }
+
+ if (request.requestType != PinItemRequest.REQUEST_TYPE_SHORTCUT) {
finish()
return
}
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/ReportCrashActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/ReportCrashActivity.kt
new file mode 100644
index 0000000..5d95769
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/ReportCrashActivity.kt
@@ -0,0 +1,57 @@
+package de.jrpie.android.launcher.ui
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import de.jrpie.android.launcher.R
+import de.jrpie.android.launcher.copyToClipboard
+import de.jrpie.android.launcher.databinding.ActivityReportCrashBinding
+import de.jrpie.android.launcher.getDeviceInfo
+import de.jrpie.android.launcher.openInBrowser
+import de.jrpie.android.launcher.writeEmail
+
+const val EXTRA_CRASH_LOG = "crashLog"
+
+class ReportCrashActivity : AppCompatActivity() {
+ // We don't know what caused the crash, so this Activity should use as little functionality as possible.
+ // In particular it is not a UIObject (and hence looks quite ugly)
+ private lateinit var binding: ActivityReportCrashBinding
+ private var report: String? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // Initialise layout
+ binding = ActivityReportCrashBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ setTitle(R.string.report_crash_title)
+ setSupportActionBar(binding.reportCrashAppbar)
+ supportActionBar?.setDisplayHomeAsUpEnabled(false)
+
+ report = intent.getStringExtra(EXTRA_CRASH_LOG)
+
+ binding.reportCrashButtonCopy.setOnClickListener {
+ copyToClipboard(this,
+ "Device Info:\n${getDeviceInfo()}\n\nCrash Log:\n${report}")
+ }
+
+ binding.reportCrashButtonMail.setOnClickListener {
+ writeEmail(
+ this,
+ getString(R.string.settings_meta_report_bug_mail),
+ "Crash in μLauncher",
+ "Hi!\nUnfortunately, μLauncher crashed:\n" +
+ "\nDevice Info\n\n${getDeviceInfo()}\n\n" +
+ "\nCrash Log\n\n${report}\n" +
+ "\nAdditional Information\n\n" +
+ "[Please add additional information: What did you do when the crash happened? Do you know how to trigger it? ... ]"
+ )
+ }
+ binding.reportCrashButtonReport.setOnClickListener {
+ openInBrowser(
+ getString(R.string.settings_meta_report_bug_link),
+ this
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt
index 8e8ed4e..8264752 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt
@@ -17,6 +17,7 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.tan
+@Suppress("PrivatePropertyName")
class TouchGestureDetector(
private val context: Context,
var width: Int,
@@ -34,13 +35,13 @@ 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
+ private val longPressHandler = Handler(Looper.getMainLooper())
+
data class Vector(val x: Float, val y: Float) {
fun absSquared(): Float {
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/UIObject.kt b/app/src/main/java/de/jrpie/android/launcher/ui/UIObject.kt
index 51324f4..b292425 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/UIObject.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/UIObject.kt
@@ -10,6 +10,7 @@ import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import de.jrpie.android.launcher.preferences.LauncherPreferences
+import de.jrpie.android.launcher.preferences.theme.Background
/**
* An interface implemented by every [Activity], Fragment etc. in Launcher.
@@ -65,8 +66,14 @@ interface UIObject {
theme,
LauncherPreferences.theme().textShadow()
)
- LauncherPreferences.theme().background().applyToTheme(theme)
- LauncherPreferences.theme().font().applyToTheme(theme)
+
+ if (isHomeScreen()) {
+ Background.TRANSPARENT.applyToTheme(theme)
+ LauncherPreferences.clock().font().applyToTheme(theme)
+ } else {
+ LauncherPreferences.theme().background().applyToTheme(theme)
+ LauncherPreferences.theme().font().applyToTheme(theme)
+ }
return theme
}
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/AppsRecyclerAdapter.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/AppsRecyclerAdapter.kt
index 65278ce..784d6fa 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/AppsRecyclerAdapter.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/AppsRecyclerAdapter.kt
@@ -237,9 +237,4 @@ class AppsRecyclerAdapter(
appFilter.favoritesVisibility = v
updateAppsList()
}
-
- fun setHiddenAppsVisibility(v: AppFilter.Companion.AppSetVisibility) {
- appFilter.hiddenVisibility = v
- updateAppsList()
- }
}
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt
index 1a55bbb..ed26729 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt
@@ -11,13 +11,16 @@ 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.openSoftKeyboard
+import kotlin.math.absoluteValue
/**
@@ -90,6 +93,19 @@ class ListFragmentApps : Fragment(), UIObject {
}
}
adapter = appsRecyclerAdapter
+ if (LauncherPreferences.functionality().searchAutoCloseKeyboard()) {
+ addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ var totalDy: Int = 0
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ totalDy += dy
+
+ if (totalDy.absoluteValue > 100) {
+ totalDy = 0
+ closeSoftKeyboard(requireActivity())
+ }
+ }
+ })
+ }
}
binding.listAppsSearchview.setOnQueryTextListener(object :
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt
index f176469..06be78a 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt
@@ -11,6 +11,7 @@ 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.actions.WidgetPanelAction
import de.jrpie.android.launcher.ui.list.ListActivity
/**
@@ -23,8 +24,10 @@ import de.jrpie.android.launcher.ui.list.ListActivity
class OtherRecyclerAdapter(val activity: Activity) :
RecyclerView.Adapter() {
- private val othersList: Array =
- LauncherAction.entries.filter { it.isAvailable(activity) }.toTypedArray()
+ private val othersList: Array =
+ LauncherAction.entries.filter { it.isAvailable(activity) }
+ .plus(WidgetPanelAction(-1))
+ .toTypedArray()
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener {
@@ -36,10 +39,12 @@ class OtherRecyclerAdapter(val activity: Activity) :
val pos = bindingAdapterPosition
val content = othersList[pos]
- activity.finish()
val gestureId = (activity as? ListActivity)?.forGesture ?: return
val gesture = Gesture.byId(gestureId) ?: return
- Action.setActionForGesture(gesture, content)
+ content.showConfigurationDialog(activity) { configuredAction ->
+ Action.setActionForGesture(gesture, configuredAction)
+ activity.finish()
+ }
}
init {
@@ -48,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 {
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt
index a8efb43..bb9df74 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt
@@ -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(
+ LauncherPreferences.widgets().keys().widgets()
+ )
+ manageWidgets?.setOnPreferenceClickListener {
+ startActivity(Intent(requireActivity(), ManageWidgetsActivity::class.java))
+ true
+ }
+
+ val manageWidgetPanels = findPreference(
+ LauncherPreferences.widgets().keys().customPanels()
+ )
+ manageWidgetPanels?.setOnPreferenceClickListener {
+ startActivity(Intent(requireActivity(), ManageWidgetPanelsActivity::class.java))
+ true
+ }
+
val hiddenApps = findPreference(
LauncherPreferences.apps().keys().hidden()
)
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt
index dea0bcf..1a0e802 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt
@@ -79,6 +79,9 @@ class SettingsFragmentMeta : Fragment(), UIObject {
// view code
bindURL(binding.settingsMetaButtonViewCode, R.string.settings_meta_link_github)
+ // view documentation
+ bindURL(binding.settingsMetaButtonViewDocs, R.string.settings_meta_link_docs)
+
// report a bug
binding.settingsMetaButtonReportBug.setOnClickListener {
val deviceInfo = getDeviceInfo()
@@ -132,7 +135,12 @@ class SettingsFragmentMeta : Fragment(), UIObject {
startActivity(Intent(this.context, LegalInfoActivity::class.java))
}
+ // version
binding.settingsMetaTextVersion.text = BuildConfig.VERSION_NAME
+ binding.settingsMetaTextVersion.setOnClickListener {
+ val deviceInfo = getDeviceInfo()
+ copyToClipboard(requireContext(), deviceInfo)
+ }
}
}
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragment5Finish.kt b/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragment5Finish.kt
index 2fd093e..548c30b 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragment5Finish.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragment5Finish.kt
@@ -5,9 +5,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
-import de.jrpie.android.launcher.BuildConfig.VERSION_CODE
import de.jrpie.android.launcher.databinding.Tutorial5FinishBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
+import de.jrpie.android.launcher.requestNotificationPermission
import de.jrpie.android.launcher.setDefaultHomeScreen
import de.jrpie.android.launcher.ui.UIObject
@@ -31,8 +31,10 @@ class TutorialFragment5Finish : Fragment(), UIObject {
override fun onStart() {
super.onStart()
super.onStart()
+ requestNotificationPermission(requireActivity())
}
+
override fun setOnClicks() {
super.setOnClicks()
binding.tutorialFinishButtonStart.setOnClickListener { finishTutorial() }
@@ -42,9 +44,9 @@ class TutorialFragment5Finish : Fragment(), UIObject {
if (!LauncherPreferences.internal().started()) {
LauncherPreferences.internal().started(true)
LauncherPreferences.internal().startedTime(System.currentTimeMillis() / 1000L)
- LauncherPreferences.internal().versionCode(VERSION_CODE)
}
context?.let { setDefaultHomeScreen(it, checkDefault = true) }
+
activity?.finish()
}
}
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/util/LauncherGestureActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/util/LauncherGestureActivity.kt
new file mode 100644
index 0000000..0be09f9
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/util/LauncherGestureActivity.kt
@@ -0,0 +1,104 @@
+package de.jrpie.android.launcher.ui.util
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.res.Configuration
+import android.os.Build
+import android.os.Bundle
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.View
+import android.window.OnBackInvokedDispatcher
+import de.jrpie.android.launcher.actions.Action
+import de.jrpie.android.launcher.actions.Gesture
+import de.jrpie.android.launcher.actions.LauncherAction
+import de.jrpie.android.launcher.preferences.LauncherPreferences
+import de.jrpie.android.launcher.ui.TouchGestureDetector
+
+/**
+ * An activity with a [TouchGestureDetector] as well as handling of volume and back keys set up.
+ */
+abstract class LauncherGestureActivity: Activity() {
+ protected var touchGestureDetector: TouchGestureDetector? = null
+
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ touchGestureDetector?.onTouchEvent(event)
+ return true
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // Handle back key / gesture on Android 13+, cf. onKeyDown()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ onBackInvokedDispatcher.registerOnBackInvokedCallback(
+ OnBackInvokedDispatcher.PRIORITY_OVERLAY
+ ) {
+ handleBack()
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ /* This should be initialized in onCreate()
+ However on some devices there seems to be a bug where the touchGestureDetector
+ is not working properly after resuming the app.
+ Reinitializing the touchGestureDetector every time the app is resumed might help to fix that.
+ (see issue #138)
+ */
+ touchGestureDetector = TouchGestureDetector(
+ this, 0, 0,
+ LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
+ ).also {
+ it.updateScreenSize(windowManager)
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ getRootView()?.setOnApplyWindowInsetsListener { _, windowInsets ->
+ @Suppress("deprecation") // required to support API 29
+ val insets = windowInsets.systemGestureInsets
+ touchGestureDetector?.setSystemGestureInsets(insets)
+
+ windowInsets
+ }
+ }
+ }
+
+ @SuppressLint("GestureBackNavigation")
+ override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+ when (keyCode) {
+ KeyEvent.KEYCODE_BACK -> {
+ // Only used pre Android 13, cf. onBackInvokedDispatcher
+ handleBack()
+ }
+
+ KeyEvent.KEYCODE_VOLUME_UP -> {
+ if (Action.forGesture(Gesture.VOLUME_UP) == LauncherAction.VOLUME_UP) {
+ // Let the OS handle the key event. This works better with some custom ROMs
+ // and apps like Samsung Sound Assistant.
+ return false
+ }
+ Gesture.VOLUME_UP(this)
+ }
+
+ KeyEvent.KEYCODE_VOLUME_DOWN -> {
+ if (Action.forGesture(Gesture.VOLUME_DOWN) == LauncherAction.VOLUME_DOWN) {
+ // see above
+ return false
+ }
+ Gesture.VOLUME_DOWN(this)
+ }
+ }
+ return true
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ touchGestureDetector?.updateScreenSize(windowManager)
+ }
+
+ protected abstract fun getRootView(): View?
+ protected abstract fun handleBack()
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt
new file mode 100644
index 0000000..cbe5395
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt
@@ -0,0 +1,79 @@
+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.WidgetClockBinding
+import de.jrpie.android.launcher.preferences.LauncherPreferences
+import de.jrpie.android.launcher.widgets.WidgetPanel
+import java.util.Locale
+
+class ClockView(context: Context, attrs: AttributeSet? = null, val appWidgetId: Int, val panelId: Int): ConstraintLayout(context, attrs) {
+ constructor(context: Context, attrs: AttributeSet?): this(context, attrs, WidgetPanel.HOME.id, -1)
+
+ val binding: WidgetClockBinding = WidgetClockBinding.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
+ }
+
+ private 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)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/DebugInfoView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/DebugInfoView.kt
new file mode 100644
index 0000000..d0ab70a
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/DebugInfoView.kt
@@ -0,0 +1,17 @@
+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 de.jrpie.android.launcher.databinding.WidgetDebugInfoBinding
+import de.jrpie.android.launcher.getDeviceInfo
+
+class DebugInfoView(context: Context, attrs: AttributeSet? = null, val appWidgetId: Int): ConstraintLayout(context, attrs) {
+
+ val binding: WidgetDebugInfoBinding = WidgetDebugInfoBinding.inflate(LayoutInflater.from(context), this, true)
+
+ init {
+ binding.debugInfoText.text = getDeviceInfo()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt
new file mode 100644
index 0000000..5eab32f
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt
@@ -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()
+
+ open fun updateWidgets(activity: Activity, widgets: Collection?) {
+ 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[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(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...onCreate(savedInstanceState)
+ super.onCreate()
+ val binding = ActivityWidgetPanelBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.HOME.id)
+
+ // The widget container should extend below the status and navigation bars,
+ // so let's set an empty WindowInsetsListener to prevent it from being moved.
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
+ windowInsets
+ }
+
+ 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 onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+
+ if (hasFocus && LauncherPreferences.display().hideNavigationBar()) {
+ hideNavigationBar()
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ super.onStart()
+ }
+
+ override fun onPause() {
+ try {
+ (application as Application).appWidgetHost.stopListening()
+ } catch (e: Exception) {
+ // Throws a NullPointerException on Android 12 an earlier, see #172
+ e.printStackTrace()
+ }
+ super.onPause()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ (application as Application).appWidgetHost.startListening()
+ }
+
+ override fun getRootView(): View? {
+ return binding?.root
+ }
+
+ override fun handleBack() {
+ finish()
+ }
+
+ override fun isHomeScreen(): Boolean {
+ return true
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt
new file mode 100644
index 0000000..163777f
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt
@@ -0,0 +1,105 @@
+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 {
+
+ @SuppressLint("NotifyDataSetChanged")
+ private val sharedPreferencesListener =
+ SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
+ if (
+ prefKey == LauncherPreferences.widgets().keys().customPanels()
+ || prefKey == LauncherPreferences.widgets().keys().widgets()
+ ) {
+ viewAdapter.widgetPanels =
+ (LauncherPreferences.widgets().customPanels() ?: setOf()).toTypedArray()
+
+ viewAdapter.notifyDataSetChanged()
+ }
+ }
+ private lateinit var binding: ActivityManageWidgetPanelsBinding
+ private lateinit var viewAdapter: WidgetPanelsRecyclerAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ super.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 {
+ 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(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(R.id.dialog_create_widget_panel_edit_text)
+ ?.setText(
+ getString(
+ R.string.widget_panel_default_name,
+ WidgetPanel.allocateId()
+ )
+ )
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ super.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() }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt
new file mode 100644
index 0000000..531cdc1
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt
@@ -0,0 +1,211 @@
+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.os.Bundle
+import android.util.Log
+import android.view.ViewGroup
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updateLayoutParams
+import de.jrpie.android.launcher.Application
+import de.jrpie.android.launcher.databinding.ActivityManageWidgetsBinding
+import de.jrpie.android.launcher.preferences.LauncherPreferences
+import de.jrpie.android.launcher.ui.UIObject
+import de.jrpie.android.launcher.widgets.AppWidget
+import de.jrpie.android.launcher.widgets.GRID_SIZE
+import de.jrpie.android.launcher.widgets.WidgetPanel
+import de.jrpie.android.launcher.widgets.WidgetPosition
+import kotlin.math.max
+import kotlin.math.roundToInt
+
+
+// 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 : UIObject, Activity() {
+
+ private var panelId: Int = WidgetPanel.HOME.id
+ private lateinit var binding: ActivityManageWidgetsBinding
+
+ private var sharedPreferencesListener =
+ SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
+ if (prefKey == LauncherPreferences.widgets().keys().widgets()) {
+ binding.manageWidgetsContainer.updateWidgets(
+ this,
+ LauncherPreferences.widgets().widgets()
+ )
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ super.onCreate()
+ binding = ActivityManageWidgetsBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ panelId = intent.extras?.getInt(EXTRA_PANEL_ID, WidgetPanel.HOME.id) ?: WidgetPanel.HOME.id
+
+ binding.manageWidgetsButtonAdd.setOnClickListener {
+ selectWidget()
+ }
+
+ // The widget container should extend below the status and navigation bars,
+ // so let's set an empty WindowInsetsListener to prevent it from being moved.
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
+ windowInsets
+ }
+
+ // The button must not be placed under the navigation bar
+ ViewCompat.setOnApplyWindowInsetsListener(binding.manageWidgetsButtonAdd) { v, windowInsets ->
+ val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ v.updateLayoutParams {
+ leftMargin = insets.left
+ bottomMargin = insets.bottom
+ rightMargin = insets.right
+ }
+ WindowInsetsCompat.CONSUMED
+ }
+
+ binding.manageWidgetsContainer.let {
+ it.widgetPanelId = panelId
+ it.updateWidgets(this, LauncherPreferences.widgets().widgets())
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ super.onStart()
+
+ LauncherPreferences.getSharedPreferences()
+ .registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
+
+ }
+
+ override fun onPause() {
+ try {
+ (application as Application).appWidgetHost.stopListening()
+ } catch (e: Exception) {
+ // Throws a NullPointerException on Android 12 an earlier, see #172
+ e.printStackTrace()
+ }
+ super.onPause()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ (application as Application).appWidgetHost.startListening()
+
+ binding.manageWidgetsContainer.updateWidgets(
+ this,
+ LauncherPreferences.widgets().widgets()
+ )
+ }
+
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+
+ if (hasFocus && LauncherPreferences.display().hideNavigationBar()) {
+ hideNavigationBar()
+ }
+ }
+
+ override fun getTheme(): Resources.Theme {
+ return modifyTheme(super.getTheme())
+ }
+
+ override fun onDestroy() {
+ LauncherPreferences.getSharedPreferences()
+ .unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener)
+ super.onDestroy()
+ }
+
+
+ private fun selectWidget() {
+ startActivityForResult(
+ Intent(this, SelectWidgetActivity::class.java).also {
+ it.putExtra(
+ EXTRA_PANEL_ID,
+ panelId
+ )
+ }, REQUEST_PICK_APPWIDGET
+ )
+ }
+
+
+ private fun createWidget(data: Intent) {
+ Log.i("Launcher", "creating widget")
+ val appWidgetManager = (application as Application).appWidgetManager
+ val appWidgetHost = (application as Application).appWidgetHost
+ val appWidgetId = data.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return
+
+ val display = windowManager.defaultDisplay
+
+ val widgetInfo = appWidgetManager.getAppWidgetInfo(appWidgetId)
+ if (widgetInfo == null) {
+ Log.w("Launcher", "can't access widget")
+ appWidgetHost.deleteAppWidgetId(appWidgetId)
+ return
+ }
+
+ val position = WidgetPosition.findFreeSpace(
+ WidgetPanel.byId(panelId),
+ max(3, (GRID_SIZE * (widgetInfo.minWidth) / display.width.toFloat()).roundToInt()),
+ max(3, (GRID_SIZE * (widgetInfo.minHeight) / display.height.toFloat()).roundToInt())
+ )
+
+ val widget = AppWidget(appWidgetId, position, panelId, widgetInfo)
+ 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
+ }
+}
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt
new file mode 100644
index 0000000..eeb98df
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt
@@ -0,0 +1,175 @@
+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.Application
+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.generateInternalId
+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 widgetPanelId: Int = WidgetPanel.HOME.id
+
+ private fun tryBindWidget(info: LauncherWidgetProvider) {
+ when (info) {
+ is LauncherAppWidgetProvider -> {
+ val widgetId =
+ (applicationContext as Application).appWidgetHost.allocateAppWidgetId()
+ 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(generateInternalId(), WidgetPosition(0, 4, 12, 3), widgetPanelId))
+ finish()
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ super.onStart()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ super.onCreate()
+
+ binding = ActivitySelectWidgetBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+
+ widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.HOME.id)
+
+ val viewManager = LinearLayoutManager(this)
+ val viewAdapter = SelectWidgetRecyclerAdapter()
+
+ binding.selectWidgetRecycler.apply {
+ setHasFixedSize(false)
+ layoutManager = viewManager
+ adapter = viewAdapter
+ }
+
+ binding.selectWidgetClose.setOnClickListener {
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+ }
+
+ 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() {
+
+ 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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt
new file mode 100644
index 0000000..bb52ca0
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt
@@ -0,0 +1,220 @@
+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 de.jrpie.android.launcher.ui.widgets.WidgetContainerView
+import de.jrpie.android.launcher.widgets.GRID_SIZE
+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
+
+/**
+ * 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 touchSlop: Int
+ val touchSlopSquare: Int
+ val longPressTimeout: Long
+
+
+ private var overlayViewById = HashMap()
+
+ init {
+ val configuration = ViewConfiguration.get(context)
+ touchSlop = configuration.scaledTouchSlop
+ touchSlopSquare = touchSlop * touchSlop
+
+ longPressTimeout = ViewConfiguration.getLongPressTimeout().toLong()
+ }
+
+
+ enum class EditMode(val resize: (dx: Int, dy: Int, screenWidth: Int, screenHeight: Int, rect: Rect) -> Rect) {
+ MOVE({ dx, dy, sw, sh, rect ->
+ val cdx = dx.coerceIn(-rect.left, sw - rect.right)
+ val cdy = dy.coerceIn(-rect.top, sh - rect.bottom)
+ Rect(rect.left + cdx, rect.top + cdy, rect.right + cdx, rect.bottom + cdy)
+ }),
+ TOP({ _, dy, _, sh, rect ->
+ val range = (-rect.top)..(rect.bottom - rect.top - (2 * sh / GRID_SIZE) + 5)
+ if (range.isEmpty()) {
+ rect
+ } else {
+ Rect(rect.left, rect.top + dy.coerceIn(range), rect.right, rect.bottom)
+ }
+ }),
+ BOTTOM({ _, dy, _, sh, rect ->
+ val range = ((2 * sh / GRID_SIZE) + 5 + rect.top - rect.bottom)..(sh - rect.bottom)
+ if (range.isEmpty()) {
+ rect
+ } else {
+ Rect(rect.left, rect.top, rect.right, rect.bottom + dy.coerceIn(range))
+ }
+ }),
+ LEFT({ dx, _, sw, _, rect ->
+ val range = (-rect.left)..(rect.right - rect.left - (2 * sw / GRID_SIZE) + 5)
+ if (range.isEmpty()) {
+ rect
+ } else {
+ Rect(rect.left + dx.coerceIn(range), rect.top, rect.right, rect.bottom)
+ }
+ }),
+ RIGHT({ dx, _, sw, _, rect ->
+ val range = ((2 * sw / GRID_SIZE) + 5 + rect.left - rect.right)..(sw - rect.right)
+ if (range.isEmpty()) {
+ rect
+ } else {
+ Rect(rect.left, rect.top, rect.right + dx.coerceIn(range), rect.bottom)
+ }
+ }),
+ }
+
+ private var selectedWidgetOverlayView: WidgetOverlayView? = null
+ private var selectedWidgetView: View? = null
+ private var currentGestureStart: Point? = null
+ private var startWidgetPosition: Rect? = null
+ private 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 = overlayViewById.asIterable()
+ .map { it.value }.firstOrNull { overlayView ->
+ RectF(
+ overlayView.x,
+ overlayView.y,
+ overlayView.x + overlayView.width,
+ overlayView.y + overlayView.height
+ )
+ .toRect()
+ .contains(start)
+ } ?: return true
+
+ val position =
+ (view.layoutParams as Companion.LayoutParams).position.getAbsoluteRect(
+ width,
+ height
+ )
+ selectedWidgetOverlayView = view
+ selectedWidgetView = widgetViewById[view.widgetId]
+ 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()
+ }
+ }, longPressTimeout)
+ }
+ 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 > touchSlopSquare) {
+ longPressHandler.removeCallbacksAndMessages(null)
+ }
+ val view = selectedWidgetOverlayView ?: return true
+ val start = startWidgetPosition ?: return true
+ val absoluteNewPosition = (view.mode ?: return true).resize(
+ distanceX.toInt(),
+ distanceY.toInt(),
+ width, height,
+ start
+ )
+ val newPosition = WidgetPosition.fromAbsoluteRect(
+ absoluteNewPosition, width, height
+ )
+ if (absoluteNewPosition != 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) {
+ val id = selectedWidgetOverlayView?.widgetId ?: return true
+ val widget = Widget.byId(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() {
+ synchronized(this) {
+ longPressHandler.removeCallbacksAndMessages(null)
+ startWidgetPosition = null
+ selectedWidgetOverlayView?.mode = null
+ }
+ }
+
+ override fun onDetachedFromWindow() {
+ endInteraction()
+ super.onDetachedFromWindow()
+ }
+
+ override fun updateWidgets(activity: Activity, widgets: Collection?) {
+ super.updateWidgets(activity, widgets)
+
+ synchronized(overlayViewById) {
+ overlayViewById.forEach { removeView(it.value) }
+ overlayViewById.clear()
+ widgets?.filter { it.panelId == widgetPanelId }?.forEach { widget ->
+ WidgetOverlayView(activity).let {
+ it.widgetId = widget.id
+ addView(it)
+ (it.layoutParams as Companion.LayoutParams).position = widget.position
+ overlayViewById[widget.id] = it
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt
new file mode 100644
index 0000000..0363069
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt
@@ -0,0 +1,137 @@
+package de.jrpie.android.launcher.ui.widgets.manage
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+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 : ViewGroup {
+
+ private val paint = Paint()
+ private val handlePaint = Paint()
+ private val selectedHandlePaint = Paint()
+
+ private val popupAnchor = View(context)
+
+ var mode: WidgetManagerView.EditMode? = null
+
+ class Handle(val mode: WidgetManagerView.EditMode, val position: Rect)
+ init {
+ addView(popupAnchor)
+ setWillNotDraw(false)
+ handlePaint.style = Paint.Style.STROKE
+ handlePaint.color = Color.WHITE
+ handlePaint.strokeWidth = 2f
+ handlePaint.setShadowLayer(10f,0f,0f, Color.BLACK)
+
+ selectedHandlePaint.style = Paint.Style.FILL_AND_STROKE
+ selectedHandlePaint.setARGB(100, 255, 255, 255)
+ handlePaint.setShadowLayer(10f,0f,0f, Color.BLACK)
+
+ paint.style = Paint.Style.STROKE
+ paint.color = Color.WHITE
+ paint.setShadowLayer(10f,0f,0f, Color.BLACK)
+ }
+
+ private var preview: Drawable? = null
+ var widgetId: Int = -1
+ set(newId) {
+ field = newId
+ preview = Widget.byId(widgetId)?.getPreview(context)
+ }
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
+ context,
+ attrs,
+ defStyle
+ )
+
+ 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)
+ }
+
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+ popupAnchor.layout(0,0,0,0)
+ }
+
+ fun showPopupMenu() {
+ val widget = Widget.byId(widgetId)?: return
+ val menu = PopupMenu(context, popupAnchor)
+ menu.menu.let {
+ it.add(
+ context.getString(R.string.widget_menu_remove)
+ ).setOnMenuItemClickListener { _ ->
+ Widget.byId(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 {
+ return listOf(
+ 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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetPanelsRecyclerAdapter.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetPanelsRecyclerAdapter.kt
new file mode 100644
index 0000000..d27ba9a
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetPanelsRecyclerAdapter.kt
@@ -0,0 +1,104 @@
+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() {
+
+ 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)
+ var infoView: TextView = itemView.findViewById(R.id.list_widget_panels_info)
+ }
+
+ override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
+ viewHolder.labelView.text = widgetPanels[i].label
+ val numWidgets = widgetPanels[i].getWidgets().size
+ viewHolder.infoView.text = context.resources.getQuantityString(
+ R.plurals.widget_panel_number_of_widgets,
+ numWidgets, numWidgets
+ )
+
+ 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(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(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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt
new file mode 100644
index 0000000..a968962
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt
@@ -0,0 +1,130 @@
+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,
+ panelId != WidgetPanel.HOME.id,
+ 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
+ /* TODO: if providerInfo is null, the corresponding app was probably uninstalled.
+ There does not seem to be a way to recover the widget when the app is installed again,
+ hence it should be deleted. */
+
+ 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): 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
+ )
+ }
+}
diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt
new file mode 100644
index 0000000..29d9308
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt
@@ -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 var 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, panelId)
+ }
+
+ override fun findView(views: Sequence): 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) { }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/DebugInfoWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/DebugInfoWidget.kt
new file mode 100644
index 0000000..75ae6d0
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/widgets/DebugInfoWidget.kt
@@ -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.DebugInfoView
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+
+@Serializable
+@SerialName("widget:debuginfo")
+class DebugInfoWidget(
+ override var id: Int,
+ override var position: WidgetPosition,
+ override val panelId: Int,
+ override var allowInteraction: Boolean = true
+) : Widget() {
+
+ override fun createView(activity: Activity): View {
+ return DebugInfoView(activity, null, id)
+ }
+
+ override fun findView(views: Sequence): DebugInfoView? {
+ return views.mapNotNull { it as? DebugInfoView }.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) { }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt
new file mode 100644
index 0000000..92f33a9
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt
@@ -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
+ }
+ }
+
+}
+
+data object 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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt
new file mode 100644
index 0000000..73f5d81
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt
@@ -0,0 +1,63 @@
+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.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?
+ 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) {
+ if (id >= 0) {
+ context.getAppWidgetHost().deleteAppWidgetId(id)
+ }
+
+ LauncherPreferences.widgets().widgets(
+ LauncherPreferences.widgets().widgets()?.also {
+ it.remove(this)
+ }
+ )
+ }
+
+ 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(id: Int): Widget? {
+ // TODO: do some caching
+ return LauncherPreferences.widgets().widgets().firstOrNull {
+ it.id == id
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt
new file mode 100644
index 0000000..e56983a
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt
@@ -0,0 +1,64 @@
+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) }
+ }
+
+ fun getWidgets(): List {
+ return LauncherPreferences.widgets().widgets().filter {
+ it.panelId == this.id
+ }
+ }
+
+
+ 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)
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPosition.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPosition.kt
new file mode 100644
index 0000000..e51f00c
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPosition.kt
@@ -0,0 +1,102 @@
+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) {
+
+ constructor(rect: Rect) : this(
+ rect.left.toShort(),
+ rect.top.toShort(),
+ (rect.right - rect.left).toShort(),
+ (rect.bottom - rect.top).toShort()
+ )
+
+ fun toRect(): Rect {
+ return Rect(x.toInt(), y.toInt(), x + width, y + height)
+ }
+
+ 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
+ )
+ }
+
+ fun findFreeSpace(
+ widgetPanel: WidgetPanel?,
+ minWidth: Int,
+ minHeight: Int
+ ): WidgetPosition {
+ val rect = Rect(0, 0, minWidth, minHeight)
+ if (widgetPanel == null) {
+ return WidgetPosition(rect)
+ }
+
+ val widgets = widgetPanel.getWidgets().map { it.position.toRect() }
+
+ for (x in 0.. {
+ val list = mutableListOf(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 { profile ->
+ appWidgetManager.getInstalledProvidersForProfile(profile)
+ .map { LauncherAppWidgetProvider(it) }
+ }.flatten()
+ )
+
+ return list
+}
+
+fun updateWidget(widget: Widget) {
+ LauncherPreferences.widgets().widgets(
+ (LauncherPreferences.widgets().widgets() ?: setOf())
+ .minus(widget)
+ .plus(widget)
+ )
+}
+
+
+// TODO: this needs to be improved
+fun generateInternalId(): Int {
+ val minId = min(-5,(LauncherPreferences.widgets().widgets() ?: setOf()).minOfOrNull { it.id } ?: 0)
+ return minId -1
+}
+
+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
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/baseline_add_24.xml b/app/src/main/res/drawable/baseline_add_24.xml
new file mode 100644
index 0000000..13267ce
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_add_24.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_bug_report_24.xml b/app/src/main/res/drawable/baseline_bug_report_24.xml
new file mode 100644
index 0000000..ef399ba
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_bug_report_24.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_clock_24.xml b/app/src/main/res/drawable/baseline_clock_24.xml
new file mode 100644
index 0000000..7968998
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_clock_24.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/baseline_widgets_24.xml b/app/src/main/res/drawable/baseline_widgets_24.xml
new file mode 100644
index 0000000..fd0f571
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_widgets_24.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml
new file mode 100644
index 0000000..717151f
--- /dev/null
+++ b/app/src/main/res/layout/activity_home.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_manage_widget_panels.xml b/app/src/main/res/layout/activity_manage_widget_panels.xml
new file mode 100644
index 0000000..f84f42f
--- /dev/null
+++ b/app/src/main/res/layout/activity_manage_widget_panels.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_manage_widgets.xml b/app/src/main/res/layout/activity_manage_widgets.xml
new file mode 100644
index 0000000..4e63ec9
--- /dev/null
+++ b/app/src/main/res/layout/activity_manage_widgets.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_pin_shortcut.xml b/app/src/main/res/layout/activity_pin_shortcut.xml
index 2519374..da724e7 100644
--- a/app/src/main/res/layout/activity_pin_shortcut.xml
+++ b/app/src/main/res/layout/activity_pin_shortcut.xml
@@ -44,6 +44,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
+ android:contentDescription="@string/content_description_close"
android:gravity="center"
android:includeFontPadding="true"
android:paddingLeft="16sp"
diff --git a/app/src/main/res/layout/activity_report_crash.xml b/app/src/main/res/layout/activity_report_crash.xml
new file mode 100644
index 0000000..c7f2110
--- /dev/null
+++ b/app/src/main/res/layout/activity_report_crash.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_select_widget.xml b/app/src/main/res/layout/activity_select_widget.xml
new file mode 100644
index 0000000..14296bd
--- /dev/null
+++ b/app/src/main/res/layout/activity_select_widget.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_widget_panel.xml b/app/src/main/res/layout/activity_widget_panel.xml
new file mode 100644
index 0000000..6ef6b20
--- /dev/null
+++ b/app/src/main/res/layout/activity_widget_panel.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_choose_color.xml b/app/src/main/res/layout/dialog_choose_color.xml
index 90d13c3..dc7ddca 100644
--- a/app/src/main/res/layout/dialog_choose_color.xml
+++ b/app/src/main/res/layout/dialog_choose_color.xml
@@ -24,6 +24,21 @@
android:layout_width="match_parent"
android:layout_height="10dp" />
+
+
+
+
+
+
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_create_widget_panel.xml b/app/src/main/res/layout/dialog_create_widget_panel.xml
new file mode 100644
index 0000000..900823d
--- /dev/null
+++ b/app/src/main/res/layout/dialog_create_widget_panel.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_rename_widget_panel.xml b/app/src/main/res/layout/dialog_rename_widget_panel.xml
new file mode 100644
index 0000000..effb783
--- /dev/null
+++ b/app/src/main/res/layout/dialog_rename_widget_panel.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_select_widget_panel.xml b/app/src/main/res/layout/dialog_select_widget_panel.xml
new file mode 100644
index 0000000..5f83d51
--- /dev/null
+++ b/app/src/main/res/layout/dialog_select_widget_panel.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list.xml b/app/src/main/res/layout/list.xml
index 8a3b5d9..958638a 100644
--- a/app/src/main/res/layout/list.xml
+++ b/app/src/main/res/layout/list.xml
@@ -62,6 +62,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
+ android:contentDescription="@string/content_description_close"
android:gravity="center"
android:includeFontPadding="true"
android:paddingLeft="16sp"
@@ -72,6 +73,7 @@
app:layout_constraintTop_toTopOf="parent" />
+ custom:tabTextColor="?attr/android:textColor"
+ tools:ignore="SpeakableTextPresentCheck" />
@@ -110,6 +113,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/list_appbar"
app:layout_constraintVertical_bias="0.0"
- custom:layout_behavior="@string/appbar_scrolling_view_behavior" />
+ custom:layout_behavior="@string/appbar_scrolling_view_behavior"
+ tools:ignore="SpeakableTextPresentCheck" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_apps_row_variant_grid.xml b/app/src/main/res/layout/list_apps_row_variant_grid.xml
index ee57c45..f63d724 100644
--- a/app/src/main/res/layout/list_apps_row_variant_grid.xml
+++ b/app/src/main/res/layout/list_apps_row_variant_grid.xml
@@ -27,6 +27,8 @@
android:text=""
android:textSize="11sp"
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"
diff --git a/app/src/main/res/layout/list_other_row.xml b/app/src/main/res/layout/list_other_row.xml
index 530cc9e..78d92ae 100644
--- a/app/src/main/res/layout/list_other_row.xml
+++ b/app/src/main/res/layout/list_other_row.xml
@@ -11,6 +11,7 @@
android:id="@+id/list_other_row_icon"
android:layout_width="35sp"
android:layout_height="35sp"
+ android:contentDescription="@null"
android:gravity="center"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
diff --git a/app/src/main/res/layout/list_widget_panels_row.xml b/app/src/main/res/layout/list_widget_panels_row.xml
new file mode 100644
index 0000000..835050f
--- /dev/null
+++ b/app/src/main/res/layout/list_widget_panels_row.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_widgets_header.xml b/app/src/main/res/layout/list_widgets_header.xml
new file mode 100644
index 0000000..7779f2b
--- /dev/null
+++ b/app/src/main/res/layout/list_widgets_header.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_widgets_row.xml b/app/src/main/res/layout/list_widgets_row.xml
new file mode 100644
index 0000000..b828169
--- /dev/null
+++ b/app/src/main/res/layout/list_widgets_row.xml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/settings.xml b/app/src/main/res/layout/settings.xml
index 987e293..8ce96c4 100644
--- a/app/src/main/res/layout/settings.xml
+++ b/app/src/main/res/layout/settings.xml
@@ -20,6 +20,22 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
+
+
-
-
+ app:tabTextColor="?attr/android:textColor"
+ tools:ignore="SpeakableTextPresentCheck" />
diff --git a/app/src/main/res/layout/settings_actions_row.xml b/app/src/main/res/layout/settings_actions_row.xml
index a60a5a4..eb3339c 100644
--- a/app/src/main/res/layout/settings_actions_row.xml
+++ b/app/src/main/res/layout/settings_actions_row.xml
@@ -68,6 +68,7 @@
android:id="@+id/settings_actions_row_remove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:contentDescription="@string/content_description_settings_actions_row_button_remove"
android:padding="8sp"
android:src="@drawable/baseline_close_24"
app:layout_constraintBottom_toBottomOf="parent"
diff --git a/app/src/main/res/layout/settings_meta.xml b/app/src/main/res/layout/settings_meta.xml
index 6f21baa..1adee37 100644
--- a/app/src/main/res/layout/settings_meta.xml
+++ b/app/src/main/res/layout/settings_meta.xml
@@ -41,6 +41,13 @@
android:text="@string/settings_meta_view_code"
android:textAllCaps="false" />
+
+
+ app:tabIndicatorHeight="0dp"
+ tools:ignore="SpeakableTextPresentCheck" />
-
+ app:layout_constraintEnd_toEndOf="parent">
+
+
+
+
+ tools:context=".ui.widgets.ClockView">
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_debug_info.xml b/app/src/main/res/layout/widget_debug_info.xml
new file mode 100644
index 0000000..1de9e43
--- /dev/null
+++ b/app/src/main/res/layout/widget_debug_info.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 1b7d9a2..39ae422 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -95,9 +95,9 @@
إخفِ شريط الحالة
تدوير الشاشة
الوظائف
- أوامر السحب بأصبعين
+ إيماءات التمرير المزدوج
اسحب باستخدام أصبعين
- أوامر السحب من الحواف
+ إيماءات تمرير الحافة
عرض الحواف
إظهار نتائج البحث
اضغط مسافة لتعطيل هذه الميزة مؤقتًا
@@ -153,7 +153,7 @@
لا تفعل شيئًا
قفل الشاشة
تبديل الفلاش
- شغل شاشة منزل أخرى
+ تشغيل شاشة رئيسية أخرى
أضف اختصارًا
اربط بإيماءة
درس تعليمي
@@ -162,11 +162,11 @@
إنه برنامج مجاني (ترخيص MIT)!\nتأكد من مراجعة المستودع!
النسخة
الاستخدام
- تحتوي شاشتك الرئيسية على التاريخ والوقت المحليين. لا الهاء.
+ تحتوي شاشة المنزل الخاصة بك على التاريخ والوقت المحليين. بدون أي مشتتات.
كل التطبيقات
بمجرد تطابق تطبيق واحد فقط، يتم تشغيله تلقائيًا.\nيمكن تعطيل ذلك عن طريق اضافة مساحة في بداية استعلام.
الإعداد
- اخترنا بعض التطبيقات الافتراضية لك. يمكنك تغييرها الآن إذا كنت تريد:
+ اخترنا لك بعض التطبيقات الافتراضية. يمكنك تغييرها الآن إذا كنت ترغب في ذلك:
يمكنك أيضًا تغيير اختيارك لاحقًا.
لنبدأ!
ابدأ
@@ -235,7 +235,7 @@
لا تظهر التطبيقات المرتبطة بإيماءة في قائمة التطبيقات
درج التطبيقات المفضلة
الموسيقى: التالي
- تم تصميم μlauncher ليكون فعال، وخالي من الهاء.\n\nلا يحتوي على أي إعلانات ولا يجمع أي بيانات.
+ تم تصميم μLauncher ليكون فعالًا وخاليًا من مشتتات الانتباه.\n\nلا يحتوي على أي إعلانات ولا يجمع أي بيانات.
أنت مستعد للبدء!\n\nآمل أن يكون هذا ذا قيمة كبيرة بالنسبة لك!\n\n- المطورين
يجب أن يكون μlauncher الشاشة الرئيسية الافتراضية للوصول إلى مساحة خاصة.
اسحب إلى اليسار من أسفل الشاشة
@@ -245,7 +245,7 @@
معلومات التطبيق
أضف إلى المفضلة
توسيع لوحة الاشعارات
- يمكنك البحث بسرعة من خلال جميع التطبيقات في قائمة التطبيقات.\n\nاسحب لأعلى لفتح القائمة، أو ربطها بإيماءة ما.
+ يمكنك البحث بسرعة في جميع التطبيقات في قائمة التطبيقات.\n\nاسحب لأعلى لفتحها، أو اربطها بإيماءة مختلفة.
خطأ: فشل في قفل الشاشة. (إذا قمت للتو بترقية التطبيق ، فحاول تعطيل خدمة الوصول وإعادة تمكينها في إعدادات الهاتف)
اسحب إلى الأسفل من حافة الشاشة اليمنى
أحادي المسافة مذيل
@@ -257,8 +257,85 @@
شكرا لك على المساعدة في تحسين μLauncher!\nيرجى إضافة المعلومات التالية إلى تقرير الأخطاء الخاص بك:
اتصل بالمطور الأصلي
اخفض الصوت
- يمكنك تشغيل أهم تطبيقاتك بوسطة إيماءات اللمس أو الضغط على الأزرار.
+ يمكنك تشغيل أهم تطبيقاتك بإيماءات اللمس أو الضغط على الأزرار.
اسحب من حواف الشاشة
خطأ: قفل الشاشة باستخدام إمكانية الوصول غير مدعوم على هذا الجهاز. الرجاء استخدام طريقة مسؤول الجهاز بدلاً من ذلك.
يتيح تعيين μlauncher كخدمة إمكانية الوصول قفل الشاشة وفتح قائمة التطبيقات الحديثة. يرجى ملاحظة أنه يتطلب كمية كبيرة من الأذونات. يجب ألا تمنح مثل هذه الأذونات باستخفاف لأي تطبيق. سوف يستخدم μlauncher خدمة إمكانية الوصول فقط لأداء الإجراءات التالية عند طلب المستخدم: * قفل شاشة * فتح التطبيقات الحديثة μlauncher لن يستخدم أبدًا خدمة إمكانية الوصول لجمع البيانات. يمكنك التحقق من شيفرة المصدر للتأكد. يرجى ملاحظة أنه يمكنك قفل الشاشة من خلال منح أذونات مسؤول الجهاز، لكنها لا تعمل مع بصمات الأصابع وفتح الوجه.
+ إنشاء لوحة أداة جديدة
+ إدارة الأدوات المصغّرة
+ إدارة لوحات الأدوات المصغّرة
+ اختر طريقة القفل
+ هناك طريقتان لقفل الشاشة.
+ للأسف، كلا الطريقتين لهما عيوب:
+
+ إدارة الجهاز
+ لا تعمل مع فتح القفل باستخدام بصمة الإصبع أو التعرف على الوجه.
+
+
+
+
+ خدمة الوصول
+ تتطلب صلاحيات واسعة.
+ سيستخدم تطبيق μLauncher هذه الصلاحيات فقط لقفل الشاشة.
+
+ (يجب ألا تثق بتطبيق عشوائي قمت بتحميله للتو بهذا الادعاء، ولكن يمكنك التحقق من الكود المصدري.)
+
+ في بعض الأجهزة، لن يُستخدم رمز PIN الخاص بالتشغيل لتشفير البيانات بعد تفعيل خدمة الوصول.
+ يمكن إعادة تفعيل هذا لاحقًا.
+
+
+ يمكنك تغيير اختيارك لاحقًا في الإعدادات.
+]]>
+ الساعة الافتراضية لـ μLauncher
+ المشغل > إدارة لوحات الأدوات.
+]]>
+ اختر الأداة
+ إزالة
+ تهيئة
+ تمكين التفاعل
+ تعطيل التفاعل
+ ساعة
+ حذف
+ إعادة التسمية
+ لوحة الأداة المصغّرة #%1$d
+ حسنا
+ لوحات الأداة
+ تحديد لوحة الأداة
+ افتح لوحة الأدوات
+ لم تعد لوحة الأدوات هذه موجودة.
+ الأدوات
+
+ - يحتوي على %1$dأداة.
+ - يحتوي على %1$d أداة.
+ - يحتوي على %1$d أدوات.
+ - يحتوي على%1$d أدوات.
+ - يحتوي على %1$d أداة.
+ - يحتوي على%1$d أداة.
+
+ إغلاق لوحة المفاتيح عند التمرير
+
+ لأسباب تتعلق بالخصوصية، لا يتم جمع سجلات الأعطال تلقائيًا.
+ ومع ذلك، فإن السجلات مفيدة جدًا لتصحيح الأخطاء، لذا سأكون ممتنًا جدًا إذا كان بإمكانك إرسال السجل المرفق عبر البريد
+ أو إنشاء تقرير عن الخطأ على GitHub.
+ يرجى ملاحظة أن سجلات الأعطال قد تحتوي على معلومات حساسة، مثل اسم التطبيق الذي حاولت تشغيله.
+ يرجى حذف مثل هذه المعلومات قبل إرسال التقرير.
+ ماذا يمكنني أن أفعل الآن؟
+ إذا ظهر هذا الخطأ مرة أخرى، يمكنك تجربة عدة أشياء:
+
+ - إيقاف μLauncher بالقوة
+ - مسح بيانات μLauncher's (ستفقد إعداداتك!)
+ - تثبيت إصدار أقدم (GitHub, F-Droid)
+
+]]>
+ تعطل μLauncher
+ آسف! انقر للحصول على مزيد من المعلومات.
+ نسخ تقرير التعطل إلى الحافظة
+ إرسال التقرير بالبريد
+ إنشاء تقرير خطأ على GitHub
+ تعطل μLauncher
+ إرسال بريد إلكتروني
+ الأعطال ومعلومات التصحيح
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index d07ff7d..0b4090c 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -97,9 +97,9 @@
Bildschirm nicht ausschalten
Bildschirm drehen
Funktionalität
- Doppelte Wischaktionen
+ Doppelte Wischgesten
Mit zwei Fingern wischen
- Kantenaktionen
+ Kantengesten
Kantenbreite
Suchergebnis starten
Beim Durchsuchen der Apps Enter drücken, um stattdessen im Internet zu suchen.
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 1a04590..bca3a81 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -315,4 +315,46 @@
Vincular al gesto
Mostrar en la lista de aplicaciones
Invertir la lista de aplicaciones
+
+ - Contiene %1$d widget.
+ - Contiene %1$d widgets.
+ - Contiene %1$d widgets.
+
+ Crear reporte de bug en GitHub
+ μLauncher dejó de funcionar
+ Enviar correo
+ Documentación
+ Elegir widget
+ Quitar
+ Configurar
+ Habilitar interacción
+ Reloj
+ El reloj predeterminado de μLauncher
+ Eliminar
+ Cambiar nombre
+ Panel de widgets #%1$d
+ Paneles de widgets
+ Seleccionar un panel de widgets
+ Abrir panel de widgets
+ Este panel de widgets ya no existe.
+ Widgets
+ Enviar reporte por correo
+ Ok
+ Añadir widget
+ Añadir panel de widgets
+ Cerrar
+ Navegar atrás
+ Navegar siguiente
+ Bloquear
+ Quitar enlace
+ Iniciar otra aplicación de inicio
+ Error: Error al mostrar las aplicaciones recientes. (Si acaba de actualizar la aplicación, intente deshabilitar y volver a habilitar el servicio de accesibilidad en la configuración del teléfono)
+ Error: Error al habilitar el servicio de accesibilidad.
+ Deshabilitar interacción
+ Crear nuevo panel de widgets
+ Copiar reporte de crash al portapapeles
+ Administrar paneles de widgets
+ Aplicaciones recientes
+ (Inverso)]]>
+ Administrar widgets
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 318ad43..8b30737 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -1,17 +1,17 @@
- Scorri verso destra sul bordo inferiore dello schermo
+ Scorri a destra sul bordo inferiore dello schermo
Aspetto
- Scegliere
+ Associa app
Tema
- Questo launcher è progettato per essere minimale, efficiente e privo di distrazioni. Non contiene pagamenti, pubblicità o servizi di tracciamento.
+ μLauncher è progettato per essere minimale, efficiente e privo di distrazione.\n\nNon contiene annunci e non raccoglie dati.
Predefinito
- Non mostrare applicazioni collegate a gesti nella lista delle app
+ Non mostrare nella lista le applicazioni associate a gesti
Testo
- Impostare µLauncher come servizio accessibilità permette l\'azione di blocco dello schermo. Si precisa che sono richiesti permessi eccessivi. Non si dovrebbe mai concedere con leggerezza permessi ad alcuna app. µLauncher utilizza i servizi di accessibilità esclusivamente per il blocco dello schermo. Puoi verificare il codice sorgente. Si segnala che il blocco schermo si può attivare anche concedendo a µLauncher i permessi di amministratore, tuttavia tale metodo non funziona con lo sblocco tramite impronta o riconoscimento facciale.
- Sans serif (senza grazie)
- Trascina due dita dal basso verso l\'alto
- Negozio non trovato
+ Impostare µLauncher come servizio accessibilità permette l\'azione di blocco dello schermo. Nota che sono richiesti permessi eccessivi. Non si dovrebbero mai concedere con leggerezza permessi ad alcuna app. µLauncher utilizza i servizi di accessibilità esclusivamente per il blocco dello schermo. Puoi verificare il codice sorgente. Nota che il blocco schermo si può attivare anche concedendo a µLauncher i permessi di amministratore, tuttavia tale metodo non funziona con lo sblocco tramite impronta o riconoscimento facciale.
+ Sans serif
+ Scorri verso l\'alto con due dita
+ Store non trovato
Griglia
Scegli un sistema di blocco
@@ -30,114 +30,118 @@
(Non ci si dovrebbe mai fidare di un\'applicazione qualsiasi appena scaricata su queste cose, ma puoi verificare il codice sorgente.)
+ In alcuni dispositivi, il PIN iniziale non verrà più utilizzato per la crittografia dei dati dopo aver attivato un servizio di accessibilità.
+
+ Può essere riattivato dopo.
+
Puoi cambiare le tue scelte in seguito nelle impostazioni.
]]>
- Impossibile aprire l\'applicazione
- Desideri modificare le impostazioni?
- Apri le impostazioni per abbinare un\'azione a questo gesto
+ Impossibile avviare l\'app
+ Modificare le impostazioni?
+ Apri impostazioni per associare un\'azione a questo gesto
Impostazioni
Launcher
Meta
Scorri verso il basso con due dita
Sinistra
- Scorrere verso sinistra
- Due dita verso sinistra
- Scorrere verso sinistra con due dita
+ Scorri verso sinistra
+ Due dita a sinistra
+ Scorri verso sinistra con due dita
Destra
Scorri verso destra
- Due dita verso destra
- Scorri a destra con due dita
+ Due dita a destra
+ Scorri verso destra con due dita
Destra (in alto)
- Scorri verso destra sul bordo superiore dello schermo
- Scorri verso sinistra sul bordo inferiore dello schermo
- Scorri verso sinistra sul bordo superiore dello schermo
- Verso l\'alto (lato sinistro)
- Verso l\'alto
- Striscia il dito dal basso verso l\'alto
- Verso il basso
- Trascina due dita dall\'alto verso il basso
- Trascina un dito dall\'alto verso il basso
- Destra (bordo inferiore)
- Sinistra (bordo inferiore)
- Sinistra (bordo superiore)
- Scorri verso l\'alto sul bordo sinistro dello schermo
- Alto (lato destro)
- Scorri verso l\'alto sul bordo destro dello schermo
- Basso (bordo sinistro)
- Scorri verso il basso sul bordo sinistro dello schermo
- Basso (Lato destro)
- Scorri verso il basso sul lato destro dello schermo
- Aumenta il volume
- Doppio click
- Doppio click in un\'area vuota
- Tocco prolungato
- Tocco prolungato su un\'area vuota
+ Scorri a destra sul bordo superiore dello schermo
+ Scorri a sinistra sul bordo inferiore dello schermo
+ Scorri a sinistra sul bordo superiore dello schermo
+ Su (bordo sinistro)
+ Su
+ Scorri verso l\'alto
+ Giù
+ Scorri giù con due dita
+ Scorri verso il basso
+ Destra (in basso)
+ Sinistra (in basso)
+ Sinistra (in alto)
+ Scorri in alto sul bordo sinistro dello schermo
+ Su (bordo destro)
+ Scorri in alto sul bordo destro dello schermo
+ Giù (bordo sinistro)
+ Scorri in basso sul bordo sinistro dello schermo
+ Giù (bordo destro)
+ Scorri in basso sul bordo destro dello schermo
+ Volume +
+ Doppio tap
+ Doppio tap in spazio vuoto
+ Tap lungo
+ Tap lungo in spazio vuoto
Data
- Premi sulla data
+ Tap sulla data
Ora
- Premi sull\'ora
- Premi il pulsante di aumento del volume
- Riduci il volume
- Premi il pulsante per ridurre il volume
- Installa le applicazioni
+ Tap sull\'ora
+ Premi il pulsante Volume +
+ Volume -
+ Premi il pulsante Volume -
+ Installa apps
Icone monocromatiche
- Mostra l\'ora
- Mostra la data
- Usa il formato ora locale
- Mostra i secondi
+ Mostra orario
+ Mostra data
+ Usa formato data locale
+ Mostra secondi
Scorri con due dita
- Azioni a scorrimento con due dita
+ Scorrimento a due dita
Apri il risultato della ricerca
- Azioni a scorrimento ai lati dello schermo
- Scorri sul lato dello schermo
- Larghezza margine laterale
- Vedi il codice sorgente
+ Scorrimento sui bordi dello schermo
+ Scorri sui bordi dello schermo
+ Larghezza bordo
+ Codice sorgente
Tutte le applicazioni
Applicazioni preferite
- Musica: passa alla traccia successiva
- Musica: riduci il volume
- Musica: torna alla traccia precedente
- Musica: Aumenta il volume
+ Musica: traccia successiva
+ Abbassa volume
+ Musica: traccia precedente
+ Alza volume
Annulla
Impostazioni rapide
- Azione necessaria per abilitare il blocco dello schermo.
- Ombreggiatura del testo
- Sfondo (lista applicazioni e impostazioni)
+ Azione necessaria per abilitare il blocco schermo.
+ Ombreggiatura testo
+ Sfondo (lista apps e impostazioni)
Font
Inverti data e ora
- Scegli uno sfondo
+ Imposta sfondo
Schermo
Mantieni lo schermo acceso
- Ruota lo schermo
- Funzionalità
- Apri automaticamente la tastiera per cercare
+ Ruota schermo
+ Funzioni
+ Apri automaticamente la tastiera
Sensibilità
- Scegli un\'applicazione
+ Scegli applicazione
Configurazione
- Abbiamo impostato alcune app predefinite per te. Puoi modificarle ora se lo desideri:
- Puoi anche cambiare la tua selezione in seguito.
+ Abbiamo impostato alcune app predefinite. Puoi modificarle ora se vuoi:
+ Puoi anche modificare la selezione in seguito.
Iniziamo!
- Sei pronto per iniziare! Spero questa applicazione ti risulti preziosa! - Finn (che ha ideato il launcher)\n \te Josia (che ha aggiunto qualche miglioramento e mantiene il fork μLauncher)
+ Sei pronto per iniziare!\n\nSpero che lo apprezzi!\n\n- Finn (che ha fatto Launcher) e Josia (che ha apportato alcuni miglioramenti e mantiene il fork μLauncher)
Inizia
Impostazioni
Altre opzioni
- Puoi aprire le tue app facendo scorrere il dito sullo schermo o premendo un pulsante. Configura i gesti nella prossima slide.
- Errore: impossibile espandere la barra di stato. Questa azione utilizza funzionalità non incluse nelle API Android pubbliche. Sfortunatamente, non sembra funzionare sul tuo dispositivo.
- Applicazione nascosta. Puoi renderla nuovamente visibile nelle impostazioni.
- µLauncher deve essere autorizzato come amministratore del dispositivo per bloccare lo schermo.
- Abilita il blocco dello schermo
+ Puoi avviare le tue app principali scorrendo il dito sullo schermo o premendo un pulsante.
+ Errore: impossibile espandere la barra di stato. Questa azione utilizza funzionalità non incluse nelle API Android. Sfortunatamente, non sembra funzionare sul tuo dispositivo.
+ Applicazione nascosta. Puoi renderla nuovamente visibile dalle impostazioni.
+ µLauncher deve amministrare il dispositivo per poter bloccare lo schermo.
+ Abilita il blocco schermo
Nessuna camera con torcia rilevata.
Errore: impossibile accedere alla torcia.
- Il servizio accessibilità per µLauncher non è attivo. Per favore attivalo nelle impostazioni
+ Il servizio accessibilità per µLauncher non è attivo. Attivalo nelle impostazioni
Errore: impossibile bloccare lo schermo. (Se hai appena aggiornato l\'app, prova a disabilitare e riattivare il servizio accessibilità nelle impostazioni del telefono)
- Errore: Il blocco schermo tramite accessibilità non è supportato su questo dispositivo. Per favore usa invece il servizio amministratore dispositivo.
- µLauncher - blocco schermo
+ Errore: Il blocco schermo tramite accessibilità non è supportato su questo dispositivo. In alternativa usa il servizio amministratore dispositivo.
+ μLauncher
Scegli come bloccare lo schermo
Usa il servizio accessibilità
- Usa l\'amministratore dispositivo
+ Usa Amministratore dispositivo
Scegli un sistema di blocco dello schermo
Scuro
Chiaro
@@ -145,22 +149,22 @@
Sfocato
Solido
Predefinito di sistema
- Serif (con grazie)
- Monospace (a larghezza fissa)
- Serif monospace (a larghezza fissa con grazie)
+ Serif
+ Monospace
+ Serif monospace
Applicazioni
Applicazioni nascoste
- Configurazione della lista applicazioni
+ Configura la lista applicazioni
Predefinito
- Offuscato
- Imposta μLauncher come predefinito per la schermata principale
- Informazioni sulle applicazioni
- Apri il tutorial del launcher
- Ripristina le impostazioni predefinite
- Stai per eliminare tutte le preferenze impostate. Vuoi continuare?
+ Soffuso
+ Imposta μLauncher come predefinito
+ Informazioni app
+ Tutorial di µLauncher
+ Ripristina le impostazioni
+ Stai per ripristinare tutte le impostazioni. Continuare?
Segnala un bug
Contatta lo sviluppatore del fork
- Partecipa alla chat di μLauncher
+ Unisciti alla chat di μLauncher
Informativa sulla privacy
Contatta lo sviluppatore originale
Unisciti a noi su Discord!
@@ -168,9 +172,9 @@
Applicazioni preferite
Applicazioni nascoste
Applicazioni
- Altri
+ Altro
Disinstalla
- Informazioni sull\'app
+ Informazioni app
Aggiungi ai preferiti
Rimuovi dai preferiti
Nascondi
@@ -178,29 +182,29 @@
Rinomina
Cerca
Impostazioni μLauncher
- Espandi il pannello notifiche
+ Espandi pannello notifiche
Non fare niente
- Blocca lo schermo
- Accendi/spegni la torcia
+ Blocca schermo
+ Torcia ON/OFF
Tutorial
- Prenditi qualche secondo per imparare ad usare questo launcher!
+ 👋\n\nPrenditi qualche secondo per imparare ad usare questo launcher!
Concetto
- L\'app è open source (sotto licenza MIT) e disponibile su GitHub! Visita il nostro archivio!
+ E\' software libero (licenza MIT)!\nAssicurati di controllare il repository!
Utilizzo
La schermata principale contiene solo data e ora. Nessuna distrazione.
Questa funzione richiede Android 6 o successivi.
Rinomina %1$s
Dinamico
Colore
- Due dita verso l\'alto
+ Scorri su con due dita
Sono consapevole che questo concederà privilegi estesi a µLauncher.
Accetto che µLauncher utilizzi il servizio di accessibilità per fornire funzionalità non correlate all\'accessibilità.
Accetto che µLauncher non raccolga alcun dato.
Nascondi le app in pausa
- Attiva/Disattiva Blocco Spazio Privato
- Questa funzionalità richiede Android 15 o successivi.
+ Blocca/Sblocca Spazio Privato
+ Questa funzione richiede Android 15 o successivi.
Rosso
- Trasparente
+ Alpha
Blu
Verde
Colore
@@ -208,24 +212,135 @@
Attiva Servizi di Accessibilità
Sono consapevole che esistono altre opzioni (utilizzando i privilegi di amministratore del dispositivo o il pulsante di accensione).
Attivazione dei Servizi di Accessibilità
- Cerca su internet
- Premi invio durante la ricerca nell\'elenco delle app per avviare una ricerca su internet.
+ Cerca sul web
+ Invio in ricerca app per avviare una ricerca web.
Cerca (senza avvio automatico)
Licenze Open Source
Licenze Open Source
Segnala un bug
- Grazie per aver contribuito a migliorare µLauncher!\nSi prega di aggiungere le seguenti informazioni alla segnalazione del bug:
+ Grazie per il tuo contributo al miglioramento di µLauncher!\nAggiungi le seguenti informazioni alla segnalazione del bug:
Copia negli appunti
- Non segnalare pubblicamente le vulnerabilità di sicurezza su GitHub, ma utilizza invece:
+ Non segnalare le vulnerabilità di sicurezza pubblicamente su GitHub, ma usa invece:
Annulla
- Premi spazio per disabilitare temporaneamente questa funzionalità.
+ Spazio per disabilitare temporaneamente.
Segnala una vulnerabilità di sicurezza
Crea una segnalazione
Spazio privato bloccato
Spazio privato sbloccato
Spazio privato non disponibile
- µLauncher deve essere la schermata iniziale predefinita per accedere allo spazio privato.
+ µLauncher deve essere il launcher predefinito per poter accedere allo spazio privato.
Impossibile aprire l\'URL: nessun browser trovato.
- Non è stata trovata un\'applicazione per gestire la ricerca.
- privilegi più ampi a µLauncher.
µLauncher utilizzerà questi privilegi solo per bloccare lo schermo. µLauncher non raccoglierà mai alcun dato. In particolare, µLauncher non usa il servizio di accessibilità per raccogliere nessun dato.]]>
+ Nessuna applicazione trovata per gestire la ricerca.
+ privilegi estesi a μLauncher.
μLauncher userà questi privilegi solo per eseguire le seguenti azioni:
+
+ - Blocco shermo
+ - App recenti
+
+ μLauncher non raccoglierà mai alcun dato. In particolare, μLauncher non utilizza il servizio di accessibilità per raccogliere dati.]]>
+ Spazio privato
+ Spazio privato
+ Regola volume
+ Associa al gesto
+ Nascondi barra di stato
+ Nascondi barra di navigazione
+ Dona
+ App recenti
+ Tap + Su
+ Tap e scorri su
+ Tap + Giù
+ Tap e scorri verso il basso
+ Tap + Sinistra
+ Tap + Destra
+ Alto sx -> centro dx -> basso sx
+ Tap e scorri vesro sinistra
+ Tap e scorri verso destra
+ (Inverso)]]>
+ Basso sx -> centro dx -> alto sx
+ Alto dx -> centro sx -> basso dx
+ V
+ Alto sx -> centro basso -> alto dx
+ V (Inverso)
+ Alto dx -> centro basso -> alto sx
+ Λ
+ Basso sx -> centro alto -> basso dx
+ Basso dx -> centro alto -> basso sx
+
+
+ Λ (Inverso)
+ Musica: Riproduci / Pausa
+ Inverti la lista applicazioni
+ Azioni
+ Pulsante / gesto indietro
+ Indietro
+ Basso dx -> centro sx -> alto dx
+ Nascondi lo spazio privato dalla lista app
+ Avvia un altro launcher
+ Aggiungi scorciatoia
+ ]]>
+ Mostra in lista app
+ Blocca spazio privato
+ Sblocca spazio privato
+ Versione
+ Errore: impossibile abilitare il servizio di accessibilità.
+ Quando corrisponde una sola app, viene avviata automaticamente.\nPuoi disabilitare l\'avvio con uno spazio prima della query.
+ Tutte le app
+ Puoi cercare rapidamente tra tutte le app nella lista app.\n\nScorri su per la lista o associa ad un gesto diverso.
+ Errore: impossibile mostrare le app recenti. (Se hai appena aggiornato l\'app, prova a disabilitare e riabilitare il servizio di accessibilità dalle impostazioni del telefono)
+ Chiudi la tastiera durante lo scorrimento
+ Gestione widget
+ Rimuovi
+ Gestione pannelli widget
+ Scegli widget
+ Configura
+ Abilita interazione
+ Disabilita interazione
+ Orologio
+ Orologio predefinito di μLauncher
+ Elimina
+ Rinomina
+ Pannello widget #%1$d
+ Ok
+ Pannelli widget
+
+ - Contiene %1$d widget.
+ - Contiene %1$d widget.
+ - Contiene %1$d widget.
+
+ Crea nuovo pannello widget
+ Apri pannello widget
+ Questo pannello widget non esiste più.
+ Widget
+ Seleziona pannello widget
+ Launcher > Gestione Pannelli Widget.]]>
+ Documentazione
+ Scusa! Clicca per altre info.
+ µLauncher è crashato
+ Copia il rapporto del crash negli appunti
+ Invia il rapporto per email
+ Crea un bug report in GitHub
+ µLauncher è crashato
+ Invia email
+ Info su crash e debug
+
+ Per motivi di privacy, i log dei crash non sono raccolti automaticamente.
+ Comunque i log sono molto utili per il debug, quindi ti sarei molto grato se potessi inviarmi i log per email
+ oppure aprire un bug report su github.
+ Nota che i crash log potrebbero contenere informazioni sensibili, come il nome dell\'app che hai provato ad avviare.
+ Please redact such information before sending the report.
+ What can I do now?
+ If this bug appears again and again, you can try several things:
+
+ - Force stop μLauncher
+ - Clear μLauncher\'s storage (Your settings will be lost!)
+ - Install an older version (GitHub, F-Droid)
+
+ ]]>
+ Aggiungi pannello widget
+ Chiudi
+ Naviga indietro
+ Naviga avanti
+ Blocca
+ Aggiungi widget
+ Rimuovi associazione
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index 0a45d57..5928650 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -1,16 +1,118 @@
+
+ Nepavyksta paleisti programėlės
Norite pakeisti nustatymus?
+ Atidarykite nustatymus norėdami pasirinkti šio gesto veiksmą
+
Nustatymai
Veiksmai
Paleidimo programėlė
Apie
+
Atgal
Grįžimo mygtukas / grįžimo gestas
+ Aukštyn
Perbraukimas aukštyn
Bakstelėkite + aukštyn
- Nepavyksta paleisti programėlės
- Atidarykite nustatymus norėdami pasirinkti šio gesto veiksmą
- Aukštyn
Bakstelėjimas ir perbraukimas aukštyn
+ Dvigubai aukštyn
+ Perbraukite aukštyn dviem pirštais
+ Žemyn
+ Perbraukite žemyn
+ Bakstelėkite + žemyn
+ Bakstelėkite ir perbraukite žemyn
+ Dvigubai žemyn
+ Perbraukite dviem pirštais
+ Kairėje
+ Perbraukite į kairę
+ Bakstelėkite + kairę
+ Bakstelėkite ir perbraukite į kairę
+ Dvigubai kairėje
+ Du pirštais perbraukite kairėn
+ Dešinė
+ Perbraukite į dešinę
+ Bakstelėkite + dešinė
+ Bakstelėkite ir perbraukite į dešinę
+ Dviguba dešinė
+ Perbraukite į dešinę dviem pirštais
+ Dešinė (viršuje)
+ Perbraukite tiesiai ekrano viršuje
+ Dešinė (apačia)
+ Perbraukite tiesiai ekrano apačioje
+ Kairė (apačia)
+ Perbraukite į kairę ekrano apačioje
+ Kairė (viršuje)
+ Perbraukite kairėn ekrano viršuje
+ Aukštyn (kairysis kraštas)
+ Perbraukite aukštyn kairiajame ekrano krašte
+ Aukštyn (dešinysis kraštas)
+ Perbraukite aukštyn dešiniajame ekrano krašte
+ Žemyn (kairysis kraštas)
+ Perbraukite žemyn kairiajame ekrano krašte
+ Žemyn (dešinysis kraštas)
+ Žemyn (dešinysis kraštas)
+ Garsumo didinimo klavišas
+ Paspauskite mygtuką „Volume Up“
+ Volume žemyn klavišas
+ Paspauskite mygtuką „Volume Down“
+ Dukart spustelėkite
+ Dukart spustelėkite tuščią sritį
+ Ilgas spustelėjimas
+ Ilgai spustelėkite tuščią sritį
+ Data
+ Spustelėkite datą
+ Laikas
+ Spustelėkite laiką
+ Tvarkykite valdiklius
+ Tvarkykite valdiklio skydelius
+ Pasirinkite programą
+ Įdiekite programas
+ Parduotuvėje nerasta
+
+ Išvaizda
+ Spalvos tema
+ Numatytasis
+ Tamsu
+ Šviesa
+ Dinaminis
+ Teksto šešėlis
+ Fonas (programų sąrašas ir nustatymas)
+ Skaidrus
+ Dim
+ Blur
+ Solidus
+ Šriftas
+
+ Sistemos numatytasis
+ Be serifo
+ Serifas
+ Monoerdvė
+ Serifo monospace
+ Vienspalvių programų piktogramos
+
+ Spalva
+ Rodyti laiką
+ Rodyti datą
+ Naudoti lokalizuotą datos formatą
+ Rodyti sekundes
+ Apversti datą ir laiką
+ Pasirinkite ekrano foną
+ Ekranas
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000..c6bf7bf
--- /dev/null
+++ b/app/src/main/res/values-nl/strings.xml
@@ -0,0 +1,312 @@
+
+
+ Met twee vingers naar boven vegen
+ Bewerk widgets
+ Kleur
+ App-winkel niet gevonden
+ Uiterlijk
+ Kleurenschema
+ Standaard
+ Donker
+ Kies app
+ µLauncher als standaard instellen
+ App info
+ Bekijk de µLauncher handleiding
+ Broncode bekijken
+ Een probleem melden
+ Een probleem melden
+ Naar klembord kopiëren
+ Meld veiligheidsproblemen niet publiek op GitHub, gebruik daarvoor het volgende:
+ Rapport maken
+ Neem contact met de ontwikkelaar van de fork op
+ Word lid van de µLauncher chat
+ Instellingen terugzetten
+ Aan gebaar binden
+ In aaps-lijst tonen
+ 👋\n\nEen korte uitleg over hoe µLauncher functioneert.
+ Foutmelding: De toegankelijkheidsdienst kon niet ingeschakeld worden.
+ Tikken en dan naar rechts vegen
+ Druk tijdens het zoeken naar apps enter, om op het internet te zoeken.
+ Instellingen openen om voor dit gebaar een actie te kiezen
+ Gebruik gelokalizeerde datumformaat
+ Annuleren
+ Terug-knop of gebaar
+ Onderin naar links vegen
+ Rechts naar beneden vegen
+ In de hoek vegen
+ Handleiding
+ Met twee vingers naar beneder vegen
+ Apps installeren
+ Rooster
+ Hierdoor verwijdert u alle instellingen. Doorgaan?
+ Bedankt voor het helpen verbeteren van μLauncher!\nHet zou behulpzaam zijn om volgende informatie aan het rapport toe te voegen:
+ Shortcut toevoegen
+ Foutmelding: Er werd geen zaklamp gevonden.
+ Foutmelding: Geen toegang tot de zaklamp.
+ Door μLauncher in te stellen als toegankelijkheidsdienst kan het scherm worden vergrendeld en het menu met recente apps worden geopend. Merk op dat veel toestemingen vereist zijn. Wees hierdoor erg vorzichtig bij het geven van zulke machten aan een app. μLauncher zal de toegankelijkheidsdienst alleen gebruiken om de volgende acties uit te voeren wanneer de gebruiker daarom vraagt: * vergrendelscherm * recente apps openen μLauncher zal de toegankelijkheidsdienst nooit gebruiken om gegevens te verzamelen. U kunt de broncode controleren om er zeker van te zijn. Merk op dat het vergrendelen van het scherm ook kan worden bereikt door μLauncher apparaatbeheerdersrechten te geven, maar dat werkt echter niet met vingerafdruk- en gezichtsontgrendeling.
+ Methode voor vergrendeling kiezen
+ Er zijn twee methodes om het scherm te vergrendelen.
+ Helaas hebben beide nadelen:
+
+ Apparaatbeheerder
+ Werkt niet met ontgrendeling via vingerafdruk of gezichtsherkenning.
+
+
+
+
+ Toegankelijkheidsdienst
+ Vereist veel rechten.
+ μLauncher gebruikt deze rechten alleen om het scherm te vergrendelen.
+
+ (Echter moet u niet zomaar een willekeurige app zulke rechten geven, maar u kunt de Broncodebekijken.)
+
+ Op sommige apparaten wordt de pin niet meer gebruikt om data te vergrendelen na het aanzetten van een toegankelijkheidsdienst.
+ Dit kan nadehand heraktiveerd worden .
+
+
+ U kunt uw keuze altijd in de instellingen veranderen.
+ ]]>
+ uitgebreide privileges aan μLauncher.
μLauncher gebruikt deze privileges alleenmaar voor volgende doeleinde:
+
+ - Vergrendelscherm
+ - Recente apps
+
+ μLauncher verzamelt nooit gegevens. Onder anderen, μLauncher gebruikt de toegankelijkheidsdienst om gegevens verzamelen.]]>
+ Bewerk widget paneel
+ Licht
+ Dynamisch
+ Tekstschaduw
+ Achtergrond (zichtbaar in de instelligen en de app-lijst)
+ Doorzichtig
+ Dimmen
+ Vervagen
+ Eenkleurig
+ Lettertype
+ Systeemstandaard
+ Sans sarif
+ Serif
+ Monospace
+ Serif monospace
+ Eenkleurige aap-icoontjes
+
+ Toetenbord in de apps-lijst automatisch openen
+ Apps
+ Tekst
+ Doneer
+ Privacybeleid
+ Contact opnemen met de oorspronkelijke ontwikkelaar
+ Wordt lid van onze Discord
+ Alle apps
+ Favorieten
+ Verstopte apps
+ Privébereik
+ App kiezen
+ Apps
+ Andere
+ Deïnstalleren
+ App info
+ Aan favorieten toevoegen
+ Uit favorieten verwijderen
+ Verstoppen
+ Zichtbaar maken
+ Hernoemen
+ Zoeken
+ Zoeken (geen snelstart)
+ µLauncher instellingen
+ Alle apps
+ Favoriete apps
+ Privéruimte
+ Privéruimte (ont)sluiten
+ Volume omhoog
+ Volume omlaag
+ Volume veranderen
+ Muziek: Volgende
+ Muziek: Vorige
+ Muziek: Afspelen / Pauzeren
+ Medling-scherm uitvouwen
+ Recente apps
+ Scherm vergrendelen
+ Zaklamp aan / uit
+ Andere launcher gebruiken
+ Concept
+ µLauncher biedt een minimalistische, efficiënte en afleidingsvrije omgeving.\n\nDe app is vrije software, het heeft geen advertenties en verzamelt geen data.
+ Versie
+ Gebruik
+ Het startscherm toont allen de datum en tijd. Geen afleiding.
+ Vaak gebruikte apps kunnen via gebaren of de volumeknoppen geopend worden.
+ Alle apps
+ In de applijst kan snel en eenvoudig naar apps gezocht worden.\n\nVeeg omhoog om de lijst te openen, of verander het gebaar in de settings.
+ Als maar één app bij het zoekbegrip past, wordt deze automatisch geopend.\nDoor een spatie voor het zoekbegrip intevoegen wordt dit onderdrukt.
+ Instellen
+ Wij hebben een paar standaard-apps voor u gekozen, u mag deze gerust veranderen:
+ De selectie kan op elk moment in de instellingen aangepast worden.
+ Starten!
+ Het kan beginnen!\n\nWij hopen dat deze app u helpt!\n\n- Finn (de ontwikelaar) en Josia (de ontwikkelaar van de fork µLauncher)
+ Starten
+ Instellingen
+ Meer opties
+ Foutmelding: Deze actie gebruikt een functie dat niet onderdeel is van de Android API, uw apparaat ondersteunt deze functie niet.
+ Deze functie is pas vanaf Android 6 beschikbaar.
+ Deze functie is pas vanaf Android 15 beschikbaar.
+ De app werd verstopt, het kan in de settings weer zichtbaar gemaakt worden.
+ Ongedaan maken
+ Snelle instellingen
+ Foutmelding: Kon geen recente apps tonen (probeer de toegankelijkheidsdienst in de apparaatinstellingen uit en weer aan te zetten)
+ µLauncher moet apparaatbeheerder zijn om het scherm te mogen vergrendelen.
+ Dit is nodig om het scherm te kunnen vergrendelen.
+ De actie \"scherm vergrendelen\" activeren
+ De toegankelijkheidsdienst voor µLauncher staat niet aan. Schakel het aan in de instellingen
+ Foutmelding: Kon het scherm niet vergrendelen (probeer de toegankelijkheidsdienst in de apparaatinstellingen uit en weer aan te zetten)
+ Privégedeelte vergrendelt
+ Privégedeelte ontgrendelt
+ Privégedeelte is niet bereikbaar
+ µLauncher moet als standaard-launcher ingesteld zijn om tot de privégegedeelte toegeang te krijgen.
+ Privégedeelte vergrendelen
+ Privégedeelte ontgrendelen
+ Foutmelding: Op dit apparaat kan het scherm via de toegankelijkheidsdienst niet vergrendelt worden. Maak µLauncher apparaatbeheerder hiervoor.
+ µLauncher
+ Methode voor vergrendeling kiezen
+ Gebruik toegankelijkheidsdienst
+ Gebruik apparaatadministratie
+ Kies een methode om het scherm te vergrendelen
+ %1$s hernoemen
+ Rood
+ Doorzichtigheid
+ Blauw
+ Groen
+ Kleur
+ Kleur kiezen
+ Ik ben me ervan bewust dat er andere opties bestaan (met beheerdersrechten of de aan/uit-knop).
+ Ik geef μLauncher toestemming om de toegankelijkheidservice te gebruiken om functionaliteit te bieden die niet gerelateerd is aan toegankelijkheid.
+ Ik ben me ervan bewust ik hierdoor uitgebreide privileges verleen aan μLauncher.
+ Ik geef μLauncher toestemming om geen gegevens te verzamelen.
+ Toegankelijkheidsservice activeren
+ Toegankelijkheidsservice activeren
+ Open Source licenties
+ Open Source licenties
+ Geen app gevonden om de zoekopdracht uit te voeren.
+ Kies widget
+ Verwijder
+ Configureren
+ Interactie inschakelen
+ Interactie uitschakelen
+ Klok
+ De standaard klok van µLauncher
+ Verwijder
+ Hernoem
+ Widget Paneel #%1$d
+
+ - Beinhoudt %d widget.
+ - Beinhoudt %d widgets.
+
+ Oké
+ Widget paneel
+ Selecteer een widget paneel
+ Maak een nieuwe widget paneel
+ Launcher > Beheer widget paneelen.]]>
+ Open widget paneel
+ Deze widget paneel bestaat niet meer.
+ Widgets
+ App kan niet geopend worden
+ App-instellingen aanpassen?
+ Instellingen
+ Acties
+ Launcher
+ Meta
+ Terug
+ Naar boven
+ Naar boven vegen
+ Tikken en dan naar boven vegen
+ Dubbel tikken en naar boven vegen
+ Naar beneden vegen
+ Naar beneden
+ Tikken en naar beneden
+ Tikken en naar beneden vegen
+ Tikken en boven
+ Dubbel naar beneden
+ Naar links
+ Naar links vegen
+ Tikken en links
+ Tikken en dan naar links vegen
+ Dubbel links
+ Met twee vingers naar links vegen
+ Naar rechts
+ Naar rechts vegen
+ Tikken en rechts
+ Dubbel rechts
+ Met twee vingers naar beneden vegen
+ Rechts (boven)
+ Bovenin naar rechts vegen
+ Rechts (beneden)
+ Onderin naar rechts vegen
+ Links (onder)
+ Links (boven)
+ Bovenin naar liniks vegen
+ Omhoog (links)
+ Links naar boven vegen
+ Omhoog (rechts)
+ Rechts naar boven vegen
+ Omlaag (links)
+ Links naar beneden vegen
+ Omlaag (rechts)
+ ]]>
+ Boven links -> midden rechts -> onder links
+ (Achteruit)]]>
+ Onder links -> midden rechts -> boven links
+
+ Volume omhoog knop
+ Druk de \"volume omhoog\" knop
+ Volume omlaag knop
+ Druk de \"volume omlaag\" knop
+ Dubbelklik
+ Dubbeltik op een lege plek
+ Lange tik
+ Op een lege plek lang tikken
+ Datum
+ Op de datum tikken
+ Klok
+ Op de tijd tikken
+ Boven rechts -> midden links -> onder rechts
+
+ Onder rechts -> midden links -> boven rechts
+ V
+ Boven links -> onder midden -> boven rechts
+ V (Achteruit)
+ Boven rechts -> onder rechts -> boven links
+ Λ
+ Onder links -> boven midden -> onder rechts
+ Λ (Achteruit)
+ Onder rechts -> boven midden -> boven links
+ Toon de tijd
+ Toon de datum
+ Toon sekondes
+ Spiegel datum en tijd
+ Achtergrond kiezen
+ Beeldscherm
+ Beeldscherm aanhouden
+ Statusbalk verbergen
+ Navigatiebalk verbergen
+ Scherm draaien
+ Functionaliteit
+ Dubbele veeg gebaaren
+ Met twee vingers vegen
+ Hoek-gebaren
+ Kantbreedte
+ Start zoekresultaten
+ Spatie drukken om deze functie tijdelijk te onderdruken.
+ Op het internet zoeken
+ Beveiligingsprobleem melden
+ MIT licentie\nDe broncode is op GitHub te vinden.
+ Toetsenbord sluiten bij scrollen
+ Gevoeligheid
+ Verstopte apps
+ Toon apps die aan een gebaar gekoppeld zijn niet in de apps-lijst
+ Gepauzeerde apps verstoppen
+ Toon de privéruimte niet in de apps-lijst
+ Opmaat van de apps-lijst
+ Apps-lijst omkeren
+ Standaard
+ Niets doen
+ URL kan niet geopend worden: geen browser gevonden.
+
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000..fb3c4d4
--- /dev/null
+++ b/app/src/main/res/values-pl/strings.xml
@@ -0,0 +1,281 @@
+
+
+ W prawo (Dół)
+ Przeciągnij w dół, po lewej krawędzi ekranu
+
+ Otwórz ustawienia by wybrać akcje dla tego gestu
+ Ustawienia
+ Akcja
+ Przycisk cofnięcia / akcja cofnięcia
+ W górę
+ Przeciągnij w górę
+ Kliknij + W górę
+ Kliknij i przeciągnij w górę
+ Dwoma w górę
+ Przeciągnij dwoma palcami w górę
+ W dół
+ Przeciągnij w dół
+ Kliknij + W dół
+ Dwoma w dół
+ Przeciągnij dwoma palcami w dół
+ W lewo
+ Przeciągnij w lewo
+ Kliknij + W lewo
+ Kliknij i przeciągnij w lewo
+ Dwoma w lewo
+ Przeciągnij dwoma palcami w lewo
+ W prawo
+ Kliknij + W prawo
+ Kliknij i przeciągnij w prawo
+ Dwoma w prawo
+ Przeciągnij dwoma palcami w prawo
+ W prawo (Góra)
+ Przeciągnij w prawo, w dolnej części ekranu
+ W lewo (Dół)
+ Przeciągnij w lewo, w dolnej części ekranu
+ W lewo (Góra)
+ W górę (Lewa krawędź)
+ Przeciągnij w górę, po lewej krawędzi ekranu
+ W górę (Prawa krawędź)
+ Przeciągnij w górę, po prawej krawędzi ekranu
+ W dół (Lewa krawędź)
+ W dół (Prawa krawędź)
+ Przeciągnij w dół, po prawej krawędzi ekranu
+ Górny lewy -> środek prawo -> dolny lewy
+ (Odwrotnie)]]>
+ ]]>
+ V
+ Λ
+ Nie można otworzyć aplikacji
+ Cofnij
+ Chcesz zmienić to ustawienie?
+ Przeciągnij w prawo
+ Dolny lewy -> środek prawo -> górny lewy
+ Kliknij i przeciągnij w dół
+ Przeciągnij w prawo, w górnej części ekranu
+ Przeciągnij w lewo, w górnej części ekranu
+ Zgłoś lukę w zabezpieczeniach
+ Wszystkie aplikacje
+ Użytek
+ Przestrzeń prywatna jest niedostępna
+ Zamień date i zegar
+ Nie pokazuje aplikacji przypisanych do gestów w liście aplikacji
+ Dziękujemy za pomoc w udoskonaleniu μLauncher!\nProszę przemyśl dodanie podanych informacji do twojego zgłoszenia błędu:
+ Dołącz do nas na Discordzie!
+ Muzyka: Poprzednie
+ Wygląd
+ Domyślna systemowa
+ Serif
+ Akcje podwójnego przeciągnięcia
+ Launcher
+ Meta
+ Prawy górny -> środek lewo -> prawy dolnym
+
+ Prawy dolnym -> środek lewo -> prawy górny
+ V (Odwrotnie)
+ Lewy dolny -> środkowy górny -> prawy dolny
+ Λ (Odwrotnie)
+ Przycisk podgłośnienia
+ Naciśnij przycisk podgłośnienia
+ Przycisk przyciszenia
+ Naciśnij przycisk przyciszenia
+ Prawy górny -> środkowy górny -> lewy górny
+ Dwuklik
+ Naciśnij dwukrotnie w pustym miejscu
+ Wydłużone kliknięcie
+ Data
+ Kliknij na date
+ Zegar
+ Zarządzaj widżetami
+ Zarządzaj panelem widżetów
+ Wybierz aplikacje
+ Zainstaluj aplikacje
+ Kliknij na zegar
+ Sklep nie został znaleziony
+ Kolor motywu
+ Domyślne
+ Ciemny
+ Jasny
+ Dynamiczny
+ Cień tekstu
+ Tło (lista aplikacji i ustawienia)
+ Przeźroczysty
+ Przygaszony
+ Rozmazanie
+ Jednolity
+ Czcionką
+ Monospace
+ Sans serif
+ Serif monospace
+ Monochromatyczne ikony aplikacji
+
+ Kolor
+ Pokazuj date
+ Pokazuj sekundy
+ Wybierz tapetę
+ Wyświetlanie
+ Zatrzymaj na ekranie
+ Schowaj pasek statusów
+ Schowaj pasek nawigacji
+ Obracają ekran
+ Funkcjonalność
+ Przeciągnij dwoma palcami
+ Przeciąganie na krawędziach
+ Przeciągnij koło krawędź ekranu
+ Rozmiary krawędź
+ Automatycznie uruchamiaj wyszukiwanie
+ Wyszukuje w sieci
+ Kliknij enter, wyszukując w liście aplikacji, by wyszukać w internecie.
+ Pokazuje klawiaturę do wyszukiwania
+ Zamknij klawiaturę podczas przewijania
+ Czułość
+ Aplikacje
+ Ukryte aplikacje
+ Ukryj zatrzymane aplikacje
+ Ukryj przestrzeń prywatną z listy aplikacji
+ Układ listy aplikacji
+ Odwróć listę aplikacji
+ Domyślny
+ Tekst
+ Siatka
+ Informacje aplikacji
+ Zobacz samouczek µLauncher
+ Zresetuj ustawienia
+ Odrzucisz wszystkie swoje preferencje. Czy chcesz kontynuować?
+ Zobacz kod źródłowy
+ Zgłoś błąd
+ Zgłoś błąd
+ Skopiuj do schowka
+ Proszę nie zgłaszaj luk w zabezpieczeniach publicznie na GitHubie, użyj natomiast podanego linku:
+ Utwórz zgłoszenie
+ Skontaktuj się z deweloperem forku
+ Dołącz do czatu μLauncher
+ Wesprzyj
+ Polityka prywatność
+ Skontaktuj się z deweloperem orginału
+ Lista aplikacji
+ Ulubione aplikacje
+ Ukryte aplikacje
+ Przestrzeń prywatna
+ Wybierz aplikacje
+ Aplikacje
+ Inne
+ Odinstaluj
+ Informacje aplikacji
+ Usuń z ulubionych
+ Ukryj
+ Pokaż
+ Zmień nazwę
+ Wyszukaj
+ Wyszukaj (brak automatycznego uruchomienia)
+ Ustawienia μLauncher
+ Ulubione aplikacje
+ Przestrzeń prywatna
+ Przełącz zamek prywatnej przestrzeni
+ Podgłośnij
+ Przycisz
+ Dostosuj głośność
+ Muzyka: Następne
+ Muzyka: Wznów / Wstrzymaj
+ Rozwiń panel powiadomień
+ Ostatnio używane aplikacje
+ Nie rób nic
+ Ekran blokady
+ Przełącz latarkę
+ Uruchomiono inny ekran główny
+ Dodaj skrót
+ Przypisz do gestu
+ Pokaż w liście aplikacji
+ Samouczek
+ 👋\n\nPoświeć kilka chwil, aby nauczyć się jak używać togo launchera!
+ Koncept
+ Wersja
+ Wszystkie aplikacje
+ Konfiguracja
+ Gratulacje!
+ Start
+ Ustawienia
+ Więcej opcji
+ Cofnij
+ Przestrzeń prywatna odblokowana
+ Zablokuj przestrzeń prywatną
+ Odblokuj przestrzeń prywatną
+ μLauncher
+ Wybierz metodę zablokowania
+ Wybierz metodę blokowania ekranu
+ Zmień nazwę %1$s
+ Czerwony
+ Przezroczystości
+ Niebieski
+ Zielony
+ Wybierz kolor
+ Anuluj
+ Nie znaleziono aplikacji do obsługi wyszukiwania.
+ Pokazuj zegar
+ Użyj lokalnego formatu daty
+ Kliknij dłużej w pustym miejscu
+ Lewy górny -> środkowy dolny -> prawy górny
+ Prawy górny -> środkowy dolny -> lewy górny
+ Kliknij spacje, by tymczasowo wyłączyć tą funkcje.
+ Szybkie ustawienia
+ Przestrzeń prywatna zablokowana
+ Ustaw μLauncher jako ekran główny
+ Kolor
+ Dodaj do ulubionych
+ Jest to darmowe oprogramowanie (na licencji MIT)!\nZachęcamy do odwiedzenia naszego repozytorium!
+ μLauncher jest projektowany z uwagą na minimalizm, efektywności i brak rozproszeń. \n\nNie zawiera reklam i nie zbiera twoich danych.
+ Twój ekran główny zawiera lokalną datę i czas. Żadnych rozproszeń.
+ Możesz uruchomić swoje najważniejsze aplikacje za pomocą gestów i użyciem przycisków.
+ Możesz szybko wyszukiwać aplikacje poprzez listę aplikacji.\n\nPrzeciągnij do góry aby ją otworzyć, lub przypisz ją do innego gestu.
+ Gdy tylko jedna aplikacja pasuje do twojego wyszukiwania, uruchomi się automatycznie.\nTa opcja może być wyłączona, przez wpisanie spacji przed wyszukaniem.
+ Wybraliśmy pare domyślnych aplikacji dla ciebie. Możesz zmienić je teraz jeśli chcesz:
+ Albo zmień je później.
+ Ta funkcja wymaga wersji 6 lub wyższej Androida.
+ Ta funkcja wymaga wersji 15 lub wyższej Androida.
+ Aplikacja schowana. Możesz przywrócić jej widoczność z powrotem w ustawieniach.
+ Error: Brak dostępu do latarki.
+ Aktywuj akcje zablokowania ekranu
+ Wyrażam zgodę by, μLauncher nie zbierał żadnych moich danych.
+ Nie można otworzyć adresu URL: nie znaleziono przeglądarki.
+ Wybierz widżet
+ Usuń
+ Skonfiguruj
+ Zegar
+ Aktywuje interakcje
+ Dezaktywuj interakcje
+ Usuń
+ Domyślny zegar μLauncher
+ Zmień nazwę
+ Panel widżetów #%1$d
+
+ - Zawiera %1$d widżet.
+ - Zawiera %1$d widżety.
+ - Zawiera %1$d widżetów.
+ - Zawiera %1$d widżetów.
+
+ Licencje Open Source
+ Licencje Open Source
+ Okej
+ Panel widżetów
+ Wybierz panel widżetów
+ Stwórz nowy panel widżetów
+ Otwórz panel widżetów
+ Widżety
+ Ten panel widżetów już nie istnieje.
+ Nie wykryto aparatu z latarką.
+ μLauncher musi być administratorem urządzenia, aby zablokować ekran.
+ Błąd: Nie można rozszerzyć paska stanu. Ta akcja wykorzystuje funkcjonalność, która nie jest częścią opublikowanego interfejsu API Androida. Niestety, wydaje się, że nie działa na Twoim urządzeniu.
+ Jest to wymagane do działania blokady ekranu.
+ Błąd: Nie udało się wyświetlić ostatnich aplikacji. (Jeśli właśnie zaktualizowałeś aplikację, spróbuj wyłączyć i ponownie włączyć usługę dostępności w ustawieniach telefonu)
+ Błąd: Nie udało się włączyć usługi ułatwień dostępu.
+ μLauncher musi być domyślnym ekranem głównym, aby uzyskać dostęp do przestrzeni prywatnej.
+ Błąd: Blokowanie ekranu za pomocą funkcji ułatwień dostępu nie jest obsługiwane na tym urządzeniu. Zamiast tego użyj opcji administratora urządzenia.
+ Skorzystaj z usługi dostępności
+ Użyj opcji administratora urządzenia
+ Zdaję sobie sprawę, że nada to μLauncherowi rozległe uprawnienia.
+ Wiem, że istnieją inne opcje (użycie uprawnień administratora urządzenia lub przycisku zasilania).
+ Błąd: Nie udało się zablokować ekranu. (Jeśli właśnie zaktualizowałeś aplikację, spróbuj wyłączyć i ponownie włączyć usługę dostępności w ustawieniach telefonu)
+ Usługa dostępności μLauncher nie jest włączona. Proszę włączyć ją w ustawieniach
+ Wszystko gotowe!\n\nMam nadzieję, że będzie to dla ciebie bardzo wartościowe!\n\n- Finn (twórca Launchera) i Josia (wprowadziła pewne ulepszenia i utrzymuje fork μLauncher)
+ Ustawienie μLauncher jako usługi ułatwień dostępu umożliwia zablokowanie ekranu i otwarcie menu ostatnich aplikacji. Należy pamiętać, że wymagane są nadmierne uprawnienia. Nigdy nie należy udzielać takich uprawnień bez powodu żadnej aplikacji. μLauncher będzie korzystał z usługi ułatwień dostępu wyłącznie w celu wykonania następujących czynności na żądanie użytkownika: * zablokuj ekran * otwórz ostatnio używane aplikacje μLauncher nigdy nie będzie korzystał z usługi ułatwień dostępu w celu zbierania danych. Możesz sprawdzić kod źródłowy, aby się upewnić. Należy pamiętać, że zablokowanie ekranu można również osiągnąć poprzez przyznanie aplikacji uprawnień administratora urządzenia. Jednak ta metoda nie działa w przypadku odblokowywania przy użyciu odcisku palca i skanu twarzy.
+
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index fcdd9e8..0d42f2e 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -102,7 +102,7 @@
Diminuir volume
Música: Próximo
Música: Anterior
- Não faça nada
+ Não fazer nada
- 16dp
- 16dp
16dp
- 16dp
- 8dp
-
40dip
- 48dip
diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml
index 89ec086..9f15b22 100644
--- a/app/src/main/res/values/donottranslate.xml
+++ b/app/src/main/res/values/donottranslate.xml
@@ -9,6 +9,8 @@
internal.started_before
internal.first_startup
internal.version_code
+ widgets.widgets
+ widgets.custom_panels
apps.favorites
apps.hidden
apps.pinned_shortcuts
@@ -147,6 +149,7 @@
functionality.search_auto_launch
functionality.search_web
functionality.search_auto_keyboard
+ functionality.search_auto_close_keyboard
settings_action_lock_method
@@ -158,7 +161,9 @@
-
-->
https://github.com/jrpie/Launcher
+ https://launcher.jrpie.de/
https://github.com/jrpie/Launcher/issues/new?template=bug_report.yaml
+ android-launcher-crash@jrpie.de
https://github.com/jrpie/Launcher/security/policy
https://s.jrpie.de/contact
https://s.jrpie.de/android-legal
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a8bc9b3..6fb9d61 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -98,6 +98,9 @@
Time
Click on time
+ Manage widgets
+ Manage widget panels
+
Choose App
@@ -156,9 +159,9 @@
Functionality
- Double swipe actions
+ Double swipe gestures
Swipe with two fingers
- Edge swipe actions
+ Edge swipe gestures
Swipe at the edge of the screen
Edge width
Launch search results
@@ -167,6 +170,7 @@
Search the web
Press return while searching the app list to launch a web search.
Start keyboard for search
+ Close keyboard when scrolling
Sensitivity
@@ -251,12 +255,12 @@
Music: Next
Music: Previous
Music: Play / Pause
- Expand notifications panel
+ Expand Notifications Panel
Recent Apps
- Do nothing
+ Do Nothing
Lock Screen
Toggle Torch
- Launch other Home Screen
+ Launch Other Home Screen
Add Shortcut
@@ -387,5 +391,69 @@
Open Source Licenses
No app found to handle search.
Can\'t open URL: no browser found.
+ Choose Widget
+ Remove
+ Configure
+ Enable Interaction
+ Disable Interaction
+
+
+ Clock
+ The default clock of μLauncher
+ Delete
+ Rename
+
+ Widget Panel #%1$d
+
+ - Contains %1$d widget.
+ - Contains %1$d widgets.
+
+
+
+ Ok
+ Widget Panels
+ Select a Widget Panel
+ Create new widget panel
+ Launcher > Manage Widget Panels.]]>
+ Open Widget Panel
+ This widget panel no longer exists.
+ Widgets
+ μLauncher crashed
+ Sorry! Click for more information.
+
+ For privacy reasons, crash logs are not collected automatically.
+ However logs are very useful for debugging, so I would be very grateful if you could send me the attached log by mail
+ or create a bug report on github.
+ Note that crash logs might contain sensitive information, e.g. the name of an app you tried to launch.
+ Please redact such information before sending the report.
+ What can I do now?
+ If this bug appears again and again, you can try several things:
+
+ - Force stop μLauncher
+ - Clear μLauncher\'s storage (Your settings will be lost!)
+ - Install an older version (GitHub, F-Droid)
+
+ ]]>
+
+ Copy crash report to clipboard
+ Send report by mail
+ Create bug report on GitHub
+ μLauncher crashed
+ Send Email
+ Crashes and Debug Information
+ Documentation
+
+ Add widget
+ Add widget panel
+ Close
+ Navigate back
+ Navigate next
+ Lock
+ Remove binding
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 20ccb67..1defe2f 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -14,6 +14,7 @@
- @style/Widget.AppCompat.Button.Colored
- ?colorAccent
+ - @color/cardview_dark_background
@@ -32,6 +33,7 @@
- @color/darkTheme_background_color
- @color/darkTheme_accent_color
- @color/darkTheme_background_color
+ - @color/cardview_dark_background
- @color/darkTheme_text_color
@@ -40,6 +42,7 @@
- @color/finnmglasTheme_background_color
- @color/finnmglasTheme_accent_color
- @color/finnmglasTheme_background_color
+ - @color/cardview_dark_background
- @color/finnmglasTheme_text_color
@@ -48,6 +51,7 @@
- @color/lightTheme_background_color
- @color/lightTheme_accent_color
- @color/lightTheme_background_color
+ - @color/cardview_light_background
- @color/lightTheme_text_color
@@ -57,6 +61,7 @@
- @color/material_dynamic_primary50
- @color/material_dynamic_tertiary50
- @color/material_dynamic_neutral10
+ - @color/cardview_dark_background
- @color/material_dynamic_neutral_variant90
@@ -66,12 +71,12 @@
- 0
- 2
+
-
-
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 6ef5d07..0ee7c17 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -3,10 +3,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
+
-
+
+
+
+
+
ACTIONS.
+
+
+## Available Gestures
+
+### Swipes
+
+- Basic swipes: Swipe up, down, left, or right
+- Double swipes: Swipe up, down, left, or right with two fingers
+- Edge swipes:
+ - Swipe up or down on the left or right edge
+ - Swipe left or right on the top or bottom edge
+
+ The size of the edges is configurable in settings.
+ For a swipe to be detected as an edge swipe, the finger must not leave the respective edge region while swiping.
+
+### Taps
+
+- Tap on date or time
+- Double tap
+- Long click
+
+### Tap-then-Swipes
+
+- Tap then swipe up, down, left, or right
+
+ To execute these gestures consistently, it is helpful to think of them as double taps,
+ where the finger stays on the screen after the second tap and then does a swipe.
+ The swipe must start very shortly after the tap ended.
+
+### Complex Gestures
+
+- Draw <, >, V, or Λ
+- Draw <, >, V, or Λ in reverse direction
+
+### Hardware Buttons as Gestures
+
+- Back button (or back gesture if gesture navigation is enabled)
+- Volume buttons
+
+***
+
+## Available Actions
+
+To any of the available gestures, one of the following actions can be bound:
+
+- Launch an app (or a pinned shortcut)
+- Open a widget panel.
+ Widget panels can hold widgets that are not needed on the home screen itself.
+ They can be created and managed in µLauncher Settings > Manage Widget Panels
+- Open a list of all, favorite, or private apps (hidden apps are excluded).
+ Actions related to private space are only shown if private space is set up on the device.
+ µLauncher's settings can be accessed from those lists.
+ If private space is set up, an icon to (un)lock it is shown on the top right.
+- Open µLauncher's settings
+- Toggle private space lock
+- Lock the screen: This allows you to lock the screen.
+ There are two mechanisms by which the screen can be locked: accessibility service and device admin.
+- Toggle the flashlight
+- Raise, lower, or adjust volume
+- Play or pause media playback
+- Skip to previous or next audio track
+- Open notifications panel: Might be useful if the top of your screen is broken.
+- Open quick settings panel: Why swipe down twice?
+- Open [recent apps](https://developer.android.com/guide/components/activities/recents): Requires accessibility service. It can be used as a workaround for an Android bug.
+- Launch another home screen: Allows using another installed home screen temporarily.
+- Do nothing: Simply prevents showing the message that no action is bound to this gesture.
diff --git a/docs/alternatives.md b/docs/alternatives.md
new file mode 100644
index 0000000..7e8d719
--- /dev/null
+++ b/docs/alternatives.md
@@ -0,0 +1,452 @@
++++
+ weight = 100
++++
+
+# FOSS Launchers
+
+This is a comparison of open-source home screens for Android.
+
+**Inclusion criteria:** Apps in this list must be [open source](https://opensource.org/licenses) and maintained
+
+
+
+
+
+## Grid-Based Launchers
+
+### Discreet Launcher
+
+**License:** `GPL-3.0`
+[Website](https://vincent-falzon.com/) | [Repository](https://github.com/falzonv/discreet-launcher) | [F-Droid](https://f-droid.org/en/packages/com.vincent_falzon.discreetlauncher/)
+
+**Main mode of interaction:** `app grid`
+
+#### Features:
+
+:white_check_mark: Search: `apps`
+:x: Search history
+:x: Customizable gestures
+:white_check_mark:Folders
+:x: Tags
+:white_check_mark: Rename apps
+:x: Widgets
+:grey_question: Private space
+:white_check_mark: Work profile
+:white_check_mark: Pinned shortcuts
+:white_check_mark: Icon packs
+:x: Material You
+
+---
+
+### Fossify
+
+**License:** `GPL-3.0`
+[Website](https://www.fossify.org/) | [Repository](https://github.com/FossifyOrg/Launcher) | [F-Droid](https://f-droid.org/en/packages/org.fossify.home/)
+
+**Main mode of interaction:** `app grid`
+
+#### Features:
+
+:white_check_mark: Search: `apps`
+:x: Search history
+:x: Customizable gestures
+:white_check_mark: Folders
+:x: Tags
+:white_check_mark: Rename apps
+:white_check_mark: Widgets
+:grey_question: Private space
+:x: Work profile
+:white_check_mark: Pinned shortcuts
+:x: Icon packs
+:white_check_mark: Material You
+
+---
+
+### Lawnchair
+
+**License:** `Apache License 2.0`
+[Website](https://lawnchair.app/) | [Repository](https://github.com/LawnchairLauncher/lawnchair)
+
+Seems to be a regular (grid of apps) launcher.
+
+**Main mode of interaction:** App grid
+
+#### Features:
+
+:white_check_mark: Search: `Apps & Shortcuts` `Web suggestions` `People` `Files` `Android Settings` `Calculator`
+:white_check_mark: Search history
+:white_check_mark: Customizable gestures: `double tap` `swipe up` `swipe down` `home button` `back button`
+:white_check_mark: Folders
+:x: Tags
+:white_check_mark: Rename apps
+:white_check_mark: Widgets
+:grey_question: Private space
+:grey_question: Work profile
+:x: Pinned shortcuts
+:white_check_mark: Icon packs
+:white_check_mark: Material You
+
+---
+
+### Rootless Pixel Launcher
+
+> **Abandoned**
+
+**License:** `Apache License 2.0`
+[Repository](https://github.com/amirzaidi/Launcher3)
+
+**Main mode of interaction:** `App grid`
+
+#### Features:
+
+:white_check_mark: Search: `Apps`
+:x: Search history
+:x: Customizable gestures
+:x: Folders
+:x: Tags
+:x: Rename apps
+:warning: Widgets `buggy/broken`
+:grey_question: Private space
+:white_check_mark: Work profile
+:white_check_mark: Pinned shortcuts
+:white_check_mark: Icon packs
+:x: Material You
+
+---
+
+## Search-Based
+
+### Aster Launcher
+
+> **Abandoned**
+>
+**License:** `GPL-3.0`
+[Repository](https://github.com/neophtex/AsterLauncher) | [F-Droid](https://f-droid.org/en/packages/com.series.aster.launcher/)
+
+**Main mode of interaction:** `search`
+
+#### Features:
+
+:warning: Search: `apps` (apps list is buggy/broken) `web`
+:x: Search history
+:x: Customizable gestures
+:x: Folders
+:x: Tags
+:x: Rename apps
+:x: Widgets
+:grey_question: Private space
+:white_check_mark: Work profile
+:white_check_mark: Pinned shortcuts
+:x: Icon packs
+:x: Material You
+
+---
+
+### KISS Launcher
+
+**License:** `GPL-3.0`
+[Website](https://kisslauncher.com/) | [Repository](https://github.com/Neamar/KISS) | [F-Droid](https://f-droid.org/packages/fr.neamar.kiss/)
+
+**Main mode of interaction:** `Search` `Some gestures available`
+
+#### Features:
+
+:white_check_mark: Search: `Apps` `Contacts` `Call history`
+:white_check_mark: Search history
+:white_check_mark: Customizable gestures: `swipe left` `swipe right` `swipe up` `swipe down` `long press` `double tap`
+:x: Folders
+:white_check_mark: Tags
+:white_check_mark: Rename apps
+:white_check_mark: Widgets
+:grey_question: Private space
+:white_check_mark: Work profile
+:white_check_mark: Pinned shortcuts
+:white_check_mark: Icon packs
+:grey_question: Material You
+
+---
+
+### Lunar Launcher
+**License:** `GPL-3.0`
+[Repository](https://github.com/iamrasel/lunar-launcher) | [F-Droid](https://f-droid.org/en/packages/rasel.lunar.launcher/)
+
+Even natively supports RSS feeds to the homescreen?
+
+**Main mode of interaction:** `alphabet scroller`
+
+#### Features:
+
+:white_check_mark: Search: `swipe up` `swipe down` `swipe left` `swipe right` `tap and hold battery indicator/clock` `tap and hold lower part of screen` `double tap` `tap and hold favorite item`
+:x: Search history
+:x: Customizable gestures
+:x: Folders
+:x: Tags
+:x: Rename apps
+:x: Widgets
+:grey_question: Private space
+:x: Work profile
+:white_check_mark: Pinned shortcuts
+:x: Icon packs
+:x: Material You
+
+---
+
+### OLauncher
+
+**License:** `GPL-3.0`
+[Repository](https://github.com/tanujnotes/Olauncher) | [F-Droid](https://f-droid.org/en/packages/app.olauncher/)
+
+Extremely minimal launcher with lots of forks.
+
+**Main mode of interaction:** `Search`
+
+#### Features:
+
+:white_check_mark: Search: `apps`
+:x: Search history.
+:white_check_mark: Customizable gestures: `swipe left` `swipe right` `double tap`
+:x: Folders
+:x: Tags
+:white_check_mark: Rename apps
+:x: Widgets
+:grey_question: Private space
+:white_check_mark: Work profile
+:grey_question: Pinned shortcuts
+:x: Icon packs
+:grey_question: Material You
+
+#### Forks:
+
+* [Olauncher Clutter Free](https://f-droid.org/en/packages/app.olaunchercf/)
+* [mLauncher](https://f-droid.org/en/packages/app.mlauncher/)
+* [CLauncher](https://f-droid.org/en/packages/app.clauncher/) (even more minimalistic, search without feedback)
+ * [CCLauncher](https://f-droid.org/en/packages/app.cclauncher/) (rewrite using compose)
+
+---
+
+### TinyBit Launcher
+
+**License:** `GPL-3.0`
+[Repository](https://github.com/TBog/TBLauncher) | [F-Droid](https://f-droid.org/en/packages/rocks.tbog.tblauncher/)
+
+**Main mode of interaction:** `search` `some gestures`
+
+#### Features:
+
+:white_check_mark: Search: `apps` `contacts` `web` `maps` `playstore` `youtube`
+:white_check_mark: Search history
+:white_check_mark: Customizable gestures: `tap` `double tap` `swipe up` `swipe left` `swipe right` `swipe down on left` `swipe down on right`
+:x: Folders
+:white_check_mark: Tags
+:white_check_mark: Rename apps
+:white_check_mark: Widgets
+:grey_question: Private space
+:white_check_mark: Work profile
+:white_check_mark: Pinned shortcuts
+:white_check_mark: Icon packs
+:x: Material You
+
+---
+
+### YAM Launcher
+
+**License:** `MIT`
+[Repository](https://codeberg.org/ottoptj/yamlauncher) | [F-Droid](https://f-droid.org/en/packages/eu.ottop.yamlauncher/)
+
+Similar to OLauncher?
+
+**Main mode of interaction:** `search` `home screen text buttons`
+
+#### Features:
+
+:white_check_mark: Search: `apps` `contacts (optional)`
+:x: Search history
+:white_check_mark: Customizable gestures: `swipe left` `swipe right`
+:x: Folders
+:x: Tags
+:white_check_mark: Rename apps
+:x: Widgets
+:grey_question: Private space
+:white_check_mark: Work profile
+:white_check_mark: Pinned shortcuts
+:x: Icon packs
+:white_check_mark: Material You
+
+---
+
+## Directory-Based
+
+### folder launcher
+
+**License:** `MIT`
+[Repository](https://github.com/Robby-Blue/SimpleFolderLauncher) | [F-Droid](https://f-droid.org/en/packages/me.robbyblue.mylauncher/)
+
+**Main mode of interaction:** `directory navigation`
+
+#### Features:
+
+:white_check_mark: Search: `apps`
+:x: Search history
+:x: Customizable gestures
+:white_check_mark: Folders
+:x: Tags
+:white_check_mark: Rename apps
+:white_check_mark: Widgets
+:grey_question: Private space
+:x: Work profile
+:white_check_mark: Pinned shortcuts
+:x: Icon packs
+:x: Material You
+
+---
+
+### Ion Launcher
+
+**License:** `GPL-3.0`
+[Repository](https://codeberg.org/zagura/ion-launcher) | [F-Droid](https://f-droid.org/en/packages/one.zagura.IonLauncher/)
+
+**Main mode of interaction:** `App grid` `Search`
+
+#### Features:
+
+:warning: Search: `apps` `contacts: buggy/broken`
+:white_check_mark: Search history: `shows recently launched apps`
+:x: Customizable gestures
+:warning: Folders `prebuilt` `not customizable`
+:x: Tags
+:white_check_mark: Rename apps
+:x: Widgets
+:grey_question: Private space
+:white_check_mark: Work profile
+:white_check_mark: Pinned shortcuts
+:white_check_mark: Icon packs
+:x: Material You
+
+---
+
+## Gesture-Based
+
+### Pie Launcher
+
+**License:** `MIT`
+[Repository](https://github.com/markusfisch/PieLauncher)
+
+**Main mode of interaction:** `Selection wheel`
+
+#### Features:
+
+:white_check_mark: Search: `apps`
+:x: Search history
+:x: Customizable gestures
+:x: Folders
+:x: Tags
+:x: Rename apps
+:x: Widgets
+:grey_question: Private space
+:white_check_mark: Work profile
+:white_check_mark: Pinned shortcuts
+:x: Icon packs
+:x: Material You
+
+---
+
+### µLauncher
+
+**License:** `MIT`
+[Repository](https://github.com/jrpie/launcher) | [F-Droid](https://f-droid.org/en/packages/de.jrpie.android.launcher/)
+
+**Main mode of interaction:** `Gestures` `Search`
+
+#### Features:
+
+:white_check_mark: Search: `apps`
+:x: Search history
+:white_check_mark: Customizable gestures: `35 avilable` [read the docs](https://github.com/jrpie/launcher/blob/master/docs/actions-and-gestures.md)
+:x: Folders
+:x: Tags
+:white_check_mark: Rename apps
+:white_check_mark: Widgets
+:white_check_mark: Private space
+:white_check_mark: Work profile
+:white_check_mark: Pinned shortcuts
+:x:Icon packs
+:white_check_mark: Material You
+
+---
+
+### Tabular Summary
+
+#### Legend:
+
+:white_check_mark: = Supported
+:x: = Unsupported
+:warning: = Buggy/Broken; check this launcher's notes above
+
+| Launcher | Search | Search history | Customizable gestures | Folders | Tags | Rename apps | Widgets | Private space | Work profile | Pinned shortcuts | Icon packs | Material You |
+|------------------------------------------------------|--------------------|--------------------|-----------------------|--------------------|--------------------|--------------------|--------------------|--------------------|--------------------|--------------------|--------------------|--------------------|
+| [µLauncher](#µLauncher) | :white_check_mark: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
+| [Fossify](#Fossify) | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :grey_question: | :x: | :white_check_mark: | :x: | :white_check_mark: |
+| [Lawnchair](#Lawnchair) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :grey_question: | :grey_question: | :x: | :white_check_mark: | :white_check_mark: |
+| [Rootless Pixel Launcher](#Rootless-Pixel-Launcher) | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :warning: | :grey_question: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
+| [KISS Launcher](#KISS-Launcher) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :grey_question: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :grey_question: |
+| [Lunar Launcher](#Lunar-Launcher) | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: | :grey_question: | :x: | :white_check_mark: | :x: | :x: |
+| [OLauncher](#OLauncher) | :white_check_mark: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :grey_question: | :white_check_mark: | :grey_question: | :x: | :grey_question: |
+| [TinyBit Launcher](#TinyBit-Launcher) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :grey_question: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
+| [YAM Launcher](#YAM-Launcher) | :white_check_mark: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :grey_question: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
+| [Ion Launcher](#Ion-Launcher) | :warning: | :white_check_mark: | :x: | :warning: | :x: | :white_check_mark: | :x: | :grey_question: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
+| [Pie Launcher](#Pie-Launcher) | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: | :grey_question: | :white_check_mark: | :white_check_mark: | :x: | :x: |
+| [folder launcher](#folder-launcher) | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :grey_question: | :x: | :white_check_mark: | :x: | :x: |
+| [Discreet Launcher](#Discreet-Launcher) | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :white_check_mark: | :x: | :grey_question: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
+| [Aster Launcher](#Aster-Launcher) | :warning: | :x: | :x: | :x: | :x: | :x: | :x: | :grey_question: | :white_check_mark: | :white_check_mark: | :x: | :x: |
+
+
+
+---
+
+## Not Tested
+Feel free to test these and add
+https://f-droid.org/en/packages/app.easy.launcher/
+https://f-droid.org/en/packages/de.nodomain.tobihille.seniorlauncher/
+https://f-droid.org/en/packages/com.mrmannwood.hexlauncher/
+https://f-droid.org/en/packages/com.simplemobiletools.applauncher/
+https://f-droid.org/en/packages/peterfajdiga.fastdraw/
+https://f-droid.org/en/packages/de.mm20.launcher2.release/
+
+[Even more launchers](https://docs.arcticons.com/faq/supported-launchers) (most of them don't seem to be FOSS)
diff --git a/docs/app-drawer.md b/docs/app-drawer.md
new file mode 100644
index 0000000..e116ac6
--- /dev/null
+++ b/docs/app-drawer.md
@@ -0,0 +1,52 @@
++++
+ weight = 10
++++
+
+# App Drawer
+
+Apps that are not needed all the time are shown in the app drawer.
+There are several such drawers, but the basic concept is the same.
+Besides regular apps, app drawers also show [pinned shortcuts](https://developer.android.com/develop/ui/views/launch/shortcuts/creating-shortcuts)[^1].
+μLauncher treats apps and shortcuts in the same way.
+
+The idea of the app drawer is to search for apps using the keyboard.
+By default[^2], an app is launched automatically once it is the only app matching the query.
+This can be prevented by typing a space.
+Usually, only two or three characters are needed, which is much faster than scrolling to find an app.
+
+[^1]: A pinned shortcut is created, for example, when pinning a website to the home screen.
+[^2]: There are [several settings](/docs/settings/#functionality) available to modify the behavior.
+
+When long-pressing an app, additional options are shown:
+* Rename the app
+* Add to / remove from Favorites: Adds the app to the [Favorite Apps](#favorite-apps) list or removes it from there.
+* Hide / Show: This hides the app from all drawers (except from [Hidden Apps](#hidden-apps)) or makes it visible again if it was hidden.
+* App Info: Opens the system settings page for the app.
+* Uninstall: Tries to uninstall the app or remove the shortcut.
+
+## All Apps
+
+This lists all apps except hidden apps.
+By default, it is bound to `Swipe up`.
+
+## Favorite Apps
+
+Only shows favorite apps.
+Pressing the star icon on the bottom right of any app drawer toggles whether only favorite apps should be shown.
+Additionally, the `Favorite Apps` action can be used to launch this drawer directly.
+By default, it is bound to `Swipe up (left edge)`.
+
+## Private Space
+
+When [private space](/docs/profiles/#private-space) is available, this drawer
+shows only apps from the private space.
+It can be opened using the `Private Space` action.
+If the private space is locked, instead of showing the list, the unlock dialog is shown.
+
+By default, apps from the private space are shown in All Apps as well; however, this is [configurable](/docs/settings/#hide-private-space-from-app-list).
+
+## Hidden Apps
+
+This list shows hidden apps.
+It is only accessible through the settings.
+The feature is intended to be used only for apps that are not needed at all but [can not be uninstalled](https://en.wikipedia.org/wiki/Software_bloat#Bloatware).
diff --git a/docs/build.md b/docs/build.md
index 75921f9..8cfbc1b 100644
--- a/docs/build.md
+++ b/docs/build.md
@@ -1,21 +1,27 @@
-# Building µLauncher
++++
+ weight = 50
++++
+
+
+# Building from Source
## Using the command line
-Install JDK 17 and the Android Sdk.
+Install JDK 17 and the Android SDK.
Make sure that `JAVA_HOME` and `ANDROID_HOME` are set correctly.
-```
+```bash
git clone https://github.com/jrpie/Launcher
cd Launcher
./gradlew assembleDefaultRelease
```
-This will create an apk file at `app/build/outputs/apk/default/release/app-default-release-unsigned.apk`.
+This will create an APK file at `app/build/outputs/apk/default/release/app-default-release-unsigned.apk`.
Note that you need to sign it:
-```
+
+```bash
apksigner sign --ks "$YOUR_KEYSTORE" \
--ks-key-alias "$YOUR_ALIAS" \
--ks-pass="pass:$YOUR_PASSWORD" \
@@ -28,13 +34,21 @@ apksigner sign --ks "$YOUR_KEYSTORE" \
app-default-release-unsigned.apk
```
-
See [this guide](https://developer.android.com/build/building-cmdline)
for further instructions.
-
## Using Android Studio
+
Install [Android Studio](https://developer.android.com/studio), import this project and build it.
See [this guide](https://developer.android.com/studio/run)
for further instructions.
+
+## CI Pipeline
+
+The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds.
+
+{{% hint warning %}}
+Note: These builds are not signed.
+They are built in debug mode and are only suitable for testing.
+{{% /hint %}}
diff --git a/docs/launcher.md b/docs/changes-fork.md
similarity index 58%
rename from docs/launcher.md
rename to docs/changes-fork.md
index cb290a0..cfc31ec 100644
--- a/docs/launcher.md
+++ b/docs/changes-fork.md
@@ -1,50 +1,59 @@
-# Notable changes compared to [Finn's Launcher][original-repo]:
++++
+title = 'Differences to the original Launcher'
++++
-µLauncher is a fork of [finnmglas's app Launcher][original-repo].
+# Notable changes compared to Finn's Launcher
+
+µLauncher is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher).
Here is an incomplete list of changes:
-
- Additional gestures:
- - Back
- - V,Λ,<,>
- - Edge gestures: There is a setting to allow distinguishing swiping at the edges of the screen from swiping in the center.
+ - Back
+ - V, Λ, <, >
+ - Edge gestures: There is a setting to allow distinguishing swiping at the edges of the screen from swiping in the center.
+
- Compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used.
- Compatible with [private space](https://source.android.com/docs/security/features/private-space)
+- Support for [app widgets](https://developer.android.com/develop/ui/views/appwidgets/overview)
+- Support for [pinned shortcuts](https://developer.android.com/develop/ui/views/launch/shortcuts/creating-shortcuts)
- Option to rename apps
- Option to hide apps
- Favorite apps
- New actions:
- Toggle Torch
- Lock screen
+ - Open a widget panel
- The home button now works as expected.
- Improved gesture detection.
-### Visual
+## Visual
+
- This app uses the system wallpaper instead of a custom solution.
- The font has been changed to [Hack][hack-font], other fonts can be selected.
- Font Awesome Icons were replaced by Material icons.
-- The gear button on the home screen was removed. A smaller button is show at the top right when necessary.
+- The gear button on the home screen was removed. A smaller button is shown at the top right when necessary.
+## Search
-### Search
-- The search algorithm was modified to prefer matches at the beginning of the app name, i.e. when searching for `"te"`, `"termux"` is sorted before `"notes"`.
+- The search algorithm was modified to prefer matches at the beginning of the app name, i.e., when searching for `"te"`, `"termux"` is sorted before `"notes"`.
- The search bar was moved to the bottom of the screen.
-### Technical
-- Improved gesture detection.
-- Different apps set as default.
-- Package name was changed to `de.jrpie.android.launcher` to avoid clashing with the original app.
-- Dropped support for API < 21 (i.e. pre Lollypop)
-- Fixed some bugs
-- Some refactoring
+## Technical
+- Improved gesture detection.
+- Different apps are set as the defaults.
+- Package name was changed to `de.jrpie.android.launcher` to avoid clashing with the original app.
+- Dropped support for API < 21 (i.e., pre Lollypop).
+- Fixed some bugs.
+- Some refactoring.
The complete list of changes can be viewed [here](https://github.com/jrpie/launcher/compare/340ee731...master).
---
- [original-repo]: https://github.com/finnmglas/Launcher
- [hack-font]: https://sourcefoundry.org/hack/
+
+[original-repo]: https://github.com/finnmglas/Launcher
+[hack-font]: https://sourcefoundry.org/hack/
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 0000000..0c12d1c
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,29 @@
++++
+ weight = 40
++++
+
+
+# Contributing
+
+There are several ways to contribute to this app:
+* You can add or improve [translations][toolate].
+
+* If you found a bug or have an idea for a new feature, you can [join the chat][chat] or open an [issue][issues].
+
+ > Please note that I work on this project in my free time. Thus, I might not respond immediately, and not all ideas will be implemented.
+
+* You can implement a new feature yourself:
+ - Create a [fork][fork] of this repository.
+ - Create a new branch named `feature/` or `fix/` and commit your changes.
+ - Open a new pull request.
+
+
+See [here](/docs/build) for instructions on how to build this project.
+The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds.
+
+
+---
+ [fork]: https://github.com/jrpie/Launcher/fork/
+ [issues]: https://github.com/jrpie/Launcher/issues/
+ [chat]: https://s.jrpie.de/launcher-chat
+ [toolate]: https://toolate.othing.xyz/projects/jrpie-launcher/
diff --git a/docs/examples/_index.md b/docs/examples/_index.md
new file mode 100644
index 0000000..b50540f
--- /dev/null
+++ b/docs/examples/_index.md
@@ -0,0 +1,7 @@
++++
+ bookCollapseSection = true
+ weight = 20
++++
+
+# Examples
+This section contains some examples how μLauncher can be tweaked.
diff --git a/docs/examples/apps-on-home-screen.md b/docs/examples/apps-on-home-screen.md
new file mode 100644
index 0000000..f63c500
--- /dev/null
+++ b/docs/examples/apps-on-home-screen.md
@@ -0,0 +1,18 @@
++++
+ title = 'Showing Apps on the Home Screen'
++++
+
+# Showing Apps on the Home Screen
+
+Even though this is somewhat contrary to the general idea of μLauncher,
+it is possible to show apps on the home screen using widgets.
+
+Users suggested:
+* [Launchy](https://launchywidget.com/) (proprietary!)
+* KWGT Kustom Widget Maker (proprietary!)
+
+{{% hint danger %}}
+Both of these apps are not open source and KWGT even has ads.
+
+Please contact me if you know FOSS alternatives!
+{{% /hint %}}
diff --git a/docs/examples/termux/index.md b/docs/examples/termux/index.md
new file mode 100644
index 0000000..7d1f59c
--- /dev/null
+++ b/docs/examples/termux/index.md
@@ -0,0 +1,23 @@
++++
+ title = 'Integration with Termux'
++++
+
+# Termux
+
+μLauncher has no special support for [Termux](https://termux.dev/).
+However it is possible to run Termux commands from μLauncher by using [Termux:Widget](https://wiki.termux.com/wiki/Termux:Widget) to create a pinned shortcut and bind that to a gesture.
+
+* Install Termux:Widget.
+* Make sure that μLauncher is set as the default home screen.[^1]
+* Put the script you want to run into `~/.shortcuts/`.
+* Run `am start com.termux.widget/com.termux.widget.TermuxCreateShortcutActivity`. This will create a pinned shortcut which is treated like an app by μLauncher, i.e. open μLauncher's activity to create a shortcut.
+
+
+
+
+
+[^1]: Only the default home screen can access shortcuts.
diff --git a/docs/examples/termux/screenshot1.png b/docs/examples/termux/screenshot1.png
new file mode 100644
index 0000000..684c8bf
Binary files /dev/null and b/docs/examples/termux/screenshot1.png differ
diff --git a/docs/examples/termux/screenshot2.png b/docs/examples/termux/screenshot2.png
new file mode 100644
index 0000000..74b6395
Binary files /dev/null and b/docs/examples/termux/screenshot2.png differ
diff --git a/docs/examples/wallpapers.md b/docs/examples/wallpapers.md
new file mode 100644
index 0000000..aa50e13
--- /dev/null
+++ b/docs/examples/wallpapers.md
@@ -0,0 +1,29 @@
++++
+ title = 'Wallpapers'
++++
+
+# Wallpapers
+
+Some other launchers, notably [OLauncher](/docs/alternatives/#olauncher),
+have an option to show daily changing wallpapers.
+
+Aiming for [composability](https://en.wikipedia.org/wiki/Unix_philosophy),
+μLauncher simply shows the wallpaper set by the system.
+You can use [Live Wallpapers](https://android-developers.googleblog.com/2010/02/live-wallpapers.html) for
+daily changing wallpapers, slideshows and the like.
+Furthermore, there exist apps which automatically change the system wallpaper,
+for example [Wall You](https://f-droid.org/en/packages/com.bnyro.wallpaper/).
+
+Many [open source options](https://search.f-droid.org/?q=wallpaper) exist.
+I can recommend:
+* [Slideshow Wallpaper](https://f-droid.org/en/packages/io.github.doubi88.slideshowwallpaper/)
+* [Shader Editor](https://f-droid.org/en/packages/de.markusfisch.android.shadereditor/) lets you write custom GLSL shaders and use them as a live wallpaper.
+
+{{% hint danger %}}
+Note that apps that offer daily wallpapers, e.g. [Wall You](https://f-droid.org/en/packages/com.bnyro.wallpaper/),
+usually download them from to proprietary sources.
+{{% /hint %}}
+
+{{% hint info %}}
+If you can recommend other FOSS wallpaper apps just send me a message!
+{{% /hint %}}
diff --git a/docs/profiles.md b/docs/profiles.md
new file mode 100644
index 0000000..2087d06
--- /dev/null
+++ b/docs/profiles.md
@@ -0,0 +1,27 @@
++++
+ title = 'User Profiles'
+ weight = 12
++++
+
+
+# Work Profile
+
+µLauncher is compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used.
+Apps from the work profile are shown in the usual app list.
+
+
+# Private Space
+
+µLauncher is compatible with [private space](https://source.android.com/docs/security/features/private-space).
+
+
+The private space can be (un)locked by a dedicated action.
+
+It is configurable whether apps from private space are
+
+1. shown in the usual app list
+ (in this case, (un)locking is accessible through a lock icon on the top right of the app drawer)
+ or
+2. only shown in a separate list.
+
+
diff --git a/docs/security.md b/docs/security.md
new file mode 100644
index 0000000..08cb1e8
--- /dev/null
+++ b/docs/security.md
@@ -0,0 +1,79 @@
++++
+ weight = 30
++++
+
+# Security Considerations
+
+In order to launch apps, μLauncher obtains a list of all apps installed on the device.
+This includes apps from other profiles such as the [private space](/docs/profiles/#private-space)
+and the [work profile](/docs/profiles/#work-profile).
+
+μLauncher aims to be minimal software. Functionality that can be provided
+by other apps[^1] is not integrated into μLauncher itself,
+thus allowing user to install only what they need.
+
+[^1]: For example [daily wallpapers](/docs/examples/wallpapers/)
+
+{{% hint info %}}
+μLauncher does **not connect to the internet**.[^2]
+Functionality that would require an internet connection will not be implemented.
+In particular, μLauncher contains no ads and no trackers.
+{{% /hint %}}
+
+[^2]: Certain functions, such as the buttons in the meta section may prompt the browser
+to open a website, but μLauncher itself does not open internet connections.
+
+
+## Requested Permissions
+
+μLauncher requests several permissions:
+
+ * [`android.permission.REQUEST_DELETE_PACKAGES`](https://developer.android.com/reference/android/Manifest.permission#REQUEST_DELETE_PACKAGES)
+ * [`android.permission.QUERY_ALL_PACKAGES`](https://developer.android.com/reference/android/Manifest.permission#QUERY_ALL_PACKAGES)
+ * [`android.permission.ACCESS_HIDDEN_PROFILES`](https://developer.android.com/reference/android/Manifest.permission#ACCESS_HIDDEN_PROFILES)
+ * [`android.permission.EXPAND_STATUS_BAR`](https://developer.android.com/reference/android/Manifest.permission#EXPAND_STATUS_BAR)
+ * [`android.permission.POST_NOTIFICATIONS`](https://developer.android.com/reference/android/Manifest.permission#POST_NOTIFICATIONS)
+ * [`android.permission.BIND_ACCESSIBILITY_SERVICE`](https://developer.android.com/reference/android/Manifest.permission#BIND_ACCESSIBILITY_SERVICE)
+ * [`android.permission.BIND_DEVICE_ADMIN`](https://developer.android.com/reference/android/Manifest.permission#BIND_DEVICE_ADMIN)
+
+
+### Accessibility Service
+
+μLauncher's accessibility service can be used to lock the screen and
+to open the list of recent apps.
+
+{{% hint danger %}}
+Enabling μLauncher's accessibility service grants excessive permissions to the app.
+Do not enable the accessibility service if you don't need it.
+Before enabling, make sure that you obtained your copy of μLauncher from a source you trust.
+The official sources can be found [here](https://launcher.jrpie.de/).
+{{% /hint %}}
+
+Due to [Accrescent's policy](https://accrescent.app/docs/guide/publish/requirements.html#androidaccessibilityserviceaccessibilityservice) on accessibility services,
+the version of μLauncher published on Accrescent does not contain an accessibility service.
+
+
+### Device Administrator Permissions
+
+Device Administrator permissions can be used for locking the device as an alternative to using the accessibility service.
+This is the preferable option, as the required permissions are far less intrusive.
+However, this method is (ab)using an API intended for emergency situations,
+hence unlocking using weak authentication methods (fingerprint, face detection)
+is not possible.
+
+## Crash Reports
+
+For privacy reasons, μLauncher does not collect crash reports automatically.
+However, crash reports help a lot for debugging issues. Thus when a crash occurs,
+μLauncher shows a notification allowing the user to share the report voluntarily.
+When sharing a crash log, please make sure that it doesn't contain personal information.
+
+## Reporting Security Issues
+
+For security related issues, please use the contact information
+from the [security.txt](https://jrpie.de/.well-known/security.txt) on my website
+or [report a vulnerability](https://github.com/jrpie/Launcher/security/advisories/new) on github.
+
+{{% hint danger %}}
+Please do not report security issues using github's issue feature!
+{{% /hint %}}
diff --git a/docs/settings.md b/docs/settings.md
new file mode 100644
index 0000000..32c0fde
--- /dev/null
+++ b/docs/settings.md
@@ -0,0 +1,245 @@
++++
+ weight = 10
++++
+
+# Settings
+
+Tweaks and customizations can be made from within the settings page.
+The settings can be opened by binding the Settings action to a gesture (this is especially useful when configuring μLauncher for the first time) or from the settings icon in the app drawer.[^1]
+
+[^1]: i.e., the 'All Apps', 'Favorite Apps', and 'Private Space' views.
+
+## Appearance
+
+### Choose a wallpaper
+
+This triggers Android's mechanism to change the wallpaper using a photo app, file explorer, or native wallpaper setting app.
+µLauncher uses the system-wide wallpaper, i.e., this change also affects other launchers.
+
+### Font (in-app font)
+
+Set the font used within the app settings. This setting does not affect the date/time home screen font.
+
+**type:** `dropdown`
+
+**options:** `Hack`,`System default`,`Sans serif`,`Serif`,`Monospace`,`Serif monospace`
+
+### Text Shadow
+
+**type:** `toggle`
+
+### Background (app list and settings)
+
+Defines which background should be used in app drawers, settings, etc.
+to increase legibility.
+* `Transparent` does not change the wallpaper.
+* `Dim` dims the wallpaper.
+* `Blur` tries to blur the wallpaper. This is not possible on all devices. Some older devices don't support the operation. Also blur can be temporarily unavailable when the device is in power saving mode. In these case, `Dim` is used as a fallback.
+* `Solid` sets the background to a solid color (depending on the color theme). For the light theme only this option is available.
+
+On the home screen and widget panels, the wallpaper is always shown unmodified.
+
+**type:** `dropdown`
+
+**type:** `Transparent`,`Dim`,`Blur`,`Solid`
+
+### Monochrome app icons
+
+Remove coloring from all app icons. Can help decrease visual stimulus when enabled.
+
+**type:** `toggle`
+
+## Date & Time
+
+These settings affect the clock shown on the home screen (or on widget panels).
+If the clock is removed, the settings are not used.
+
+### Font (home screen)
+
+Set the home screen font for date and time. This setting does not affect the font of other components.
+
+**type:** `dropdown`
+
+**options:** `Hack`,`System default`,`Sans serif`,`Serif`,`Monospace`,`Serif monospace`
+
+### Color
+
+Set the color for the home screen date and time.
+
+Accepts a 6-digit RGB or 8-digit ARGB color code characters.[^2]
+Note that on Android, the ARGB color format is used, i.e., the alpha component is specified first.
+This differs from the more common RGBA, which is used in web development.
+
+
+[^2]: More precisely, everything that is valid input for [parseColor](https://developer.android.com/reference/android/graphics/Color#parseColor(java.lang.String)) can be used.
+
+
+**type:** `ARGB`
+
+### Use localized date format
+
+Adapt the display of dates and times to the specific conventions of a particular locale or region as set by the system. Different locales use different date orders (e.g., MM/DD/YYYY in the US, DD/MM/YYYY in Europe).
+
+**type:** `toggle`
+
+### Show time
+
+Show the current time on the home screen.
+
+**type:** `toggle`
+
+### Show seconds
+
+Show the current time down to the second on the home screen.
+
+**type:** `toggle`
+
+### Show date
+
+Show the current date on the home screen.
+
+**type:** `toggle`
+
+### Flip date and time
+
+Place the current time above the current date on the home screen.
+
+**type:** `toggle`
+
+## Functionality
+
+### Launch search results
+
+Launches any app that matches the user's keyboard input when no other apps match.
+
+As you type inside the app drawer, the app narrows down the list of apps shown based on the app title matching your text input.
+With the 'launch search results' setting, once only one matching app remains, it is launched immediately.
+Usually it suffices to type two or three characters the single out the desired app.
+
+This feature becomes more powerful when combined with [renaming](#additional-settings) apps, effectively letting you define custom app names that could be considered 'aliases' or shortcuts.
+For instance, if you want the keyboard input `gh` to open your `GitHub` app, you could rename `GitHub` to `GitHub gh`, `gh GitHub`, or simply `gh`.
+Assuming no other installed apps have the `gh` combination of letters in them, opening the app drawer and typing `gh` would immediately launch your `GitHub` app.
+
+Press space to temporarily disable this feature and allow text entry without prematurely launching an app. Useful when combined with the [Search the web](#search-the-web) feature.
+
+**type:** `toggle`
+
+### Search the web
+
+Press return while searching the app list to launch a web search.
+
+**type:** `toggle`
+
+### Start keyboard for search
+
+Automatically open the keyboard when the app drawer is opened.
+
+**type:** `toggle`
+
+### Double swipe gestures
+
+Enable double swipe (two finger) gestures in launcher settings. Does not erase gesture bindings if accidentally turned off.
+
+**type:** `toggle`
+
+### Edge swipe gestures
+
+Enable edge swipe (near edges of screen) gestures in launcher settings. Does not erase gesture bindings if accidentally turned off.
+
+**type:** `toggle`
+
+### Edge width
+
+Change how large a margin is used for detecting edge gestures. Shows the edge margin preview when using the slider.
+
+**type:** `slider`
+
+### Choose method for locking the screen
+
+There are two methods to lock the screen, and unfortunately, both have downsides.
+
+1. **`Device Admin`**
+
+ - Doesn't work with unlocking by fingerprint or face recognition.
+
+2. **`Accessibility Service`**
+
+ - Requires excessive privileges.
+ - μLauncher will use those privileges *only* for locking the screen.
+ - As a rule of thumb, it is [not recommended](https://android.stackexchange.com/questions/248171/is-it-safe-to-give-accessibility-permission-to-an-app) to grant access to accessibility services to a random app. Always review the [source code](https://github.com/jrpie/Launcher) before granting accessibility permissions so you can familiarize yourself with what the code might do.
+ - On some devices, the start-up PIN will no longer be used for encrypting data after activating an accessibility service.
+ - This can be [reactivated](https://issuetracker.google.com/issues/37010136#comment36) afterwards.
+
+ **type:** `text buttons`
+
+ **options:** `USE DEVICE ADMIN`,`USE ACCESSIBILITY SERVICE`
+
+## Apps
+
+### Hidden apps
+
+Open an app drawer containing only hidden apps.
+
+### Don't show apps that are bound to a gesture in the app list
+
+Remove certain apps from the app drawer if they are already accessible via a gesture.
+
+Reduces redundancy and tidies up the app drawer.
+
+**type:** `toggle`
+
+### Hide paused apps
+
+Remove paused apps from the app drawer.
+For example, an app belonging to the work profile is paused when the work profile is inactive.
+
+**type:** `toggle`
+
+### Hide private space from app list
+
+Remove private space from the app drawer.
+Private space apps can be accessed using a separate app drawer, which can be opened with the Private Space action.
+
+**type:** `toggle`
+
+### Layout of app list
+
+Changes how the apps are displayed when accessing the app drawer.
+
+- `Default`: All apps in the drawer will show in a vertically-scrolled list with their app icon and title.
+- `Text`: Removes the app icons, shows only app titles in the drawer as a vertically-scrolled list.
+ Work profile and private space apps are distinguished by a different label instead of a badge.
+- `Grid`: Shows apps with their app icon and title in a grid layout.
+
+**type:** `dropdown`
+
+**options:** `Default`,`Text`,`Grid`
+
+### Reverse the app list
+
+Enable reverse alphabetical sorting of apps in the app drawer.
+Useful for keeping apps within easier reach from the keyboard.
+
+**type:** `toggle`
+
+## Display
+
+### Rotate screen
+
+**type:** `toggle`
+
+### Keep screen on
+
+**type:** `toggle`
+
+### Hide status bar
+
+Remove the top status bar from the home screen.
+
+**type:** `toggle`
+
+### Hide navigation bar
+
+Remove the navigation bar from the home screen. Enabling this setting may make it difficult to use the device if gestures are not set up properly.
+
+**type:** `toggle`
diff --git a/docs/widgets.md b/docs/widgets.md
new file mode 100644
index 0000000..15ac7bb
--- /dev/null
+++ b/docs/widgets.md
@@ -0,0 +1,28 @@
++++
+ title = 'Widgets'
+ weight = 11
++++
+
+# Widgets
+
+μLauncher allows to add [app widgets](https://developer.android.com/develop/ui/views/appwidgets/overview) to the home screen and to widget panels.
+
+Widgets can be added, moved, removed, and configured in `Settings > Manage Widgets`.
+
+It is configurable whether or not interaction with a widget should be enabled.
+
+* If interaction is enabled, touch events are forwarded to the widget as usual.
+However, μLauncher [gestures](/docs/actions-and-gestures/) can not be executed in areas where such a widget is present.
+
+* If interaction is disabled, the widget does not respond to any touch events.
+ This is recommended when using a widget only to display information.
+
+μLauncher's clock behaves similarly to an app widget and can be managed in the same way.[^1]
+
+[^1]: However, it is technically not an app widget and cannot be used with other launchers.
+
+# Widget Panels
+
+Widget panels can contain widgets that are not needed on the home screen.
+They can be managed in `Settings > Manage Widget Panels`.
+Widget panels can be opened by using the [Open Widget Panel](/docs/actions-and-gestures/#available-actions) action.
diff --git a/fastlane/metadata/android/en-US/changelogs/45.txt b/fastlane/metadata/android/en-US/changelogs/45.txt
new file mode 100644
index 0000000..6d20b8d
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/45.txt
@@ -0,0 +1,10 @@
+* support for app widgets
+* widget panels
+
+* added documentation (thank you, wassupluke!)
+* added Dutch translation (thank you, renar and Sven van de Lagemaat!)
+* added Polish translation (thank you, AsLoLoks!)
+* improved Arabic translation (thank you, abdelbasset jabrane!)
+* improved Chinese translation (thank you, monkeyotw!)
+* improved Italian translation (thank you, Vladi69 and Nicola Bortoletto!)
+* improved Portuguese translation (thank you, "Vossa Excelencia"!)
diff --git a/fastlane/metadata/android/en-US/changelogs/46.txt b/fastlane/metadata/android/en-US/changelogs/46.txt
new file mode 100644
index 0000000..7d7f599
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/46.txt
@@ -0,0 +1,2 @@
+* Fixed several bugs related to widgets
+* Copy device info when clicking the version number (thank you, wassupluke!)
diff --git a/fastlane/metadata/android/en-US/changelogs/47.txt b/fastlane/metadata/android/en-US/changelogs/47.txt
new file mode 100644
index 0000000..33899dd
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/47.txt
@@ -0,0 +1,10 @@
+ * Fixed a bug related to widget causing crashes on Android 12 and earlier making the app unusable
+ * Fixed some additional bugs related to widgets
+
+ * Improved Lithuanian translation (thank you, wassupluke!)
+ * Improved Arabic translation (thank you, anonymous contributor!)
+ * Improved Chinese translation (thank you, class0068!)
+ * Improved Dutch translation (thank you, renar!)
+ * Improved German translation (thank you, renar!)
+ * Improved Italian translation (thank you, renar!)
+ * Improved Portuguese translation (thank you, anonymous contributor!)
diff --git a/fastlane/metadata/android/en-US/changelogs/48.txt b/fastlane/metadata/android/en-US/changelogs/48.txt
new file mode 100644
index 0000000..9a88f03
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/48.txt
@@ -0,0 +1,5 @@
+* Documentation at https://launcher.jrpie.de
+* Improved layout of widget list (thank you, wassupluke!)
+* Fixed some bugs related to widgets
+* Improved translations (thank you, abdelbasset jabrane, AsLoLoks, class0068, example, Julian Malinowski Lukas Hamm, renar, rimopa, Vladi69, Vossa Excelencia, and anonymous contributors!)
+
diff --git a/fastlane/metadata/android/it-IT/changelogs/26.txt b/fastlane/metadata/android/it-IT/changelogs/26.txt
new file mode 100644
index 0000000..d43e091
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/26.txt
@@ -0,0 +1 @@
+bugfix
diff --git a/fastlane/metadata/android/nl-NL/full_description.txt b/fastlane/metadata/android/nl-NL/full_description.txt
new file mode 100644
index 0000000..3040dfd
--- /dev/null
+++ b/fastlane/metadata/android/nl-NL/full_description.txt
@@ -0,0 +1,4 @@
+µLauncher is een thuisscherm die je andere apps laat starten met gebruik van veeg gebaren en knoppen indrukken.
+Het is minimalistisch, efficiënt en vrij van afleiding.
+
+Je thuisscherm laat alleen de datum, tijd en achtergrond zien.
diff --git a/fastlane/metadata/android/nl-NL/short_description.txt b/fastlane/metadata/android/nl-NL/short_description.txt
new file mode 100644
index 0000000..43e1dbf
--- /dev/null
+++ b/fastlane/metadata/android/nl-NL/short_description.txt
@@ -0,0 +1 @@
+Een afleidingsvrije, minimalistisch thuisscherm voor Android.
diff --git a/fastlane/metadata/android/nl-NL/title.txt b/fastlane/metadata/android/nl-NL/title.txt
new file mode 100644
index 0000000..4305604
--- /dev/null
+++ b/fastlane/metadata/android/nl-NL/title.txt
@@ -0,0 +1 @@
+µLauncher
diff --git a/fastlane/metadata/android/pl-PL/short_description.txt b/fastlane/metadata/android/pl-PL/short_description.txt
new file mode 100644
index 0000000..5fd868b
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/short_description.txt
@@ -0,0 +1 @@
+Minimalny ekran główny bez rozpraszania uwagi dla Androida.
diff --git a/fastlane/metadata/android/pl-PL/title.txt b/fastlane/metadata/android/pl-PL/title.txt
new file mode 100644
index 0000000..4305604
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/title.txt
@@ -0,0 +1 @@
+µLauncher