From 4f795289d5176fb6c9130cd8aa136061eb6d0793 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Tue, 15 Apr 2025 18:55:13 +0200 Subject: [PATCH 01/93] improve English translation --- app/src/main/res/values/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8bc9b3..21f25f5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -251,12 +251,12 @@ Music: Next Music: Previous Music: Play / Pause - Expand notifications panel + Expand Notifications Panel Recent Apps - Do nothing + Do Nothing Lock Screen Toggle Torch - Launch other Home Screen + Launch Other Home Screen Add Shortcut From 22633bdac3d057b552258d8bd84868c9cc7473b8 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Tue, 15 Apr 2025 19:24:23 +0200 Subject: [PATCH 02/93] try to fix #138 --- .../jrpie/android/launcher/ui/HomeActivity.kt | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 53a0876..2ab5d9f 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 @@ -37,7 +37,7 @@ import java.util.Locale class HomeActivity : UIObject, AppCompatActivity() { private lateinit var binding: HomeBinding - private lateinit var touchGestureDetector: TouchGestureDetector + private var touchGestureDetector: TouchGestureDetector? = null private var sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> @@ -56,29 +56,12 @@ class HomeActivity : UIObject, AppCompatActivity() { super.onCreate(savedInstanceState) super.onCreate() - touchGestureDetector = TouchGestureDetector( - this, 0, 0, - LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f - ) - touchGestureDetector.updateScreenSize(windowManager) // Initialise layout binding = HomeBinding.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( @@ -94,7 +77,7 @@ class HomeActivity : UIObject, AppCompatActivity() { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - touchGestureDetector.updateScreenSize(windowManager) + touchGestureDetector?.updateScreenSize(windowManager) } override fun onStart() { @@ -188,8 +171,28 @@ class HomeActivity : UIObject, AppCompatActivity() { override fun onResume() { super.onResume() - touchGestureDetector.edgeWidth = + /* This should be initialized in onCreate() + However on some devices there seems to be a bug where the touchGestureDetector + is not working properly after resuming the app. + Reinitializing the touchGestureDetector every time the app is resumed might help to fix that. + (see issue #138) + */ + touchGestureDetector = TouchGestureDetector( + this, 0, 0, LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f + ).also { + it.updateScreenSize(windowManager) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + binding.root.setOnApplyWindowInsetsListener { _, windowInsets -> + @Suppress("deprecation") // required to support API 29 + val insets = windowInsets.systemGestureInsets + touchGestureDetector?.setSystemGestureInsets(insets) + + windowInsets + } + } initClock() updateSettingsFallbackButtonVisibility() @@ -230,7 +233,7 @@ class HomeActivity : UIObject, AppCompatActivity() { } override fun onTouchEvent(event: MotionEvent): Boolean { - touchGestureDetector.onTouchEvent(event) + touchGestureDetector?.onTouchEvent(event) return true } From 077bd1ce448808f969254e704681924143873da7 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Wed, 23 Apr 2025 01:51:01 +0200 Subject: [PATCH 03/93] add option to hide keyboard when scrolling (cf. #142) --- .../preferences/LauncherPreferences$Config.java | 1 + .../java/de/jrpie/android/launcher/ui/Helper.kt | 16 ++++++++++++---- .../launcher/ui/list/apps/ListFragmentApps.kt | 17 +++++++++++++++++ app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/preferences.xml | 4 ++++ 6 files changed, 36 insertions(+), 4 deletions(-) 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..4653910 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 @@ -72,6 +72,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"), 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/list/apps/ListFragmentApps.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt index 1a55bbb..a8e59ba 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,20 @@ class ListFragmentApps : Fragment(), UIObject { } } adapter = appsRecyclerAdapter + if (LauncherPreferences.functionality().searchAutoCloseKeyboard()) { + addOnScrollListener(object : RecyclerView.OnScrollListener() { + var totalDy: Int = 0 + var threshold = (resources.displayMetrics.density * 100).toInt() + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + totalDy += dy + + if (totalDy.absoluteValue > 100) { + totalDy = 0 + closeSoftKeyboard(requireActivity()) + } + } + }) + } } binding.listAppsSearchview.setOnQueryTextListener(object : diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 89ec086..69c7f6a 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -147,6 +147,7 @@ functionality.search_auto_launch functionality.search_web functionality.search_auto_keyboard + functionality.search_auto_close_keyboard settings_action_lock_method diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21f25f5..ed2bb66 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -167,6 +167,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 diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 6ef5d07..7d906ff 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -104,6 +104,10 @@ android:key="@string/settings_functionality_search_auto_open_keyboard_key" android:defaultValue="true" android:title="@string/settings_functionality_auto_keyboard" /> + Date: Thu, 24 Apr 2025 14:37:05 +0200 Subject: [PATCH 04/93] add support for app widgets (see #44) --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 10 +- .../de/jrpie/android/launcher/Application.kt | 26 ++- .../de/jrpie/android/launcher/Functions.kt | 5 +- .../LauncherPreferences$Config.java | 4 + .../launcher/preferences/Preferences.kt | 10 + .../serialization/PreferenceSerializers.kt | 19 ++ .../jrpie/android/launcher/ui/HomeActivity.kt | 95 +++------- .../launcher/ui/PinShortcutActivity.kt | 16 +- .../launcher/SettingsFragmentLauncher.kt | 9 + .../android/launcher/ui/widgets/ClockView.kt | 80 ++++++++ .../ui/widgets/WidgetContainerView.kt | 137 ++++++++++++++ .../widgets/manage/ManageWidgetsActivity.kt | 173 ++++++++++++++++++ .../ui/widgets/manage/SelectWidgetActivity.kt | 168 +++++++++++++++++ .../ui/widgets/manage/WidgetManagerView.kt | 172 +++++++++++++++++ .../ui/widgets/manage/WidgetOverlayView.kt | 131 +++++++++++++ .../android/launcher/widgets/AppWidget.kt | 120 ++++++++++++ .../android/launcher/widgets/ClockWidget.kt | 41 +++++ .../widgets/LauncherWidgetProvider.kt | 58 ++++++ .../jrpie/android/launcher/widgets/Widget.kt | 60 ++++++ .../launcher/widgets/WidgetPosition.kt | 58 ++++++ .../jrpie/android/launcher/widgets/Widgets.kt | 87 +++++++++ app/src/main/res/drawable/baseline_add_24.xml | 11 ++ .../main/res/drawable/baseline_clock_24.xml | 15 ++ .../res/layout/activity_manage_widgets.xml | 25 +++ .../res/layout/activity_select_widget.xml | 72 ++++++++ app/src/main/res/layout/clock.xml | 35 ++++ app/src/main/res/layout/home.xml | 27 +-- .../main/res/layout/list_widgets_header.xml | 35 ++++ app/src/main/res/layout/list_widgets_row.xml | 60 ++++++ app/src/main/res/values/colors.xml | 4 + app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 12 ++ app/src/main/res/values/styles.xml | 10 +- app/src/main/res/xml/preferences.xml | 4 + 35 files changed, 1691 insertions(+), 100 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPosition.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt create mode 100644 app/src/main/res/drawable/baseline_add_24.xml create mode 100644 app/src/main/res/drawable/baseline_clock_24.xml create mode 100644 app/src/main/res/layout/activity_manage_widgets.xml create mode 100644 app/src/main/res/layout/activity_select_widget.xml create mode 100644 app/src/main/res/layout/clock.xml create mode 100644 app/src/main/res/layout/list_widgets_header.xml create mode 100644 app/src/main/res/layout/list_widgets_row.xml diff --git a/app/build.gradle b/app/build.gradle index 1a0a6fb..eaf97f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -106,6 +106,7 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") implementation "eu.jonahbauer:android-preference-annotations:1.1.2" + implementation 'androidx.activity:activity:1.10.1' annotationProcessor "eu.jonahbauer:android-preference-annotations:1.1.2" annotationProcessor "com.android.databinding:compiler:$android_plugin_version" testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a5f8831..841c9bd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ tools:ignore="QueryAllPackagesPermission" /> + + + - + \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/Application.kt b/app/src/main/java/de/jrpie/android/launcher/Application.kt index e6cce23..775621c 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 @@ -20,13 +22,22 @@ import de.jrpie.android.launcher.apps.isPrivateSpaceLocked import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion import de.jrpie.android.launcher.preferences.resetPreferences +import de.jrpie.android.launcher.widgets.LauncherWidgetProvider +import de.jrpie.android.launcher.widgets.Widget import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch + +const val APP_WIDGET_HOST_ID = 42; + + class Application : android.app.Application() { val apps = MutableLiveData>() + val widgets = MutableLiveData>() val privateSpaceLocked = MutableLiveData() + lateinit var appWidgetHost: AppWidgetHost + lateinit var appWidgetManager: AppWidgetManager private val profileAvailabilityBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -90,6 +101,8 @@ class Application : android.app.Application() { customAppNames = LauncherPreferences.apps().customNames() } else if (pref == LauncherPreferences.apps().keys().pinnedShortcuts()) { loadApps() + } else if (pref == LauncherPreferences.widgets().keys().widgets()) { + widgets.postValue(LauncherPreferences.widgets().widgets() ?: setOf()) } } @@ -103,10 +116,15 @@ class Application : android.app.Application() { torchManager = TorchManager(this) } + appWidgetHost = AppWidgetHost(this.applicationContext, APP_WIDGET_HOST_ID) + appWidgetManager = AppWidgetManager.getInstance(this.applicationContext) + + appWidgetHost.startListening() + + val preferences = PreferenceManager.getDefaultSharedPreferences(this) LauncherPreferences.init(preferences, this.resources) - // Try to restore old preferences migratePreferencesToNewVersion(this) @@ -157,4 +175,10 @@ class Application : android.app.Application() { apps.postValue(getApps(packageManager, applicationContext)) } } + + override fun onTerminate() { + appWidgetHost.stopListening() + super.onTerminate() + + } } diff --git a/app/src/main/java/de/jrpie/android/launcher/Functions.kt b/app/src/main/java/de/jrpie/android/launcher/Functions.kt index afc2c31..9679ae5 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -6,6 +6,9 @@ import android.app.role.RoleManager import android.content.ActivityNotFoundException import android.content.ClipData import android.content.ClipboardManager +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.appwidget.AppWidgetProviderInfo import android.content.Context import android.content.Intent import android.content.pm.LauncherApps @@ -223,4 +226,4 @@ fun copyToClipboard(context: Context, text: String) { val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipData = ClipData.newPlainText("Debug Info", text) clipboardManager.setPrimaryClip(clipData) -} \ No newline at end of file +} 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 4653910..575346a 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,7 @@ 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.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; @@ -82,5 +83,8 @@ 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) + }), }) 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..332f4df 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 @@ -13,6 +13,9 @@ import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersio import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown import de.jrpie.android.launcher.ui.HomeActivity +import de.jrpie.android.launcher.widgets.ClockWidget +import de.jrpie.android.launcher.widgets.WidgetPosition +import de.jrpie.android.launcher.widgets.deleteAllWidgets /* Current version of the structure of preferences. * Increase when breaking changes are introduced and write an appropriate case in @@ -71,6 +74,13 @@ fun resetPreferences(context: Context) { Log.i(TAG, "Resetting preferences") LauncherPreferences.clear() LauncherPreferences.internal().versionCode(PREFERENCE_VERSION) + deleteAllWidgets(context) + + LauncherPreferences.widgets().widgets( + setOf( + ClockWidget(-500, WidgetPosition(1,4,10,3)) + ) + ) val hidden: MutableSet = mutableSetOf() 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..a2749ae 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,7 @@ 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 eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer import kotlinx.serialization.Serializable @@ -28,6 +29,24 @@ 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 SetPinnedShortcutInfoPreferenceSerializer : PreferenceSerializer?, java.util.Set?> { 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 2ab5d9f..3f1e497 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,6 +1,7 @@ package de.jrpie.android.launcher.ui import android.annotation.SuppressLint +import android.app.Activity import android.content.SharedPreferences import android.content.res.Configuration import android.content.res.Resources @@ -10,8 +11,7 @@ import android.view.KeyEvent import android.view.MotionEvent import android.view.View import android.window.OnBackInvokedDispatcher -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.isVisible +import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.R import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Gesture @@ -20,7 +20,6 @@ import de.jrpie.android.launcher.databinding.HomeBinding import de.jrpie.android.launcher.openTutorial import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.tutorial.TutorialActivity -import java.util.Locale /** * [HomeActivity] is the actual application Launcher, @@ -34,7 +33,7 @@ import java.util.Locale * - Setting global variables (preferences etc.) * - Opening the [TutorialActivity] on new installations */ -class HomeActivity : UIObject, AppCompatActivity() { +class HomeActivity : UIObject, Activity() { private lateinit var binding: HomeBinding private var touchGestureDetector: TouchGestureDetector? = null @@ -45,15 +44,18 @@ 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() @@ -81,8 +83,7 @@ class HomeActivity : UIObject, AppCompatActivity() { } override fun onStart() { - super.onStart() - + super.onStart() super.onStart() // If the tutorial was not finished, start it @@ -93,6 +94,15 @@ class HomeActivity : UIObject, AppCompatActivity() { LauncherPreferences.getSharedPreferences() .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) + (application as Application).appWidgetHost.startListening() + + } + + + + override fun onStop() { + (application as Application).appWidgetHost.stopListening() + super.onStop() } override fun onWindowFocusChanged(hasFocus: Boolean) { @@ -118,44 +128,6 @@ class HomeActivity : UIObject, AppCompatActivity() { } } - private fun initClock() { - val locale = Locale.getDefault() - val dateVisible = LauncherPreferences.clock().dateVisible() - val timeVisible = LauncherPreferences.clock().timeVisible() - - var dateFMT = "yyyy-MM-dd" - var timeFMT = "HH:mm" - if (LauncherPreferences.clock().showSeconds()) { - timeFMT += ":ss" - } - - if (LauncherPreferences.clock().localized()) { - dateFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, dateFMT) - timeFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, timeFMT) - } - - var upperFormat = dateFMT - var lowerFormat = timeFMT - var upperVisible = dateVisible - var lowerVisible = timeVisible - - if (LauncherPreferences.clock().flipDateTime()) { - upperFormat = lowerFormat.also { lowerFormat = upperFormat } - upperVisible = lowerVisible.also { lowerVisible = upperVisible } - } - - binding.homeUpperView.isVisible = upperVisible - binding.homeLowerView.isVisible = lowerVisible - - binding.homeUpperView.setTextColor(LauncherPreferences.clock().color()) - binding.homeLowerView.setTextColor(LauncherPreferences.clock().color()) - - binding.homeLowerView.format24Hour = lowerFormat - binding.homeUpperView.format24Hour = upperFormat - binding.homeLowerView.format12Hour = lowerFormat - binding.homeUpperView.format12Hour = upperFormat - } - override fun getTheme(): Resources.Theme { val mTheme = modifyTheme(super.getTheme()) mTheme.applyStyle(R.style.backgroundWallpaper, true) @@ -193,9 +165,11 @@ class HomeActivity : UIObject, AppCompatActivity() { windowInsets } } - - initClock() updateSettingsFallbackButtonVisibility() + + binding.homeWidgetContainer.updateWidgets(this@HomeActivity, + LauncherPreferences.widgets().widgets() + ) } override fun onDestroy() { @@ -233,30 +207,11 @@ class HomeActivity : UIObject, AppCompatActivity() { } override fun onTouchEvent(event: MotionEvent): Boolean { + android.util.Log.e("Launcher", "on touch") touchGestureDetector?.onTouchEvent(event) return true } - override fun setOnClicks() { - - binding.homeUpperView.setOnClickListener { - if (LauncherPreferences.clock().flipDateTime()) { - Gesture.TIME(this) - } else { - Gesture.DATE(this) - } - } - - binding.homeLowerView.setOnClickListener { - if (LauncherPreferences.clock().flipDateTime()) { - Gesture.DATE(this) - } else { - Gesture.TIME(this) - } - } - } - - private fun handleBack() { Gesture.BACK(this) } 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..3dbdda8 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 @@ -49,7 +49,21 @@ class PinShortcutActivity : AppCompatActivity(), UIObject { 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 + 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/settings/launcher/SettingsFragmentLauncher.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt index a8efb43..8907f04 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,7 @@ 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.ManageWidgetsActivity /** @@ -81,6 +82,14 @@ class SettingsFragmentLauncher : PreferenceFragmentCompat() { true } + val manageWidgets = findPreference( + LauncherPreferences.widgets().keys().widgets() + ) + manageWidgets?.setOnPreferenceClickListener { + startActivity(Intent(requireActivity(), ManageWidgetsActivity::class.java)) + true + } + val hiddenApps = findPreference( LauncherPreferences.apps().keys().hidden() ) 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..33c4888 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt @@ -0,0 +1,80 @@ +package de.jrpie.android.launcher.ui.widgets + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import de.jrpie.android.launcher.actions.Gesture +import de.jrpie.android.launcher.databinding.ClockBinding +import de.jrpie.android.launcher.preferences.LauncherPreferences +import java.util.Locale + +class ClockView(context: Context, attrs: AttributeSet? = null, val appWidgetId: Int): ConstraintLayout(context, attrs) { + + val binding: ClockBinding = ClockBinding.inflate(LayoutInflater.from(context), this, true) + init { + initClock() + setOnClicks() + } + + + private fun initClock() { + val locale = Locale.getDefault() + val dateVisible = LauncherPreferences.clock().dateVisible() + val timeVisible = LauncherPreferences.clock().timeVisible() + + var dateFMT = "yyyy-MM-dd" + var timeFMT = "HH:mm" + if (LauncherPreferences.clock().showSeconds()) { + timeFMT += ":ss" + } + + if (LauncherPreferences.clock().localized()) { + dateFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, dateFMT) + timeFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, timeFMT) + } + + var upperFormat = dateFMT + var lowerFormat = timeFMT + var upperVisible = dateVisible + var lowerVisible = timeVisible + + if (LauncherPreferences.clock().flipDateTime()) { + upperFormat = lowerFormat.also { lowerFormat = upperFormat } + upperVisible = lowerVisible.also { lowerVisible = upperVisible } + } + + binding.clockUpperView.isVisible = upperVisible + binding.clockLowerView.isVisible = lowerVisible + + binding.clockUpperView.setTextColor(LauncherPreferences.clock().color()) + binding.clockLowerView.setTextColor(LauncherPreferences.clock().color()) + + binding.clockLowerView.format24Hour = lowerFormat + binding.clockUpperView.format24Hour = upperFormat + binding.clockLowerView.format12Hour = lowerFormat + binding.clockUpperView.format12Hour = upperFormat + } + + fun setOnClicks() { + binding.clockUpperView.setOnClickListener { + if (LauncherPreferences.clock().flipDateTime()) { + Gesture.TIME(context) + } else { + Gesture.DATE(context) + } + } + + binding.clockLowerView.setOnClickListener { + if (LauncherPreferences.clock().flipDateTime()) { + Gesture.DATE(context) + } else { + Gesture.TIME(context) + } + } + } + + + +} \ 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..04668ca --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt @@ -0,0 +1,137 @@ +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.WidgetPosition +import kotlin.math.max + + +/** + * This only works in an Activity, not AppCompatActivity + */ +open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) : ViewGroup(context, attrs) { + + var widgetViewById = HashMap() + + open fun updateWidgets(activity: Activity, widgets: Set?) { + if (widgets == null) { + return + } + Log.i("WidgetContainer", "updating ${activity.localClassName}") + widgetViewById.clear() + (0.. + widget.createView(activity)?.let { + addView(it, WidgetContainerView.Companion.LayoutParams(widget.position)) + widgetViewById.put(widget.id, it) + } + } + } + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + if (ev == null) { + return false + } + val position = PointF(ev.x, ev.y) + + return widgetViewById.filter { + RectF( + it.value.x, + it.value.y, + it.value.x + it.value.width, + it.value.y + it.value.height + ).contains(position) == true + }.any { + Widget.byId(context, it.key)?.allowInteraction == false + } + } + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + + var maxHeight = suggestedMinimumHeight + var maxWidth = suggestedMinimumWidth + + val mWidth = MeasureSpec.getSize(widthMeasureSpec) + val mHeight = MeasureSpec.getSize(heightMeasureSpec) + + (0.. + if (prefKey == LauncherPreferences.widgets().keys().widgets()) { + // We can't observe the livedata because this is not an AppCompatActivity + findViewById(R.id.manage_widgets_container).updateWidgets(this, + LauncherPreferences.widgets().widgets() + ) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + super.onCreate() + setContentView(R.layout.activity_manage_widgets) + findViewById(R.id.manage_widgets_button_add).setOnClickListener { + selectWidget() + } + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + findViewById(R.id.manage_widgets_container).updateWidgets(this, + (application as Application).widgets.value + ) + } + + override fun onStart() { + super.onStart() + super.onStart() + + LauncherPreferences.getSharedPreferences() + .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) + + } + + override fun onResume() { + super.onResume() + findViewById(R.id.manage_widgets_container).updateWidgets(this, + LauncherPreferences.widgets().widgets() + ) + + } + override fun getTheme(): Resources.Theme { + val mTheme = modifyTheme(super.getTheme()) + mTheme.applyStyle(R.style.backgroundWallpaper, true) + LauncherPreferences.clock().font().applyToTheme(mTheme) + LauncherPreferences.theme().colorTheme().applyToTheme( + mTheme, + LauncherPreferences.theme().textShadow() + ) + return mTheme + } + + override fun onDestroy() { + LauncherPreferences.getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener) + super.onDestroy() + } + + + fun selectWidget() { + val appWidgetHost = (application as Application).appWidgetHost + startActivityForResult( + Intent(this, SelectWidgetActivity::class.java).also { + it.putExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + appWidgetHost.allocateAppWidgetId() + ) + }, REQUEST_PICK_APPWIDGET + ) + } + + + fun createWidget(data: Intent) { + Log.i("Launcher", "creating widget") + val appWidgetManager = (application as Application).appWidgetManager + val appWidgetId = data.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return + + val provider = appWidgetManager.getAppWidgetInfo(appWidgetId) + + val display = windowManager.defaultDisplay + + val position = WidgetPosition.fromAbsoluteRect( + Rect(0,0, + min(400, appWidgetManager.getAppWidgetInfo(appWidgetId).minWidth), + min(400, appWidgetManager.getAppWidgetInfo(appWidgetId).minHeight) + ), + display.width, + display.height + ) + + val widget = AppWidget(appWidgetId, provider, position) + 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) + 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..c414db6 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt @@ -0,0 +1,168 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.Intent +import android.content.res.Resources +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.databinding.ActivitySelectWidgetBinding +import de.jrpie.android.launcher.ui.UIObject +import de.jrpie.android.launcher.widgets.ClockWidget +import de.jrpie.android.launcher.widgets.LauncherAppWidgetProvider +import de.jrpie.android.launcher.widgets.LauncherClockWidgetProvider +import de.jrpie.android.launcher.widgets.LauncherWidgetProvider +import de.jrpie.android.launcher.widgets.WidgetPosition +import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission +import de.jrpie.android.launcher.widgets.getAppWidgetHost +import de.jrpie.android.launcher.widgets.getAppWidgetProviders +import de.jrpie.android.launcher.widgets.updateWidget + + +private const val REQUEST_WIDGET_PERMISSION = 29 + +/** + * This activity lets the user pick an app widget to add. + * It provides an interface similar to [android.appwidget.AppWidgetManager.ACTION_APPWIDGET_PICK], + * but shows more information and also shows widgets from other user profiles. + */ +class SelectWidgetActivity : AppCompatActivity(), UIObject { + lateinit var binding: ActivitySelectWidgetBinding + var widgetId: Int = -1 + + private fun tryBindWidget(info: LauncherWidgetProvider) { + when (info) { + is LauncherAppWidgetProvider -> { + if (bindAppWidgetOrRequestPermission( + this, + info.info, + widgetId, + REQUEST_WIDGET_PERMISSION + ) + ) { + setResult( + RESULT_OK, + Intent().also { + it.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) + } + ) + finish() + } + } + is LauncherClockWidgetProvider -> { + updateWidget(ClockWidget(widgetId, WidgetPosition(0,4,12,3))) + 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) + + + widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + if (widgetId == -1) { + widgetId = getAppWidgetHost().allocateAppWidgetId() + } + + val viewManager = LinearLayoutManager(this) + val viewAdapter = SelectWidgetRecyclerAdapter() + + binding.selectWidgetRecycler.apply { + setHasFixedSize(false) + layoutManager = viewManager + adapter = viewAdapter + } + } + + override fun getTheme(): Resources.Theme { + return modifyTheme(super.getTheme()) + } + + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == REQUEST_WIDGET_PERMISSION && resultCode == RESULT_OK) { + data ?: return + val provider = (data.getSerializableExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER) as? AppWidgetProviderInfo) ?: return + tryBindWidget(LauncherAppWidgetProvider(provider)) + } + } + + inner class SelectWidgetRecyclerAdapter() : + RecyclerView.Adapter() { + + 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..2d41e13 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt @@ -0,0 +1,172 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.graphics.Point +import android.graphics.Rect +import android.graphics.RectF +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.AttributeSet +import android.view.HapticFeedbackConstants +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import androidx.core.graphics.contains +import androidx.core.graphics.minus +import androidx.core.graphics.toRect +import androidx.core.view.children +import de.jrpie.android.launcher.ui.widgets.WidgetContainerView +import de.jrpie.android.launcher.widgets.Widget +import de.jrpie.android.launcher.widgets.WidgetPosition +import de.jrpie.android.launcher.widgets.updateWidget +import kotlin.math.max +import kotlin.math.min + +/** + * A variant of the [WidgetContainerView] which allows to manage widgets. + */ +class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : + WidgetContainerView(context, attrs) { + + val TOUCH_SLOP: Int + val TOUCH_SLOP_SQUARE: Int + val LONG_PRESS_TIMEOUT: Long + + init { + val configuration = ViewConfiguration.get(context) + TOUCH_SLOP = configuration.scaledTouchSlop + TOUCH_SLOP_SQUARE = TOUCH_SLOP * TOUCH_SLOP + + LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong() + } + + + + enum class EditMode(val resize: (dx: Int, dy: Int, rect: Rect) -> Rect) { + MOVE({ dx, dy, rect -> + Rect(rect.left + dx, rect.top + dy, rect.right + dx, rect.bottom + dy) + }), + TOP({ dx, dy, rect -> + Rect(rect.left, min(rect.top + dy, rect.bottom - 200), rect.right, rect.bottom) + }), + BOTTOM({ dx, dy, rect -> + Rect(rect.left, rect.top, rect.right, max(rect.top + 200, rect.bottom + dy)) + }), + LEFT({ dx, dy, rect -> + Rect(min(rect.left + dx, rect.right - 200), rect.top, rect.right, rect.bottom) + }), + RIGHT({ dx, dy, rect -> + Rect(rect.left, rect.top, max(rect.left + 200, rect.right + dx), rect.bottom) + }), + } + + var selectedWidgetOverlayView: WidgetOverlayView? = null + var selectedWidgetView: View? = null + var currentGestureStart: Point? = null + var startWidgetPosition: Rect? = null + var lastPosition = Rect() + + private val longPressHandler = Handler(Looper.getMainLooper()) + + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + return true + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent?): Boolean { + if (event == null) { + return false + } + synchronized(this) { + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + val start = Point(event.x.toInt(), event.y.toInt()) + currentGestureStart = start + val view = children.mapNotNull { it as? WidgetOverlayView }.firstOrNull { + RectF(it.x, it.y, it.x + it.width, it.y + it.height).toRect().contains(start) == true + } ?: return false + + val position = (view.layoutParams as Companion.LayoutParams).position.getAbsoluteRect(width, height) + selectedWidgetOverlayView = view + selectedWidgetView = widgetViewById.get(view.widgetId) ?: return true + startWidgetPosition = position + + val positionInView = start.minus(Point(position.left, position.top)) + view.mode = view.getHandles().firstOrNull { it.position.contains(positionInView) }?.mode ?: EditMode.MOVE + + longPressHandler.postDelayed({ + synchronized(this@WidgetManagerView) { + view.showPopupMenu() + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + endInteraction() + } + }, LONG_PRESS_TIMEOUT) + } + if (event.actionMasked == MotionEvent.ACTION_MOVE || + event.actionMasked == MotionEvent.ACTION_UP + ) { + val distanceX = event.x - (currentGestureStart?.x ?: return true) + val distanceY = event.y - (currentGestureStart?.y ?: return true) + if (distanceX * distanceX + distanceY * distanceY > TOUCH_SLOP_SQUARE) { + longPressHandler.removeCallbacksAndMessages(null) + } + val view = selectedWidgetOverlayView ?: return true + val start = startWidgetPosition ?: return true + val absoluteNewPosition = view.mode?.resize( + distanceX.toInt(), + distanceY.toInt(), + start + ) ?: return true + val newPosition = WidgetPosition.fromAbsoluteRect( + absoluteNewPosition, width, height + ) + if (newPosition != lastPosition) { + lastPosition = absoluteNewPosition + (view.layoutParams as Companion.LayoutParams).position = newPosition + (selectedWidgetView?.layoutParams as? Companion.LayoutParams)?.position = newPosition + requestLayout() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_PRESS) + } + } + + if (event.actionMasked == MotionEvent.ACTION_UP) { + longPressHandler.removeCallbacksAndMessages(null) + val id = selectedWidgetOverlayView?.widgetId ?: return true + val widget = Widget.byId(context, id) ?: return true + widget.position = newPosition + endInteraction() + updateWidget(widget) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END) + } + } + } + } + + + return true + } + private fun endInteraction() { + startWidgetPosition = null + selectedWidgetOverlayView?.mode = null + } + + override fun updateWidgets(activity: Activity, widgets: Set?) { + super.updateWidgets(activity, widgets) + if (widgets == null) { + return + } + + widgets.forEach { widget -> + WidgetOverlayView(activity).let { + addView(it) + it.widgetId = widget.id + (it.layoutParams as Companion.LayoutParams).position = widget.position + } + } + } +} \ 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..0ce789f --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt @@ -0,0 +1,131 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.View +import android.widget.PopupMenu +import androidx.core.graphics.toRectF +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.widgets.Widget +import de.jrpie.android.launcher.widgets.updateWidget + +/** + * An overlay to show configuration options for a widget. + */ + +private const val HANDLE_SIZE = 100 +private const val HANDLE_EDGE_SIZE = (1.2 * HANDLE_SIZE).toInt() +class WidgetOverlayView : View { + + + val paint = Paint() + val handlePaint = Paint() + val selectedHandlePaint = Paint() + var mode: WidgetManagerView.EditMode? = null + class Handle(val mode: WidgetManagerView.EditMode, val position: Rect) + init { + handlePaint.style = Paint.Style.STROKE + handlePaint.setARGB(255, 255, 255, 255) + + selectedHandlePaint.style = Paint.Style.FILL_AND_STROKE + selectedHandlePaint.setARGB(100, 255, 255, 255) + + paint.style = Paint.Style.STROKE + paint.setARGB(255, 255, 255, 255) + } + + private var preview: Drawable? = null + var widgetId: Int = -1 + set(newId) { + field = newId + preview = Widget.byId(context, widgetId)?.getPreview(context) + } + + constructor(context: Context) : super(context) { + init(null, 0) + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init(attrs, 0) + } + + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( + context, + attrs, + defStyle + ) { + init(attrs, defStyle) + } + + private fun init(attrs: AttributeSet?, defStyle: Int) { } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + getHandles().forEach { + if (it.mode == mode) { + canvas.drawRoundRect(it.position.toRectF(), 5f, 5f, selectedHandlePaint) + } else { + canvas.drawRoundRect(it.position.toRectF(), 5f, 5f, handlePaint) + } + } + val bounds = getBounds() + canvas.drawRoundRect(bounds.toRectF(), 5f, 5f, paint) + + if (mode == null) { + return + } + + //preview?.bounds = bounds + //preview?.draw(canvas) + + + } + + fun showPopupMenu() { + val widget = Widget.byId(context, widgetId)?: return + val menu = PopupMenu(context, this) + menu.menu.let { + it.add( + context.getString(R.string.widget_menu_remove) + ).setOnMenuItemClickListener { _ -> + Widget.byId(context, widgetId)?.delete(context) + return@setOnMenuItemClickListener true + } + it.add( + if (widget.allowInteraction) { + context.getString(R.string.widget_menu_disable_interaction) + } else { + context.getString(R.string.widget_menu_enable_interaction) + } + ).setOnMenuItemClickListener { _ -> + widget.allowInteraction = !widget.allowInteraction + updateWidget(widget) + return@setOnMenuItemClickListener true + } + } + menu.show() + } + + fun getHandles(): List { + 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/widgets/AppWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt new file mode 100644 index 0000000..3e9a2eb --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt @@ -0,0 +1,120 @@ +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 de.jrpie.android.launcher.ui.HomeActivity +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 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, widgetProviderInfo: AppWidgetProviderInfo, position: WidgetPosition) : + this( + id, + position, + false, + widgetProviderInfo.provider.packageName, + widgetProviderInfo.provider.className, + widgetProviderInfo.profile.hashCode() + ) + + /** + * Get the [AppWidgetProviderInfo] by [id]. + * If the widget is not installed, use [restoreAppWidgetProviderInfo] instead. + */ + fun getAppWidgetProviderInfo(context: Context): AppWidgetProviderInfo? { + if (id < 0) { + return null + } + return (context.applicationContext as Application).appWidgetManager + .getAppWidgetInfo(id) + } + + /** + * Restore the AppWidgetProviderInfo from [user], [packageName] and [className]. + * Only use this when the widget is not installed, + * in normal operation use [getAppWidgetProviderInfo] instead. + */ + /*fun restoreAppWidgetProviderInfo(context: Context): AppWidgetProviderInfo? { + return getAppWidgetProviders(context).firstOrNull { + it.profile.hashCode() == user + && it.provider.packageName == packageName + && it.provider.className == className + } + }*/ + + override fun toString(): String { + return "WidgetInfo(id=$id, position=$position, packageName=$packageName, className=$className, user=$user)" + } + + override fun createView(activity: Activity): AppWidgetHostView? { + val providerInfo = activity.getAppWidgetManager().getAppWidgetInfo(id) ?: return null + val view = activity.getAppWidgetHost() + .createView(activity, this.id, providerInfo) + + val dp = activity.resources.displayMetrics.density + val screenWidth = activity.resources.displayMetrics.widthPixels + val screenHeight = activity.resources.displayMetrics.heightPixels + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val absolutePosition = position.getAbsoluteRect(screenWidth, screenHeight) + view.updateAppWidgetSize(Bundle.EMPTY, + listOf(SizeF( + absolutePosition.width() / dp, + absolutePosition.height() / dp + ))) + } + view.setPadding(0,0,0,0) + return view + } + + override fun findView(views: Sequence): 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..d819538 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt @@ -0,0 +1,41 @@ +package de.jrpie.android.launcher.widgets + +import android.app.Activity +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.View +import de.jrpie.android.launcher.ui.widgets.ClockView +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +@SerialName("widget:clock") +class ClockWidget( + override val id: Int, + override var position: WidgetPosition, + override var allowInteraction: Boolean = true +) : Widget() { + + override fun createView(activity: Activity): View? { + return ClockView(activity, null, id) + } + + 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/LauncherWidgetProvider.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt new file mode 100644 index 0000000..018b29b --- /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 + } + } + +} +class LauncherClockWidgetProvider : LauncherWidgetProvider() { + + override fun loadLabel(context: Context): CharSequence? { + return context.getString(R.string.widget_clock_label) + } + + override fun loadDescription(context: Context): CharSequence? { + return context.getString(R.string.widget_clock_description) + } + + override fun loadPreviewImage(context: Context): Drawable? { + return null + } + + override fun loadIcon(context: Context): Drawable? { + return AppCompatResources.getDrawable(context, R.drawable.baseline_clock_24) + } +} + 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..d3610dd --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt @@ -0,0 +1,60 @@ +package de.jrpie.android.launcher.widgets + +import android.app.Activity +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.View +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.preferences.LauncherPreferences +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + + +@Serializable +sealed class Widget { + abstract val id: Int + abstract var position: WidgetPosition + abstract 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) { + 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(context: Context, id: Int): Widget? { + return (context.applicationContext as Application).widgets.value?.firstOrNull { + it.id == id + } + } + } +} 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..b575665 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPosition.kt @@ -0,0 +1,58 @@ +package de.jrpie.android.launcher.widgets + +import android.graphics.Rect +import kotlinx.serialization.Serializable +import kotlin.math.ceil +import kotlin.math.roundToInt +import kotlin.math.max + +const val GRID_SIZE: Short = 12 + +@Serializable +data class WidgetPosition(var x: Short, var y: Short, var width: Short, var height: Short) { + + fun getAbsoluteRect(screenWidth: Int, screenHeight: Int): Rect { + val gridWidth = screenWidth / GRID_SIZE.toFloat() + val gridHeight= screenHeight / GRID_SIZE.toFloat() + + return Rect( + (x * gridWidth).toInt(), + (y * gridHeight).toInt(), + ((x + width) * gridWidth).toInt(), + ((y + height) * gridHeight).toInt() + ) + } + + companion object { + fun fromAbsoluteRect(absolute: Rect, screenWidth: Int, screenHeight: Int): WidgetPosition { + val gridWidth = screenWidth / GRID_SIZE.toFloat() + val gridHeight= screenHeight / GRID_SIZE.toFloat() + + val x = (absolute.left / gridWidth).roundToInt().toShort().coerceIn(0, (GRID_SIZE-1).toShort()) + val y = (absolute.top / gridHeight).roundToInt().toShort().coerceIn(0, (GRID_SIZE-1).toShort()) + + + val w = max(2, ((absolute.right - absolute.left) / gridWidth).roundToInt()).toShort() + val h = max(2, ((absolute.bottom - absolute.top) / gridHeight).roundToInt()).toShort() + + return WidgetPosition(x,y,w,h) + + } + + fun center(minWidth: Int, minHeight: Int, screenWidth: Int, screenHeight: Int): WidgetPosition { + val gridWidth = screenWidth / GRID_SIZE.toFloat() + val gridHeight= screenHeight / GRID_SIZE.toFloat() + + val cellsWidth = ceil(minWidth / gridWidth).toInt().toShort() + val cellsHeight = ceil(minHeight / gridHeight).toInt().toShort() + + return WidgetPosition( + ((GRID_SIZE - cellsWidth) / 2).toShort(), + ((GRID_SIZE - cellsHeight) / 2).toShort(), + cellsWidth, + cellsHeight + ) + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt new file mode 100644 index 0000000..cd4ef29 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -0,0 +1,87 @@ +package de.jrpie.android.launcher.widgets + +import android.app.Activity +import android.app.Service +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import android.os.Build +import android.os.UserManager +import android.util.Log +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.preferences.LauncherPreferences + +fun deleteAllWidgets(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.getAppWidgetHost().appWidgetIds.forEach { AppWidget(it).delete(context) } + } +} + +/** + * Tries to bind [providerInfo] to the id [id]. + * @param providerInfo The widget to be bound. + * @param id The id to bind the widget to. If -1 is provided, a new id is allocated. + * @param + * @param requestCode Used to start an activity to request permission to bind the widget. + * + * @return true iff the app widget was bound successfully. + */ +fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidgetProviderInfo, id: Int, requestCode: Int? = null): Boolean { + val appWidgetId = if(id == -1) { + activity.getAppWidgetHost().allocateAppWidgetId() + } else { id } + + Log.i("Launcher", "Binding new widget ${appWidgetId}") + if (!activity.getAppWidgetManager().bindAppWidgetIdIfAllowed( + appWidgetId, + providerInfo.provider + ) + ) { + Log.i("Widgets", "requesting permission for widget") + val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND).apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,appWidgetId) + putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, providerInfo.provider) + } + activity.startActivityForResult(intent, requestCode ?: 0) + return false + } + return true +} + + +fun getAppWidgetProviders( context: Context ): List { + 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 { + appWidgetManager.getInstalledProvidersForProfile(it) + .map { LauncherAppWidgetProvider(it) } + }.flatten() + ) + + + return list +} + + +fun updateWidget(widget: Widget) { + var widgets = LauncherPreferences.widgets().widgets() ?: setOf() + widgets = widgets.minus(widget).plus(widget) + LauncherPreferences.widgets().widgets(widgets) +} + +fun Context.getAppWidgetHost(): AppWidgetHost { + return (this.applicationContext as Application).appWidgetHost +} +fun Context.getAppWidgetManager(): AppWidgetManager { + return (this.applicationContext as Application).appWidgetManager +} 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_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/layout/activity_manage_widgets.xml b/app/src/main/res/layout/activity_manage_widgets.xml new file mode 100644 index 0000000..c77f0e3 --- /dev/null +++ b/app/src/main/res/layout/activity_manage_widgets.xml @@ -0,0 +1,25 @@ + + + + + + \ 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..82db94d --- /dev/null +++ b/app/src/main/res/layout/activity_select_widget.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/clock.xml b/app/src/main/res/layout/clock.xml new file mode 100644 index 0000000..d81fc5f --- /dev/null +++ b/app/src/main/res/layout/clock.xml @@ -0,0 +1,35 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/home.xml b/app/src/main/res/layout/home.xml index ecefdea..717151f 100644 --- a/app/src/main/res/layout/home.xml +++ b/app/src/main/res/layout/home.xml @@ -10,29 +10,10 @@ android:fitsSystemWindows="true" tools:context=".ui.HomeActivity"> - - - + + + + + + + + \ 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..878aaad --- /dev/null +++ b/app/src/main/res/layout/list_widgets_row.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1e4d12b..2712036 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -11,5 +11,9 @@ #fff #9999ff #000 + #FF29B6F6 + #FF039BE5 + #FFBDBDBD + #FF757575 diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 69c7f6a..30e4cda 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -9,6 +9,7 @@ internal.started_before internal.first_startup internal.version_code + widgets.widgets apps.favorites apps.hidden apps.pinned_shortcuts diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ed2bb66..a8813f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -98,6 +98,8 @@ Time Click on time + Manage widgets + Choose App @@ -388,5 +390,15 @@ 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 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 20ccb67..c5b7252 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -66,12 +66,12 @@ 0 2 + - - + + + + + + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 7d906ff..b4bc5f0 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -6,6 +6,10 @@ + + Date: Fri, 25 Apr 2025 11:25:00 +0200 Subject: [PATCH 05/93] fix #145 --- app/src/main/res/layout/list_apps_row_variant_grid.xml | 2 ++ 1 file changed, 2 insertions(+) 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" From a4fcdf60c7d3be3d37be15fb3fc8c9b286a7f953 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 26 Apr 2025 21:52:21 +0200 Subject: [PATCH 06/93] add widget panels (see #44) --- app/src/main/AndroidManifest.xml | 10 +- .../jrpie/android/launcher/actions/Action.kt | 10 +- .../launcher/actions/WidgetPanelAction.kt | 83 ++++++++++++++ .../LauncherPreferences$Config.java | 4 +- .../launcher/preferences/Preferences.kt | 24 +++- .../launcher/preferences/legacy/Version1.kt | 9 +- .../launcher/preferences/legacy/Version2.kt | 5 +- .../launcher/preferences/legacy/Version3.kt | 10 +- .../launcher/preferences/legacy/Version4.kt | 27 +++++ .../preferences/legacy/VersionUnknown.kt | 4 +- .../serialization/PreferenceSerializers.kt | 17 +++ .../jrpie/android/launcher/ui/HomeActivity.kt | 1 - .../ui/list/other/OtherRecyclerAdapter.kt | 19 ++-- .../launcher/SettingsFragmentLauncher.kt | 9 ++ .../ui/widgets/WidgetContainerView.kt | 31 ++++-- .../ui/widgets/WidgetPanelActivity.kt | 50 +++++++++ .../manage/ManageWidgetPanelsActivity.kt | 104 ++++++++++++++++++ .../widgets/manage/ManageWidgetsActivity.kt | 24 +++- .../ui/widgets/manage/SelectWidgetActivity.kt | 6 +- .../ui/widgets/manage/WidgetManagerView.kt | 11 +- .../ui/widgets/manage/WidgetOverlayView.kt | 7 +- .../manage/WidgetPanelsRecyclerAdapter.kt | 98 +++++++++++++++++ .../android/launcher/widgets/AppWidget.kt | 12 +- .../android/launcher/widgets/ClockWidget.kt | 1 + .../jrpie/android/launcher/widgets/Widget.kt | 5 + .../android/launcher/widgets/WidgetPanel.kt | 58 ++++++++++ .../jrpie/android/launcher/widgets/Widgets.kt | 16 ++- .../main/res/drawable/baseline_widgets_24.xml | 11 ++ .../layout/activity_manage_widget_panels.xml | 73 ++++++++++++ .../main/res/layout/activity_widget_panel.xml | 15 +++ .../res/layout/dialog_create_widget_panel.xml | 18 +++ .../res/layout/dialog_rename_widget_panel.xml | 18 +++ .../res/layout/dialog_select_widget_panel.xml | 26 +++++ .../res/layout/list_widget_panels_row.xml | 23 ++++ app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 19 ++++ app/src/main/res/xml/preferences.xml | 16 ++- 37 files changed, 807 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetPanelsRecyclerAdapter.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt create mode 100644 app/src/main/res/drawable/baseline_widgets_24.xml create mode 100644 app/src/main/res/layout/activity_manage_widget_panels.xml create mode 100644 app/src/main/res/layout/activity_widget_panel.xml create mode 100644 app/src/main/res/layout/dialog_create_widget_panel.xml create mode 100644 app/src/main/res/layout/dialog_rename_widget_panel.xml create mode 100644 app/src/main/res/layout/dialog_select_widget_panel.xml create mode 100644 app/src/main/res/layout/list_widget_panels_row.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 841c9bd..5a1d5a0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,9 +21,15 @@ android:theme="@style/launcherBaseTheme" tools:ignore="UnusedAttribute"> + + 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/WidgetPanelAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt new file mode 100644 index 0000000..d7829a6 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt @@ -0,0 +1,83 @@ +package de.jrpie.android.launcher.actions + +import android.content.Context +import android.content.Intent +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.View +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.ui.widgets.WidgetPanelActivity +import de.jrpie.android.launcher.ui.widgets.manage.EXTRA_PANEL_ID +import de.jrpie.android.launcher.ui.widgets.manage.WidgetPanelsRecyclerAdapter +import de.jrpie.android.launcher.widgets.WidgetPanel +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("action:panel") +class WidgetPanelAction(val widgetPanelId: Int) : Action { + + override fun invoke(context: Context, rect: Rect?): Boolean { + + if (WidgetPanel.byId(widgetPanelId) == null) { + Toast.makeText(context, R.string.alert_widget_panel_not_found, Toast.LENGTH_LONG).show() + } else { + context.startActivity(Intent(context, WidgetPanelActivity::class.java).also { + it.putExtra(EXTRA_PANEL_ID, widgetPanelId) + }) + } + return true + } + + override fun label(context: Context): String { + return WidgetPanel.byId(widgetPanelId)?.label + ?: context.getString(R.string.list_other_open_widget_panel) + } + + override fun isAvailable(context: Context): Boolean { + return true + } + + override fun canReachSettings(): Boolean { + return false + } + + override fun getIcon(context: Context): Drawable? { + return ResourcesCompat.getDrawable( + context.resources, + R.drawable.baseline_widgets_24, + context.theme + ) + } + + override fun showConfigurationDialog(context: Context, onSuccess: (Action) -> Unit) { + AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { + setTitle(R.string.dialog_select_widget_panel_title) + setNegativeButton(R.string.dialog_cancel) { _, _ -> } + setView(R.layout.dialog_select_widget_panel) + }.create().also { it.show() }.also { alertDialog -> + val infoTextView = + alertDialog.findViewById(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/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java index 575346a..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,7 @@ 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; @@ -84,7 +85,8 @@ import eu.jonahbauer.android.preference.annotations.Preferences; @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 = "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 332f4df..e8e717e 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 @@ -2,18 +2,21 @@ package de.jrpie.android.launcher.preferences import android.content.Context import android.util.Log +import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.actions.Action -import de.jrpie.android.launcher.apps.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.migratePreferencesFromVersion2 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3 +import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion4 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown import de.jrpie.android.launcher.ui.HomeActivity import de.jrpie.android.launcher.widgets.ClockWidget +import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.deleteAllWidgets @@ -21,7 +24,7 @@ import de.jrpie.android.launcher.widgets.deleteAllWidgets * Increase when breaking changes are introduced and write an appropriate case in * `migratePreferencesToNewVersion` */ -const val PREFERENCE_VERSION = 4 +const val PREFERENCE_VERSION = 5 const val UNKNOWN_PREFERENCE_VERSION = -1 private const val TAG = "Launcher - Preferences" @@ -43,18 +46,23 @@ 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}).") } + 4 -> { + migratePreferencesFromVersion4(context) + Log.i(TAG, "migration of preferences complete (4 -> ${PREFERENCE_VERSION}).") + } + else -> { Log.w( TAG, @@ -78,7 +86,11 @@ fun resetPreferences(context: Context) { LauncherPreferences.widgets().widgets( setOf( - ClockWidget(-500, WidgetPosition(1,4,10,3)) + ClockWidget( + (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + WidgetPosition(1, 3, 10, 4), + WidgetPanel.HOME.id + ) ) ) 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/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..d4c7441 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt @@ -0,0 +1,27 @@ +package de.jrpie.android.launcher.preferences.legacy + +import android.content.Context +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION +import de.jrpie.android.launcher.widgets.ClockWidget +import de.jrpie.android.launcher.widgets.WidgetPanel +import de.jrpie.android.launcher.widgets.WidgetPosition + +fun migratePreferencesFromVersion4(context: Context) { + assert(PREFERENCE_VERSION == 5) + assert(LauncherPreferences.internal().versionCode() == 4) + + LauncherPreferences.widgets().widgets( + setOf( + ClockWidget( + (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + WidgetPosition(1, 3, 10, 4), + WidgetPanel.HOME.id + ) + ) + ) + + + LauncherPreferences.internal().versionCode(5) +} \ 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 a2749ae..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 @@ -5,6 +5,7 @@ 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 @@ -46,6 +47,22 @@ class SetWidgetSerializer : } } +@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 : 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 3f1e497..192a8e9 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 @@ -207,7 +207,6 @@ class HomeActivity : UIObject, Activity() { } override fun onTouchEvent(event: MotionEvent): Boolean { - android.util.Log.e("Launcher", "on touch") touchGestureDetector?.onTouchEvent(event) return true } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt index f176469..06be78a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/other/OtherRecyclerAdapter.kt @@ -11,6 +11,7 @@ import de.jrpie.android.launcher.R import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.LauncherAction +import de.jrpie.android.launcher.actions.WidgetPanelAction import de.jrpie.android.launcher.ui.list.ListActivity /** @@ -23,8 +24,10 @@ import de.jrpie.android.launcher.ui.list.ListActivity class OtherRecyclerAdapter(val activity: Activity) : RecyclerView.Adapter() { - private val othersList: Array = - LauncherAction.entries.filter { it.isAvailable(activity) }.toTypedArray() + private val othersList: Array = + LauncherAction.entries.filter { it.isAvailable(activity) } + .plus(WidgetPanelAction(-1)) + .toTypedArray() inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { @@ -36,10 +39,12 @@ class OtherRecyclerAdapter(val activity: Activity) : val pos = bindingAdapterPosition val content = othersList[pos] - activity.finish() val gestureId = (activity as? ListActivity)?.forGesture ?: return val gesture = Gesture.byId(gestureId) ?: return - Action.setActionForGesture(gesture, content) + content.showConfigurationDialog(activity) { configuredAction -> + Action.setActionForGesture(gesture, configuredAction) + activity.finish() + } } init { @@ -48,11 +53,11 @@ class OtherRecyclerAdapter(val activity: Activity) : } override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { - val otherLabel = activity.getString(othersList[i].label) - val icon = othersList[i].icon + val otherLabel = othersList[i].label(activity) + val icon = othersList[i].getIcon(activity) viewHolder.textView.text = otherLabel - viewHolder.iconView.setImageResource(icon) + viewHolder.iconView.setImageDrawable(icon) } override fun getItemCount(): Int { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/launcher/SettingsFragmentLauncher.kt index 8907f04..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,7 @@ 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 @@ -90,6 +91,14 @@ class SettingsFragmentLauncher : PreferenceFragmentCompat() { 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/widgets/WidgetContainerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt index 04668ca..d071771 100644 --- 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 @@ -13,6 +13,7 @@ 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 @@ -20,22 +21,29 @@ import kotlin.math.max /** * This only works in an Activity, not AppCompatActivity */ -open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) : ViewGroup(context, attrs) { +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: Set?) { - if (widgets == null) { - return - } - Log.i("WidgetContainer", "updating ${activity.localClassName}") - widgetViewById.clear() - (0.. + 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, WidgetContainerView.Companion.LayoutParams(widget.position)) + addView(it, LayoutParams(widget.position)) widgetViewById.put(widget.id, it) } + } } } @@ -67,7 +75,6 @@ open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) : (0...onCreate(savedInstanceState) + super.onCreate() + widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.Companion.HOME.id) + val binding = ActivityWidgetPanelBinding.inflate(layoutInflater) + setContentView(binding.root) + binding.widgetPanelWidgetContainer.widgetPanelId = widgetPanelId + binding.widgetPanelWidgetContainer.updateWidgets( + this, + LauncherPreferences.widgets().widgets() + ) + } + + override fun getTheme(): Resources.Theme { + val mTheme = modifyTheme(super.getTheme()) + mTheme.applyStyle(R.style.backgroundWallpaper, true) + LauncherPreferences.clock().font().applyToTheme(mTheme) + LauncherPreferences.theme().colorTheme().applyToTheme( + mTheme, + LauncherPreferences.theme().textShadow() + ) + + return mTheme + } + + + override fun onStart() { + super.onStart() + super.onStart() + } + + 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..b18852f --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetPanelsActivity.kt @@ -0,0 +1,104 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.SharedPreferences +import android.content.res.Resources +import android.os.Bundle +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.databinding.ActivityManageWidgetPanelsBinding +import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.ui.UIObject +import de.jrpie.android.launcher.widgets.WidgetPanel +import de.jrpie.android.launcher.widgets.updateWidgetPanel + +class ManageWidgetPanelsActivity : AppCompatActivity(), UIObject { + + private val sharedPreferencesListener = + SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> + if (prefKey == LauncherPreferences.widgets().keys().customPanels()) { + viewAdapter.widgetPanels = + (LauncherPreferences.widgets().customPanels() ?: setOf()).toTypedArray() + + @SuppressLint("NotifyDataSetChanged") + viewAdapter.notifyDataSetChanged() + } + } + private lateinit var binding: ActivityManageWidgetPanelsBinding + private lateinit var viewAdapter: WidgetPanelsRecyclerAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.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 { + // improve performance (since content changes don't change the layout size) + setHasFixedSize(true) + layoutManager = viewManager + adapter = viewAdapter + } + binding.manageWidgetPanelsClose.setOnClickListener { finish() } + binding.manageWidgetPanelsAddPanel.setOnClickListener { + AlertDialog.Builder(this@ManageWidgetPanelsActivity, R.style.AlertDialogCustom).apply { + setTitle(R.string.dialog_create_widget_panel_title) + setNegativeButton(R.string.dialog_cancel) { _, _ -> } + setPositiveButton(R.string.dialog_ok) { dialogInterface, _ -> + val panelId = WidgetPanel.allocateId() + val label = (dialogInterface as? AlertDialog) + ?.findViewById(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() + ) + ) + } + true + } + } + + 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 index 3172401..d191b70 100644 --- 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 @@ -2,7 +2,6 @@ package de.jrpie.android.launcher.ui.widgets.manage import android.app.Activity import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProviderInfo import android.content.Intent import android.content.SharedPreferences import android.content.res.Resources @@ -18,6 +17,7 @@ import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.widgets.WidgetContainerView import de.jrpie.android.launcher.widgets.AppWidget +import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition import kotlin.math.min @@ -27,9 +27,13 @@ import kotlin.math.min const val REQUEST_CREATE_APPWIDGET = 1 const val REQUEST_PICK_APPWIDGET = 2 +const val EXTRA_PANEL_ID = "widgetPanelId" + // We can't use AppCompatActivity, since some AppWidgets don't work there. class ManageWidgetsActivity : Activity(), UIObject { + var panelId: Int = WidgetPanel.HOME.id + private var sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> if (prefKey == LauncherPreferences.widgets().keys().widgets()) { @@ -44,6 +48,9 @@ class ManageWidgetsActivity : Activity(), UIObject { super.onCreate(savedInstanceState) super.onCreate() setContentView(R.layout.activity_manage_widgets) + + panelId = intent.extras?.getInt(EXTRA_PANEL_ID, WidgetPanel.HOME.id) ?: WidgetPanel.HOME.id + findViewById(R.id.manage_widgets_button_add).setOnClickListener { selectWidget() } @@ -54,9 +61,10 @@ class ManageWidgetsActivity : Activity(), UIObject { insets } - findViewById(R.id.manage_widgets_container).updateWidgets(this, - (application as Application).widgets.value - ) + findViewById(R.id.manage_widgets_container).let { + it.widgetPanelId = panelId + it.updateWidgets(this, (application as Application).widgets.value) + } } override fun onStart() { @@ -101,6 +109,10 @@ class ManageWidgetsActivity : Activity(), UIObject { AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetHost.allocateAppWidgetId() ) + it.putExtra( + EXTRA_PANEL_ID, + panelId + ) }, REQUEST_PICK_APPWIDGET ) } @@ -124,7 +136,7 @@ class ManageWidgetsActivity : Activity(), UIObject { display.height ) - val widget = AppWidget(appWidgetId, provider, position) + val widget = AppWidget(appWidgetId, position, panelId, provider) LauncherPreferences.widgets().widgets( (LauncherPreferences.widgets().widgets() ?: HashSet()).also { it.add(widget) @@ -135,7 +147,7 @@ class ManageWidgetsActivity : Activity(), UIObject { private fun configureWidget(data: Intent) { val extras = data.extras val appWidgetId = extras!!.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) - val widget = AppWidget(appWidgetId) + val widget = AppWidget(appWidgetId, panelId = panelId) if (widget.isConfigurable(this)) { widget.configure(this, REQUEST_CREATE_APPWIDGET) } else { 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 index c414db6..a1bd3b5 100644 --- 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 @@ -21,6 +21,7 @@ import de.jrpie.android.launcher.widgets.ClockWidget import de.jrpie.android.launcher.widgets.LauncherAppWidgetProvider import de.jrpie.android.launcher.widgets.LauncherClockWidgetProvider import de.jrpie.android.launcher.widgets.LauncherWidgetProvider +import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission import de.jrpie.android.launcher.widgets.getAppWidgetHost @@ -38,6 +39,7 @@ private const val REQUEST_WIDGET_PERMISSION = 29 class SelectWidgetActivity : AppCompatActivity(), UIObject { lateinit var binding: ActivitySelectWidgetBinding var widgetId: Int = -1 + var widgetPanelId: Int = WidgetPanel.HOME.id private fun tryBindWidget(info: LauncherWidgetProvider) { when (info) { @@ -53,13 +55,14 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { RESULT_OK, Intent().also { it.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) + it.putExtra(EXTRA_PANEL_ID, widgetPanelId) } ) finish() } } is LauncherClockWidgetProvider -> { - updateWidget(ClockWidget(widgetId, WidgetPosition(0,4,12,3))) + updateWidget(ClockWidget(widgetId, WidgetPosition(0, 4, 12, 3), widgetPanelId)) finish() } } @@ -79,6 +82,7 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.HOME.id) if (widgetId == -1) { widgetId = getAppWidgetHost().allocateAppWidgetId() } 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 index 2d41e13..7a355f7 100644 --- 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 @@ -20,6 +20,7 @@ import androidx.core.graphics.toRect import androidx.core.view.children import de.jrpie.android.launcher.ui.widgets.WidgetContainerView import de.jrpie.android.launcher.widgets.Widget +import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.updateWidget import kotlin.math.max @@ -28,8 +29,9 @@ import kotlin.math.min /** * A variant of the [WidgetContainerView] which allows to manage widgets. */ -class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : - WidgetContainerView(context, attrs) { +class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSet? = null) : + WidgetContainerView(widgetPanelId, context, attrs) { + constructor(context: Context, attrs: AttributeSet?) : this(WidgetPanel.HOME.id, context, attrs) val TOUCH_SLOP: Int val TOUCH_SLOP_SQUARE: Int @@ -155,13 +157,14 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : selectedWidgetOverlayView?.mode = null } - override fun updateWidgets(activity: Activity, widgets: Set?) { + override fun updateWidgets(activity: Activity, widgets: Collection?) { super.updateWidgets(activity, widgets) if (widgets == null) { return } + children.mapNotNull { it as? WidgetOverlayView }.forEach { removeView(it) } - widgets.forEach { widget -> + widgets.filter { it.panelId == widgetPanelId }.forEach { widget -> WidgetOverlayView(activity).let { addView(it) it.widgetId = widget.id 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 index 0ce789f..1b8a2d2 100644 --- 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 @@ -13,12 +13,13 @@ import de.jrpie.android.launcher.R import de.jrpie.android.launcher.widgets.Widget import de.jrpie.android.launcher.widgets.updateWidget -/** - * An overlay to show configuration options for a widget. - */ private const val HANDLE_SIZE = 100 private const val HANDLE_EDGE_SIZE = (1.2 * HANDLE_SIZE).toInt() + +/** + * An overlay to show configuration options for a widget in [WidgetManagerView] + */ class WidgetOverlayView : View { 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..40c2c2f --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetPanelsRecyclerAdapter.kt @@ -0,0 +1,98 @@ +package de.jrpie.android.launcher.ui.widgets.manage + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import android.widget.PopupMenu +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.recyclerview.widget.RecyclerView +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.widgets.WidgetPanel +import de.jrpie.android.launcher.widgets.updateWidgetPanel + + +class WidgetPanelsRecyclerAdapter( + val context: Context, + val showMenu: Boolean = false, + val onSelectWidgetPanel: (WidgetPanel) -> Unit +) : + RecyclerView.Adapter() { + + var widgetPanels = (LauncherPreferences.widgets().customPanels() ?: setOf()).toTypedArray() + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var labelView: TextView = itemView.findViewById(R.id.list_widget_panels_label) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { + viewHolder.labelView.text = widgetPanels[i].label + + viewHolder.itemView.setOnClickListener { + onSelectWidgetPanel(widgetPanels[i]) + } + + if (showMenu) { + viewHolder.itemView.setOnLongClickListener { + showOptionsPopup( + viewHolder, + widgetPanels[i] + ) + } + } + } + + @Suppress("SameReturnValue") + private fun showOptionsPopup( + viewHolder: ViewHolder, + widgetPanel: WidgetPanel + ): Boolean { + //create the popup menu + + val popup = PopupMenu(context, viewHolder.labelView) + popup.menu.add(R.string.manage_widget_panels_delete).setOnMenuItemClickListener { _ -> + widgetPanel.delete(context) + true + } + popup.menu.add(R.string.manage_widget_panels_rename).setOnMenuItemClickListener { _ -> + AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { + setNegativeButton(R.string.dialog_cancel) { _, _ -> } + setPositiveButton(R.string.dialog_ok) { dialogInterface, _ -> + var newLabel = (dialogInterface as? AlertDialog) + ?.findViewById(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 index 3e9a2eb..22a63eb 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt @@ -1,4 +1,4 @@ -package de.jrpie.android.launcher.widgets; +package de.jrpie.android.launcher.widgets import android.app.Activity import android.appwidget.AppWidgetHostView @@ -11,7 +11,6 @@ import android.util.DisplayMetrics import android.util.SizeF import android.view.View import de.jrpie.android.launcher.Application -import de.jrpie.android.launcher.ui.HomeActivity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -20,6 +19,7 @@ import kotlinx.serialization.Serializable 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 @@ -31,10 +31,16 @@ class AppWidget( ): Widget() { - constructor(id: Int, widgetProviderInfo: AppWidgetProviderInfo, position: WidgetPosition) : + constructor( + id: Int, + position: WidgetPosition, + panelId: Int, + widgetProviderInfo: AppWidgetProviderInfo + ) : this( id, position, + panelId, false, widgetProviderInfo.provider.packageName, widgetProviderInfo.provider.className, 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 index d819538..d0d1c0e 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt @@ -14,6 +14,7 @@ import kotlinx.serialization.Serializable class ClockWidget( override val id: Int, override var position: WidgetPosition, + override val panelId: Int, override var allowInteraction: Boolean = true ) : Widget() { 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 index d3610dd..dbe667b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt @@ -14,6 +14,7 @@ import kotlinx.serialization.json.Json sealed class Widget { abstract val id: Int abstract var position: WidgetPosition + abstract val panelId: Int abstract var allowInteraction: Boolean /** @@ -36,6 +37,10 @@ sealed class Widget { ) } + fun getPanel(): WidgetPanel? { + return WidgetPanel.byId(panelId) + } + override fun hashCode(): Int { return 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..93e588d --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetPanel.kt @@ -0,0 +1,58 @@ +package de.jrpie.android.launcher.widgets + +import android.content.Context +import de.jrpie.android.launcher.preferences.LauncherPreferences +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + + +@Serializable +@SerialName("panel") +class WidgetPanel(val id: Int, var label: String) { + + override fun equals(other: Any?): Boolean { + return (other as? WidgetPanel)?.id == id + } + + override fun hashCode(): Int { + return id + } + + fun serialize(): String { + return Json.encodeToString(this) + } + + fun delete(context: Context) { + LauncherPreferences.widgets().customPanels( + (LauncherPreferences.widgets().customPanels() ?: setOf()).minus(this) + ) + (LauncherPreferences.widgets().widgets() ?: return) + .filter { it.panelId == this.id }.forEach { it.delete(context) } + } + + + companion object { + val HOME = WidgetPanel(0, "home") + fun byId(id: Int): WidgetPanel? { + if (id == 0) { + return HOME + } + return LauncherPreferences.widgets().customPanels()?.firstOrNull { it.id == id } + } + + fun allocateId(): Int { + return ( + (LauncherPreferences.widgets().customPanels() ?: setOf()) + .plus(HOME) + .maxOfOrNull { it.id } ?: 0 + ) + 1 + } + + fun deserialize(serialized: String): WidgetPanel { + return Json.decodeFromString(serialized) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt index cd4ef29..b7f140b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -74,9 +74,19 @@ fun getAppWidgetProviders( context: Context ): List { fun updateWidget(widget: Widget) { - var widgets = LauncherPreferences.widgets().widgets() ?: setOf() - widgets = widgets.minus(widget).plus(widget) - LauncherPreferences.widgets().widgets(widgets) + LauncherPreferences.widgets().widgets( + (LauncherPreferences.widgets().widgets() ?: setOf()) + .minus(widget) + .plus(widget) + ) +} + +fun updateWidgetPanel(widgetPanel: WidgetPanel) { + LauncherPreferences.widgets().customPanels( + (LauncherPreferences.widgets().customPanels() ?: setOf()) + .minus(widgetPanel) + .plus(widgetPanel) + ) } fun Context.getAppWidgetHost(): AppWidgetHost { 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_manage_widget_panels.xml b/app/src/main/res/layout/activity_manage_widget_panels.xml new file mode 100644 index 0000000..60413a3 --- /dev/null +++ b/app/src/main/res/layout/activity_manage_widget_panels.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + 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_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_widget_panels_row.xml b/app/src/main/res/layout/list_widget_panels_row.xml new file mode 100644 index 0000000..53f7449 --- /dev/null +++ b/app/src/main/res/layout/list_widget_panels_row.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 30e4cda..f783d2a 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -10,6 +10,7 @@ internal.first_startup internal.version_code widgets.widgets + widgets.custom_panels apps.favorites apps.hidden apps.pinned_shortcuts diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8813f8..7d29128 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -99,6 +99,7 @@ Click on time Manage widgets + Manage widget panels Choose App @@ -400,5 +401,23 @@ Clock The default clock of μLauncher + Delete + Rename + + Widget Panel #%1$d + + Contains %d widget. + Contains %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 diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b4bc5f0..0ee7c17 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -3,14 +3,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + - - - + + + + Date: Tue, 29 Apr 2025 22:19:08 -0500 Subject: [PATCH 07/93] add wiki --- .gitmodules | 3 +++ launcher.wiki | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 launcher.wiki diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f1aceb4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "launcher.wiki"] + path = launcher.wiki + url = https://github.com/wassupluke/Launcher.wiki.git diff --git a/launcher.wiki b/launcher.wiki new file mode 160000 index 0000000..d40df78 --- /dev/null +++ b/launcher.wiki @@ -0,0 +1 @@ +Subproject commit d40df78a56336aa0ae26ad1308e4aac48456dff8 From f6155102560d140f72e9e5c77937166a0f157aba Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Wed, 30 Apr 2025 20:26:05 -0500 Subject: [PATCH 08/93] migrate wiki to docs directory --- docs/{build.md => Building-from-Source.md} | 21 +- ...cher.md => Changes-from-Finns-Launcher.md} | 0 docs/Contributing.md | 10 + docs/Gestures-and-Bindings.md | 47 ++++ docs/Home.md | 37 +++ docs/Launcher-Settings.md | 222 ++++++++++++++++++ docs/Work-Profile.md | 3 + launcher.wiki | 1 - 8 files changed, 331 insertions(+), 10 deletions(-) rename docs/{build.md => Building-from-Source.md} (76%) rename docs/{launcher.md => Changes-from-Finns-Launcher.md} (100%) create mode 100644 docs/Contributing.md create mode 100644 docs/Gestures-and-Bindings.md create mode 100644 docs/Home.md create mode 100644 docs/Launcher-Settings.md create mode 100644 docs/Work-Profile.md delete mode 160000 launcher.wiki diff --git a/docs/build.md b/docs/Building-from-Source.md similarity index 76% rename from docs/build.md rename to docs/Building-from-Source.md index 75921f9..3c2ef1f 100644 --- a/docs/build.md +++ b/docs/Building-from-Source.md @@ -1,11 +1,9 @@ -# Building µLauncher +# Using the command line -## 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 @@ -15,7 +13,7 @@ cd Launcher 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 +26,18 @@ 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 +# 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. +for further instructions. How to + +# CI Pipeline + +The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds. +> Note: These builds are *not* signed. diff --git a/docs/launcher.md b/docs/Changes-from-Finns-Launcher.md similarity index 100% rename from docs/launcher.md rename to docs/Changes-from-Finns-Launcher.md diff --git a/docs/Contributing.md b/docs/Contributing.md new file mode 100644 index 0000000..bd56a7a --- /dev/null +++ b/docs/Contributing.md @@ -0,0 +1,10 @@ +# Ways to contribute + +- Found a **bug** or have an idea for a **new feature**? [Join the chat](https://s.jrpie.de/launcher-chat) or open an [issue](https://github.com/jrpie/Launcher/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. +- Implement a new feature yourself: + 1. [Fork](https://github.com/jrpie/launcher/fork) this repository. + 2. Create a **new branch** named *feature/\* or *fix/\* and commit your changes. + 3. Open a new pull request. +- Add or improve [translations](https://toolate.othing.xyz/projects/jrpie-launcher/). +translation status \ No newline at end of file diff --git a/docs/Gestures-and-Bindings.md b/docs/Gestures-and-Bindings.md new file mode 100644 index 0000000..d87a406 --- /dev/null +++ b/docs/Gestures-and-Bindings.md @@ -0,0 +1,47 @@ +# Available Gestures +## Swipes +- Up, down, left, or right. +- Up, down, left, or right with two fingers. +- Up or down on the left or right edge. +- Left or right on the top or bottom edge. + +## Taps +- Tap on date or time. +- Double tap. +- Long tap. + +## Tap-then-Swipes +tap then swipe up, down, left, or right + +## Complex Gestures +- Draw <, >, V, or Λ. +- Draw <, >, V, or Λ in reverse direction. + +## Hardware Buttons as Gestures +- Back button. +- Volume up or down button. + + +*** + + +# Available Bindings +Any of the above gestures can be bound to any of the following bindings. +## Launcher Bindings +- Open µLauncher settings. +- Open a list of all*, favorite, or private* apps. + +## App Bindings +- Launch an app. +- Launch another Home Screen. + +## Android / Device Bindings +- Toggle private space lock. +- Lock the screen. +- Toggle the torch (flashlight) +- Raise or lower volume. +- Skip to previous or next audio track. +- Open Notifications shade. +- Open Quick Settings shade. + +> \* excludes apps hidden by the user \ No newline at end of file diff --git a/docs/Home.md b/docs/Home.md new file mode 100644 index 0000000..8ce432c --- /dev/null +++ b/docs/Home.md @@ -0,0 +1,37 @@ +# Welcome to the μLauncher wiki! +We're thrilled you're here and confident you'll love your new Android launcher! Check out this wiki to get familiar with your new app. + +## What is μLauncher? +µLauncher is an *minimal* and *distraction-free* Android home screen that lets you launch apps using *efficient* swipe gestures and button presses. This project is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). An incomplete list of changes can be found [here](https://github.com/wassupluke/Launcher/blob/master/docs/launcher.md). + +## Where can I get μLauncher? +Get it on F-Droid +Get it on Accrescent +Get it on Obtainium +Get it on GitHub + +> You can also [get it on Google Play](https://play.google.com/store/apps/details?id=de.jrpie.android.launcher), but this is not recommend. + +## Screenshots +μLauncher Home Screen screenshot +μLauncher Settings screenshot +μLauncher All Apps list view with icons screenshot +μLauncher Favorite Apps list view with icons screenshot +μLauncher Choose App to bind to gesture screenshot +μLauncher App options card from list view with icons screenshot +μLauncher All Apps list view without icons screenshot + \ No newline at end of file diff --git a/docs/Launcher-Settings.md b/docs/Launcher-Settings.md new file mode 100644 index 0000000..ac665fa --- /dev/null +++ b/docs/Launcher-Settings.md @@ -0,0 +1,222 @@ +Tweaks and customizations can be made from within the Launcher Settings page. + +These settings let you change wallpapers, change colors and fonts, enable monochrome app icons, change the app drawer layout, and much more. + +In the following documentation, 'app drawer' will be used to refer to the 'All Apps' and 'Favorite Apps' views. + + +# Appearance + +### Choose a wallpaper +Lets you change the wallpaper using a photos app, file explorer, or native wallpaper setting app. + + +> ### Font + +Set the font used within the app settings. This setting does not affect the date/time [home screen font](https://github.com/wassupluke/Launcher/wiki/Tweaks-and-Customizations/_edit#font-1). + +**type:** `dropdown` + +**options:** `Hack`,`System default`,`Sans serif`,`Serif`,`Monospace`,`Serif monospace` + +> ### Text Shadow + +**type:** `toggle` + +> ### Background (app list and setting) + +**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 + +> ### Font + +Set the home screen font for date and time. This setting does not affect the [app settings font](https://github.com/wassupluke/Launcher/wiki/Tweaks-and-Customizations/_edit#font). + +**type:** `dropdown` + +**options:** `Hack`,`System default`,`Sans serif`,`Serif`,`Monospace`,`Serif monospace` + +> ### Color [`[bug]`](https://github.com/jrpie/launcher/issues/151) + +Set the color for the home screen date and time. + +Accepts a HEX color code (consisting of a '#' followed by three sets of two alphanumeric (letters and numbers) characters. A fourth set of two alphanumeric characters may be added to set the transparency of the color. + +[Color wheel picker](https://rgbacolorpicker.com/color-wheel-picker) + +**type:** `HEX`,`RGBA` + +> ### Use localized date format + +Adapt the display of dates and times to the specific conventions of a particular locale or region. 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 user 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. For example, if you type, `a`, the app list narrows to any apps with a title containing the letter `a`. Continuing the example, if you then follow your `a` with the letter `m`, the list now shows only apps containing the letter combination `am` in that order. If the only app matching this combination was, for example, `Amazon`, simply typing `am` in the app drawer would immediately launch the `Amazon` app for you. + +This feature becomes more powerful when combined with [renaming](https://github.com/wassupluke/Launcher/wiki/Launcher-Settings/_edit#additional-settings) apps, effectively letting you define custom app names that could be considered 'aliases' or shortcuts. For instance, if you wanted to "bind" 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](https://github.com/wassupluke/Launcher/wiki/Launcher-Settings/_edit#search-the-web) feature. + +**type:** `toggle` + +> ### Search the web + +Press return/enter 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 actions + +Enable double swipe (two finger) actions as bindable gestures in launcher settings. Does not erase gesture bindings if accidentally turned off. + +**type:** `toggle` + +> ### Edge swipe actions + +Enable edge swipe (near edges of screen) actions as bindable 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 on a random app. Always review the [source code](https://github.com/jrpie/Launcher) before granting accessibility permissions so you 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 app drawer. + +**type:** `toggle` + +> ### Hide paused apps + +Remove paused apps from the app drawer. + +**type:** `toggle` + +> ### Hide private space from app list + +Remove private space from app drawer. + +**type:** `toggle` + +> ### Layout of app list + +Change how the apps are displayed when accessing the app drawer. By `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. `Grid` shows apps with their app icon and title in a grid layout. + +**type:** `dropdown` + +**options:** `Default`,`Text`,`Grid` + +> ### Reverse the app list + +Enable Z-A 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 setup properly. + +**type:** `toggle` + + +# Additional Settings + +> ### App Drawer Long Press on App + +Access additional per-app details and settings. To use, open the app drawer and long press on any app. + +**type:** `dropdown` + +**options:** `App Info`,`Add to favorites`,`Hide`,`Rename`,`Uninstall` \ No newline at end of file diff --git a/docs/Work-Profile.md b/docs/Work-Profile.md new file mode 100644 index 0000000..38c6598 --- /dev/null +++ b/docs/Work-Profile.md @@ -0,0 +1,3 @@ +# Android Enterprise 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. \ No newline at end of file diff --git a/launcher.wiki b/launcher.wiki deleted file mode 160000 index d40df78..0000000 --- a/launcher.wiki +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d40df78a56336aa0ae26ad1308e4aac48456dff8 From b2b823446f03c6d66c7e45c8088f2e9e4644d18e Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Wed, 30 Apr 2025 20:34:06 -0500 Subject: [PATCH 09/93] remove stray .gitmodules after wiki deletion --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index f1aceb4..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "launcher.wiki"] - path = launcher.wiki - url = https://github.com/wassupluke/Launcher.wiki.git From 8f9f8ac928697939ec09be78bdaac0f8c2e143d4 Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Wed, 30 Apr 2025 22:34:28 -0500 Subject: [PATCH 10/93] improve codestyle --- docs/Building-from-Source.md | 10 +++-- docs/Changes-from-Finns-Launcher.md | 27 +++++++------ docs/Contributing.md | 11 +++++- docs/Gestures-and-Bindings.md | 37 +++++++++++------- docs/Home.md | 59 +++++++++++++++-------------- docs/Launcher-Settings.md | 49 ++++++++++++------------ docs/Work-Profile.md | 3 +- 7 files changed, 111 insertions(+), 85 deletions(-) diff --git a/docs/Building-from-Source.md b/docs/Building-from-Source.md index 3c2ef1f..fc8de3d 100644 --- a/docs/Building-from-Source.md +++ b/docs/Building-from-Source.md @@ -1,4 +1,6 @@ -# Using the command line +# Building from Source + +## Using the command line Install JDK 17 and the Android SDK. Make sure that `JAVA_HOME` and `ANDROID_HOME` are set correctly. @@ -13,6 +15,7 @@ cd Launcher 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" \ @@ -29,15 +32,14 @@ apksigner sign --ks "$YOUR_KEYSTORE" \ See [this guide](https://developer.android.com/build/building-cmdline) for further instructions. - -# Using Android Studio +## 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. How to -# CI Pipeline +## CI Pipeline The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds. > Note: These builds are *not* signed. diff --git a/docs/Changes-from-Finns-Launcher.md b/docs/Changes-from-Finns-Launcher.md index cb290a0..f08678d 100644 --- a/docs/Changes-from-Finns-Launcher.md +++ b/docs/Changes-from-Finns-Launcher.md @@ -1,17 +1,17 @@ -# Notable changes compared to [Finn's Launcher][original-repo]: +# Notable changes compared to Finn's Launcher -µLauncher is a fork of [finnmglas's app Launcher][original-repo]. +µ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) - Option to rename apps @@ -23,18 +23,20 @@ The decision to create a hard fork was made two years later.--> - 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. +## 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 bar was moved to the bottom of the screen. -### Technical +## 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. @@ -42,9 +44,10 @@ The decision to create a hard fork was made two years later.--> - 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 index bd56a7a..cc79e87 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -1,10 +1,17 @@ -# Ways to contribute +# Contributing + +## Ways to contribute - Found a **bug** or have an idea for a **new feature**? [Join the chat](https://s.jrpie.de/launcher-chat) or open an [issue](https://github.com/jrpie/Launcher/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. + - Implement a new feature yourself: + 1. [Fork](https://github.com/jrpie/launcher/fork) this repository. 2. Create a **new branch** named *feature/\* or *fix/\* and commit your changes. 3. Open a new pull request. + - Add or improve [translations](https://toolate.othing.xyz/projects/jrpie-launcher/). -translation status \ No newline at end of file + +![translation status](https://toolate.othing.xyz/widget/jrpie-launcher/launcher/horizontal-auto.svg) diff --git a/docs/Gestures-and-Bindings.md b/docs/Gestures-and-Bindings.md index d87a406..09ff172 100644 --- a/docs/Gestures-and-Bindings.md +++ b/docs/Gestures-and-Bindings.md @@ -1,41 +1,52 @@ -# Available Gestures -## Swipes +# Gestures and Bindings + +## Available Gestures + +### Swipes + - Up, down, left, or right. - Up, down, left, or right with two fingers. - Up or down on the left or right edge. - Left or right on the top or bottom edge. -## Taps +### Taps + - Tap on date or time. - Double tap. - Long tap. -## Tap-then-Swipes -tap then swipe up, down, left, or right +### Tap-then-Swipes + +Tap then swipe up, down, left, or right + +### Complex Gestures -## Complex Gestures - Draw <, >, V, or Λ. - Draw <, >, V, or Λ in reverse direction. -## Hardware Buttons as Gestures +### Hardware Buttons as Gestures + - Back button. - Volume up or down button. - *** +## Available Bindings -# Available Bindings Any of the above gestures can be bound to any of the following bindings. -## Launcher Bindings + +### Launcher Bindings + - Open µLauncher settings. - Open a list of all*, favorite, or private* apps. -## App Bindings +### App Bindings + - Launch an app. - Launch another Home Screen. -## Android / Device Bindings +### Android / Device Bindings + - Toggle private space lock. - Lock the screen. - Toggle the torch (flashlight) @@ -44,4 +55,4 @@ Any of the above gestures can be bound to any of the following bindings. - Open Notifications shade. - Open Quick Settings shade. -> \* excludes apps hidden by the user \ No newline at end of file +> \* excludes apps hidden by the user diff --git a/docs/Home.md b/docs/Home.md index 8ce432c..2cb6093 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -1,37 +1,38 @@ -# Welcome to the μLauncher wiki! +# Welcome to the μLauncher wiki + We're thrilled you're here and confident you'll love your new Android launcher! Check out this wiki to get familiar with your new app. -## What is μLauncher? +## What is μLauncher + µLauncher is an *minimal* and *distraction-free* Android home screen that lets you launch apps using *efficient* swipe gestures and button presses. This project is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). An incomplete list of changes can be found [here](https://github.com/wassupluke/Launcher/blob/master/docs/launcher.md). -## Where can I get μLauncher? -Get it on F-Droid -Get it on Accrescent -Get it on Obtainium -Get it on GitHub +## Where can I get μLauncher + +[![Get it on F-Droid](https://fdroid.gitlab.io/artwork/badge/get-it-on.png)](https://f-droid.org/packages/de.jrpie.android.launcher/) + +[![Get it on Accrescent](https://accrescent.app/badges/get-it-on.png)](https://accrescent.app/app/de.jrpie.android.launcher.accrescent) + +[![Get it on Obtainium](https://raw.githubusercontent.com/ImranR98/Obtainium/b1c8ac6f2ab08497189721a788a5763e28ff64cd/assets/graphics/badge_obtainium.png)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/{%22id%22:%22de.jrpie.android.launcher%22,%22url%22:%22https://github.com/jrpie/Launcher%22,%22author%22:%22jrpie%22,%22name%22:%22%c2%b5Launcher%22,%22additionalSettings%22:%22{\%22apkFilterRegEx\%22:\%22release\%22,\%22invertAPKFilter\%22:false,\%22about\%22:\%22%c2%b5Launcher%20is%20a%20minimal%20home%20screen.\%22}%22}) + +[![Get it on GitHub](https://raw.githubusercontent.com/NeoApplications/Neo-Backup/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png)](https://github.com/jrpie/launcher/releases/latest) > You can also [get it on Google Play](https://play.google.com/store/apps/details?id=de.jrpie.android.launcher), but this is not recommend. ## Screenshots -μLauncher Home Screen screenshot -μLauncher Settings screenshot -μLauncher All Apps list view with icons screenshot -μLauncher Favorite Apps list view with icons screenshot -μLauncher Choose App to bind to gesture screenshot -μLauncher App options card from list view with icons screenshot -μLauncher All Apps list view without icons screenshot - \ No newline at end of file + +![μLauncher Home Screen screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg) + +![μLauncher Settings screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg) + +![μLauncher All Apps list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg) + +![μLauncher Favorite Apps list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg) + +![μLauncher Choose App to bind to gesture screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg) + +![μLauncher App options card from list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/7.jpg +) + +![μLauncher All Apps list view without icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/8.jpg) + + diff --git a/docs/Launcher-Settings.md b/docs/Launcher-Settings.md index ac665fa..1444c73 100644 --- a/docs/Launcher-Settings.md +++ b/docs/Launcher-Settings.md @@ -1,19 +1,20 @@ +# Launcher Settings + Tweaks and customizations can be made from within the Launcher Settings page. These settings let you change wallpapers, change colors and fonts, enable monochrome app icons, change the app drawer layout, and much more. In the following documentation, 'app drawer' will be used to refer to the 'All Apps' and 'Favorite Apps' views. +## Appearance -# Appearance +> ### Choose a wallpaper -### Choose a wallpaper Lets you change the wallpaper using a photos app, file explorer, or native wallpaper setting app. - -> ### Font +> ### Font (in-app font) -Set the font used within the app settings. This setting does not affect the date/time [home screen font](https://github.com/wassupluke/Launcher/wiki/Tweaks-and-Customizations/_edit#font-1). +Set the font used within the app settings. This setting does not affect the date/time home screen font. **type:** `dropdown` @@ -35,12 +36,11 @@ Remove coloring from all app icons. Can help decrease visual stimulus when enabl **type:** `toggle` +## Date & Time -# Date & Time +> ### Font (home screen) -> ### Font - -Set the home screen font for date and time. This setting does not affect the [app settings font](https://github.com/wassupluke/Launcher/wiki/Tweaks-and-Customizations/_edit#font). +Set the home screen font for date and time. This setting does not affect the in-app font. **type:** `dropdown` @@ -86,8 +86,7 @@ Place the current time above the current date on the home screen. **type:** `toggle` - -# Functionality +## Functionality > ### Launch search results @@ -134,20 +133,24 @@ Change how large a margin is used for detecting edge gestures. Shows the edge ma > ### 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. + + - 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 on a random app. Always review the [source code](https://github.com/jrpie/Launcher) before granting accessibility permissions so you 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. + + - 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 on a random app. Always review the [source code](https://github.com/jrpie/Launcher) before granting accessibility permissions so you 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` + **type:** `text buttons` -**options:** `USE DEVICE ADMIN`,`USE ACCESSIBILITY SERVICE` + **options:** `USE DEVICE ADMIN`,`USE ACCESSIBILITY SERVICE` -# Apps +## Apps > ### Hidden apps @@ -187,8 +190,7 @@ Enable Z-A sorting of apps in the app drawer. Useful for keeping apps within eas **type:** `toggle` - -# Display +## Display > ### Rotate screen @@ -210,8 +212,7 @@ Remove the navigation bar from the home screen. Enabling this setting may make i **type:** `toggle` - -# Additional Settings +## Additional Settings > ### App Drawer Long Press on App @@ -219,4 +220,4 @@ Access additional per-app details and settings. To use, open the app drawer and **type:** `dropdown` -**options:** `App Info`,`Add to favorites`,`Hide`,`Rename`,`Uninstall` \ No newline at end of file +**options:** `App Info`,`Add to favorites`,`Hide`,`Rename`,`Uninstall` diff --git a/docs/Work-Profile.md b/docs/Work-Profile.md index 38c6598..718fe28 100644 --- a/docs/Work-Profile.md +++ b/docs/Work-Profile.md @@ -1,3 +1,4 @@ # Android Enterprise 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. \ No newline at end of file +µ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. + From 6d271970fe082a3aec726dfe4f6f418966d376ba Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Wed, 30 Apr 2025 22:39:03 -0500 Subject: [PATCH 11/93] fix links --- docs/Changes-from-Finns-Launcher.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Changes-from-Finns-Launcher.md b/docs/Changes-from-Finns-Launcher.md index f08678d..30ae32b 100644 --- a/docs/Changes-from-Finns-Launcher.md +++ b/docs/Changes-from-Finns-Launcher.md @@ -48,6 +48,6 @@ The complete list of changes can be viewed [here](https://github.com/jrpie/launc --- -[original-repo]: https://github.com/finnmglas/Launcher +\[original-repo\]: [https://github.com/finnmglas/Launcher](https://github.com/finnmglas/Launcher) -[hack-font]: https://sourcefoundry.org/hack/ +\[hack-font\]: [https://sourcefoundry.org/hack/](https://sourcefoundry.org/hack/) From 106254664df7a9d90328f25c077070a3a951c532 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Fri, 9 May 2025 21:31:58 +0200 Subject: [PATCH 12/93] fix #151 - move alpha slider to the top to clarify that the format is ARGB not RGBA --- .../main/res/layout/dialog_choose_color.xml | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) 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 From 49785e66f2e11fb7761ad4293ced78d140287bd1 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Fri, 9 May 2025 23:54:23 +0200 Subject: [PATCH 13/93] improved documentation --- .scripts/release.sh | 19 ++++- app/src/main/res/values/strings.xml | 4 +- docs/Contributing.md | 17 ----- docs/Gestures-and-Bindings.md | 58 --------------- docs/Work-Profile.md | 4 -- docs/actions-and-gestures.md | 71 +++++++++++++++++++ docs/{Building-from-Source.md => build.md} | 2 +- ...from-Finns-Launcher.md => changes-fork.md} | 5 +- docs/contributing.md | 24 +++++++ docs/{Home.md => home.md} | 15 ++-- docs/profiles.md | 21 ++++++ docs/{Launcher-Settings.md => settings.md} | 34 ++++++--- 12 files changed, 173 insertions(+), 101 deletions(-) delete mode 100644 docs/Contributing.md delete mode 100644 docs/Gestures-and-Bindings.md delete mode 100644 docs/Work-Profile.md create mode 100644 docs/actions-and-gestures.md rename docs/{Building-from-Source.md => build.md} (92%) rename docs/{Changes-from-Finns-Launcher.md => changes-fork.md} (89%) create mode 100644 docs/contributing.md rename docs/{Home.md => home.md} (82%) create mode 100644 docs/profiles.md rename docs/{Launcher-Settings.md => settings.md} (72%) diff --git a/.scripts/release.sh b/.scripts/release.sh index f207c87..dc6959d 100755 --- a/.scripts/release.sh +++ b/.scripts/release.sh @@ -1,9 +1,25 @@ #!/bin/bash + +# This script builds all variants of µLauncher to create a release, namely: +# - app-release.apk (GitHub release; used by F-Droid for reproducible builds) +# - launcher-accrescent.apks (Accrescent) +# - app-release.aab (Play Store) + +# This is only intended to work on my (@jrpie) computer. +# To use this script for building a fork you need to: +# - install bundletool.jar and +# - create a keystore and modify the variables below accordingly + export JAVA_HOME="/usr/lib/jvm/java-21-openjdk/" OUTPUT_DIR="$HOME/launcher-release" BUILD_TOOLS_DIR="$HOME/Android/Sdk/build-tools/35.0.0" + +# keystore for the default release KEYSTORE="$HOME/data/keys/launcher_jrpie.jks" +# keystore for the default accrescent release KEYSTORE_ACCRESCENT="$HOME/data/keys/launcher_jrpie_accrescent.jks" + +# keepassxc-password is a custom script to fetch passwords from my password manager KEYSTORE_PASS=$(keepassxc-password "android_keys/launcher") KEYSTORE_ACCRESCENT_PASS=$(keepassxc-password "android_keys/launcher-accrescent") @@ -11,12 +27,11 @@ if [[ $(git status --porcelain) ]]; then echo "There are uncommitted changes." read -p "Continue anyway? (y/n) " -n 1 -r - echo # (optional) move to a new line + echo if ! [[ $REPLY =~ ^[Yy]$ ]] then exit 1 fi - fi rm -rf "$OUTPUT_DIR" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d29128..46fce3d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -159,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 diff --git a/docs/Contributing.md b/docs/Contributing.md deleted file mode 100644 index cc79e87..0000000 --- a/docs/Contributing.md +++ /dev/null @@ -1,17 +0,0 @@ -# Contributing - -## Ways to contribute - -- Found a **bug** or have an idea for a **new feature**? [Join the chat](https://s.jrpie.de/launcher-chat) or open an [issue](https://github.com/jrpie/Launcher/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. - -- Implement a new feature yourself: - - 1. [Fork](https://github.com/jrpie/launcher/fork) this repository. - 2. Create a **new branch** named *feature/\* or *fix/\* and commit your changes. - 3. Open a new pull request. - -- Add or improve [translations](https://toolate.othing.xyz/projects/jrpie-launcher/). - -![translation status](https://toolate.othing.xyz/widget/jrpie-launcher/launcher/horizontal-auto.svg) diff --git a/docs/Gestures-and-Bindings.md b/docs/Gestures-and-Bindings.md deleted file mode 100644 index 09ff172..0000000 --- a/docs/Gestures-and-Bindings.md +++ /dev/null @@ -1,58 +0,0 @@ -# Gestures and Bindings - -## Available Gestures - -### Swipes - -- Up, down, left, or right. -- Up, down, left, or right with two fingers. -- Up or down on the left or right edge. -- Left or right on the top or bottom edge. - -### Taps - -- Tap on date or time. -- Double tap. -- Long tap. - -### Tap-then-Swipes - -Tap then swipe up, down, left, or right - -### Complex Gestures - -- Draw <, >, V, or Λ. -- Draw <, >, V, or Λ in reverse direction. - -### Hardware Buttons as Gestures - -- Back button. -- Volume up or down button. - -*** - -## Available Bindings - -Any of the above gestures can be bound to any of the following bindings. - -### Launcher Bindings - -- Open µLauncher settings. -- Open a list of all*, favorite, or private* apps. - -### App Bindings - -- Launch an app. -- Launch another Home Screen. - -### Android / Device Bindings - -- Toggle private space lock. -- Lock the screen. -- Toggle the torch (flashlight) -- Raise or lower volume. -- Skip to previous or next audio track. -- Open Notifications shade. -- Open Quick Settings shade. - -> \* excludes apps hidden by the user diff --git a/docs/Work-Profile.md b/docs/Work-Profile.md deleted file mode 100644 index 718fe28..0000000 --- a/docs/Work-Profile.md +++ /dev/null @@ -1,4 +0,0 @@ -# Android Enterprise 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. - diff --git a/docs/actions-and-gestures.md b/docs/actions-and-gestures.md new file mode 100644 index 0000000..f5d831f --- /dev/null +++ b/docs/actions-and-gestures.md @@ -0,0 +1,71 @@ +# Gestures and Actions + +µLauncher's central mechanism for accessing important functionality quickly +is to bind actions (e.g. launching an app) to gestures (e.g. swiping up). +These bindings can be configured in µLauncher Settings > 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 gesture 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 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. Can be used as a workaround for a Android bug. +- Launch another home screen: Allows using another installed home screen temporarily. +- Do nothing: Just prevents showing the message saying that no action is bound to this gesture. diff --git a/docs/Building-from-Source.md b/docs/build.md similarity index 92% rename from docs/Building-from-Source.md rename to docs/build.md index fc8de3d..1ffc338 100644 --- a/docs/Building-from-Source.md +++ b/docs/build.md @@ -42,4 +42,4 @@ for further instructions. How to ## CI Pipeline The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds. -> Note: These builds are *not* signed. +> Note: These builds are *not* signed. They are in built in debug mode and only suitable for testing. diff --git a/docs/Changes-from-Finns-Launcher.md b/docs/changes-fork.md similarity index 89% rename from docs/Changes-from-Finns-Launcher.md rename to docs/changes-fork.md index 30ae32b..8efc965 100644 --- a/docs/Changes-from-Finns-Launcher.md +++ b/docs/changes-fork.md @@ -14,12 +14,15 @@ The decision to create a hard fork was made two years later.--> - 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. @@ -48,6 +51,6 @@ The complete list of changes can be viewed [here](https://github.com/jrpie/launc --- -\[original-repo\]: [https://github.com/finnmglas/Launcher](https://github.com/finnmglas/Launcher) +\[original-repo\]: [https://github.com/finnmglas/Launcher](https://github.com/finnmglas/Launcher) \[hack-font\]: [https://sourcefoundry.org/hack/](https://sourcefoundry.org/hack/) diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..8e9de53 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,24 @@ +# Contributing + +There are several ways to contribute to this app: +* You can add or improve [translations][toolate]. +
translation status +* 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 [build.md](build.md) for instructions 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/Home.md b/docs/home.md similarity index 82% rename from docs/Home.md rename to docs/home.md index 2cb6093..6ddf2b5 100644 --- a/docs/Home.md +++ b/docs/home.md @@ -1,12 +1,12 @@ -# Welcome to the μLauncher wiki +# Welcome to the μLauncher Documentation -We're thrilled you're here and confident you'll love your new Android launcher! Check out this wiki to get familiar with your new app. +## What is μLauncher? -## What is μLauncher +µLauncher is an *minimal* and *distraction-free* Android home screen that lets you launch apps using [swipe gestures and button presses](docs/actions-and-gestured.md). -µLauncher is an *minimal* and *distraction-free* Android home screen that lets you launch apps using *efficient* swipe gestures and button presses. This project is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). An incomplete list of changes can be found [here](https://github.com/wassupluke/Launcher/blob/master/docs/launcher.md). +This project is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). An incomplete list of changes can be found [here](https://github.com/wassupluke/Launcher/blob/master/docs/launcher.md). -## Where can I get μLauncher +## Where can I get μLauncher? [![Get it on F-Droid](https://fdroid.gitlab.io/artwork/badge/get-it-on.png)](https://f-droid.org/packages/de.jrpie.android.launcher/) @@ -18,6 +18,11 @@ We're thrilled you're here and confident you'll love your new Android launcher! > You can also [get it on Google Play](https://play.google.com/store/apps/details?id=de.jrpie.android.launcher), but this is not recommend. + +## How can I contribute? + +See [docs/contribute](docs/contribute.md) + ## Screenshots ![μLauncher Home Screen screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg) diff --git a/docs/profiles.md b/docs/profiles.md new file mode 100644 index 0000000..d9eaf52 --- /dev/null +++ b/docs/profiles.md @@ -0,0 +1,21 @@ +# 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/Launcher-Settings.md b/docs/settings.md similarity index 72% rename from docs/Launcher-Settings.md rename to docs/settings.md index 1444c73..f033f62 100644 --- a/docs/Launcher-Settings.md +++ b/docs/settings.md @@ -4,13 +4,14 @@ Tweaks and customizations can be made from within the Launcher Settings page. These settings let you change wallpapers, change colors and fonts, enable monochrome app icons, change the app drawer layout, and much more. -In the following documentation, 'app drawer' will be used to refer to the 'All Apps' and 'Favorite Apps' views. +In the following documentation, 'app drawer' will be used to refer to the 'All Apps', 'Favorite Apps' and 'Private Space' views. ## Appearance > ### Choose a wallpaper -Lets you change the wallpaper using a photos app, file explorer, or native wallpaper setting app. +This triggers Android's mechanism to change the wallpaper using a photos 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) @@ -92,11 +93,15 @@ Place the current time above the current date on the home screen. Launches any app that matches user 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. For example, if you type, `a`, the app list narrows to any apps with a title containing the letter `a`. Continuing the example, if you then follow your `a` with the letter `m`, the list now shows only apps containing the letter combination `am` in that order. If the only app matching this combination was, for example, `Amazon`, simply typing `am` in the app drawer would immediately launch the `Amazon` app for you. +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](https://github.com/wassupluke/Launcher/wiki/Launcher-Settings/_edit#additional-settings) apps, effectively letting you define custom app names that could be considered 'aliases' or shortcuts. For instance, if you wanted to "bind" 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. +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](https://github.com/wassupluke/Launcher/wiki/Launcher-Settings/_edit#search-the-web) feature. +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` @@ -112,15 +117,15 @@ Automatically open the keyboard when the app drawer is opened. **type:** `toggle` -> ### Double swipe actions +> ### Double swipe gestures -Enable double swipe (two finger) actions as bindable gestures in launcher settings. Does not erase gesture bindings if accidentally turned off. +Enable double swipe (two finger) gestures in launcher settings. Does not erase gesture bindings if accidentally turned off. **type:** `toggle` -> ### Edge swipe actions +> ### Edge swipe gestures -Enable edge swipe (near edges of screen) actions as bindable gestures in launcher settings. Does not erase gesture bindings if accidentally turned off. +Enable edge swipe (near edges of screen) gestures in launcher settings. Does not erase gesture bindings if accidentally turned off. **type:** `toggle` @@ -167,6 +172,7 @@ Reduces redundancy and tidies up app drawer. > ### 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` @@ -178,7 +184,12 @@ Remove private space from app drawer. > ### Layout of app list -Change how the apps are displayed when accessing the app drawer. By `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. `Grid` shows apps with their app icon and title in a grid layout. +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` @@ -186,7 +197,8 @@ Change how the apps are displayed when accessing the app drawer. By `Default`, a > ### Reverse the app list -Enable Z-A sorting of apps in the app drawer. Useful for keeping apps within easier reach from the keyboard. +Enable reverse alphabetical sorting of apps in the app drawer. +Useful for keeping apps within easier reach from the keyboard. **type:** `toggle` From 24e90deb627f76c2d528fbc12510ea4825dc9a98 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 10 May 2025 00:52:56 +0200 Subject: [PATCH 14/93] improve widget management --- .../de/jrpie/android/launcher/Application.kt | 5 --- .../manage/ManageWidgetPanelsActivity.kt | 1 - .../widgets/manage/ManageWidgetsActivity.kt | 5 +-- .../ui/widgets/manage/WidgetManagerView.kt | 32 ++++++++++++------- .../jrpie/android/launcher/widgets/Widget.kt | 3 +- 5 files changed, 25 insertions(+), 21 deletions(-) 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 775621c..3c2e3bc 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Application.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt @@ -22,8 +22,6 @@ import de.jrpie.android.launcher.apps.isPrivateSpaceLocked import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion import de.jrpie.android.launcher.preferences.resetPreferences -import de.jrpie.android.launcher.widgets.LauncherWidgetProvider -import de.jrpie.android.launcher.widgets.Widget import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -34,7 +32,6 @@ const val APP_WIDGET_HOST_ID = 42; class Application : android.app.Application() { val apps = MutableLiveData>() - val widgets = MutableLiveData>() val privateSpaceLocked = MutableLiveData() lateinit var appWidgetHost: AppWidgetHost lateinit var appWidgetManager: AppWidgetManager @@ -101,8 +98,6 @@ class Application : android.app.Application() { customAppNames = LauncherPreferences.apps().customNames() } else if (pref == LauncherPreferences.apps().keys().pinnedShortcuts()) { loadApps() - } else if (pref == LauncherPreferences.widgets().keys().widgets()) { - widgets.postValue(LauncherPreferences.widgets().widgets() ?: setOf()) } } 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 index b18852f..cb57fda 100644 --- 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 @@ -49,7 +49,6 @@ class ManageWidgetPanelsActivity : AppCompatActivity(), UIObject { }) } binding.manageWidgetPanelsRecycler.apply { - // improve performance (since content changes don't change the layout size) setHasFixedSize(true) layoutManager = viewManager adapter = viewAdapter 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 index d191b70..665a851 100644 --- 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 @@ -34,10 +34,11 @@ class ManageWidgetsActivity : Activity(), UIObject { var panelId: Int = WidgetPanel.HOME.id + + // We can't observe the livedata because this is not an AppCompatActivity private var sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> if (prefKey == LauncherPreferences.widgets().keys().widgets()) { - // We can't observe the livedata because this is not an AppCompatActivity findViewById(R.id.manage_widgets_container).updateWidgets(this, LauncherPreferences.widgets().widgets() ) @@ -63,7 +64,7 @@ class ManageWidgetsActivity : Activity(), UIObject { findViewById(R.id.manage_widgets_container).let { it.widgetPanelId = panelId - it.updateWidgets(this, (application as Application).widgets.value) + it.updateWidgets(this, LauncherPreferences.widgets().widgets()) } } 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 index 7a355f7..f6c6c0d 100644 --- 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 @@ -19,6 +19,7 @@ import androidx.core.graphics.minus import androidx.core.graphics.toRect import androidx.core.view.children import de.jrpie.android.launcher.ui.widgets.WidgetContainerView +import de.jrpie.android.launcher.widgets.GRID_SIZE import de.jrpie.android.launcher.widgets.Widget import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition @@ -47,21 +48,27 @@ class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSe - enum class EditMode(val resize: (dx: Int, dy: Int, rect: Rect) -> Rect) { - MOVE({ dx, dy, rect -> - Rect(rect.left + dx, rect.top + dy, rect.right + dx, rect.bottom + dy) + 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({ dx, dy, rect -> - Rect(rect.left, min(rect.top + dy, rect.bottom - 200), rect.right, rect.bottom) + TOP({ dx, dy, sw, sh, rect -> + val cdy = dy.coerceIn(-rect.top, rect.bottom - rect.top - (2 * sh / GRID_SIZE) + 5) + Rect(rect.left, rect.top + cdy, rect.right, rect.bottom) }), - BOTTOM({ dx, dy, rect -> - Rect(rect.left, rect.top, rect.right, max(rect.top + 200, rect.bottom + dy)) + BOTTOM({ dx, dy, sw, sh, rect -> + val cdy = dy.coerceIn((2 * sh / GRID_SIZE) + 5 + rect.top - rect.bottom, sh - rect.bottom) + Rect(rect.left, rect.top, rect.right, rect.bottom + cdy) }), - LEFT({ dx, dy, rect -> - Rect(min(rect.left + dx, rect.right - 200), rect.top, rect.right, rect.bottom) + LEFT({ dx, dy, sw, sh, rect -> + val cdx = dx.coerceIn(-rect.left, rect.right - rect.left - (2 * sw / GRID_SIZE) + 5) + Rect(rect.left + cdx, rect.top, rect.right, rect.bottom) }), - RIGHT({ dx, dy, rect -> - Rect(rect.left, rect.top, max(rect.left + 200, rect.right + dx), rect.bottom) + RIGHT({ dx, dy, sw, sh, rect -> + val cdx = dx.coerceIn((2 * sw / GRID_SIZE) + 5 + rect.left - rect.right, sw - rect.right) + Rect(rect.left, rect.top, rect.right + cdx, rect.bottom) }), } @@ -120,6 +127,7 @@ class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSe val absoluteNewPosition = view.mode?.resize( distanceX.toInt(), distanceY.toInt(), + width, height, start ) ?: return true val newPosition = WidgetPosition.fromAbsoluteRect( @@ -162,7 +170,7 @@ class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSe if (widgets == null) { return } - children.mapNotNull { it as? WidgetOverlayView }.forEach { removeView(it) } + children.filter { it is WidgetOverlayView }.forEach { removeView(it) } widgets.filter { it.panelId == widgetPanelId }.forEach { widget -> WidgetOverlayView(activity).let { 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 index dbe667b..e31250b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt @@ -57,7 +57,8 @@ sealed class Widget { return Json.decodeFromString(serialized) } fun byId(context: Context, id: Int): Widget? { - return (context.applicationContext as Application).widgets.value?.firstOrNull { + // TODO: do some caching + return LauncherPreferences.widgets().widgets().firstOrNull() { it.id == id } } From f5b8953601016816fad92042d5d95c55709db756 Mon Sep 17 00:00:00 2001 From: Sven van de Lagemaat Date: Mon, 14 Apr 2025 08:14:03 +0000 Subject: [PATCH 15/93] Added translation using Weblate (Dutch) --- app/src/main/res/values-nl/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-nl/strings.xml 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..a6b3dae --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From ba2c117eb5bbd88a186d0b745011c21679cdc58a Mon Sep 17 00:00:00 2001 From: Sven van de Lagemaat Date: Mon, 14 Apr 2025 08:16:32 +0000 Subject: [PATCH 16/93] Translated using Weblate (Dutch) Currently translated at 14.2% (3 of 21 strings) Translation: jrpie-Launcher/metadata Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/nl/ --- fastlane/metadata/android/nl-NL/full_description.txt | 4 ++++ fastlane/metadata/android/nl-NL/short_description.txt | 1 + fastlane/metadata/android/nl-NL/title.txt | 1 + 3 files changed, 6 insertions(+) create mode 100644 fastlane/metadata/android/nl-NL/full_description.txt create mode 100644 fastlane/metadata/android/nl-NL/short_description.txt create mode 100644 fastlane/metadata/android/nl-NL/title.txt 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 From b9fa079048b5e24a4efd73c7b9b5d0338fbcd46b Mon Sep 17 00:00:00 2001 From: Vladi69 Date: Sat, 19 Apr 2025 09:45:17 +0000 Subject: [PATCH 17/93] Translated using Weblate (Italian) Currently translated at 87.9% (227 of 258 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/ --- app/src/main/res/values-it/strings.xml | 201 +++++++++++++++---------- 1 file changed, 121 insertions(+), 80 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 318ad43..33ab7dd 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2,15 +2,15 @@ Scorri verso destra sul bordo inferiore dello schermo Aspetto - Scegliere + Imposta app Tema - Questo launcher è progettato per essere minimale, efficiente e privo di distrazioni. Non contiene pagamenti, pubblicità o servizi di tracciamento. + μLauncher is designed to be minimal, efficient and free of distraction. \n\nIt contains no ads and collects no data. Predefinito - Non mostrare applicazioni collegate a gesti nella lista delle app + Non mostrare applicazioni abbinate a gesti nella lista delle app Testo Impostare µLauncher come servizio accessibilità permette l\'azione di blocco dello schermo. Si precisa che sono richiesti permessi eccessivi. Non si dovrebbe mai concedere con leggerezza permessi ad alcuna app. µLauncher utilizza i servizi di accessibilità esclusivamente per il blocco dello schermo. Puoi verificare il codice sorgente. Si segnala che il blocco schermo si può attivare anche concedendo a µLauncher i permessi di amministratore, tuttavia tale metodo non funziona con lo sblocco tramite impronta o riconoscimento facciale. - Sans serif (senza grazie) - Trascina due dita dal basso verso l\'alto + Sans serif + Scorri con due dita verso l\'alto Negozio non trovato Griglia


