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 0c71f4a..dc6959d 100755
--- a/.scripts/release.sh
+++ b/.scripts/release.sh
@@ -1,9 +1,25 @@
#!/bin/bash
-export JAVA_HOME="/usr/lib/jvm/java-23-openjdk/"
+
+# 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 349f75c..7cc9422 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -23,8 +23,8 @@ android {
minSdkVersion 21
targetSdkVersion 35
compileSdk 35
- versionCode 42
- versionName "0.1.2"
+ 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 93f6ce8..087ec28 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
tools:ignore="QueryAllPackagesPermission" />
+
+
+
+
+
+
+
@@ -96,5 +115,4 @@
android:resource="@xml/accessibility_service_config" />
-
\ 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 7bbbdb5..b6df30b 100644
--- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt
@@ -38,6 +38,8 @@ import androidx.core.net.toUri
const val LOG_TAG = "Launcher"
+const val REQUEST_SET_DEFAULT_HOME = 42
+
fun isDefaultHomeScreen(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val roleManager = context.getSystemService(RoleManager::class.java)
@@ -59,11 +61,12 @@ fun setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
&& context is Activity
- && !isDefault // using role manager only works when µLauncher is not already the default.
+ && checkDefault // using role manager only works when µLauncher is not already the default.
) {
val roleManager = context.getSystemService(RoleManager::class.java)
- context.startActivity(
- roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME)
+ context.startActivityForResult(
+ roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME),
+ REQUEST_SET_DEFAULT_HOME
)
return
}
@@ -220,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/LauncherAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt
index 5d2be94..6ba467e 100644
--- a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt
@@ -11,7 +11,9 @@ import android.view.KeyEvent
import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import de.jrpie.android.launcher.Application
+import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.R
+import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService
import de.jrpie.android.launcher.apps.AppFilter
import de.jrpie.android.launcher.apps.hidePrivateSpaceWhenLocked
import de.jrpie.android.launcher.apps.isPrivateSpaceSupported
@@ -132,6 +134,14 @@ enum class LauncherAction(
R.drawable.baseline_settings_applications_24,
::expandSettingsPanel
),
+ RECENT_APPS(
+ "recent_apps",
+ R.string.list_other_recent_apps,
+ R.drawable.baseline_apps_24,
+ LauncherAccessibilityService::openRecentApps,
+ false,
+ { _ -> BuildConfig.USE_ACCESSIBILITY_SERVICE }
+ ),
LOCK_SCREEN(
"lock_screen",
R.string.list_other_lock_screen,
@@ -142,7 +152,13 @@ enum class LauncherAction(
"toggle_torch",
R.string.list_other_torch,
R.drawable.baseline_flashlight_on_24,
- ::toggleTorch
+ ::toggleTorch,
+ ),
+ LAUNCH_OTHER_LAUNCHER(
+ "launcher_other_launcher",
+ R.string.list_other_launch_other_launcher,
+ R.drawable.baseline_home_24,
+ ::launchOtherLauncher
),
NOP("nop", R.string.list_other_nop, R.drawable.baseline_not_interested_24, {});
@@ -248,6 +264,15 @@ private fun expandSettingsPanel(context: Context) {
}
}
+private fun launchOtherLauncher(context: Context) {
+ context.startActivity(
+ Intent.createChooser(
+ Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) },
+ context.getString(R.string.list_other_launch_other_launcher)
+ )
+ )
+}
+
private fun openSettings(context: Context) {
context.startActivity(Intent(context, SettingsActivity::class.java))
}
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/LauncherAccessibilityService.kt b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherAccessibilityService.kt
index a8ef6f2..7cb32d9 100644
--- a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherAccessibilityService.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LauncherAccessibilityService.kt
@@ -22,26 +22,44 @@ class LauncherAccessibilityService : AccessibilityService() {
companion object {
private const val TAG = "Launcher Accessibility"
+ private const val ACTION_REQUEST_ENABLE = "ACTION_REQUEST_ENABLE"
const val ACTION_LOCK_SCREEN = "ACTION_LOCK_SCREEN"
+ const val ACTION_RECENT_APPS = "ACTION_RECENT_APPS"
- fun lockScreen(context: Context) {
+ private fun invoke(context: Context, action: String, failureMessageRes: Int) {
try {
context.startService(
Intent(
context,
LauncherAccessibilityService::class.java
).apply {
- action = ACTION_LOCK_SCREEN
+ this.action = action
})
- } catch (e: Exception) {
+ } catch (_: Exception) {
Toast.makeText(
context,
- context.getString(R.string.alert_lock_screen_failed),
+ context.getString(failureMessageRes),
Toast.LENGTH_LONG
).show()
}
}
+ fun lockScreen(context: Context) {
+ if (!isEnabled(context)) {
+ showEnableDialog(context)
+ } else {
+ invoke(context, ACTION_LOCK_SCREEN, R.string.alert_lock_screen_failed)
+ }
+ }
+
+ fun openRecentApps(context: Context) {
+ if (!isEnabled(context)) {
+ showEnableDialog(context)
+ } else {
+ invoke(context, ACTION_RECENT_APPS, R.string.alert_recent_apps_failed)
+ }
+ }
+
fun isEnabled(context: Context): Boolean {
val enabledServices = Settings.Secure.getString(
context.contentResolver,
@@ -58,7 +76,7 @@ class LauncherAccessibilityService : AccessibilityService() {
setView(R.layout.dialog_consent_accessibility)
setTitle(R.string.dialog_consent_accessibility_title)
setPositiveButton(R.string.dialog_consent_accessibility_ok) { _, _ ->
- lockScreen(context)
+ invoke(context, ACTION_REQUEST_ENABLE, R.string.alert_enable_accessibility_failed)
}
setNegativeButton(R.string.dialog_cancel) { _, _ -> }
}.create().also { it.show() }.apply {
@@ -94,7 +112,9 @@ class LauncherAccessibilityService : AccessibilityService() {
}
when (action) {
+ ACTION_REQUEST_ENABLE -> {} // do nothing
ACTION_LOCK_SCREEN -> handleLockScreen()
+ ACTION_RECENT_APPS -> performGlobalAction(GLOBAL_ACTION_RECENTS)
}
}
return super.onStartCommand(intent, flags, startId)
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 a658cd1..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.Resources
-import android.os.Build
import android.os.Bundle
-import android.util.DisplayMetrics
-import android.view.KeyEvent
-import android.view.MotionEvent
import android.view.View
-import android.window.OnBackInvokedDispatcher
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.isVisible
-import de.jrpie.android.launcher.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,67 +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()
- val displayMetrics = DisplayMetrics()
-
- @Suppress("deprecation") // required to support API < 30
- windowManager.defaultDisplay.getMetrics(displayMetrics)
-
- val width = displayMetrics.widthPixels
- val height = displayMetrics.heightPixels
-
- touchGestureDetector = TouchGestureDetector(
- this,
- width,
- height,
- LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
- )
-
-
-
// Initialise layout
- binding = HomeBinding.inflate(layoutInflater)
+ 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 onStart() {
- super.onStart()
-
+ super.onStart()
super.onStart()
// If the tutorial was not finished, start it
@@ -126,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.
@@ -141,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 9000fa8..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
@@ -5,8 +5,10 @@ import android.graphics.Insets
import android.os.Build
import android.os.Handler
import android.os.Looper
+import android.util.DisplayMetrics
import android.view.MotionEvent
import android.view.ViewConfiguration
+import android.view.WindowManager
import androidx.annotation.RequiresApi
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.preferences.LauncherPreferences
@@ -15,10 +17,11 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.tan
+@Suppress("PrivatePropertyName")
class TouchGestureDetector(
private val context: Context,
- val width: Int,
- val height: Int,
+ var width: Int,
+ var height: Int,
var edgeWidth: Float
) {
private val ANGULAR_THRESHOLD = tan(Math.PI / 6)
@@ -32,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 {
@@ -319,6 +322,14 @@ class TouchGestureDetector(
}
}
+ fun updateScreenSize(windowManager: WindowManager) {
+ val displayMetrics = DisplayMetrics()
+ @Suppress("deprecation") // required to support API < 30
+ windowManager.defaultDisplay.getMetrics(displayMetrics)
+ width = displayMetrics.widthPixels
+ height = displayMetrics.heightPixels
+ }
+
@RequiresApi(Build.VERSION_CODES.Q)
fun setSystemGestureInsets(insets: Insets) {
systemGestureInsetTop = insets.top
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/SettingsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt
index 4c464e5..cd59726 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt
@@ -109,7 +109,7 @@ class SettingsActivity : AppCompatActivity(), UIObject {
}
private val TAB_TITLES = arrayOf(
- R.string.settings_tab_app,
+ R.string.settings_tab_actions,
R.string.settings_tab_launcher,
R.string.settings_tab_meta
)
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_apps_24.xml b/app/src/main/res/drawable/baseline_apps_24.xml
new file mode 100644
index 0000000..c5a49a0
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_apps_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_home_24.xml b/app/src/main/res/drawable/baseline_home_24.xml
new file mode 100644
index 0000000..935d1b6
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_home_24.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
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 df449c4..eb3339c 100644
--- a/app/src/main/res/layout/settings_actions_row.xml
+++ b/app/src/main/res/layout/settings_actions_row.xml
@@ -43,13 +43,15 @@
android:id="@+id/settings_actions_row_button_choose"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:maxWidth="100dp"
android:text="@string/settings_apps_choose"
android:textAllCaps="false"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- />
+ app:layout_constraintTop_toTopOf="parent" />
+
+
+ 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
new file mode 100644
index 0000000..39ae422
--- /dev/null
+++ b/app/src/main/res/values-ar/strings.xml
@@ -0,0 +1,341 @@
+
+
+ لا يمكن فتح التطبيق
+ هل تريد تعديل اعداداته؟
+ افتح الاعدادات لاختيار أمر لهذه الإيماءة
+ الاعدادات
+ الأوامر
+ المشغل
+ بيانات
+ رجوع
+ أعلى
+ لأعلى بأصبعين
+ اسحب لأعلى بأصبعين
+ أنقر ثم اسحب لأعلى
+ اضغط على زر الرجوع
+ اسحب لأعلى
+ نقرة + أعلى
+ أسفل
+ اسحب لأسفل
+ نقرة + لأسفل
+ لأسفل بأصبعين
+ يسار
+ اسحب من اليسار
+ نقرة + يسار
+ أنقر ثم اسحب إلى اليسار
+ يسار بأصبعين
+ يمين
+ اسحب إلى اليمين
+ نقرة + يمين
+ أنقر ثم اسحب إلى اليمين
+ يمين بأصبعين
+ اسحب إلى اليمين بأصبعين
+ يمين (في الأعلى)
+ يمين (في الأسفل)
+ اسحب إلى اليمين من أسفل الشاشة
+ يسار (في الأعلى)
+ اسحب إلى اليسار من أعلى الشاشة
+ أعلى (الحافة اليسرى)
+ اسحب إلى الأعلى من حافة الشاشة اليسرى
+ أعلى (الحافة اليمنى)
+ أسفل (الحافة اليسرى)
+ اسحب إلى الأسفل من حافة الشاشة اليسرى
+ أسفل (الحافة اليمنى)
+ أعلى اليسار -> وسط اليمين -> أسفل اليسار
+ أسفل اليسار -> وسط اليمين -> أعلى اليسار
+ أعلى اليمين -> وسط اليسار -> أسفل اليمين
+ أعلى اليسار -> الأسفل الوسط -> أعلى اليمين
+ V (معكوس)
+ أعلى اليمين -> الأسفل الوسط -> أعلى اليسار
+ أسفل اليسار -> أعلى الوسط -> أسفل اليمين
+ Λ (معكوس)
+ زر رفع الصوت
+ اضغط على زر رفع الصوت
+ زر خفض الصوت
+ اضغط على زر خفض الصوت
+ نقرة مزدوجة
+ نقرة مطولة
+ أنقر مطولًا في مكان فارغ على الشاشة
+ التاريخ
+ أنقر على التاريخ
+ الوقت
+ أنقر على الوقت
+ اختر تطبيق
+ ثبت تطبيقات
+ المظهر
+ السمة
+ ]]>
+ (معكوس)]]>
+
+
+ V
+ Λ
+ داكن
+ فاتح
+ متغير
+ ظل النص
+ شفاف
+ مظلل
+ مُموه
+ لون ثابت
+ الخط
+ خط النظام
+ بدون تذييل
+ مذيل
+ أحادي المسافة
+ أيقونات تطبيقات أحادية اللون
+ اللون
+ أظهر الوقت
+ أظهر التاريخ
+ استخدم تنسيق التاريخ المحلي
+ إقلب مكان التاريخ مع الوقت
+ اختر خلفية
+ العرض
+ حافظ على بقاء الشاشة قيد التشغيل
+ إخفِ شريط الحالة
+ تدوير الشاشة
+ الوظائف
+ إيماءات التمرير المزدوج
+ اسحب باستخدام أصبعين
+ إيماءات تمرير الحافة
+ عرض الحواف
+ إظهار نتائج البحث
+ اضغط مسافة لتعطيل هذه الميزة مؤقتًا
+ ابحث في الويب
+ أظهر لوحة المفاتيح عند البحث
+ الحساسية
+ التطبيقات
+ التطبيقات المخفية
+
+ إخفِ المساحة الخاصة من قائمة التطبيقات
+ تخطيط قائمة التطبيقات
+ الافتراضي
+ نص
+ شبكة
+ تعيين μauncher كشاشة المنزل
+ معلومات التطبيق
+ إعادة تعيين الإعدادات
+ أنت على وشك تجاهل كل تفضيلاتك، هل تريد الإكمال؟
+ عرض شيفرة المصدر
+ الإبلاغ عن خطأ
+ النسخ إلى الحافظة
+ يرجى عدم الإبلاغ عن الثغرات الأمنية علنًا على Github ، استخدم ما يلي بدلاً من ذلك:
+ الإبلاغ عن ثغرة أمنية
+ إنشاء تقرير
+ اتصل بمطور النسخة
+ انضم إلى دردشة μauncher
+ تبرع
+ سياسة الخصوصية
+ كل التطبيقات
+ التطبيقات المفضلة
+ التطبيقات المخفية
+ المساحة الخاصة
+ اختر تطبيق
+ التطبيقات
+ أخرى
+ إلغاء التثبيت
+ إزالة من المفضلة
+ إخفِ
+ أظهر
+ إعادة التسمية
+ بحث
+ بحث (بدون تشغيل تلقائي)
+ اعدادات التطبيق
+ درج التطبيقات
+ المساحة الخاصة
+ تبديل قفل المساحة الخاصة
+ ارفع الصوت
+ إنضم إلى مجموعة discord!
+ ضبط مستوى الصوت
+ الموسيقى: السابق
+ الموسيقى: تشغيل / ايقاف مؤقت
+ التطبيقات الحديثة
+ لا تفعل شيئًا
+ قفل الشاشة
+ تبديل الفلاش
+ تشغيل شاشة رئيسية أخرى
+ أضف اختصارًا
+ اربط بإيماءة
+ درس تعليمي
+ 👋\n\nخذ بضع ثوان لمعرفة كيفية استخدام هذا المشغل!
+ المفهوم
+ إنه برنامج مجاني (ترخيص MIT)!\nتأكد من مراجعة المستودع!
+ النسخة
+ الاستخدام
+ تحتوي شاشة المنزل الخاصة بك على التاريخ والوقت المحليين. بدون أي مشتتات.
+ كل التطبيقات
+ بمجرد تطابق تطبيق واحد فقط، يتم تشغيله تلقائيًا.\nيمكن تعطيل ذلك عن طريق اضافة مساحة في بداية استعلام.
+ الإعداد
+ اخترنا لك بعض التطبيقات الافتراضية. يمكنك تغييرها الآن إذا كنت ترغب في ذلك:
+ يمكنك أيضًا تغيير اختيارك لاحقًا.
+ لنبدأ!
+ ابدأ
+ الإعدادات
+ المزيد من الخيارات
+ هذه الوظيفة تتطلب أندرويد 6 أو أحدث.
+ هذه الوظيفة تتطلب أندرويد 15 أو أحدث.
+ تراجع
+ تم إخفاء التطبيق. يمكنك جعله مرئيًا مرة أخرى في الإعدادات.
+ الاعدادات السريعة
+ يجب أن يكون μlauncher مسؤولًا من أجل قفل الشاشة.
+ هذا مطلوب لإجراء شاشة القفل.
+ تمكين إجراء شاشة القفل
+ لم يتم اكتشاف كاميرا مع فلاش.
+ خطأ: لا يمكن الوصول إلى الفلاش.
+ خطأ: فشل في إظهار التطبيقات الحديثة. (إذا قمت للتو بترقية التطبيق ، فحاول تعطيل خدمة الوصول وإعادة تمكينها في إعدادات الهاتف)
+ خطأ: فشل في تمكين خدمة الوصول.
+ لم يتم تمكين خدمة الوصول إلى μlauncher. يرجى تمكينه في الإعدادات
+ المساحة الخاصة مقفلة
+ المساحة الخاصة مفتوحة
+ المساحة الخاصة غير متوافرة
+ قفل المساحة الخاصة
+ فتح المساحة الخاصة
+ μLauncher
+ اختر طريقة القفل
+ استخدام خدمة إمكانية الوصول
+ استخدم مسؤول الجهاز
+ اختر طريقة لقفل الشاشة
+ اعادة تسمية %1$s
+ أحمر
+ شفافية
+ أزرق
+ أخضر
+ اللون
+ اختر لونًا
+ أدرك أن هذا سيمنح أذونات كثيرة لـμauncher.
+ أدرك وجود خيارات أخرى (باستخدام أذونات مسؤول الجهاز أو زر الطاقة).
+ أوافق لـμlauncher باستخدام خدمة إمكانية الوصول لتوفير الوظائف ليس لها صلة بإمكانية الوصول.
+ أوافق على أن μlauncher لا يجمع أي بيانات.
+ يتم تفعيل خدمة إمكانية الوصول
+ تفعيل خدمة إمكانية الوصول
+ إلغاء
+ تراخيص المصادر المفتوحة
+ تراخيص المصادر المفتوحة
+ لم يتم العثور على تطبيق للتعامل مع البحث.
+ لا يمكن فتح عنوان URL: لم يتم العثور على متصفح.
+ أذونات كثيرة إلى التطبيق:
+
+ - قفل الشاشة
+ - التطبيقات الحديثة
+
+ لن يقومμlauncher أبدًا بجمع أي بيانات . على وجه الخصوص ، لا يستخدم μlauncher خدمة إمكانية الوصول لجمع أي بيانات.]]>
+ أنقر ثم اسحب لأسفل
+ الافتراضي
+ أظهر في قائمة التطبيقات
+ اسحب لأسفل بأصبعين
+ أظهر الثواني
+ الإبلاغ عن خطأ
+ اسحب إلى اليسار بأصبعين
+ أنقر مرتين في مكان فارغ على الشاشة
+ اضغط على زر الرجوع أثناء البحث في قائمة التطبيقات لإظهار البحث على الويب
+ اسحب إلى اليمين من أعلى الشاشة
+ يسار (في الأسفل)
+ اسحب إلى الأعلى من حافة الشاشة اليمنى
+ أسفل اليمين -> أعلى الوسط -> أسفل اليسار
+ لا تظهر التطبيقات المرتبطة بإيماءة في قائمة التطبيقات
+ درج التطبيقات المفضلة
+ الموسيقى: التالي
+ تم تصميم μLauncher ليكون فعالًا وخاليًا من مشتتات الانتباه.\n\nلا يحتوي على أي إعلانات ولا يجمع أي بيانات.
+ أنت مستعد للبدء!\n\nآمل أن يكون هذا ذا قيمة كبيرة بالنسبة لك!\n\n- المطورين
+ يجب أن يكون μlauncher الشاشة الرئيسية الافتراضية للوصول إلى مساحة خاصة.
+ اسحب إلى اليسار من أسفل الشاشة
+ أسفل اليمين -> وسط اليسار -> أعلى اليمين
+ لم يتم العثور على تطبيق المتجر
+ إخفِ شريط التنقل
+ معلومات التطبيق
+ أضف إلى المفضلة
+ توسيع لوحة الاشعارات
+ يمكنك البحث بسرعة في جميع التطبيقات في قائمة التطبيقات.\n\nاسحب لأعلى لفتحها، أو اربطها بإيماءة مختلفة.
+ خطأ: فشل في قفل الشاشة. (إذا قمت للتو بترقية التطبيق ، فحاول تعطيل خدمة الوصول وإعادة تمكينها في إعدادات الهاتف)
+ اسحب إلى الأسفل من حافة الشاشة اليمنى
+ أحادي المسافة مذيل
+ عكس قائمة التطبيقات
+ خطأ: لا يمكن توسيع شريط الحالة. يستخدم هذا الإجراء وظيفة ليست جزءًا من نظام أندرويد. لسوء الحظ ، لا يبدو أنه يعمل على جهازك.
+ خلفية القوائم
+ إخفِ التطبيقات المتوقفة
+ أظهر العرض التعليمي للتطبيق
+ شكرا لك على المساعدة في تحسين μ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 735aa9d..0b4090c 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -14,7 +14,7 @@
-
-->
Einstellungen
- Apps
+ Aktionen
Launcher
Meta
No se pudo abrir la aplicación
¿Desea cambiar la configuración?
- Abra la configuración para elegir una aplicación para esta acción
+ Abra la configuración para elegir una acción para este gesto
Configuración
- Aplicaciones
Launcher
Meta
- Deslizar Arriba
+ Arriba
Doble Arriba
- Deslizar Abajo
+ Abajo
Doble Abajo
- Deslizar Izquierda
+ Izquierda
Doble Izquierda
- Deslizar Derecha
+ Derecha
Doble Derecha
- Subir Volumen
- Bajar Volumen
+ Tecla para subir el volumen
+ Tecla para bajar el volumen
Doble Click
Click Largo
- Toca la fecha
- Toca el reloj
+ Fecha
+ reloj
Elegir Aplicación
Instalar aplicaciones
No se encontró la Store
@@ -73,7 +72,7 @@
-->
Seleccionar Launcher
Información de la aplicación
- Ver tutorial de Launcher
+ Ver el tutorial de µLauncher
Configuración por defecto
Todas sus preferencias se eliminarán. Desea continuar?
Reportar un error
@@ -92,11 +91,11 @@
Otros
Desinstalar
Información
- Buscar Aplicaciones
- Configuración de Launcher
+ Buscar
+ Configuración de μLauncher
Aplicaciones
- Música: Subir
- Música: Bajar
+ Subir el volumen
+ Bajar volumen
Música: Siguiente
Música: Anterior
Nada
@@ -106,19 +105,256 @@
-
-->
Tutorial
- Tómate unos segundos para aprender a usar este launcher
+ 👋\n\n¡Tómate unos segundos para aprender a utilizar este Launcher!
Concepto
- Launcher está diseñado para ser minimalista, eficiente y libre de distracciones.\n\nEs gratis y libre de anuncios y servicios de rastreo.
- La aplicación es de código abierto (licencia MIT) y está disponible en GitHub!\n\nNo olvides echarle un vistazo al repositorio!
+ μLauncher está diseñado para ser mínimo, eficiente y libre de distracciones.\n\nNo contiene anuncios ni recopila datos.
+ ¡Es software libre (licencia MIT)!\n¡No olvides visitar el repositorio!
Uso
Tu pantalla de inicio contiene la fecha y hora local. Sin distracciones.
- Puedes iniciar tus aplicaciones con solo presionar o deslizar una vez. Elige algunas en la siguiente pantalla.
+ Puede iniciar sus aplicaciones más importantes con gestos táctiles o presionando botones.
Puesta a punto
- Elegimos algunas aplicaciones por defecto para ti, si lo deseas puedes cambiarlas ahora.
+ Elegimos algunas aplicaciones predeterminadas para ti. Puedes cambiarlos ahora si lo deseas:
También puedes cambiar tu selección más tarde.
Vamos!
- Estás listo para iniciar!\n\n Esperamos que esto te sea de gran ayuda!\n\n- Finn M Glas\n(el desarrollador)
+ ¡Estás listo para comenzar!\n\n¡Espero que esto sea de gran valor para ti!\n\n- Finn (quien creó Launcher) y Josia (quien hizo algunas mejoras y mantiene la bifurcación μLauncher)
Iniciar
Configuración
Más opciones
+ atrás
+ Toca + Derecha Toca y desliza hacia la derecha
+ Desliza el dedo hacia la izquierda en la parte inferior de la pantalla
+ Desliza hacia abajo en el borde derecho de la pantalla
+ Botón de retroceso / gesto de retroceso
+ Desliza el dedo hacia arriba en el borde izquierdo de la pantalla
+ Desliza el dedo hacia arriba en el borde derecho de la pantalla
+ Desliza el dedo hacia abajo en el borde izquierdo de la pantalla
+ Dinámico
+ Color
+ Toca y desliza hacia arriba
+ Toque + Izquierda
+ Desliza el dedo hacia la derecha en la parte inferior de la pantalla
+ Desliza el dedo hacia la izquierda en la parte superior de la pantalla
+ Abajo (borde izquierdo)
+ Presione el botón para subir el volumen
+ Presione la barra espaciadora para desactivar esta función temporalmente.
+ Aplicaciones
+ Arriba a la izquierda -> Abajo en el medio -> Arriba a la derecha
+ Presione regresar mientras busca en la lista de aplicaciones para iniciar una búsqueda web.
+ Error: No se puede acceder a la antorcha.
+ Fondo (lista de aplicaciones y configuración)
+ Tema de color
+ Fuente
+ Iconos de aplicaciones monocromáticos
+ Derecha (arriba)
+ Derecha (Abajo)
+ Izquierda (arriba)
+ nunca recopilará ningún dato. En particular, μLauncher no utiliza el servicio de accesibilidad para recopilar ningún dato.]]>
+ Configurar μLauncher como un servicio de accesibilidad le permite bloquear la pantalla. Tenga en cuenta que se requieren permisos excesivos. Nunca debe otorgar dichos permisos a la ligera a ninguna aplicación. μLauncher utilizará el servicio de accesibilidad solo para bloquear la pantalla. Puedes consultar el código fuente para asegurarte. Tenga en cuenta que también se puede bloquear la pantalla otorgando permisos de administrador del dispositivo μLauncher. Sin embargo, ese método no funciona con el desbloqueo mediante huellas dactilares y rostro.
+ Ocultar el espacio privado de la lista de aplicaciones
+ Diseño de la lista de aplicaciones
+ Espacio privado
+ No se detectó ninguna cámara con linterna.
+ Activar o desactivar el bloqueo del espacio privado
+ Activar o desactivar la antorcha
+ Ocultar aplicaciones pausadas
+ Aplicaciones ocultas
+ Cambiar el nombre de %1$s
+ Cancelar
+ V
+ Informar un error
+ mostrar
+ μLauncher debe ser la pantalla de inicio predeterminada para acceder al espacio privado.
+ No informe vulnerabilidades de seguridad públicamente en GitHub, sino utilice lo siguiente:
+ Informar sobre una vulnerabilidad de seguridad
+ Crear informe
+ Únete al chat de μLauncher
+ Donar
+ Ajustar el volumen
+ Expandir el panel de notificaciones
+ El espacio privado no está disponible
+ Desliza hacia arriba
+ Haga doble clic en un área vacía
+ Presione el botón para bajar el volumen
+ Haga clic largo en un área vacía
+ Haga clic en la fecha
+ Haga clic en el reloj
+ Mostrar la hora
+ Sombra de texto
+ Ocultar la barra de estado
+ Ocultar la barra de navegación
+ Deslizar con dos dedos
+ predeterminado
+ Aplicaciones favoritas
+ Aplicaciones ocultas
+ Deshacer
+ Doy mi consentimiento para que μLauncher utilice el servicio de accesibilidad para proporcionar una funcionalidad no relacionada con la accesibilidad.
+ Buscar (sin inicio automático)
+ Soy consciente de que esto otorgará privilegios de gran alcance a μLauncher.
+ Soy consciente de que existen otras opciones (utilizando privilegios de administrador del dispositivo o el botón de encendido).
+ Licencias de código abierto
+ Licencias de código abierto
+ versión
+ Todas las aplicaciones
+ Elija el método para bloquear
+
+
+ Administrador del dispositivo
+ No funciona con desbloqueo por huella dactilar ni reconocimiento facial.
+
+
+
+
+ Servicio de Accesibilidad
+ Requiere privilegios excesivos.
+ μLauncher utilizará esos privilegios solo para bloquear la pantalla.
+
+ (Realmente no deberías confiar en una aplicación aleatoria que acabas de descargar con tal afirmación, pero puedes consultar el código fuente).
+
+ En algunos dispositivos, el PIN de inicio ya no se utilizará para cifrar datos después de activar un servicio de accesibilidad.
+ Esto se puede reactivar posteriormente.
+
+
+ Puede cambiar su selección más tarde en la configuración.
+ ]]>
+ Utilice el servicio de accesibilidad
+ Usar el administrador del dispositivo
+ Elija el método para bloquear la pantalla
+ Rojo
+ alfa
+ Azul
+ Verde
+ Sans serif
+ Serif
+ Monoespaciado
+ Serif monoespaciado
+ Mostrar la fecha
+ Utilice el formato de fecha localizado
+ Mostrar segundos
+ Cambiar fecha y hora
+ Girar la pantalla
+ Acciones de deslizamiento de borde
+ Desliza el dedo por el borde de la pantalla
+ Ancho del borde
+ No mostrar aplicaciones que estén vinculadas a un gesto en la lista de aplicaciones
+ Ver el código fuente
+ ¡Gracias por ayudarnos a mejorar μLauncher!\nConsidere agregar la siguiente información a su informe de error:
+ Copiar al portapapeles
+ Añadir a favoritos
+ Eliminar de favoritos
+ Esconder
+ Renombrar
+ Aplicaciones favoritas
+ Espacio privado
+ Puede buscar rápidamente entre todas las aplicaciones en la lista de aplicaciones.\n\nDesliza hacia arriba para abrirlo o asigne un gesto diferente.
+ Una vez que sólo coincide una aplicación, se inicia automáticamente.\nEsto se puede desactivar anteponiendo un espacio a la consulta.
+ Error: No se puede expandir la barra de estado. Esta acción utiliza una funcionalidad que no forma parte de la API de Android publicada. Lamentablemente, no parece funcionar en su dispositivo.
+ Esta funcionalidad requiere Android 6 o posterior.
+ Esta funcionalidad requiere Android 15 o posterior.
+ Aplicación oculta. Puedes hacerlo visible nuevamente en la configuración.
+ Configuración rápida
+ μLauncher debe ser administrador del dispositivo para poder bloquear la pantalla.
+ Esto es necesario para la acción de la pantalla de bloqueo.
+ Habilitar la acción de bloqueo de pantalla
+ Error: Error al bloquear la pantalla. (Si acaba de actualizar la aplicación, intente deshabilitar y volver a habilitar el servicio de accesibilidad en la configuración del teléfono)
+ El servicio de accesibilidad de μLauncher no está habilitado. Por favor, habilítelo en la configuración
+ Espacio privado bloqueado
+ Espacio privado desbloqueado
+ Bloquear el espacio privado
+ Desbloquear espacio privado
+ Error: El bloqueo de la pantalla mediante accesibilidad no es compatible con este dispositivo. En su lugar, utilice el administrador del dispositivo.
+ μLauncher - pantalla de bloqueo
+ Color
+ Elige el color
+ Doy mi consentimiento para que μLauncher no recopile ningún dato.
+ Activación del servicio de accesibilidad
+ Activar el servicio de accesibilidad
+ No se encontró ninguna aplicación para gestionar la búsqueda.
+ No se puede abrir la URL: no se encontró ningún navegador.
+ Acciones
+ Toca + Arriba
+ Desliza hacia arriba con dos dedos
+ Desliza hacia abajo
+ Toque + Abajo
+ Toca y desliza hacia abajo
+ Desliza hacia abajo con dos dedos
+ Desliza hacia la izquierda
+ Toca y desliza hacia la izquierda
+ Desliza dos dedos hacia la izquierda
+ Desliza hacia la derecha
+ Toque + Derecha
+ Desliza con dos dedos hacia la derecha
+ Desliza el dedo hacia la derecha en la parte superior de la pantalla
+ Izquierda (Abajo)
+ Arriba (borde izquierdo)
+ Arriba (borde derecho)
+ Abajo (borde derecho)
+ Arriba a la izquierda -> centro a la derecha -> abajo a la izquierda
+ Abajo a la izquierda -> centro a la derecha -> arriba a la izquierda
+ Arriba a la derecha -> centro a la izquierda -> abajo a la derecha
+ Abajo a la derecha -> centro a la izquierda -> arriba a la derecha
+ V (Reversa)
+ Arriba a la derecha -> Abajo en el medio -> Arriba a la izquierda
+ Λ
+ Abajo a la izquierda -> arriba en el medio -> abajo a la derecha
+ Λ (Inverso)
+ Abajo a la derecha -> arriba en el medio -> abajo a la izquierda
+ Atenuación
+ Sólido
+ Valor predeterminado del sistema
+ Transparente
+ Difuminar
+ Buscar en la web
+ texto
+ Red
+ Música: Reproducir / Pausa
+ Pantalla de bloqueo
+ Agregar acceso directo
+ 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-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index e84d6ae..c1d11b3 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -14,7 +14,6 @@
-
-->
Réglages
- Applications
Launcher
Meta
+ 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
+ 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 f0c0295..0d42f2e 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -14,7 +14,6 @@
-
-->
Configurações
- Apps
Launcher
Meta
Definir o μLauncher como tela inicial
Informações do aplicativo
- Ver tutorial do launcher
+ Ver tutorial do µLauncher
Redefinir configuraçãos
Você vai descartar todas as suas preferências. Continuar?
Reportar um bug
@@ -97,31 +96,31 @@
Desinstalar
Informações do aplicativo
Busque
- Configurações do µLauncher
+ Configurações do μLauncher
Todos os apps
- Música: Mais alto
- Música: Mais silencioso
+ Aumentar volume
+ Diminuir volume
Música: Próximo
Música: Anterior
- Não faça nada
+ Não fazer nada
Tutorial
- Tire alguns segundos para aprender a usar este Launcher!
+ 👋\n\nTire alguns segundos para aprender a usar este Launcher!
Conceito
- O Launcher foi criado para ser minimalista, eficiente e livre de distrações. Ele é livre de pagamentos, anúncios e serviços de rastreamento.
- O app é de código aberto (licença MIT) e está disponível no GitHub! Não deixe de conferir o repositório!
+ O μLauncher foi criado para ser minimalista, eficiente e livre de distrações. \n\n\nNão contém anúncios e não coleta dados.
+ É um software livre (sob licença MIT)!\nNão deixe de conferir o repositório!
Uso
Sua tela inicial contém a data e hora local. Sem distrações.
- Você pode iniciar seus aplicativos com um gesto único ou apertando um botão. Escolha algumas ações no próximo slide.
+ Você pode iniciar seus aplicativos com gestos de toque ou apertando um botão.
Configurar
Selecionamos alguns aplicativos padrão para você. Se quiser, você pode alterá-los agora:
Você pode alterar suas escolhas mais tarde.
Vamos lá!
- Tá todo pronto para começar! Espero que isso seja de grande valor para você! - Finn (que criou o Launcher) \te Josia (que fez algumas melhorias e tb mantém o fork do μLauncher)
+ Tá todo pronto para começar!\n\nEspero que isso seja de grande valor para você!\n\n- Finn (que criou o Launcher) e Josia (que fez algumas melhorias e tb mantém o fork do μLauncher)
Começar
Configurações
Mais opções
@@ -148,11 +147,11 @@
Erro: Falha ao bloquear a tela. (Se você acabou de atualizar o app, tente desativar e reativar o Serviço de acessibilidade em configurações do aparelho)
O Serviço de acessibilidade do μLauncher não está ativado. Entre em configurações para ativar
Não mostrar apps com um gesto atribuído na lista de aplicativos
- O µLauncher precisa tornar-se o Administrador do dispositivo para poder bloquear a tela.
+ O μLauncher precisa virar um Administrador do dispositivo para poder bloquear a tela.
Isto é necessário para realizar a ação de bloqueio da tela.
Permitir a ação de bloqueio da tela
Erro: Não é possível acessar a lanterna.
- µLauncher - bloqueio da tela
+ μLauncher
Usar o Serviço de acessibilidade
Usar o Administrador do dispositivo
Escolha um método de bloqueio
@@ -161,7 +160,7 @@
Essa funcionalidade requer o Android 6 ou mais recente.
Nenhuma câmera com lanterna detectada.
Erro: O bloqueio da tela via Serviço de acessibilidade não é compatível com este aparelho. Tente usar Administrador do dispositivo como método alternativo.
- Definindo µLauncher como Serviço de acessibilidade permite a ele bloquear a tela. Considere que é necessário conceder as permissões elevadas. Você nunca deveria autorizar essas permissões a qualquer aplicativo sem avaliação. O µLauncher usará o Serviço de acessibilidade somente para bloquear a tela. Você pode verificar o código-fonte para ter certeza. O bloqueio da tela também pode ser realizado dando ao µLauncher permissões de Administrador do dispositivo. Apesar de que esse método não funciona com impressão digital e desbloqueio facial.
+ Definindo o μLauncher como Serviço de acessibilidade permite bloquear a tela e abrir o menu de apps recentes. Considere que é necessário conceder as permissões elevadas. Você nunca deveria autorizar essas permissões a qualquer aplicativo sem avaliação. O μLauncher usará o Serviço de acessibilidade somente para executar as seguintes ações quando solicitado pelo usuário: * bloquear a tela * abrir aplicativos recentes μLauncher nunca usará o Serviço de acessibilidade para coletar os dados. Você pode verificar o código-fonte para ter certeza. O bloqueio da tela também pode ser realizado dando ao μLauncher permissões de Administrador do dispositivo. Apesar de que esse método não funciona com impressão digital e desbloqueio facial.
Escolha um método de bloqueio
Há dois métodos para bloquear a tela.
@@ -175,9 +174,9 @@
Serviço de acessibilidade
Exige permissões elevadas.
- O µLauncher usará essas permissões apenas para bloquear a tela.
+ O μLauncher usará essas permissões apenas para bloquear a tela.
- (Você realmente não deveria confiar num app aleatório que você baixou que tá pedindo estas permissões, mas pode verificar o código-fonte.)
+ (Você realmente não deveria confiar num app aleatório que você baixou e tá pedindo estas permissões, mas pode verificar o código-fonte.)
Em alguns aparelhos após ativação do Serviço de acessibilidade não será mais exigido o PIN para acessar dados criptografados, na inicialização do celular.
Isto pode ser reativado depois.
@@ -211,7 +210,7 @@
Deslize na borda da tela
Largura da borda
Ver código-fonte
- Entre no chat do µLauncher
+ Entre no chat do μLauncher
Bloquear a tela
Sombra no texto
Transparente
@@ -237,13 +236,18 @@
Verde
Cor
Escola a cor
- Autorizo a utilização do Serviço de acessibilidade para disponibilizar funcionalidades não relacionadas com a acessibilidade.
- Não autorizo ao µLauncher a coleta de quaisquer dados.
+ Autorizo o μLauncher a usar Serviço de acessibilidade para acessar funcionalidades não relacionadas com a acessibilidade.
+ Não autorizo ao μLauncher coletar quaisquer dados.
Ativação do Serviço de acessibilidade
Ativar o Serviço de acessibilidade
Cancelar
- permissões elevadas ao µLauncher.
µLauncher usará estas permissões apenas para bloquear a tela. µLauncher nunca coletará nenhum dado. Sobretudo, o µLauncher não implementa o Serviço de acessibilidade para coletar dados.]]>
- Estou ciente de que isto concederá permissões elevadas ao µLauncher.
+ permissões elevadas ao μLauncher.
μLauncher usará estas permissões apenas para executar as seguintes ações:
+
+ - Bloquear a tela
+ - Apps recentes
+
+ μLaunchernunca coletará nenhum dado. Sobretudo, o μLauncher não implementa o Serviço de acessibilidade para coletar os dados.]]>
+ Estou ciente de que isto concederá permissões elevadas ao μLauncher.
Estou ciente de que existem outras opções (permissões de Administrador do aparelho ou o botão de ligar).
Pesquise na internet
Ao buscar na lista de apps toque no Enter para iniciar uma pesquisa na internet.
@@ -256,13 +260,13 @@
Espaço privado trancado
Espaço privado liberado
Espaço privado indisponível
- O µLauncher precisa ser definido como a tela inicial padrão para poder usar Espaço privado.
+ O μLauncher precisa ser definido como a tela inicial padrão para poder usar Espaço privado.
Copiar para memória
Não relate vulnerabilidades de segurança publicamente no GitHub, use o seguinte:
Relatar vulnerabilidade de segurança
Criar relatório
Relatar um bug
- Obrigado por ajudar a melhorar o µLauncher!\nConsidere adicionar as seguintes informações ao relatório de bug:
+ Obrigado por ajudar a melhorar o μLauncher!\nConsidere adicionar as seguintes informações ao relatório dos bugs:
Toque no espaço para temporariamente desativar esta funcionalidade.
Não foi possível abrir a URL: nenhum navegador encontrado.
Nenhum app encontrado para efetuar a pesquisa.
@@ -301,5 +305,44 @@
Música: Reproduzir / Pausar
Canto inferior direito -> centro esquerdo -> canto superior direito
Inferior direito -> superior médio -> inferior esquerdo
- Lista de apps inversa
+ Inverter a lista de apps
+ Doar
+ Ajustar volume
+ Ocultar barra de status
+ Ocultar barra de navegação
+ Versão
+ Todos apps
+ Você pode encontrar rápido todos os apps na lista de aplicativos.\n\nDeslize para cima para abrir ou definir um gesto específico.
+ Quando apenas um aplicativo corresponde, vai ser iniciado automaticamente.\nIsso pode ser desativado acrescentando um espaço durante a busca.
+ Ações
+ Apps recentes
+ Erro: Falha ao ativar o Serviço de acessibilidade.
+ Usar outro iniciador
+ Erro: Falha ao mostrar apps recentes. (Se você acabou de atualizar o app, tente desativar e reativar o Serviço de acessibilidade em configurações do Android)
+ Gerenciar widgets
+ Escolha widget
+ Gerenciar painéis de widgets
+ Configurar
+ Ativar interação
+ Relógio
+ O relógio padrão do μLauncher
+ Apagar
+ Renomear
+ Painel do widget #%1$d
+
+ - Contém %d widget.
+ - Contém %d widgets.
+
+
+ Ok
+ Painéis de widget
+ Criar novo painel de widget
+ Abrir painel de widget
+ Esse painel de widget não existe mais.
+ Widgets
+ Remover
+ Desativar interação
+ Selecione um painel de widget
+ Launcher > Gerenciar painéis de widget.]]>
+ Esconder o teclado durante a rolagem
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 0cc240e..8c5a2e7 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -52,7 +52,6 @@
Ayarlarını değiştirmek ister misiniz?
Bu harekete bir eylem atamak için ayarları açın
Ayarlar
- Uygulamalar
Başlatıcı
Daha Fazlası
Yukarı
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 55376c3..10fe925 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -5,37 +5,36 @@
更多选项
设置
外观
- 主题风格
+ 色调风格
显示
其他
- 上
- 下
+ 上滑
+ 下滑
无法打开应用
要更改其设置吗?
打开设置,为该手势绑定一个应用程序
- 应用程序
启动器
杂项
左
- 左滑两次
- 上滑两次
- 下滑两次
+ 双指左滑
+ 双指上滑
+ 双指下滑
右
- 右滑两次
- 右(顶部)
- 右(底部)
- 左(底部)
- 左(顶部)
- 上(左边缘)
- 上(右边缘)
- 下(左边缘)
- 下(右边缘)
- 音量加
- 音量减
+ 双指右滑
+ 右滑(顶部)
+ 右滑(底部)
+ 左滑(底部)
+ 左滑(顶部)
+ 上滑(左边缘)
+ 上滑(右边缘)
+ 下滑(左边缘)
+ 下滑(右边缘)
+ 音量增加键
+ 音量降低键
双击
长按
- 日期
- 时间
+ 桌面日期
+ 桌面时钟
选择应用
安装应用
没有找到应用市场
@@ -43,9 +42,9 @@
选择壁纸
保持屏幕常亮
功能
- 边缘滑动动作
- 启动搜索匹配项
- 搜索时呼出键盘
+ 边缘滑动手势
+ 自动启动搜索匹配项
+ 自动激活搜索
灵敏度
应用信息
查看 µLauncher 的使用教程
@@ -62,10 +61,10 @@
概念
这是一款自由软件(遵循 MIT 许可)!\n欢迎查看项目仓库!
使用方法
- 您的主屏幕仅包含本地日期和时间,没有多余项目。
+ 您的桌面仅包含本地日期和时间,没有多余的项目。
设置
- 我们为您选择了一些默认应用。如果您希望进行更改,现在就可以:
- 您也可以稍后对您的选择进行更改。
+ 我们为您预设了一些快捷操作。如果您不满意,现在就试试点击右侧图标:
+ 您也可以稍后更改您的选择。
开始!
应用
卸载
@@ -77,18 +76,18 @@
降低音量
音乐:上一首
音乐:下一首
- 啥也不干
+ 不做任何设置
教程
μLauncher 的设计理念是简约、高效,无干扰。\n\n不含广告、且不收集任何数据。
- 您可以通过手势或按键来启动最重要的应用程序。
- 将 μLauncher 设为默认桌面
- 您已经准备好开始使用本启动器了!\n\n希望这对你有帮助!\n\n- Finn(Launcher 的作者)和 Josia(对 μLauncher 进行了改进和维护)
- 双滑动作
+ 您可以通过手势或按键来启动对您来说最重要的应用程序。
+ 将 μLauncher 设为默认启动器
+ 您已经准备好开始使用本启动器了!\n\n希望本快捷教程能对您有所帮助!\n\n- Finn(Launcher 的作者)和 Josia(对 μLauncher 进行了改进和维护)
+ 双指滑动手势
使用本地日期格式
显示时间
显示日期
- 翻转日期和时间
- 背景(应用列表和设置)
+ 交换日期和时间位置
+ 背景(应用程序列表和设置页面)
字体
黑白应用图标
显示秒
@@ -101,12 +100,12 @@
错误:无法访问闪光灯。
选择锁屏方法
选择锁屏的方法
- 不要在应用抽屉中显示被绑定到手势的应用
+ 不要在应用程序列表中显示已被绑定到手势操作的应用
此功能需要 Android 6 或更高版本。
应用程序已隐藏。您可在设置中让它再次显示。
- µLauncher 需要获得设备管理员权限才能够锁定屏幕。
+ µLauncher 需要激活“设备管理应用”权限才能够锁定屏幕。
这是执行锁屏操作所必需的。
- µLauncher - 锁屏
+ µLauncher
收藏的应用
添加到收藏夹
从收藏夹中移除
@@ -115,34 +114,34 @@
撤销
隐藏的应用
隐藏的应用
- 上滑
- 用双指向上滑动
- 下滑
- 双指向下滑动
- 左滑
- 双指向左滑动
- 右滑
- 双指向右滑动
- 在屏幕顶部向右滑动
- 在屏幕底部向右滑动
- 在屏幕底部向左滑动
- 在屏幕顶部向左滑动
- 在屏幕左边缘向上滑动
- 在屏幕右边缘向上滑动
- 在屏幕左边缘向下滑动
- 在屏幕右边缘向下滑动
- 按下音量增大按钮
- 按下音量降低按钮
+ 向上滑动
+ 使用双指向上滑动
+ 向下滑动
+ 使用双指向下滑动
+ 向左滑动
+ 使用双指向左滑动
+ 向右滑动
+ 使用双指向右滑动
+ 在桌面顶部向右滑动
+ 在桌面底部向右滑动
+ 在桌面底部向左滑动
+ 在桌面顶部向左滑动
+ 在桌面左边缘向上滑动
+ 在桌面右边缘向上滑动
+ 在桌面左边缘向下滑动
+ 在桌面右边缘向下滑动
+ 按下音量增加键
+ 按下音量降低键
双击空白区域
长按空白区域
- 点击日期
- 点击时间
+ 点击桌面日期
+ 点击桌面时钟
查看源代码
加入 μLauncher 的聊天群
收藏的应用
锁屏
文本阴影
- 双指滑动
+ 使用双指进行滑动手势操作
重命名 %1$s
默认
暗色
@@ -158,17 +157,17 @@
边缘宽度
重命名
启用锁屏动作
- μLauncher 的无障碍服务未启用,请在设置中启用它。
- 错误:此设备不支持使用无障碍功能锁定屏幕。请改用设备管理员模式。
- 使用无障碍服务
- 使用设备管理员模式
+ μLauncher 的“无障碍”服务未启用,请在设置中启用它。
+ 错误:此设备不支持使用“无障碍”服务锁定屏幕。请改用激活“设备管理应用”权限。
+ 使用“无障碍”服务
+ 激活“设备管理应用”权限
取消
亮色
- 快速设置
- 错误:锁定屏幕失败。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用并重新启用无障碍服务)
- 在屏幕边缘滑动
- 将 µLauncher 设为无障碍服务允许其锁定屏幕。请注意,这需要过多的权限。你永远不应该轻易地授予任何应用程序这样的权限。µLauncher 将仅使用无障碍服务功能锁屏。您可以审核源代码。请注意,锁屏也可以通过授予 µLauncher 设备管理员权限来实现,然而,这种方法不适用于以指纹和面部解锁。
- 返回
+ 启动器设置
+ 错误:锁定屏幕失败。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用再重新启用“无障碍”服务。)
+ 在桌面边缘进行滑动手势操作
+ 将 µLauncher 设置为“无障碍”服务以允许其锁定屏幕和展示最近应用屏幕。请注意,这会使 µLauncher 获得额外的权限。你永远不应该轻易地授予任何应用程序这样的权限。μLauncher 仅在被用户要求时才会使用“无障碍”服务权限以实现: * 锁定屏幕 * 展示最近应用屏幕。μLauncher 不会使用“无障碍”服务来收集任何数据。您可以审核我们的源代码。请注意,锁定屏幕也可以通过激活 µLauncher 的“设备管理应用”权限来实现,然而,这种方法无法与于指纹解锁和面部解锁兼容。
+ 返回操作
红色
蓝色
透明度
@@ -176,51 +175,51 @@
动态
私人空间
私人空间
- 选择颜色
+ 设置颜色
颜色
错误反馈
]]>
锁定私人空间
V
Λ
- 文本
+ 纯文本
网格
创建报告
解锁私人空间
默认
- 颜色
+ 文本颜色
复制到剪贴板
此功能需要 Android 15 或更高版本。
切换私人空间锁
- 激活无障碍服务
- 正在激活无障碍服务
+ 激活“无障碍”服务
+ 正在激活“无障碍”服务
开源许可证
开源许可证
- 在应用列表中显示
+ 在应用程序列表中显示
添加快捷方式
私人空间已锁定
私人空间已解锁
私人空间不可用
- µLauncher 需要作为默认主屏幕才能访问私人空间。
+ µLauncher 需要作为默认启动器才能访问私人空间。
没有找到处理搜索的应用。
无法打开 URL:找不到浏览器。
我已知晓,这将赋予 μLauncher 广泛且重要的权限。
在应用程序列表中隐藏私人空间
隐藏已被暂停的应用
返回按键 / 返回手势
- 先单击然后再下滑
+ 先单击然后再向下滑动
在网络上搜索
- (从)右上 (滑向)中左(滑向)右下
- 通过按回车键在应用列表搜索界面激活网络搜索。
- (从)左下 (滑向)中上(滑向)右下
+ (从)右上(滑向)中左(滑向)右下
+ 输入搜索内容后,按回车键直接在应用程序列表界面启动网络搜索。
+ (从)左下(滑向)中上(滑向)右下
选择锁定设备的方式
有2种方式可以用来锁定屏幕。
遗憾的是,两者都有缺点:
- 通过设置“设备管理应用”
- 无法和指纹解锁和脸部解锁共同使用。
+ 通过激活“设备管理应用”权限
+ 该方法无法和指纹解锁和脸部解锁共同使用。
@@ -229,50 +228,109 @@
需要更多的权限。
μLauncher 将这些权限仅用于锁定屏幕。
- (对于任何一个从网上下载的应用所做的类似声明,你都不应该抱持“默认为可信”的态度,你可以并应该检查一下它的源代码.)
+ (对于任何一个从网上下载的应用所做的类似声明,你都不应该抱持“默认为可信”的态度,你可以并应该检查一下它的源代码.)
- 在某些设备上,激活辅助功能服务后,启动PIN码将不再用于加密数据。
- 如果遇到该问题,可以通过该方法重新激活启动PIN码用于数据加密。
+ 在某些设备上,激活“无障碍”服务后,启动 PIN 码将不再用于加密数据。
+ 如果遇到该问题,可以通过该方法重新激活启动 PIN 码用于数据加密。
你可以在设置中随时更改这个选项。
]]>
搜索(不触发自动启动匹配项)
- 广泛且重要的权限。
μLauncher 将这些权限仅用于锁定屏幕。µLauncher 绝不会收集任何数据。尤其是,μLauncher 不会使用“无障碍”服务来收集任何数据。]]>
- (从)左上 (滑向)中右(滑向)左下
+ 广泛且重要的权限。
但 μLauncher 仅将这些权限用于:
+
+µLauncher 绝不会收集任何数据。尤其是,μLauncher 不会使用“无障碍”服务来收集任何数据。]]>
+ (从)左上(滑向)中右(滑向)左下
单击 + 上滑
单击 + 下滑
单击 + 左滑
- 先单击然后再左滑
- 先单击然后再上滑
+ 先单击然后再向左滑动
+ 先单击然后再向上滑动
单击 + 右滑
- 先单击然后再右滑
- (从)左下 (滑向)中右(滑向)左上
- (从)右下 (滑向)中左(滑向)右上
- (从)左上 (滑向)中下(滑向)右上
- (从)右上 (滑向)中下(滑向)左上
- (从)右下 (滑向)中上(滑向)左下
+ 先单击然后再向右滑动
+ (从)左下(滑向)中右(滑向)左上
+ (从)右下(滑向)中左(滑向)右上
+ (从)左上(滑向)中下(滑向)右上
+ (从)右上(滑向)中下(滑向)左上
+ (从)右下(滑向)中上(滑向)左下
Λ (反向)
V(反向)
(反向)]]>
- 开启后将直接启动匹配搜索内容的应用,可以通过按空格键临时暂停该功能。
+ 启用后将直接启动搜索所匹配到的应用,可以通过在搜索内容前添加空格来临时停用该功能。
应用程序列表样式
绑定到手势
音乐:播放 / 暂停
报告安全漏洞
请不要在 Github 上以公开的方式报告安全漏洞,请使用以下方式进行报告:
感谢您帮助改进 µLauncher!\n请考虑在您的应用程序错误反馈中添加以下信息:
- 我已知晓,还有其他替代方法(使用设备管理员模式或电源按键)。
+ 我已知晓,还有其他替代方法(激活“设备管理应用”权限或通过电源按键)。
我同意 μLauncher 不收集任何数据。
捐赠
调整音量
版本
所有应用
- 您可以在应用程序列表中快速所搜所有应用。\n\n您可以通过上滑打开应用程序列表,也可以通过绑定其他手势操作来打开应用程序列表。
- 当匹配到唯一的应用程序后,该应用将自动启动。\n如果你不想触发自动启动,在查询内容前加上空格即可禁用。
+ 您可以在应用程序列表中快速找到已安装的应用程序。\n\n您可以通过上滑打开应用程序列表,也可以通过绑定其他手势操作来打开应用程序列表。
+ 您还可以搜索,当匹配到唯一的应用程序后,该应用将自动启动。\n如果你不想触发自动启动,可以在搜索内容前加上空格以禁用。
隐藏状态栏
隐藏导航栏
倒序排列应用程序
我同意 μLauncher 使用无障碍服务来提供与无障碍服务无关的其他功能。
+ 快捷操作
+ 最近应用屏幕
+ 错误:启用“无障碍”服务失败。
+ 错误:无法展示最近应用屏幕。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用再重新启用“无障碍”服务。)
+ 启动其他启动器
+ 滚动应用程序列表时自动隐藏键盘
+ 设置小部件
+ 删除
+ 启用交互功能
+ 关闭交互功能
+ 时钟
+ 删除
+ 重命名
+ 小部件面板 #%1$d
+ 小部件面板
+ 选择小部件面板
+ 打开小部件面板
+ 创建新面板
+ 启动器 > 设置小部件面板”中进行创建。]]>
+ 桌面小部件
+ 该小部件面板不存在
+ μLauncher 默认时钟小部件
+ 选择小部件
+ 设置小部件面板
+ 确认
+
+ - 包含 %1$d 个小部件。
+
+ μLauncher 崩溃了
+ 抱歉!点击查看更多信息。
+ 复制崩溃报告到剪贴板
+ 通过 Email 发送报告
+ μLauncher 崩溃了
+ 在 GitHub 上创建错误反馈
+ 发送 Email
+ 崩溃和调试信息
+
+ 出于保护隐私的考量,崩溃日志不会被自动采集。
+ 但是,日志对应用调试非常有帮助,若您原意,请将所附日志文件通过 Email 发送给我们,
+ 或在 GitHub 上创建错误报告,非常感谢!
+ 请注意,崩溃日志可能包含敏感信息,如,您尝试启动的应用名称。
+ 因此,请在发送报告前,先将此类信息去除。
+ 我现在应该怎么做?
+ 如果这个错误重复发生,您可以做如下尝试:
+
+ - 强行停止 μLauncher
+ - 清空 μLauncher 的存储空间(您的设置将被重置!)
+ - 安装旧版本(GitHub, F-Droid)
+
+ ]]>
+ 添加小部件
+ 添加小部件面板
+ 文档
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index bdf620a..0d109bc 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -1,11 +1,5 @@
- 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 6205281..6fb9d61 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -16,7 +16,7 @@
-->
Settings
- Apps
+ Actions
Launcher
Meta
@@ -85,9 +85,9 @@
Λ (Reverse)
Bottom right -> top mid -> bottom left
- Volume Up
+ Volume Up Key
Press the volume up button
- Volume Down
+ Volume Down Key
Press the volume down button
Double Click
Double click an empty area
@@ -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,10 +255,12 @@
Music: Next
Music: Previous
Music: Play / Pause
- Expand notifications panel
- Do nothing
+ Expand Notifications Panel
+ Recent Apps
+ Do Nothing
Lock Screen
Toggle Torch
+ Launch Other Home Screen
Add Shortcut
@@ -307,6 +313,8 @@
No camera with torch detected.
Error: Can\'t access torch.
Error: Failed to lock screen. (If you just upgraded the app, try to disable and re-enable the accessibility service in phone settings)
+ Error: Failed to show recent apps. (If you just upgraded the app, try to disable and re-enable the accessibility service in phone settings)
+ Error: Failed to enable the accessibility service.
μLauncher\'s accessibility service is not enabled. Please enable it in settings
Private space locked
Private space unlocked
@@ -315,12 +323,17 @@
Lock private space
Unlock private space
Error: Locking the screen using accessibility is not supported on this device. Please use device admin instead.
- μLauncher - lock screen
+ μLauncher
- Setting μLauncher as an accessibility service allows it to lock the screen.
+ Setting μLauncher as an accessibility service allows it to lock the screen and open the recent apps menu.
Note that excessive permissions are required. You should never grant such permissions lightly to any app.
- μLauncher will use the accessibility service only for locking the screen. You can check the source code to make sure.
+ μLauncher will use the accessibility service only for performing the following actions when requested by the user:
+
+ * lock screen
+ * open recent apps
+
+ μLauncher will never use the accessibility service to collect data. You can check the source code to make sure.
Note that locking the screen can also be accomplished by granting μLauncher device administrator permissions. However that method doesn\'t work with fingerprint and face unlock.
@@ -365,7 +378,12 @@
I am aware that other options exist (using device administrator privileges or the power button).
I consent to μLauncher using the accessibility service to provide functionality unrelated to accessibility.
I consent to μLauncher not collecting any data.
- far-reaching privileges to μLauncher.
μLauncher will use these privileges only to lock the screen. μLauncher will never collect any data. In particular, μLauncher does not use the accessibility service to collect any data.]]>
+ far-reaching privileges to μLauncher.
μLauncher will use these privileges only to perform the following actions:
+
+ - Lock Screen
+ - Recent Apps
+
+ μLauncher will never collect any data. In particular, μLauncher does not use the accessibility service to collect any data.]]>
Activating the Accessibility Service
Activate Accessibility Service
Cancel
@@ -373,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/43.txt b/fastlane/metadata/android/en-US/changelogs/43.txt
new file mode 100644
index 0000000..2bca600
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/43.txt
@@ -0,0 +1 @@
+* Fixed gesture detection in landscape orientation
diff --git a/fastlane/metadata/android/en-US/changelogs/44.txt b/fastlane/metadata/android/en-US/changelogs/44.txt
new file mode 100644
index 0000000..6c6c4f7
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/44.txt
@@ -0,0 +1,11 @@
+* New action: Launch other launchers
+* New action: Show recent apps (workaround for an Android bug)
+* Fixed "Set µLauncher as home screen" button
+* Size of "choose app" button was limited
+
+* Added Arabic translation (thank you, letterhaven!)
+* Started Lithuanian translation (thank you, IdeallyGrey!)
+* Improved Chinese translation (thank you, monkeyotg!)
+* Improved Portuguese translation (thank you, "Vossa Excelencia"!)
+* Improved Spanish translation (thank you, T!)
+
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
diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt
index b4defca..89025a5 100644
--- a/fastlane/metadata/android/zh-CN/full_description.txt
+++ b/fastlane/metadata/android/zh-CN/full_description.txt
@@ -1,19 +1,21 @@
-µLauncher 是主屏幕启动程序,允许您使用滑动手势和按下按钮来启动其他应用。
-它是最小、高效且无干扰。
+µLauncher 是桌面启动器程序,允许您使用各种滑动手势和按下按钮来启动其他应用。
+它是简约、高效且无干扰的。
-您的主屏幕仅显示日期、时间和壁纸。
-按返回或向上滑动(可以配置)打开
-所有已安装应用的列表,可以高效地搜索。
+您的桌面仅显示日期、时间和壁纸。
+按返回按键或向上滑动(可自定义其他手势)即可打开
+应用程序列表,且支持高效地搜索。
-这是 Finn M Glas 的应用 Launcher 的一个 fork。
+本启动器是基于 Finn M Glas 开发的 Launcher 启动器 的一个派生应用程序。
-显著变化:
-* 边缘手势:可分为屏幕边缘滑动和中心滑动的设置。
-* 与工作配置文件兼容,因此可以使用 Shelter 等应用。
-* 此应用使用系统壁纸而不是自定义解决方案。
-* 字体已更改为 Hack。
-* Material 图标所取代了 Font Awesome 图标。
-* 移除了主屏幕上的齿轮按钮。按返回按钮会打开应用列表,可以从那里访问应用设置。
-* 搜索算法已修改为优先匹配应用名称开头的内容,即当搜索“te”时,“termux”会排在“notes”之前。
-* 搜索栏已移动到屏幕底部
+功能:
+* 您可以设定 35 个不同的手势操作。如:
+ - 启动一个应用程序
+ - 打开应用程序列表
+ - 打开收藏的应用程序列表
+ - 调整音量
+ - 快速切换 上一首/下一首 音乐
+ - 锁定屏幕
+ - 开启/关闭 手机闪光灯
+ - 展开通知栏 / 快捷设定栏
+* 兼容工作空间配置,因此支持使用 Shelter 等应用。
diff --git a/fastlane/metadata/android/zh-CN/short_description.txt b/fastlane/metadata/android/zh-CN/short_description.txt
index d49c27c..66d4684 100644
--- a/fastlane/metadata/android/zh-CN/short_description.txt
+++ b/fastlane/metadata/android/zh-CN/short_description.txt
@@ -1 +1 @@
-无干扰的最小主屏幕应用启动器。
+无干扰的简约风格启动器。