Puoi cambiare le tue scelte in seguito nelle impostazioni. ]]>
- Impossibile aprire l\'applicazione - Desideri modificare le impostazioni? - Apri le impostazioni per abbinare un\'azione a questo gesto + Impossibile avviare l\'app + Modificare le impostazioni? + Apri impostazioni per associare un\'azione a questo gesto Impostazioni Launcher Meta Scorri verso il basso con due dita Sinistra - Scorrere verso sinistra - Due dita verso sinistra - Scorrere verso sinistra con due dita + Scorri verso sinistra + Due dita a sx + Scorri verso sinistra con due dita Destra Scorri verso destra - Due dita verso destra - Scorri a destra con due dita + Due dita verso dx + Scorri verso destra con due dita Destra (in alto) - Scorri verso destra sul bordo superiore dello schermo + Scorri verso destra sul bordo alto dello schermo Scorri verso sinistra sul bordo inferiore dello schermo Scorri verso sinistra sul bordo superiore dello schermo - Verso l\'alto (lato sinistro) - Verso l\'alto - Striscia il dito dal basso verso l\'alto - Verso il basso - Trascina due dita dall\'alto verso il basso - Trascina un dito dall\'alto verso il basso - Destra (bordo inferiore) - Sinistra (bordo inferiore) - Sinistra (bordo superiore) + Su (bordo sinistro) + Su + Scorri su + Giù + Scorri con due dita giù + Scorri verso il basso + Destra (in basso) + Sinistra (in basso) + Sinistra (in alto) Scorri verso l\'alto sul bordo sinistro dello schermo - Alto (lato destro) + Su (bordo destro) Scorri verso l\'alto sul bordo destro dello schermo - Basso (bordo sinistro) + Giù (bordo sinistro) Scorri verso il basso sul bordo sinistro dello schermo - Basso (Lato destro) - Scorri verso il basso sul lato destro dello schermo - Aumenta il volume - Doppio click - Doppio click in un\'area vuota - Tocco prolungato - Tocco prolungato su un\'area vuota + Giù (bordo destro) + Scorri verso il basso sul bordo destro dello schermo + Volume + + Doppio tap + Doppio tap in spazio vuoto + Tap lungo + Tap lungo in spazio vuoto Data - Premi sulla data + Tap sulla data Ora - Premi sull\'ora - Premi il pulsante di aumento del volume - Riduci il volume - Premi il pulsante per ridurre il volume - Installa le applicazioni + Tap sull\'ora + Premi il pulsante Volume + + Volume - + Premi il pulsante Volume - + Installa apps Icone monocromatiche - Mostra l\'ora - Mostra la data - Usa il formato ora locale + Mostra orario + Mostra data + Usa formato data locale Mostra i secondi Scorri con due dita - Azioni a scorrimento con due dita + Azioni di scorrimento a due dita Apri il risultato della ricerca - Azioni a scorrimento ai lati dello schermo - Scorri sul lato dello schermo - Larghezza margine laterale - Vedi il codice sorgente + Azioni di scorrimento sui bordi dello schermo + Scorri sui bordi dello schermo + Larghezza bordo + Vai al codice sorgente Tutte le applicazioni Applicazioni preferite - Musica: passa alla traccia successiva - Musica: riduci il volume - Musica: torna alla traccia precedente - Musica: Aumenta il volume + Musica: traccia successiva + Volume - + Musica: traccia precedente + Volume + Annulla Impostazioni rapide Azione necessaria per abilitare il blocco dello schermo. - Ombreggiatura del testo - Sfondo (lista applicazioni e impostazioni) + Ombreggiatura testo + Sfondo (lista apps e impostazioni) Font Inverti data e ora - Scegli uno sfondo + Imposta sfondo Schermo - Mantieni lo schermo acceso - Ruota lo schermo - Funzionalità - Apri automaticamente la tastiera per cercare + Mantieni schermo acceso + Ruota schermo + Funzioni + Apri automaticamente la tastiera Sensibilità - Scegli un\'applicazione + Scegli applicazione Configurazione Abbiamo impostato alcune app predefinite per te. Puoi modificarle ora se lo desideri: Puoi anche cambiare la tua selezione in seguito. @@ -145,22 +145,22 @@ Sfocato Solido Predefinito di sistema - Serif (con grazie) - Monospace (a larghezza fissa) - Serif monospace (a larghezza fissa con grazie) - Applicazioni + Serif + Monospace + Serif monospace + Apps Applicazioni nascoste - Configurazione della lista applicazioni + Configura la lista applicazioni Predefinito Offuscato - Imposta μLauncher come predefinito per la schermata principale - Informazioni sulle applicazioni - Apri il tutorial del launcher - Ripristina le impostazioni predefinite - Stai per eliminare tutte le preferenze impostate. Vuoi continuare? + Imposta μLauncher come launcher predefinito + Informazioni app + Apri il tutorial di µLauncher + Ripristina le impostazioni + Stai per eliminare le preferenze impostate. Continuare? Segnala un bug Contatta lo sviluppatore del fork - Partecipa alla chat di μLauncher + Unisciti alla chat di μLauncher Informativa sulla privacy Contatta lo sviluppatore originale Unisciti a noi su Discord! @@ -168,7 +168,7 @@ Applicazioni preferite Applicazioni nascoste Applicazioni - Altri + Altro Disinstalla Informazioni sull\'app Aggiungi ai preferiti @@ -178,21 +178,21 @@ Rinomina Cerca Impostazioni μLauncher - Espandi il pannello notifiche + Espandi pannello notifiche Non fare niente - Blocca lo schermo - Accendi/spegni la torcia + Blocca schermo + Torcia ON/OFF Tutorial - Prenditi qualche secondo per imparare ad usare questo launcher! + 👋\n\nPrenditi qualche secondo per imparare ad usare questo launcher! Concetto - L\'app è open source (sotto licenza MIT) e disponibile su GitHub! Visita il nostro archivio! + E\' software libero (licenza MIT)!\nAssicurati di controllare il repository! Utilizzo La schermata principale contiene solo data e ora. Nessuna distrazione. Questa funzione richiede Android 6 o successivi. Rinomina %1$s Dinamico Colore - Due dita verso l\'alto + Scorri con due dita verso l\'alto Sono consapevole che questo concederà privilegi estesi a µLauncher. Accetto che µLauncher utilizzi il servizio di accessibilità per fornire funzionalità non correlate all\'accessibilità. Accetto che µLauncher non raccolga alcun dato. @@ -208,17 +208,17 @@ Attiva Servizi di Accessibilità Sono consapevole che esistono altre opzioni (utilizzando i privilegi di amministratore del dispositivo o il pulsante di accensione). Attivazione dei Servizi di Accessibilità - Cerca su internet - Premi invio durante la ricerca nell\'elenco delle app per avviare una ricerca su internet. + Cerca sul web + Premi invio durante la ricerca delle app per avviare una ricerca web. Cerca (senza avvio automatico) Licenze Open Source Licenze Open Source Segnala un bug - Grazie per aver contribuito a migliorare µLauncher!\nSi prega di aggiungere le seguenti informazioni alla segnalazione del bug: + Grazie per il tuo contributo al miglioramento di µLauncher!\nAggiungi le seguenti informazioni alla segnalazione del bug: Copia negli appunti Non segnalare pubblicamente le vulnerabilità di sicurezza su GitHub, ma utilizza invece: Annulla - Premi spazio per disabilitare temporaneamente questa funzionalità. + Premi spazio per disabilitare temporaneamente questa funzione Segnala una vulnerabilità di sicurezza Crea una segnalazione Spazio privato bloccato @@ -228,4 +228,45 @@ Impossibile aprire l\'URL: nessun browser trovato. Non è stata trovata un\'applicazione per gestire la ricerca. privilegi più ampi a µLauncher.
µLauncher utilizzerà questi privilegi solo per bloccare lo schermo. µLauncher non raccoglierà mai alcun dato. In particolare, µLauncher non usa il servizio di accessibilità per raccogliere nessun dato.]]>
+ Spazio privato + Spazio privato + Regola volume + Associa al gesto + Nascondi barra di stato + Nascondi barra di navigazione + Dona + App recenti + Tap + Su + Tap e scorri su + Tap + Giù + Tap e scorri verso il basso + Tap + Sinistra + Tap + Destra + Alto sx -> centro dx -> basso sx + Tap e scorri vesro sinistra + Tap e scorri verso destra + (Inverso)]]> + Basso sx -> centro dx -> alto sx + Alto dx -> centro sx -> basso dx + V + Alto sx -> centro basso -> alto dx + V (Reverse) + Alto dx -> centro basso -> alto sx + Λ + Basso sx -> centro alto -> basso dx + Basso dx -> centro alto -> basso sx + + + Λ (Inverso) + "Musica: Riproduci / Pausa" + Inverti la lista applicazioni + Azioni + Pulsante / gesto indietro + Indietro + Basso dx -> centro sx -> alto dx + Nascondi lo spazio privato dalla lista app + Lancia un altro launcher + Aggiungi scorciatoia + ]]> + Mostra in lista app
From 3d4bddbb4c5015373c8b8b8f140d218bf5898e13 Mon Sep 17 00:00:00 2001 From: Vladi69 Date: Sat, 19 Apr 2025 21:23:55 +0000 Subject: [PATCH 18/93] Translated using Weblate (Italian) Currently translated at 98.0% (253 of 258 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/ --- app/src/main/res/values-it/strings.xml | 138 +++++++++++-------------- 1 file changed, 63 insertions(+), 75 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 33ab7dd..de71a7b 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,39 +1,19 @@ - Scorri verso destra sul bordo inferiore dello schermo + Scorri a destra sul bordo inferiore dello schermo Aspetto - Imposta app + Associa app Tema - μLauncher is designed to be minimal, efficient and free of distraction. \n\nIt contains no ads and collects no data. + μLauncher è progettato per essere minimale, efficiente e privo di distrazione.\n\nNon contiene annunci e non raccoglie dati. Predefinito - Non mostrare applicazioni abbinate a gesti nella lista delle app + Non mostrare nella lista le applicazioni associate a gesti Testo - Impostare µLauncher come servizio accessibilità permette l\'azione di blocco dello schermo. Si precisa che sono richiesti permessi eccessivi. Non si dovrebbe mai concedere con leggerezza permessi ad alcuna app. µLauncher utilizza i servizi di accessibilità esclusivamente per il blocco dello schermo. Puoi verificare il codice sorgente. Si segnala che il blocco schermo si può attivare anche concedendo a µLauncher i permessi di amministratore, tuttavia tale metodo non funziona con lo sblocco tramite impronta o riconoscimento facciale. + Impostare µLauncher come servizio accessibilità permette l\'azione di blocco dello schermo. Nota che sono richiesti permessi eccessivi. Non si dovrebbero mai concedere con leggerezza permessi ad alcuna app. µLauncher utilizza i servizi di accessibilità esclusivamente per il blocco dello schermo. Puoi verificare il codice sorgente. Nota che il blocco schermo si può attivare anche concedendo a µLauncher i permessi di amministratore, tuttavia tale metodo non funziona con lo sblocco tramite impronta o riconoscimento facciale. Sans serif - Scorri con due dita verso l\'alto - Negozio non trovato + Scorri verso l\'alto con due dita + Store non trovato Griglia - Scegli un sistema di blocco - Esistono due modi di bloccare lo schermo. - Sfortunatamente entrambi presentano svantaggi:

- -

Amministratore Dispositivo

- Non funziona con impronta e riconoscimento facciale. - -
-
- -

Servizio Accessibilità

- Richiede privilegi eccessivi. - µLauncher userà quei privilegi esclusivamente per bloccare lo schermo. -
- (Non ci si dovrebbe mai fidare di un\'applicazione qualsiasi appena scaricata su queste cose, ma puoi verificare il codice sorgente.) - - -



- Puoi cambiare le tue scelte in seguito nelle impostazioni. - ]]>
+ "<a href=\"https://issuetracker.google.com/issues/37010136#comment36\"><![CDATA[\n <h1>Scegli un sistema di blocco</h1>\n Esistono due modi di bloccare lo schermo.\n Sfortunatamente entrambi presentano svantaggi:<br/><br/>\n\n <h3>Amministratore Dispositivo</h3>\n Non funziona con impronta e riconoscimento facciale.\n\n <br/>\n <br/>\n\n <h3>Servizio Accessibilità</h3>\n Richiede privilegi eccessivi.\n µLauncher userà quei privilegi esclusivamente per bloccare lo schermo.\n <br/>\n (Non ci si dovrebbe mai fidare di un\\\'applicazione qualsiasi appena scaricata su queste cose, ma puoi verificare il <a href=\\\"https://github.com/jrpie/Launcher\\\">codice sorgente</a>.)\n\n In alcuni dispositivi, il PIN iniziale non verrà più utilizzato per la crittografia dei dati dopo aver attivato un servizio di accessibilità.\n\n Può essere <a href=\"https://issuetracker.google.com/issues/37010136#comment36\">riattivato</a> dopo.\n\n\n <br/><br/><br/><br/>\n Puoi cambiare le tue scelte in seguito nelle impostazioni.\n ]]>" Impossibile avviare l\'app Modificare le impostazioni? Apri impostazioni per associare un\'azione a questo gesto @@ -43,32 +23,32 @@ Scorri verso il basso con due dita Sinistra Scorri verso sinistra - Due dita a sx + Due dita a sinistra Scorri verso sinistra con due dita Destra Scorri verso destra - Due dita verso dx + Due dita a destra Scorri verso destra con due dita Destra (in alto) - Scorri verso destra sul bordo alto dello schermo - Scorri verso sinistra sul bordo inferiore dello schermo - Scorri verso sinistra sul bordo superiore dello schermo + Scorri a destra sul bordo superiore dello schermo + Scorri a sinistra sul bordo inferiore dello schermo + Scorri a sinistra sul bordo superiore dello schermo Su (bordo sinistro) Su - Scorri su + Scorri verso l\'alto Giù - Scorri con due dita giù + Scorri giù con due dita Scorri verso il basso Destra (in basso) Sinistra (in basso) Sinistra (in alto) - Scorri verso l\'alto sul bordo sinistro dello schermo + Scorri in alto sul bordo sinistro dello schermo Su (bordo destro) - Scorri verso l\'alto sul bordo destro dello schermo + Scorri in alto sul bordo destro dello schermo Giù (bordo sinistro) - Scorri verso il basso sul bordo sinistro dello schermo + Scorri in basso sul bordo sinistro dello schermo Giù (bordo destro) - Scorri verso il basso sul bordo destro dello schermo + Scorri in basso sul bordo destro dello schermo Volume + Doppio tap Doppio tap in spazio vuoto @@ -86,55 +66,55 @@ Mostra orario Mostra data Usa formato data locale - Mostra i secondi + Mostra secondi Scorri con due dita - Azioni di scorrimento a due dita + Azioni a due dita Apri il risultato della ricerca - Azioni di scorrimento sui bordi dello schermo + Azioni sui bordi dello schermo Scorri sui bordi dello schermo Larghezza bordo - Vai al codice sorgente + Codice sorgente Tutte le applicazioni Applicazioni preferite Musica: traccia successiva - Volume - + Abbassa volume Musica: traccia precedente - Volume + + Alza volume Annulla Impostazioni rapide - Azione necessaria per abilitare il blocco dello schermo. + Azione necessaria per abilitare il blocco schermo. Ombreggiatura testo Sfondo (lista apps e impostazioni) Font Inverti data e ora Imposta sfondo Schermo - Mantieni schermo acceso + Mantieni lo schermo acceso Ruota schermo Funzioni Apri automaticamente la tastiera Sensibilità Scegli applicazione Configurazione - Abbiamo impostato alcune app predefinite per te. Puoi modificarle ora se lo desideri: - Puoi anche cambiare la tua selezione in seguito. + Abbiamo impostato alcune app predefinite. Puoi modificarle ora se vuoi: + Puoi anche modificare la selezione in seguito. Iniziamo! - Sei pronto per iniziare! Spero questa applicazione ti risulti preziosa! - Finn (che ha ideato il launcher)\n \te Josia (che ha aggiunto qualche miglioramento e mantiene il fork μLauncher) + Sei pronto per iniziare!\n\nSpero che lo apprezzi!\n\n- Finn (che ha fatto Launcher) e Josia (che ha apportato alcuni miglioramenti e mantiene il fork μLauncher) Inizia Impostazioni Altre opzioni - Puoi aprire le tue app facendo scorrere il dito sullo schermo o premendo un pulsante. Configura i gesti nella prossima slide. - Errore: impossibile espandere la barra di stato. Questa azione utilizza funzionalità non incluse nelle API Android pubbliche. Sfortunatamente, non sembra funzionare sul tuo dispositivo. - Applicazione nascosta. Puoi renderla nuovamente visibile nelle impostazioni. - µLauncher deve essere autorizzato come amministratore del dispositivo per bloccare lo schermo. - Abilita il blocco dello schermo - Nessuna camera con torcia rilevata. - Errore: impossibile accedere alla torcia. - Il servizio accessibilità per µLauncher non è attivo. Per favore attivalo nelle impostazioni + Puoi avviare le tue app principali scorrendo il dito sullo schermo o premendo un pulsante. + Errore: impossibile espandere la barra di stato. Questa azione utilizza funzionalità non incluse nelle API Android. Sfortunatamente, non sembra funzionare sul tuo dispositivo. + Applicazione nascosta. Puoi renderla nuovamente visibile dalle impostazioni. + µLauncher deve amministrare il dispositivo per poter bloccare lo schermo. + Abilita il blocco schermo + Nessuna camera con torcia rilevata + Errore: impossibile accedere alla torcia + Il servizio accessibilità per µLauncher non è attivo. Attivalo nelle impostazioni Errore: impossibile bloccare lo schermo. (Se hai appena aggiornato l\'app, prova a disabilitare e riattivare il servizio accessibilità nelle impostazioni del telefono) - Errore: Il blocco schermo tramite accessibilità non è supportato su questo dispositivo. Per favore usa invece il servizio amministratore dispositivo. - µLauncher - blocco schermo + Errore: Il blocco schermo tramite accessibilità non è supportato su questo dispositivo. In alternativa usa il servizio amministratore dispositivo. + μLauncher Scegli come bloccare lo schermo Usa il servizio accessibilità Usa l\'amministratore dispositivo @@ -148,16 +128,16 @@ Serif Monospace Serif monospace - Apps + Applicazioni Applicazioni nascoste Configura la lista applicazioni Predefinito Offuscato - Imposta μLauncher come launcher predefinito + Imposta μLauncher come predefinito Informazioni app - Apri il tutorial di µLauncher + Tutorial di µLauncher Ripristina le impostazioni - Stai per eliminare le preferenze impostate. Continuare? + Stai per ripristinare tutte le impostazioni. Continuare? Segnala un bug Contatta lo sviluppatore del fork Unisciti alla chat di μLauncher @@ -170,7 +150,7 @@ Applicazioni Altro Disinstalla - Informazioni sull\'app + Informazioni app Aggiungi ai preferiti Rimuovi dai preferiti Nascondi @@ -192,13 +172,13 @@ Rinomina %1$s Dinamico Colore - Scorri con due dita verso l\'alto + Scorri su con due dita Sono consapevole che questo concederà privilegi estesi a µLauncher. Accetto che µLauncher utilizzi il servizio di accessibilità per fornire funzionalità non correlate all\'accessibilità. Accetto che µLauncher non raccolga alcun dato. Nascondi le app in pausa - Attiva/Disattiva Blocco Spazio Privato - Questa funzionalità richiede Android 15 o successivi. + Blocca/Sblocca Spazio Privato + Questa funzione richiede Android 15 o successivi. Rosso Trasparente Blu @@ -209,24 +189,24 @@ Sono consapevole che esistono altre opzioni (utilizzando i privilegi di amministratore del dispositivo o il pulsante di accensione). Attivazione dei Servizi di Accessibilità Cerca sul web - Premi invio durante la ricerca delle app per avviare una ricerca web. + Invio in ricerca app per avviare una ricerca web Cerca (senza avvio automatico) Licenze Open Source Licenze Open Source Segnala un bug Grazie per il tuo contributo al miglioramento di µLauncher!\nAggiungi le seguenti informazioni alla segnalazione del bug: Copia negli appunti - Non segnalare pubblicamente le vulnerabilità di sicurezza su GitHub, ma utilizza invece: + Non segnalare le vulnerabilità di sicurezza pubblicamente su GitHub, ma usa invece: Annulla - Premi spazio per disabilitare temporaneamente questa funzione + Spazio per disabilitare temporaneamente Segnala una vulnerabilità di sicurezza Crea una segnalazione Spazio privato bloccato Spazio privato sbloccato Spazio privato non disponibile - µLauncher deve essere la schermata iniziale predefinita per accedere allo spazio privato. + µLauncher deve essere il launcher predefinito per poter accedere allo spazio privato. Impossibile aprire l\'URL: nessun browser trovato. - Non è stata trovata un\'applicazione per gestire la ricerca. + Nessuna applicazione trovata per gestire la ricerca. privilegi più ampi a µLauncher.
µLauncher utilizzerà questi privilegi solo per bloccare lo schermo. µLauncher non raccoglierà mai alcun dato. In particolare, µLauncher non usa il servizio di accessibilità per raccogliere nessun dato.]]>
Spazio privato Spazio privato @@ -250,7 +230,7 @@ Alto dx -> centro sx -> basso dx V Alto sx -> centro basso -> alto dx - V (Reverse) + V (Inverso) Alto dx -> centro basso -> alto sx Λ Basso sx -> centro alto -> basso dx @@ -265,8 +245,16 @@ Indietro Basso dx -> centro sx -> alto dx Nascondi lo spazio privato dalla lista app - Lancia un altro launcher + Avvia un altro launcher Aggiungi scorciatoia ]]> Mostra in lista app + Blocca spazio privato + Sblocca spazio privato + Versione + Errore: impossibile abilitare il servizio di accessibilità + Quando corrisponde una sola app, viene avviata automaticamente.\nPuoi disabilitare l\'avvio con uno spazio prima della query. + Tutte le app + Puoi cercare rapidamente tra tutte le app nella lista app.\n\nScorri su per la lista o associa ad un gesto diverso. + Errore: impossibile mostrare le app recenti. (Se hai appena aggiornato l\'app, prova a disabilitare e riabilitare il servizio di accessibilità dalle impostazioni del telefono)
From 68eb59a9348bf71a2d9a19d3d228ec748002f635 Mon Sep 17 00:00:00 2001 From: Vladi69 Date: Sat, 19 Apr 2025 17:39:37 +0000 Subject: [PATCH 19/93] Translated using Weblate (Italian) Currently translated at 4.5% (1 of 22 strings) Translation: jrpie-Launcher/metadata Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/it/ --- fastlane/metadata/android/it-IT/changelogs/26.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/it-IT/changelogs/26.txt 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 From aabc607ed63c377753a5c226e17067ddf63dffe2 Mon Sep 17 00:00:00 2001 From: Vladi69 Date: Tue, 22 Apr 2025 07:10:03 +0000 Subject: [PATCH 20/93] Translated using Weblate (Italian) Currently translated at 99.6% (257 of 258 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/ --- app/src/main/res/values-it/strings.xml | 51 ++++++++++++++++++++------ 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index de71a7b..c7e8fcb 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -13,7 +13,31 @@ Scorri verso l\'alto con due dita Store non trovato Griglia - "<a href=\"https://issuetracker.google.com/issues/37010136#comment36\"><![CDATA[\n <h1>Scegli un sistema di blocco</h1>\n Esistono due modi di bloccare lo schermo.\n Sfortunatamente entrambi presentano svantaggi:<br/><br/>\n\n <h3>Amministratore Dispositivo</h3>\n Non funziona con impronta e riconoscimento facciale.\n\n <br/>\n <br/>\n\n <h3>Servizio Accessibilità</h3>\n Richiede privilegi eccessivi.\n µLauncher userà quei privilegi esclusivamente per bloccare lo schermo.\n <br/>\n (Non ci si dovrebbe mai fidare di un\\\'applicazione qualsiasi appena scaricata su queste cose, ma puoi verificare il <a href=\\\"https://github.com/jrpie/Launcher\\\">codice sorgente</a>.)\n\n In alcuni dispositivi, il PIN iniziale non verrà più utilizzato per la crittografia dei dati dopo aver attivato un servizio di accessibilità.\n\n Può essere <a href=\"https://issuetracker.google.com/issues/37010136#comment36\">riattivato</a> dopo.\n\n\n <br/><br/><br/><br/>\n Puoi cambiare le tue scelte in seguito nelle impostazioni.\n ]]>" + Scegli un sistema di blocco + Esistono due modi di bloccare lo schermo. + Sfortunatamente entrambi presentano svantaggi:

+ +

Amministratore Dispositivo

+ Non funziona con impronta e riconoscimento facciale. + +
+
+ +

Servizio Accessibilità

+ Richiede privilegi eccessivi. + µLauncher userà quei privilegi esclusivamente per bloccare lo schermo. +
+ (Non ci si dovrebbe mai fidare di un\'applicazione qualsiasi appena scaricata su queste cose, ma puoi verificare il codice sorgente.) + + In alcuni dispositivi, il PIN iniziale non verrà più utilizzato per la crittografia dei dati dopo aver attivato un servizio di accessibilità. + + Può essere riattivato dopo. + + +



+ Puoi cambiare le tue scelte in seguito nelle impostazioni. + ]]>
Impossibile avviare l\'app Modificare le impostazioni? Apri impostazioni per associare un\'azione a questo gesto @@ -109,15 +133,15 @@ Applicazione nascosta. Puoi renderla nuovamente visibile dalle impostazioni. µLauncher deve amministrare il dispositivo per poter bloccare lo schermo. Abilita il blocco schermo - Nessuna camera con torcia rilevata - Errore: impossibile accedere alla torcia + Nessuna camera con torcia rilevata. + Errore: impossibile accedere alla torcia. Il servizio accessibilità per µLauncher non è attivo. Attivalo nelle impostazioni Errore: impossibile bloccare lo schermo. (Se hai appena aggiornato l\'app, prova a disabilitare e riattivare il servizio accessibilità nelle impostazioni del telefono) Errore: Il blocco schermo tramite accessibilità non è supportato su questo dispositivo. In alternativa usa il servizio amministratore dispositivo. μLauncher Scegli come bloccare lo schermo Usa il servizio accessibilità - Usa l\'amministratore dispositivo + Usa Amministratore dispositivo Scegli un sistema di blocco dello schermo Scuro Chiaro @@ -132,7 +156,7 @@ Applicazioni nascoste Configura la lista applicazioni Predefinito - Offuscato + Soffuso Imposta μLauncher come predefinito Informazioni app Tutorial di µLauncher @@ -180,7 +204,7 @@ Blocca/Sblocca Spazio Privato Questa funzione richiede Android 15 o successivi. Rosso - Trasparente + Alpha Blu Verde Colore @@ -189,7 +213,7 @@ Sono consapevole che esistono altre opzioni (utilizzando i privilegi di amministratore del dispositivo o il pulsante di accensione). Attivazione dei Servizi di Accessibilità Cerca sul web - Invio in ricerca app per avviare una ricerca web + Invio in ricerca app per avviare una ricerca web. Cerca (senza avvio automatico) Licenze Open Source Licenze Open Source @@ -198,7 +222,7 @@ Copia negli appunti Non segnalare le vulnerabilità di sicurezza pubblicamente su GitHub, ma usa invece: Annulla - Spazio per disabilitare temporaneamente + Spazio per disabilitare temporaneamente. Segnala una vulnerabilità di sicurezza Crea una segnalazione Spazio privato bloccato @@ -207,7 +231,12 @@ µLauncher deve essere il launcher predefinito per poter accedere allo spazio privato. Impossibile aprire l\'URL: nessun browser trovato. Nessuna applicazione trovata per gestire la ricerca. - privilegi più ampi a µLauncher.
µLauncher utilizzerà questi privilegi solo per bloccare lo schermo. µLauncher non raccoglierà mai alcun dato. In particolare, µLauncher non usa il servizio di accessibilità per raccogliere nessun dato.]]>
+ privilegi estesi a μLauncher.
μLauncher userà questi privilegi solo per eseguire le seguenti azioni: +
    +
  • Blocco shermo
  • +
  • App recenti
  • +
+ μLauncher non raccoglierà mai alcun dato. In particolare, μLauncher non utilizza il servizio di accessibilità per raccogliere dati.]]>
Spazio privato Spazio privato Regola volume @@ -238,7 +267,7 @@ Λ (Inverso) - "Musica: Riproduci / Pausa" + Musica: Riproduci / Pausa Inverti la lista applicazioni Azioni Pulsante / gesto indietro @@ -252,7 +281,7 @@ Blocca spazio privato Sblocca spazio privato Versione - Errore: impossibile abilitare il servizio di accessibilità + Errore: impossibile abilitare il servizio di accessibilità. Quando corrisponde una sola app, viene avviata automaticamente.\nPuoi disabilitare l\'avvio con uno spazio prima della query. Tutte le app Puoi cercare rapidamente tra tutte le app nella lista app.\n\nScorri su per la lista o associa ad un gesto diverso. From bcf722ae34019ae9caa0682285f996611ad8adb2 Mon Sep 17 00:00:00 2001 From: Nicola Bortoletto Date: Wed, 23 Apr 2025 19:41:28 +0000 Subject: [PATCH 21/93] Translated using Weblate (Italian) Currently translated at 100.0% (259 of 259 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/ --- app/src/main/res/values-it/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c7e8fcb..71bc28e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -286,4 +286,5 @@ Tutte le app Puoi cercare rapidamente tra tutte le app nella lista app.\n\nScorri su per la lista o associa ad un gesto diverso. Errore: impossibile mostrare le app recenti. (Se hai appena aggiornato l\'app, prova a disabilitare e riabilitare il servizio di accessibilità dalle impostazioni del telefono) + Chiudi la tastiera durante lo scorrimento From 0f92e6cd988d7d0e6440907fcce50a06268d2d58 Mon Sep 17 00:00:00 2001 From: class0068 Date: Thu, 24 Apr 2025 20:57:36 +0000 Subject: [PATCH 22/93] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (259 of 259 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/zh_Hans/ --- app/src/main/res/values-zh-rCN/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 608e57b..32502fa 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -284,4 +284,5 @@ 错误:启用“无障碍”服务失败。 错误:无法展示最近应用屏幕。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用再重新启用“无障碍”服务。) 启动其他启动器 + 滚动应用程序列表时自动隐藏键盘 From 3ac1794e14f3f2ad62a0ee0caa447d541790be9e Mon Sep 17 00:00:00 2001 From: Vossa Excelencia Date: Sun, 27 Apr 2025 13:42:12 +0000 Subject: [PATCH 23/93] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (280 of 280 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/pt_BR/ --- app/src/main/res/values-pt-rBR/strings.xml | 28 +++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index fcdd9e8..3efb611 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -102,7 +102,7 @@ Diminuir volume Música: Próximo Música: Anterior - Não faça nada + Não fazer nada https://github.com/jrpie/Launcher 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 d399d12..998fced 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -419,5 +419,28 @@ 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 diff --git a/build.gradle b/build.gradle index 2ef0f7e..e128ac7 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.kotlin_version = '2.0.0' - ext.android_plugin_version = '8.9.2' + ext.android_plugin_version = '8.10.0' repositories { google() mavenCentral() @@ -10,7 +10,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.9.2' + classpath 'com.android.tools.build:gradle:8.10.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.android.tools.build:gradle:$android_plugin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" From eaece8e334b11ca43b3321cc64f9c105283303c1 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Thu, 15 May 2025 17:48:27 +0200 Subject: [PATCH 54/93] fix #168 --- .../de/jrpie/android/launcher/Application.kt | 8 -------- .../jrpie/android/launcher/ui/HomeActivity.kt | 18 ++++++++---------- .../launcher/ui/widgets/WidgetPanelActivity.kt | 12 ++++++++++++ .../manage/ManageWidgetPanelsActivity.kt | 1 + .../ui/widgets/manage/ManageWidgetsActivity.kt | 8 +++++++- .../ui/widgets/manage/WidgetManagerView.kt | 2 +- .../android/launcher/widgets/AppWidget.kt | 4 ++++ 7 files changed, 33 insertions(+), 20 deletions(-) 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 cf9e697..e0d1d00 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Application.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt @@ -120,8 +120,6 @@ class Application : android.app.Application() { appWidgetHost = AppWidgetHost(this.applicationContext, APP_WIDGET_HOST_ID) appWidgetManager = AppWidgetManager.getInstance(this.applicationContext) - appWidgetHost.startListening() - val preferences = PreferenceManager.getDefaultSharedPreferences(this) LauncherPreferences.init(preferences, this.resources) @@ -178,10 +176,4 @@ class Application : android.app.Application() { apps.postValue(getApps(packageManager, applicationContext)) } } - - override fun onTerminate() { - appWidgetHost.stopListening() - super.onTerminate() - - } } 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 f501107..03c55fc 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 @@ -93,15 +93,6 @@ class HomeActivity : UIObject, Activity() { LauncherPreferences.getSharedPreferences() .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) - (application as Application).appWidgetHost.startListening() - - } - - - - override fun onStop() { - (application as Application).appWidgetHost.stopListening() - super.onStop() } override fun onWindowFocusChanged(hasFocus: Boolean) { @@ -112,7 +103,6 @@ class HomeActivity : UIObject, Activity() { } } - private fun updateSettingsFallbackButtonVisibility() { // If µLauncher settings can not be reached from any action bound to an enabled gesture, // show the fallback button. @@ -131,6 +121,11 @@ class HomeActivity : UIObject, Activity() { return modifyTheme(super.getTheme()) } + override fun onPause() { + (application as Application).appWidgetHost.stopListening() + super.onPause() + } + override fun onResume() { super.onResume() @@ -161,6 +156,9 @@ class HomeActivity : UIObject, Activity() { binding.homeWidgetContainer.updateWidgets(this@HomeActivity, LauncherPreferences.widgets().widgets() ) + + + (application as Application).appWidgetHost.startListening() } override fun onDestroy() { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt index 4392451..ef7bf25 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.res.Resources import android.os.Bundle import androidx.core.view.ViewCompat +import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.R import de.jrpie.android.launcher.databinding.ActivityWidgetPanelBinding import de.jrpie.android.launcher.preferences.LauncherPreferences @@ -57,6 +58,17 @@ class WidgetPanelActivity : Activity(), UIObject { override fun onStart() { super.onStart() super.onStart() + + } + + override fun onPause() { + (application as Application).appWidgetHost.stopListening() + super.onPause() + } + + override fun onResume() { + super.onResume() + (application as Application).appWidgetHost.startListening() } override fun isHomeScreen(): Boolean { 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 index 163777f..89e1057 100644 --- 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 @@ -9,6 +9,7 @@ import android.widget.EditText import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager +import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.R import de.jrpie.android.launcher.databinding.ActivityManageWidgetPanelsBinding import de.jrpie.android.launcher.preferences.LauncherPreferences 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 index 4b5c0c2..58b47be 100644 --- 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 @@ -92,13 +92,19 @@ class ManageWidgetsActivity : UIObject, Activity() { } + override fun onPause() { + (application as Application).appWidgetHost.stopListening() + super.onPause() + } + override fun onResume() { super.onResume() + (application as Application).appWidgetHost.startListening() + binding.manageWidgetsContainer.updateWidgets( this, LauncherPreferences.widgets().widgets() ) - } override fun onWindowFocusChanged(hasFocus: Boolean) { 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 index 76a2572..d1c84d0 100644 --- 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 @@ -113,7 +113,7 @@ class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSe height ) selectedWidgetOverlayView = view - selectedWidgetView = widgetViewById[view.widgetId] ?: return true + selectedWidgetView = widgetViewById[view.widgetId] startWidgetPosition = position val positionInView = start.minus(Point(position.left, position.top)) 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 index 22a63eb..788f709 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt @@ -78,6 +78,10 @@ class AppWidget( 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) From 5f847a8d401d36f36318909fc4a83f2858849611 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Thu, 15 May 2025 20:55:15 +0200 Subject: [PATCH 55/93] enable widget interaction by default on widget panels --- .../main/java/de/jrpie/android/launcher/widgets/AppWidget.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 788f709..a968962 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt @@ -41,7 +41,7 @@ class AppWidget( id, position, panelId, - false, + panelId != WidgetPanel.HOME.id, widgetProviderInfo.provider.packageName, widgetProviderInfo.provider.className, widgetProviderInfo.profile.hashCode() From 271850f75a4ab2375d49282627b2e094082d276b Mon Sep 17 00:00:00 2001 From: renar Date: Tue, 13 May 2025 07:22:28 +0000 Subject: [PATCH 56/93] Translated using Weblate (Italian) Currently translated at 100.0% (280 of 280 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/ --- app/src/main/res/values-it/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 10362a0..bca9108 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -302,9 +302,9 @@ Ok Pannelli widget - Contiene %d widget. - Contiene %d widget. - Contiene %d widget. + Contiene %1$d widget. + Contiene %1$d widget. + Contiene %1$d widget. Crea nuovo pannello widget Apri pannello widget From 013b835ed8f4abb5c453cb8dfecec9a2caea2215 Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Fri, 16 May 2025 02:53:31 -0500 Subject: [PATCH 57/93] more translations --- app/src/main/res/values-lt/strings.xml | 126 ++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 0a45d57..d5c9ecf 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -1,16 +1,136 @@ + + Nepavyksta paleisti programėlės Norite pakeisti nustatymus? + + Atidarykite nustatymus norėdami pasirinkti šio gesto veiksmą + + Nustatymai + Veiksmai Paleidimo programėlė Apie + + Atgal Grįžimo mygtukas / grįžimo gestas + Aukštyn Perbraukimas aukštyn Bakstelėkite + aukštyn - Nepavyksta paleisti programėlės - Atidarykite nustatymus norėdami pasirinkti šio gesto veiksmą - Aukštyn Bakstelėjimas ir perbraukimas aukštyn + Dvigubai aukštyn + Perbraukite aukštyn dviem pirštais + Žemyn + Perbraukite žemyn + Bakstelėkite + žemyn + Bakstelėkite ir perbraukite žemyn + Dvigubai žemyn + Perbraukite dviem pirštais + Kairėje + Perbraukite į kairę + Bakstelėkite + kairę + Bakstelėkite ir perbraukite į kairę + Dvigubai kairėje + Du pirštais perbraukite kairėn + Dešinė + Perbraukite į dešinę + Bakstelėkite + dešinė + Bakstelėkite ir perbraukite į dešinę + Dviguba dešinė + Perbraukite į dešinę dviem pirštais + Dešinė (viršuje) + Perbraukite tiesiai ekrano viršuje + Dešinė (apačia) + Perbraukite tiesiai ekrano apačioje + Kairė (apačia) + Perbraukite į kairę ekrano apačioje + Kairė (viršuje) + Perbraukite kairėn ekrano viršuje + Aukštyn (kairysis kraštas) + Perbraukite aukštyn kairiajame ekrano krašte + Aukštyn (dešinysis kraštas) + Perbraukite aukštyn dešiniajame ekrano krašte + Žemyn (kairysis kraštas) + Perbraukite žemyn kairiajame ekrano krašte + Žemyn (dešinysis kraštas) + Žemyn (dešinysis kraštas) + + Garsumo didinimo klavišas + Paspauskite mygtuką „Volume Up“ + Volume žemyn klavišas + Paspauskite mygtuką „Volume Down“ + Dukart spustelėkite + Dukart spustelėkite tuščią sritį + Ilgas spustelėjimas + Ilgai spustelėkite tuščią sritį + Data + Spustelėkite datą + Laikas + Spustelėkite laiką + + Tvarkykite valdiklius + Tvarkykite valdiklio skydelius + + + Pasirinkite programą + + Įdiekite programas + Parduotuvėje nerasta + + + Išvaizda + + + Spalvos tema + Numatytasis + Tamsu + Šviesa + Dinaminis + + Teksto šešėlis + Fonas (programų sąrašas ir nustatymas) + Skaidrus + Dim + Blur + Solidus + + + Šriftas + + + Sistemos numatytasis + Be serifo + Serifas + Monoerdvė + Serifo monospace + Vienspalvių programų piktogramos + + + Spalva + Rodyti laiką + Rodyti datą + Naudoti lokalizuotą datos formatą + Rodyti sekundes + Apversti datą ir laiką + + Pasirinkite ekrano foną + Ekranas From 31a9049861b3b08e8e8c953818544e5d7013bef5 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 17 May 2025 11:42:21 +0200 Subject: [PATCH 58/93] try to mitigate #172 --- .../main/java/de/jrpie/android/launcher/ui/HomeActivity.kt | 7 ++++++- .../android/launcher/ui/widgets/WidgetPanelActivity.kt | 7 ++++++- .../launcher/ui/widgets/manage/ManageWidgetsActivity.kt | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) 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 03c55fc..f3cde9a 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 @@ -122,7 +122,12 @@ class HomeActivity : UIObject, Activity() { } override fun onPause() { - (application as Application).appWidgetHost.stopListening() + try { + (application as Application).appWidgetHost.stopListening() + } catch (e: Exception) { + // Throws a NullPointerException on Android 12 an earlier, see #172 + e.printStackTrace() + } super.onPause() } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt index ef7bf25..3c884db 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt @@ -62,7 +62,12 @@ class WidgetPanelActivity : Activity(), UIObject { } override fun onPause() { - (application as Application).appWidgetHost.stopListening() + try { + (application as Application).appWidgetHost.stopListening() + } catch (e: Exception) { + // Throws a NullPointerException on Android 12 an earlier, see #172 + e.printStackTrace() + } super.onPause() } 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 index 58b47be..984df85 100644 --- 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 @@ -93,7 +93,12 @@ class ManageWidgetsActivity : UIObject, Activity() { } override fun onPause() { - (application as Application).appWidgetHost.stopListening() + try { + (application as Application).appWidgetHost.stopListening() + } catch (e: Exception) { + // Throws a NullPointerException on Android 12 an earlier, see #172 + e.printStackTrace() + } super.onPause() } From bd7df4f6a04d92240a4e40c3c807cc6c9c10d70d Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 17 May 2025 12:12:03 +0200 Subject: [PATCH 59/93] (try to) fix #172 --- .../launcher/preferences/Preferences.kt | 12 ++++-- .../launcher/preferences/legacy/Version100.kt | 39 +++++++++++++++++++ .../launcher/preferences/legacy/Version4.kt | 5 ++- .../widgets/manage/ManageWidgetsActivity.kt | 4 -- .../ui/widgets/manage/SelectWidgetActivity.kt | 12 +++--- .../android/launcher/widgets/ClockWidget.kt | 2 +- .../launcher/widgets/DebugInfoWidget.kt | 2 +- .../jrpie/android/launcher/widgets/Widget.kt | 4 +- .../jrpie/android/launcher/widgets/Widgets.kt | 17 +++++--- 9 files changed, 72 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt 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 8936675..e5877f5 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 @@ -10,6 +10,7 @@ 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 @@ -21,12 +22,13 @@ 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.deleteAllWidgets +import de.jrpie.android.launcher.widgets.generateInternalId /* Current version of the structure of preferences. * Increase when breaking changes are introduced and write an appropriate case in * `migratePreferencesToNewVersion` */ -const val PREFERENCE_VERSION = 100 +const val PREFERENCE_VERSION = 101 const val UNKNOWN_PREFERENCE_VERSION = -1 private const val TAG = "Launcher - Preferences" @@ -65,6 +67,10 @@ fun migratePreferencesToNewVersion(context: Context) { 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( @@ -91,7 +97,7 @@ fun resetPreferences(context: Context) { LauncherPreferences.widgets().widgets( setOf( ClockWidget( - (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + generateInternalId(), WidgetPosition(1, 3, 10, 4), WidgetPanel.HOME.id ) @@ -103,7 +109,7 @@ fun resetPreferences(context: Context) { LauncherPreferences.widgets().widgets().also { it.add( DebugInfoWidget( - (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + generateInternalId(), WidgetPosition(1, 1, 10, 4), WidgetPanel.HOME.id ) 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/Version4.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt index a9ab3a1..b13978b 100644 --- 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 @@ -7,19 +7,20 @@ import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION import de.jrpie.android.launcher.widgets.ClockWidget import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition +import de.jrpie.android.launcher.widgets.generateInternalId fun migratePreferencesFromVersion4(context: Context) { - assert(PREFERENCE_VERSION == 100) assert(LauncherPreferences.internal().versionCode() < 100) LauncherPreferences.widgets().widgets( setOf( ClockWidget( - (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + 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/ui/widgets/manage/ManageWidgetsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt index 984df85..953fc28 100644 --- 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 @@ -135,10 +135,6 @@ class ManageWidgetsActivity : UIObject, Activity() { val appWidgetHost = (application as Application).appWidgetHost startActivityForResult( Intent(this, SelectWidgetActivity::class.java).also { - it.putExtra( - AppWidgetManager.EXTRA_APPWIDGET_ID, - appWidgetHost.allocateAppWidgetId() - ) it.putExtra( EXTRA_PANEL_ID, panelId 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 index 0efdb43..eeb98df 100644 --- 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 @@ -14,6 +14,7 @@ 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 @@ -24,7 +25,7 @@ import de.jrpie.android.launcher.widgets.LauncherWidgetProvider import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission -import de.jrpie.android.launcher.widgets.getAppWidgetHost +import de.jrpie.android.launcher.widgets.generateInternalId import de.jrpie.android.launcher.widgets.getAppWidgetProviders import de.jrpie.android.launcher.widgets.updateWidget @@ -38,12 +39,13 @@ private const val REQUEST_WIDGET_PERMISSION = 29 */ class SelectWidgetActivity : AppCompatActivity(), UIObject { lateinit var binding: ActivitySelectWidgetBinding - var widgetId: Int = -1 var widgetPanelId: Int = WidgetPanel.HOME.id private fun tryBindWidget(info: LauncherWidgetProvider) { when (info) { is LauncherAppWidgetProvider -> { + val widgetId = + (applicationContext as Application).appWidgetHost.allocateAppWidgetId() if (bindAppWidgetOrRequestPermission( this, info.info, @@ -62,7 +64,7 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { } } is LauncherClockWidgetProvider -> { - updateWidget(ClockWidget(widgetId, WidgetPosition(0, 4, 12, 3), widgetPanelId)) + updateWidget(ClockWidget(generateInternalId(), WidgetPosition(0, 4, 12, 3), widgetPanelId)) finish() } } @@ -81,11 +83,7 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { setContentView(binding.root) - widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.HOME.id) - if (widgetId == -1) { - widgetId = getAppWidgetHost().allocateAppWidgetId() - } val viewManager = LinearLayoutManager(this) val viewAdapter = SelectWidgetRecyclerAdapter() 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 index d0d1c0e..f864ee8 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt @@ -12,7 +12,7 @@ import kotlinx.serialization.Serializable @Serializable @SerialName("widget:clock") class ClockWidget( - override val id: Int, + override var id: Int, override var position: WidgetPosition, override val panelId: Int, override var allowInteraction: Boolean = true 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 index 01ecddc..75ae6d0 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/DebugInfoWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/DebugInfoWidget.kt @@ -12,7 +12,7 @@ import kotlinx.serialization.Serializable @Serializable @SerialName("widget:debuginfo") class DebugInfoWidget( - override val id: Int, + override var id: Int, override var position: WidgetPosition, override val panelId: Int, override var allowInteraction: Boolean = true 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 index 28539a2..fd96f14 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt @@ -27,7 +27,9 @@ sealed class Widget { abstract fun configure(activity: Activity, requestCode: Int) fun delete(context: Context) { - context.getAppWidgetHost().deleteAppWidgetId(id) + if (id >= 0) { + context.getAppWidgetHost().deleteAppWidgetId(id) + } LauncherPreferences.widgets().widgets( LauncherPreferences.widgets().widgets()?.also { diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt index cded50c..593f3b3 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -13,6 +13,7 @@ import android.os.UserManager import android.util.Log import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.preferences.LauncherPreferences +import kotlin.math.min fun deleteAllWidgets(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -29,12 +30,9 @@ fun deleteAllWidgets(context: Context) { * * @return true iff the app widget was bound successfully. */ -fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidgetProviderInfo, id: Int, requestCode: Int? = null): Boolean { - val appWidgetId = if(id == -1) { - activity.getAppWidgetHost().allocateAppWidgetId() - } else { id } +fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidgetProviderInfo, appWidgetId: Int, requestCode: Int? = null): Boolean { - Log.i("Launcher", "Binding new widget ${appWidgetId}") + Log.i("Launcher", "Binding new widget $appWidgetId") if (!activity.getAppWidgetManager().bindAppWidgetIdIfAllowed( appWidgetId, providerInfo.provider @@ -79,6 +77,13 @@ fun updateWidget(widget: 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()) @@ -92,4 +97,4 @@ fun Context.getAppWidgetHost(): AppWidgetHost { } fun Context.getAppWidgetManager(): AppWidgetManager { return (this.applicationContext as Application).appWidgetManager -} +} \ No newline at end of file From dd3a2e91bd7baaa510ea0b12f1ce7f2db3f5d327 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 17 May 2025 13:00:16 +0200 Subject: [PATCH 60/93] 0.2.2 --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/47.txt | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/47.txt diff --git a/app/build.gradle b/app/build.gradle index 42f0a2b..54e7b2b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { minSdkVersion 21 targetSdkVersion 35 compileSdk 35 - versionCode 46 - versionName "0.2.1" + versionCode 47 + versionName "0.2.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } 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!) From ba3255d9ec6218c274f3be575e610012cced48cc Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Mon, 19 May 2025 16:34:56 -0500 Subject: [PATCH 61/93] Improve widget list layout --- app/src/main/res/layout/list_widgets_row.xml | 131 ++++++++++++------- 1 file changed, 85 insertions(+), 46 deletions(-) diff --git a/app/src/main/res/layout/list_widgets_row.xml b/app/src/main/res/layout/list_widgets_row.xml index 878aaad..147cc36 100644 --- a/app/src/main/res/layout/list_widgets_row.xml +++ b/app/src/main/res/layout/list_widgets_row.xml @@ -6,55 +6,94 @@ android:id="@+id/list_apps_row_container" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="15sp"> + android:layout_marginTop="15sp" + android:layout_marginHorizontal="30sp"> - - - - - - - - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From a56cc772f71ea38560dc46c57ad5932277038a2b Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Tue, 20 May 2025 11:58:12 -0500 Subject: [PATCH 62/93] Add visual cues and interactive styling to the settings fragment --- app/src/main/res/layout/tutorial_4_setup.xml | 26 ++++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/layout/tutorial_4_setup.xml b/app/src/main/res/layout/tutorial_4_setup.xml index 9650c57..c55bdc8 100644 --- a/app/src/main/res/layout/tutorial_4_setup.xml +++ b/app/src/main/res/layout/tutorial_4_setup.xml @@ -33,17 +33,27 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tutorial_setup_title" /> - + app:layout_constraintEnd_toEndOf="parent"> + + + + Date: Sat, 24 May 2025 13:31:00 +0200 Subject: [PATCH 63/93] prevent crash when unable to access widgetproviderinfo --- .../ui/widgets/manage/ManageWidgetsActivity.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 index 953fc28..38bbb84 100644 --- 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 @@ -147,13 +147,17 @@ class ManageWidgetsActivity : UIObject, Activity() { 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 provider = appWidgetManager.getAppWidgetInfo(appWidgetId) - 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), @@ -161,7 +165,7 @@ class ManageWidgetsActivity : UIObject, Activity() { max(3, (GRID_SIZE * (widgetInfo.minHeight) / display.height.toFloat()).roundToInt()) ) - val widget = AppWidget(appWidgetId, position, panelId, provider) + val widget = AppWidget(appWidgetId, position, panelId, widgetInfo) LauncherPreferences.widgets().widgets( (LauncherPreferences.widgets().widgets() ?: HashSet()).also { it.add(widget) From e4c7ad099420606631011f8bcdafbe894f2ba718 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 17:40:32 +0200 Subject: [PATCH 64/93] prepare docs/ for hugo ssg (see #176) --- docs/_index.md | 0 docs/actions-and-gestures.md | 2 +- docs/alternatives.md | 3 ++ docs/changes-fork.md | 9 ++-- docs/contributing.md | 2 +- docs/home.md | 43 ----------------- docs/settings.md | 89 +++++++++++++++++++----------------- docs/widgets.md | 27 +++++++++++ 8 files changed, 86 insertions(+), 89 deletions(-) create mode 100644 docs/_index.md create mode 100644 docs/alternatives.md delete mode 100644 docs/home.md create mode 100644 docs/widgets.md diff --git a/docs/_index.md b/docs/_index.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/actions-and-gestures.md b/docs/actions-and-gestures.md index f5d831f..a5348a3 100644 --- a/docs/actions-and-gestures.md +++ b/docs/actions-and-gestures.md @@ -1,4 +1,4 @@ -# Gestures and Actions +# Actions and Gestures µLauncher's central mechanism for accessing important functionality quickly is to bind actions (e.g. launching an app) to gestures (e.g. swiping up). diff --git a/docs/alternatives.md b/docs/alternatives.md new file mode 100644 index 0000000..4c875f4 --- /dev/null +++ b/docs/alternatives.md @@ -0,0 +1,3 @@ + +TODO: move the [hedgedoc](https://pad.abstractnonsen.se/foss-launchers) document here. + diff --git a/docs/changes-fork.md b/docs/changes-fork.md index 8efc965..3abe96a 100644 --- a/docs/changes-fork.md +++ b/docs/changes-fork.md @@ -1,3 +1,7 @@ ++++ +title = 'Differences to the original Launcher' ++++ + # Notable changes compared to Finn's Launcher µLauncher is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). @@ -51,6 +55,5 @@ The complete list of changes can be viewed [here](https://github.com/jrpie/launc --- -\[original-repo\]: [https://github.com/finnmglas/Launcher](https://github.com/finnmglas/Launcher) - -\[hack-font\]: [https://sourcefoundry.org/hack/](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 index 8e9de53..350ba01 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -13,7 +13,7 @@ There are several ways to contribute to this app: - Open a new pull request. -See [build.md](build.md) for instructions how to build this project. +See [here](/docs/build) for instructions how to build this project. The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds. diff --git a/docs/home.md b/docs/home.md deleted file mode 100644 index 9812e93..0000000 --- a/docs/home.md +++ /dev/null @@ -1,43 +0,0 @@ -# Welcome to the μLauncher Documentation - -## What is μLauncher? - -µLauncher is an *minimal* and *distraction-free* Android home screen that lets you launch apps using [swipe gestures and button presses](/actions-and-gestured.md). - -This project is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). An incomplete list of changes can be found [here](https://github.com/wassupluke/Launcher/blob/master/docs/launcher.md). - -## Where can I get μLauncher? - -[![Get it on F-Droid](https://fdroid.gitlab.io/artwork/badge/get-it-on.png)](https://f-droid.org/packages/de.jrpie.android.launcher/) - -[![Get it on Accrescent](https://accrescent.app/badges/get-it-on.png)](https://accrescent.app/app/de.jrpie.android.launcher.accrescent) - -[![Get it on Obtainium](https://raw.githubusercontent.com/ImranR98/Obtainium/b1c8ac6f2ab08497189721a788a5763e28ff64cd/assets/graphics/badge_obtainium.png)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/{%22id%22:%22de.jrpie.android.launcher%22,%22url%22:%22https://github.com/jrpie/Launcher%22,%22author%22:%22jrpie%22,%22name%22:%22%c2%b5Launcher%22,%22additionalSettings%22:%22{\%22apkFilterRegEx\%22:\%22release\%22,\%22invertAPKFilter\%22:false,\%22about\%22:\%22%c2%b5Launcher%20is%20a%20minimal%20home%20screen.\%22}%22}) - -[![Get it on GitHub](https://raw.githubusercontent.com/NeoApplications/Neo-Backup/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png)](https://github.com/jrpie/launcher/releases/latest) - -> You can also [get it on Google Play](https://play.google.com/store/apps/details?id=de.jrpie.android.launcher), but this is not recommend. - - -## How can I contribute? - -See [docs/contribute](/contribute.md) - -## Screenshots - -![μLauncher Home Screen screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg) - -![μLauncher Settings screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg) - -![μLauncher All Apps list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg) - -![μLauncher Favorite Apps list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg) - -![μLauncher Choose App to bind to gesture screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg) - -![μLauncher App options card from list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/7.jpg -) - -![μLauncher All Apps list view without icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/8.jpg) - - diff --git a/docs/settings.md b/docs/settings.md index f033f62..3cbfbec 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -1,19 +1,18 @@ -# Launcher Settings +# Settings -Tweaks and customizations can be made from within the Launcher Settings page. +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] -These settings let you change wallpapers, change colors and fonts, enable monochrome app icons, change the app drawer layout, and much more. - -In the following documentation, 'app drawer' will be used to refer to the 'All Apps', 'Favorite Apps' and 'Private Space' views. +[^1]: i.e. the 'All Apps', 'Favorite Apps' and 'Private Space' views. ## Appearance -> ### Choose a wallpaper +### Choose a wallpaper This triggers Android's mechanism to change the wallpaper using a photos 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) +### Font (in-app font) Set the font used within the app settings. This setting does not affect the date/time home screen font. @@ -21,17 +20,17 @@ Set the font used within the app settings. This setting does not affect the date **options:** `Hack`,`System default`,`Sans serif`,`Serif`,`Monospace`,`Serif monospace` -> ### Text Shadow +### Text Shadow **type:** `toggle` -> ### Background (app list and setting) +### Background (app list and setting) **type:** `dropdown` **type:** `Transparent`,`Dim`,`Blur`,`Solid` -> ### Monochrome app icons +### Monochrome app icons Remove coloring from all app icons. Can help decrease visual stimulus when enabled. @@ -39,49 +38,56 @@ Remove coloring from all app icons. Can help decrease visual stimulus when enabl ## Date & Time -> ### Font (home screen) +These settings effect the clock shown on the home screen (or on widget panels). +If the clock is removed, the settings are not used. -Set the home screen font for date and time. This setting does not affect the in-app font. +### 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 [`[bug]`](https://github.com/jrpie/launcher/issues/151) +### Color Set the color for the home screen date and time. -Accepts a HEX color code (consisting of a '#' followed by three sets of two alphanumeric (letters and numbers) characters. A fourth set of two alphanumeric characters may be added to set the transparency of the color. +Accepts an 6 digit RGB or 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. -[Color wheel picker](https://rgbacolorpicker.com/color-wheel-picker) -**type:** `HEX`,`RGBA` +[^2]: More precisely, everything that is vaild input for [parseColor](https://developer.android.com/reference/android/graphics/Color#parseColor(java.lang.String)) can be used. -> ### Use localized date format -Adapt the display of dates and times to the specific conventions of a particular locale or region. Different locales use different date orders (e.g., MM/DD/YYYY in the US, DD/MM/YYYY in Europe). +**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 time Show the current time on the home screen. **type:** `toggle` -> ### Show seconds +### Show seconds Show the current time down to the second on the home screen. **type:** `toggle` -> ### Show date +### Show date Show the current date on the home screen. **type:** `toggle` -> ### Flip date and time +### Flip date and time Place the current time above the current date on the home screen. @@ -89,7 +95,7 @@ Place the current time above the current date on the home screen. ## Functionality -> ### Launch search results +### Launch search results Launches any app that matches user keyboard input when no other apps match. @@ -105,37 +111,37 @@ Press space to temporarily disable this feature and allow text entry without pre **type:** `toggle` -> ### Search the web +### Search the web -Press return/enter while searching the app list to launch a web search. +Press return while searching the app list to launch a web search. **type:** `toggle` -> ### Start keyboard for search +### Start keyboard for search Automatically open the keyboard when the app drawer is opened. **type:** `toggle` -> ### Double swipe gestures +### 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 +### 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 +### 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 +### Choose method for locking the screen There are two methods to lock the screen and unfortunately both have downsides. @@ -157,11 +163,11 @@ There are two methods to lock the screen and unfortunately both have downsides. ## Apps -> ### Hidden 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 +### 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. @@ -169,20 +175,21 @@ Reduces redundancy and tidies up app drawer. **type:** `toggle` -> ### Hide paused apps +### 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 +### Hide private space from app list Remove private space from 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 +### Layout of app list Changes how the apps are displayed when accessing the app drawer. @@ -195,7 +202,7 @@ Changes how the apps are displayed when accessing the app drawer. **options:** `Default`,`Text`,`Grid` -> ### Reverse the app list +### Reverse the app list Enable reverse alphabetical sorting of apps in the app drawer. Useful for keeping apps within easier reach from the keyboard. @@ -204,21 +211,21 @@ Useful for keeping apps within easier reach from the keyboard. ## Display -> ### Rotate screen +### Rotate screen **type:** `toggle` -> ### Keep screen on +### Keep screen on **type:** `toggle` -> ### Hide status bar +### Hide status bar Remove the top status bar from the home screen. **type:** `toggle` -> ### Hide navigation bar +### 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 setup properly. @@ -226,7 +233,7 @@ Remove the navigation bar from the home screen. Enabling this setting may make i ## Additional Settings -> ### App Drawer Long Press on App +### App Drawer Long Press on App Access additional per-app details and settings. To use, open the app drawer and long press on any app. diff --git a/docs/widgets.md b/docs/widgets.md new file mode 100644 index 0000000..ab5e21f --- /dev/null +++ b/docs/widgets.md @@ -0,0 +1,27 @@ ++++ + title = 'Widgets' ++++ + +# 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 similar as 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. From ac1639b77ed1c940f69d055c50e2a1583c4270ef Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 19:01:15 +0200 Subject: [PATCH 65/93] add termux example to docs (see #147 and #149) --- docs/examples/_index.md | 2 ++ docs/examples/termux/index.md | 23 +++++++++++++++++++++++ docs/examples/termux/screenshot1.png | Bin 0 -> 47999 bytes docs/examples/termux/screenshot2.png | Bin 0 -> 33737 bytes 4 files changed, 25 insertions(+) create mode 100644 docs/examples/_index.md create mode 100644 docs/examples/termux/index.md create mode 100644 docs/examples/termux/screenshot1.png create mode 100644 docs/examples/termux/screenshot2.png diff --git a/docs/examples/_index.md b/docs/examples/_index.md new file mode 100644 index 0000000..0847315 --- /dev/null +++ b/docs/examples/_index.md @@ -0,0 +1,2 @@ +# Examples +This section contains some examples how μLauncher can be tweaked. 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. + +screenshot +screenshot + + +[^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 0000000000000000000000000000000000000000..684c8bfebd7c30da1655870bd5c52e4ecf8dfe53 GIT binary patch literal 47999 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 vUYP>$iMm$J-Uc#9=@uY#f8 Date: Sat, 24 May 2025 19:22:08 +0200 Subject: [PATCH 66/93] add example to docs --- docs/examples/_index.md | 5 +++++ docs/examples/apps-on-home-screen.md | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 docs/examples/apps-on-home-screen.md diff --git a/docs/examples/_index.md b/docs/examples/_index.md index 0847315..b50540f 100644 --- a/docs/examples/_index.md +++ b/docs/examples/_index.md @@ -1,2 +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 %}} From ea5a4ad7bfd4b03a132c719ce94b1512620d73a4 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 19:22:08 +0200 Subject: [PATCH 67/93] add example to docs --- docs/actions-and-gestures.md | 4 ++++ docs/alternatives.md | 5 ++++- docs/build.md | 5 +++++ docs/contributing.md | 5 +++++ docs/examples/_index.md | 5 +++++ docs/examples/apps-on-home-screen.md | 18 ++++++++++++++++++ docs/profiles.md | 6 ++++++ docs/settings.md | 4 ++++ docs/widgets.md | 1 + 9 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 docs/examples/apps-on-home-screen.md diff --git a/docs/actions-and-gestures.md b/docs/actions-and-gestures.md index a5348a3..dc00970 100644 --- a/docs/actions-and-gestures.md +++ b/docs/actions-and-gestures.md @@ -1,3 +1,7 @@ ++++ + weight = 10 ++++ + # Actions and Gestures µLauncher's central mechanism for accessing important functionality quickly diff --git a/docs/alternatives.md b/docs/alternatives.md index 4c875f4..174ee8a 100644 --- a/docs/alternatives.md +++ b/docs/alternatives.md @@ -1,3 +1,6 @@ ++++ + weight = 100 ++++ -TODO: move the [hedgedoc](https://pad.abstractnonsen.se/foss-launchers) document here. +ODO: move the [hedgedoc](https://pad.abstractnonsen.se/foss-launchers) document here. diff --git a/docs/build.md b/docs/build.md index 1ffc338..a45ab4c 100644 --- a/docs/build.md +++ b/docs/build.md @@ -1,3 +1,8 @@ ++++ + weight = 50 ++++ + + # Building from Source ## Using the command line diff --git a/docs/contributing.md b/docs/contributing.md index 350ba01..f74bcca 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,3 +1,8 @@ ++++ + weight = 40 ++++ + + # Contributing There are several ways to contribute to this app: diff --git a/docs/examples/_index.md b/docs/examples/_index.md index 0847315..b50540f 100644 --- a/docs/examples/_index.md +++ b/docs/examples/_index.md @@ -1,2 +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/profiles.md b/docs/profiles.md index d9eaf52..2087d06 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -1,3 +1,9 @@ ++++ + 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. diff --git a/docs/settings.md b/docs/settings.md index 3cbfbec..eff0417 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -1,3 +1,7 @@ ++++ + weight = 10 ++++ + # Settings Tweaks and customizations can be made from within the settings page. diff --git a/docs/widgets.md b/docs/widgets.md index ab5e21f..acbdf05 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -1,5 +1,6 @@ +++ title = 'Widgets' + weight = 11 +++ # Widgets From 580644f9d4a756499c31277a3d4170afca838977 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 20:29:01 +0200 Subject: [PATCH 68/93] add documentation of app drawer --- docs/app-drawer.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++ docs/settings.md | 10 --------- 2 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 docs/app-drawer.md diff --git a/docs/app-drawer.md b/docs/app-drawer.md new file mode 100644 index 0000000..d977c28 --- /dev/null +++ b/docs/app-drawer.md @@ -0,0 +1,54 @@ ++++ + 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 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 clicking 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 which are not needed at all but [can not be uninstalled](https://en.wikipedia.org/wiki/Software_bloat#Bloatware). diff --git a/docs/settings.md b/docs/settings.md index eff0417..eb780fc 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -234,13 +234,3 @@ Remove the top status bar from the home screen. Remove the navigation bar from the home screen. Enabling this setting may make it difficult to use the device if gestures are not setup properly. **type:** `toggle` - -## Additional Settings - -### App Drawer Long Press on App - -Access additional per-app details and settings. To use, open the app drawer and long press on any app. - -**type:** `dropdown` - -**options:** `App Info`,`Add to favorites`,`Hide`,`Rename`,`Uninstall` From 71193a2e50b03bf46f16a37cdea357740563fb74 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 20:34:15 +0200 Subject: [PATCH 69/93] fixed typo --- docs/build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build.md b/docs/build.md index a45ab4c..3d1605c 100644 --- a/docs/build.md +++ b/docs/build.md @@ -42,7 +42,7 @@ for further instructions. 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. How to +for further instructions. ## CI Pipeline From 1d10d65adbc35c4f18945fe4edc41b10a6177365 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 20:41:37 +0200 Subject: [PATCH 70/93] update documentation --- docs/build.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/build.md b/docs/build.md index 3d1605c..28e889a 100644 --- a/docs/build.md +++ b/docs/build.md @@ -47,4 +47,8 @@ for further instructions. ## CI Pipeline The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds. -> Note: These builds are *not* signed. They are in built in debug mode and only suitable for testing. + +{{% hint warning %}} +Note: These builds are not signed. +They are in built in debug mode and only suitable for testing. +{{% /hint %}} From 85a7ed24ab2e6816f97e6cbe3feb3add3eda5465 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 24 May 2025 21:08:45 +0200 Subject: [PATCH 71/93] add documentation button --- .../launcher/ui/settings/meta/SettingsFragmentMeta.kt | 3 +++ app/src/main/res/layout/settings_meta.xml | 7 +++++++ app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 12 insertions(+) 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 759d8cd..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() diff --git a/app/src/main/res/layout/settings_meta.xml b/app/src/main/res/layout/settings_meta.xml index 6f21baa..1adee37 100644 --- a/app/src/main/res/layout/settings_meta.xml +++ b/app/src/main/res/layout/settings_meta.xml @@ -41,6 +41,13 @@ android:text="@string/settings_meta_view_code" android:textAllCaps="false" /> +