From 0a9890111cd45bbad0c935816f585c328da34dd5 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Fri, 31 Jan 2025 01:38:20 +0100 Subject: [PATCH 001/103] add explanation for pressing space to disable auto launch --- app/src/main/res/xml/preferences.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 57e50df..314f85f 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -92,7 +92,8 @@ + android:title="@string/settings_functionality_auto_launch" + android:summary="@string/settings_functionality_auto_launch_summary" /> Date: Fri, 31 Jan 2025 01:54:12 +0100 Subject: [PATCH 002/103] updated bug report template --- .github/ISSUE_TEMPLATE/bug_report.md | 38 -------------------- .github/ISSUE_TEMPLATE/bug_report.yaml | 50 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 38 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index d14b126..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help improve this app -title: '[bug] ' -labels: bug -assignees: '' - ---- - -# Describe the bug - - - -# To Reproduce - - - -# Expected behavior - - - -# Screenshots - - -# Smartphone (please complete the following information) - - Device: - - Android Version: - - µLauncher Version: - -# Additional info - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..fa112ae --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,50 @@ +name: Bug report +description: Create a report to help improve this app +title: '[bug] ' +labels: bug +body: + - type: markdown + attributes: + value: | + Thank you for helping to improve µLauncher! + - type: textarea + id: bug + attributes: + label: Describe the Bug + description: What happened? + placeholder: A clear and concise description of what the bug is. + render: markdown + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What did you expect to happen instead? + render: markdown + validations: + required: false + - type: textarea + id: reproduce + attributes: + label: To Reproduce + description: What steps are required to reproduce the bug? + render: markdown + placeholder: | + Steps to reproduce the behavior: + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: false + - type: textarea + id: device + attributes: + label: Your Device + description: | + What device are you using? Adding this information helps to reproduce the bug. + You can copy this from µLauncher > Settings > Meta > Report Bug. + render: markdown + validations: + required: false From 8699b92246d110c366d1806c1bbb027471e5f1b2 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Wed, 5 Feb 2025 22:07:23 +0100 Subject: [PATCH 003/103] update the bug report url --- app/src/main/res/values/donottranslate.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 6a28185..53aabad 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -154,7 +154,7 @@ - --> https://github.com/jrpie/Launcher - https://github.com/jrpie/Launcher/issues/new?template=bug_report.md + https://github.com/jrpie/Launcher/issues/new?template=bug_report.yaml https://github.com/jrpie/Launcher/security/policy https://s.jrpie.de/contact https://s.jrpie.de/android-legal From fa2f1c4127a892a2956b8b54e8ea697f097cfd38 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Wed, 5 Feb 2025 23:47:37 +0100 Subject: [PATCH 004/103] fix #106 (ugly workaround) --- .../launcher/ui/settings/SettingsActivity.kt | 23 ++++++++++++++++++- app/src/main/res/values/styles.xml | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt index e46a956..fde61a7 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/SettingsActivity.kt @@ -16,6 +16,8 @@ import de.jrpie.android.launcher.R import de.jrpie.android.launcher.REQUEST_CHOOSE_APP import de.jrpie.android.launcher.databinding.SettingsBinding import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.preferences.theme.Background +import de.jrpie.android.launcher.preferences.theme.ColorTheme import de.jrpie.android.launcher.saveListActivityChoice import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.settings.actions.SettingsFragmentActions @@ -33,8 +35,24 @@ import de.jrpie.android.launcher.ui.settings.meta.SettingsFragmentMeta */ class SettingsActivity : AppCompatActivity(), UIObject { - private var sharedPreferencesListener = + private val solidBackground = LauncherPreferences.theme().background() == Background.SOLID + || LauncherPreferences.theme().colorTheme() == ColorTheme.LIGHT + + private val sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> + if (solidBackground && + (prefKey == LauncherPreferences.theme().keys().background() || + prefKey == LauncherPreferences.theme().keys().colorTheme()) + ) { + // Switching from solid background to a transparent background using `recreate()` + // causes a very ugly glitch, making the settings unreadable. + // This ugly workaround causes a jump to the top of the list, but at least + // the text stays readable. + val i = Intent(this, SettingsActivity::class.java) + .also { it.putExtra("tab", 1) } + finish() + startActivity(i) + } else if (prefKey?.startsWith("theme.") == true || prefKey?.startsWith("display.") == true ) { @@ -59,6 +77,9 @@ class SettingsActivity : AppCompatActivity(), UIObject { val tabs: TabLayout = findViewById(R.id.settings_tabs) tabs.setupWithViewPager(viewPager) + if (intent.hasExtra("tab")) { + tabs.getTabAt(intent.getIntExtra("tab", 0))?.select() + } } override fun onStart() { diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 11a92fc..02d809e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -78,10 +78,10 @@ @android:color/transparent true + @null From 944eb89fef32a0d6c1901f00ec5321c2ccde38ef Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Thu, 6 Feb 2025 22:27:58 +0100 Subject: [PATCH 005/103] implement #93 - treat back button as a gesture --- .../jrpie/android/launcher/actions/Action.kt | 8 +++++- .../android/launcher/actions/AppAction.kt | 4 +++ .../jrpie/android/launcher/actions/Gesture.kt | 6 +++++ .../launcher/actions/LauncherAction.kt | 19 ++++++++++---- .../launcher/preferences/Preferences.kt | 12 ++++++--- .../launcher/preferences/legacy/Version1.kt | 3 ++- .../launcher/preferences/legacy/Version2.kt | 20 ++++++++++++++ .../preferences/legacy/VersionUnknown.kt | 1 - .../jrpie/android/launcher/ui/HomeActivity.kt | 26 ++++++++++++++++++- app/src/main/res/layout/home.xml | 14 ++++++++++ app/src/main/res/values/defaults.xml | 6 +++++ app/src/main/res/values/strings.xml | 2 ++ 12 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt 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 2d03061..ddef92a 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 @@ -21,6 +21,9 @@ sealed interface Action { fun getIcon(context: Context): Drawable? fun isAvailable(context: Context): Boolean + // Can the action be used to reach µLauncher settings? + fun canReachSettings(): Boolean + fun bindToGesture(prefEditor: Editor, id: String) { prefEditor.putString(id, Json.encodeToString(this)) @@ -50,7 +53,10 @@ sealed interface Action { .map { Pair(it, Json.decodeFromString(it)) } .firstOrNull { it.second.isAvailable(context) } ?.apply { - boundActions.add(first) + // allow to bind CHOOSE to multiple gestures + if (second != LauncherAction.CHOOSE) { + boundActions.add(first) + } second.bindToGesture(editor, gesture.id) } } diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/AppAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/AppAction.kt index 4b71a90..90145aa 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/AppAction.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/AppAction.kt @@ -74,4 +74,8 @@ class AppAction(val app: AppInfo) : Action { // check if app is installed return DetailedAppInfo.fromAppInfo(app, context) != null } + + override fun canReachSettings(): Boolean { + return false + } } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt b/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt index e7358ba..dba314d 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt @@ -168,6 +168,12 @@ enum class Gesture( R.string.settings_gesture_description_double_right, R.array.default_double_right, R.anim.left_right + ), + BACK( + "action.back", + R.string.settings_gesture_back, + R.string.settings_gesture_description_back, + R.array.default_up ); enum class Edge { diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt index 03f4b11..ac6958b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt @@ -13,6 +13,7 @@ import android.os.UserManager import android.provider.Settings import android.view.KeyEvent import android.widget.Toast +import androidx.appcompat.widget.AppCompatDrawableManager import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.R import de.jrpie.android.launcher.apps.AppFilter @@ -40,25 +41,29 @@ enum class LauncherAction( val label: Int, val icon: Int, val launch: (Context) -> Unit, - val available: (Context) -> Boolean = { true } + private val canReachSettings: Boolean = false, + val available: (Context) -> Boolean = { true }, ) : Action { SETTINGS( "settings", R.string.list_other_settings, R.drawable.baseline_settings_24, - ::openSettings + ::openSettings, + true ), CHOOSE( "choose", R.string.list_other_list, R.drawable.baseline_menu_24, - ::openAppsList + ::openAppsList, + true ), CHOOSE_FROM_FAVORITES( "choose_from_favorites", R.string.list_other_list_favorites, R.drawable.baseline_favorite_24, - { context -> openAppsList(context, true) } + { context -> openAppsList(context, true) }, + true ), TOGGLE_PRIVATE_SPACE_LOCK( "toggle_private_space_lock", @@ -127,7 +132,11 @@ enum class LauncherAction( } override fun isAvailable(context: Context): Boolean { - return true + return this.available(context) + } + + override fun canReachSettings(): Boolean { + return this.canReachSettings } companion object { 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 bb59948..9460125 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 @@ -7,6 +7,7 @@ import de.jrpie.android.launcher.actions.Action 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.migratePreferencesFromVersionUnknown import de.jrpie.android.launcher.ui.HomeActivity @@ -14,7 +15,7 @@ import de.jrpie.android.launcher.ui.HomeActivity * Increase when breaking changes are introduced and write an appropriate case in * `migratePreferencesToNewVersion` */ -const val PREFERENCE_VERSION = 2 +const val PREFERENCE_VERSION = 3 const val UNKNOWN_PREFERENCE_VERSION = -1 private const val TAG = "Launcher - Preferences" @@ -32,13 +33,16 @@ fun migratePreferencesToNewVersion(context: Context) { UNKNOWN_PREFERENCE_VERSION -> { /* still using the old preferences file */ migratePreferencesFromVersionUnknown(context) - - Log.i(TAG, "migration of preferences complete.") + Log.i(TAG, "migration of preferences complete (${UNKNOWN_PREFERENCE_VERSION} -> ${PREFERENCE_VERSION}).") } 1 -> { migratePreferencesFromVersion1() - Log.i(TAG, "migration of preferences complete.") + Log.i(TAG, "migration of preferences complete (1 -> ${PREFERENCE_VERSION}).") + } + 2 -> { + migratePreferencesFromVersion2() + Log.i(TAG, "migration of preferences complete (2 -> ${PREFERENCE_VERSION}).") } else -> { 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 66723ad..d617127 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 @@ -117,11 +117,12 @@ private fun migrateAction(key: String) { * (see [PREFERENCE_VERSION]) */ fun migratePreferencesFromVersion1() { - assert(PREFERENCE_VERSION == 2) assert(LauncherPreferences.internal().versionCode() == 1) Gesture.entries.forEach { g -> migrateAction(g.id) } migrateAppInfoSet(LauncherPreferences.apps().keys().hidden()) migrateAppInfoSet(LauncherPreferences.apps().keys().favorites()) migrateAppInfoStringMap(LauncherPreferences.apps().keys().customNames()) LauncherPreferences.internal().versionCode(2) + + migratePreferencesFromVersion2() } \ 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 new file mode 100644 index 0000000..bcac3ae --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version2.kt @@ -0,0 +1,20 @@ +package de.jrpie.android.launcher.preferences.legacy + +import de.jrpie.android.launcher.actions.Action +import de.jrpie.android.launcher.actions.Gesture +import de.jrpie.android.launcher.actions.LauncherAction +import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION + + +/** + * Migrate preferences from version 2 (used until version 0.0.21) to the current format + * (see [PREFERENCE_VERSION]) + */ +fun migratePreferencesFromVersion2() { + assert(PREFERENCE_VERSION == 3) + assert(LauncherPreferences.internal().versionCode() == 2) + // previously there was no setting for this + Action.setActionForGesture(Gesture.BACK, LauncherAction.CHOOSE) + LauncherPreferences.internal().versionCode(3) +} \ 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 1ecbd74..c61ca95 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 @@ -50,7 +50,6 @@ private const val TAG = "Preferences ? -> 1" * and a different file was used. */ fun migratePreferencesFromVersionUnknown(context: Context) { - assert(PREFERENCE_VERSION == 2) Log.i( TAG, 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 1d5381d..42dc01b 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 @@ -9,6 +9,7 @@ import android.util.DisplayMetrics import android.view.GestureDetector import android.view.KeyEvent import android.view.MotionEvent +import android.view.View import android.view.ViewConfiguration import android.window.OnBackInvokedDispatcher import androidx.appcompat.app.AppCompatActivity @@ -54,6 +55,10 @@ class HomeActivity : UIObject, AppCompatActivity(), ) { recreate() } + + if (prefKey?.startsWith("action.") == true) { + updateSettingsFallbackButtonVisibility() + } } private var edgeWidth = 0.15f @@ -80,6 +85,10 @@ class HomeActivity : UIObject, AppCompatActivity(), handleBack() } } + binding.buttonFallbackSettings.setOnClickListener { + LauncherAction.SETTINGS.invoke(this) + } + } @@ -96,6 +105,20 @@ class HomeActivity : UIObject, AppCompatActivity(), } + private fun updateSettingsFallbackButtonVisibility() { + // If µLauncher settings can not be reached from any action bound to an enabled gesture, + // show the fallback button. + binding.buttonFallbackSettings.visibility = if ( + !Gesture.entries.any { g -> + g.isEnabled() && Action.forGesture(g)?.canReachSettings() == true + } + ) { + View.VISIBLE + } else { + View.GONE + } + } + private fun initClock() { val locale = Locale.getDefault() val dateVisible = LauncherPreferences.clock().dateVisible() @@ -152,6 +175,7 @@ class HomeActivity : UIObject, AppCompatActivity(), edgeWidth = LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f initClock() + updateSettingsFallbackButtonVisibility() } override fun onDestroy() { @@ -299,7 +323,7 @@ class HomeActivity : UIObject, AppCompatActivity(), private fun handleBack() { - LauncherAction.CHOOSE.launch(this) + Gesture.BACK(this) } override fun isHomeScreen(): Boolean { diff --git a/app/src/main/res/layout/home.xml b/app/src/main/res/layout/home.xml index f59b211..ecefdea 100644 --- a/app/src/main/res/layout/home.xml +++ b/app/src/main/res/layout/home.xml @@ -34,4 +34,18 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> + + + \ No newline at end of file diff --git a/app/src/main/res/values/defaults.xml b/app/src/main/res/values/defaults.xml index 5ed9327..276651d 100644 --- a/app/src/main/res/values/defaults.xml +++ b/app/src/main/res/values/defaults.xml @@ -3,6 +3,12 @@ + + + {\"type\": \"action:launcher\", \"value\": \"choose\"} + + + {\"type\": \"action:launcher\", \"value\": \"choose\"} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b12eb4c..9c07ed6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,8 @@ - Settings : Apps - --> + Back + Back button / back gesture Up Swipe up Double Up From d69e3caf71cada50b8300b91726cd5dadb3cde05 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sun, 9 Feb 2025 18:49:41 +0100 Subject: [PATCH 006/103] implement #98 - add option to show private space in different list --- .../de/jrpie/android/launcher/Application.kt | 3 + .../de/jrpie/android/launcher/Functions.kt | 26 ++-- .../launcher/actions/LauncherAction.kt | 74 +++++------ .../jrpie/android/launcher/apps/AppFilter.kt | 3 + .../android/launcher/apps/DetailedAppInfo.kt | 8 +- .../android/launcher/apps/PrivateSpace.kt | 118 ++++++++++++++++++ .../LauncherPreferences$Config.java | 1 + .../android/launcher/ui/list/ListActivity.kt | 66 +++++++++- .../launcher/ui/list/apps/ListFragmentApps.kt | 2 + ...ine_lock_24px.xml => baseline_lock_24.xml} | 0 .../res/drawable/baseline_lock_open_24.xml | 10 ++ app/src/main/res/layout/list.xml | 19 ++- app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 5 + app/src/main/res/xml/preferences.xml | 5 + 15 files changed, 279 insertions(+), 62 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/apps/PrivateSpace.kt rename app/src/main/res/drawable/{baseline_lock_24px.xml => baseline_lock_24.xml} (100%) create mode 100644 app/src/main/res/drawable/baseline_lock_open_24.xml 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 6e924c2..c856066 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Application.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt @@ -17,12 +17,14 @@ import androidx.preference.PreferenceManager import de.jrpie.android.launcher.actions.TorchManager import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.apps.DetailedAppInfo +import de.jrpie.android.launcher.apps.isPrivateSpaceLocked import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion import de.jrpie.android.launcher.preferences.resetPreferences class Application : android.app.Application() { val apps = MutableLiveData>() + val privateSpaceLocked = MutableLiveData() private val profileAvailabilityBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -144,6 +146,7 @@ class Application : android.app.Application() { } private fun loadApps() { + privateSpaceLocked.postValue(isPrivateSpaceLocked(this)) AsyncTask.execute { apps.postValue(getApps(packageManager, applicationContext)) } } } 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 721caa2..7cc5c39 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -22,6 +22,8 @@ import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.apps.DetailedAppInfo +import de.jrpie.android.launcher.apps.getPrivateSpaceUser +import de.jrpie.android.launcher.apps.isPrivateSpaceSupported import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.tutorial.TutorialActivity @@ -48,14 +50,15 @@ fun isDefaultHomeScreen(context: Context): Boolean { } fun setDefaultHomeScreen(context: Context, checkDefault: Boolean = false) { - if (checkDefault && isDefaultHomeScreen(context)) { + val isDefault = isDefaultHomeScreen(context) + if (checkDefault && isDefault) { // Launcher is already the default home app return } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && context is Activity - && checkDefault // using role manager only works when µLauncher is not already the default. + && !isDefault // using role manager only works when µLauncher is not already the default. ) { val roleManager = context.getSystemService(RoleManager::class.java) context.startActivityForResult( @@ -78,16 +81,6 @@ fun getUserFromId(userId: Int?, context: Context): UserHandle { return profiles.firstOrNull { it.hashCode() == userId } ?: profiles[0] } -fun getPrivateSpaceUser(context: Context): UserHandle? { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { - return null - } - val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager - val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - return userManager.userProfiles.firstOrNull { u -> - launcherApps.getLauncherUserInfo(u)?.userType == UserManager.USER_TYPE_PROFILE_PRIVATE - } -} fun openInBrowser(url: String, context: Context) { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) @@ -116,6 +109,8 @@ fun getApps(packageManager: PackageManager, context: Context): MutableList= Build.VERSION_CODES.VANILLA_ICE_CREAM && + if (isPrivateSpaceSupported() && launcherApps.getLauncherUserInfo(user)?.userType == UserManager.USER_TYPE_PROFILE_PRIVATE ) { continue @@ -135,7 +130,7 @@ fun getApps(packageManager: PackageManager, context: Context): MutableList openAppsList(context, true) }, + { context -> openAppsList(context, favorite = true) }, true ), + CHOOSE_FROM_PRIVATE_SPACE( + "choose_from_private_space", + R.string.list_other_list_private_space, + R.drawable.baseline_security_24, + { context -> + openAppsList(context, private = true) + }, + available = { _ -> + isPrivateSpaceSupported() + } + ), TOGGLE_PRIVATE_SPACE_LOCK( "toggle_private_space_lock", R.string.list_other_toggle_private_space_lock, R.drawable.baseline_security_24, ::togglePrivateSpaceLock, - available = { Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM } + available = { _ -> isPrivateSpaceSupported() } ), VOLUME_UP( "volume_up", @@ -107,7 +113,7 @@ enum class LauncherAction( LOCK_SCREEN( "lock_screen", R.string.list_other_lock_screen, - R.drawable.baseline_lock_24px, + R.drawable.baseline_lock_24, { c -> LauncherPreferences.actions().lockMethod().lockOrEnable(c) } ), TORCH( @@ -230,37 +236,6 @@ private fun expandNotificationsPanel(context: Context) { } } -private fun togglePrivateSpaceLock(context: Context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { - Toast.makeText( - context, - context.getString(R.string.alert_requires_android_v), - Toast.LENGTH_LONG - ).show() - return - } - val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager - val privateSpaceUser = getPrivateSpaceUser(context) - if (privateSpaceUser == null) { - Toast.makeText(context, context.getString(R.string.toast_private_space_not_available), Toast.LENGTH_LONG).show() - - if (!isDefaultHomeScreen(context)) { - Toast.makeText(context, context.getString(R.string.toast_private_space_default_home_screen), Toast.LENGTH_LONG).show() - return - } - - try { - context.startActivity(Intent(Settings.ACTION_PRIVACY_SETTINGS)) - } catch (_: ActivityNotFoundException) {} - return - } - if (userManager.isQuietModeEnabled(privateSpaceUser)) { - userManager.requestQuietModeEnabled(false, privateSpaceUser) - return - } - userManager.requestQuietModeEnabled(true, privateSpaceUser) - Toast.makeText(context, context.getString(R.string.toast_private_space_locked), Toast.LENGTH_LONG).show() -} private fun expandSettingsPanel(context: Context) { /* https://stackoverflow.com/a/31898506 */ @@ -283,7 +258,12 @@ private fun openSettings(context: Context) { context.startActivity(Intent(context, SettingsActivity::class.java)) } -fun openAppsList(context: Context, favorite: Boolean = false, hidden: Boolean = false) { +fun openAppsList( + context: Context, + favorite: Boolean = false, + hidden: Boolean = false, + private: Boolean = false +) { val intent = Intent(context, ListActivity::class.java) intent.putExtra("intention", ListActivity.ListActivityIntention.VIEW.toString()) intent.putExtra( @@ -302,6 +282,16 @@ fun openAppsList(context: Context, favorite: Boolean = false, hidden: Boolean = AppFilter.Companion.AppSetVisibility.HIDDEN } ) + intent.putExtra( + "privateSpaceVisibility", + if (private) { + AppFilter.Companion.AppSetVisibility.EXCLUSIVE + } else if (!hidden && LauncherPreferences.apps().hidePrivateSpaceApps()) { + AppFilter.Companion.AppSetVisibility.HIDDEN + } else { + AppFilter.Companion.AppSetVisibility.VISIBLE + } + ) context.startActivity(intent) } diff --git a/app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt b/app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt index f3640d2..ecc7eaa 100644 --- a/app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt +++ b/app/src/main/java/de/jrpie/android/launcher/apps/AppFilter.kt @@ -15,6 +15,7 @@ class AppFilter( var query: String, var favoritesVisibility: AppSetVisibility = AppSetVisibility.VISIBLE, var hiddenVisibility: AppSetVisibility = AppSetVisibility.HIDDEN, + var privateSpaceVisibility: AppSetVisibility = AppSetVisibility.VISIBLE ) { operator fun invoke(apps: List): List { @@ -23,10 +24,12 @@ class AppFilter( val hidden = LauncherPreferences.apps().hidden() ?: setOf() val favorites = LauncherPreferences.apps().favorites() ?: setOf() + val private = apps.filter { it.isPrivateSpaceApp }.map { it.app }.toSet() apps = apps.filter { info -> favoritesVisibility.predicate(favorites, info) && hiddenVisibility.predicate(hidden, info) + && privateSpaceVisibility.predicate(private, info) } if (LauncherPreferences.apps().hideBoundApps()) { diff --git a/app/src/main/java/de/jrpie/android/launcher/apps/DetailedAppInfo.kt b/app/src/main/java/de/jrpie/android/launcher/apps/DetailedAppInfo.kt index 519798d..1984d47 100644 --- a/app/src/main/java/de/jrpie/android/launcher/apps/DetailedAppInfo.kt +++ b/app/src/main/java/de/jrpie/android/launcher/apps/DetailedAppInfo.kt @@ -15,10 +15,11 @@ class DetailedAppInfo( val app: AppInfo, val label: CharSequence, val icon: Drawable, + val isPrivateSpaceApp: Boolean, val isSystemApp: Boolean = false, ) { - constructor(activityInfo: LauncherActivityInfo) : this( + constructor(activityInfo: LauncherActivityInfo, private: Boolean) : this( AppInfo( activityInfo.applicationInfo.packageName, activityInfo.name, @@ -26,6 +27,7 @@ class DetailedAppInfo( ), activityInfo.label, activityInfo.getBadgedIcon(0), + private, activityInfo.applicationInfo.flags.and(ApplicationInfo.FLAG_SYSTEM) != 0 ) @@ -51,7 +53,9 @@ class DetailedAppInfo( companion object { fun fromAppInfo(appInfo: AppInfo, context: Context): DetailedAppInfo? { - return appInfo.getLauncherActivityInfo(context)?.let { DetailedAppInfo(it) } + return appInfo.getLauncherActivityInfo(context)?.let { + DetailedAppInfo(it, it.user == getPrivateSpaceUser(context)) + } } } } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/apps/PrivateSpace.kt b/app/src/main/java/de/jrpie/android/launcher/apps/PrivateSpace.kt new file mode 100644 index 0000000..9b37d60 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/apps/PrivateSpace.kt @@ -0,0 +1,118 @@ +package de.jrpie.android.launcher.apps + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import android.os.Build +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import android.widget.Toast +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.isDefaultHomeScreen +import de.jrpie.android.launcher.setDefaultHomeScreen + + +/* + * Checks whether the device supports private space. + */ +fun isPrivateSpaceSupported(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM +} + +fun getPrivateSpaceUser(context: Context): UserHandle? { + if (!isPrivateSpaceSupported()) { + return null + } + val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager + val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + return userManager.userProfiles.firstOrNull { u -> + launcherApps.getLauncherUserInfo(u)?.userType == UserManager.USER_TYPE_PROFILE_PRIVATE + } +} + +/** + * Check whether the user has created a private space and whether µLauncher can access it. + */ +fun isPrivateSpaceSetUp( + context: Context, + showToast: Boolean = false, + launchSettings: Boolean = false +): Boolean { + if (!isPrivateSpaceSupported()) { + if (showToast) { + Toast.makeText( + context, + context.getString(R.string.alert_requires_android_v), + Toast.LENGTH_LONG + ).show() + } + return false + } + val privateSpaceUser = getPrivateSpaceUser(context) + if (privateSpaceUser != null) { + return true + } + if (!isDefaultHomeScreen(context)) { + if (showToast) { + Toast.makeText( + context, + context.getString(R.string.toast_private_space_default_home_screen), + Toast.LENGTH_LONG + ).show() + } + if (launchSettings) { + setDefaultHomeScreen(context) + } + } else { + if (showToast) { + Toast.makeText( + context, + context.getString(R.string.toast_private_space_not_available), + Toast.LENGTH_LONG + ).show() + } + if (launchSettings) { + try { + context.startActivity(Intent(Settings.ACTION_PRIVACY_SETTINGS)) + } catch (_: ActivityNotFoundException) { + } + } + } + return false +} + +fun isPrivateSpaceLocked(context: Context): Boolean { + if (!isPrivateSpaceSupported()) { + return false + } + val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager + val privateSpaceUser = getPrivateSpaceUser(context) ?: return false + return userManager.isQuietModeEnabled(privateSpaceUser) +} +fun lockPrivateSpace(context: Context, lock: Boolean) { + if (!isPrivateSpaceSupported()) { + return + } + val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager + val privateSpaceUser = getPrivateSpaceUser(context) ?: return + userManager.requestQuietModeEnabled(lock, privateSpaceUser) +} + +fun togglePrivateSpaceLock(context: Context) { + if (!isPrivateSpaceSetUp(context, showToast = true, launchSettings = true)) { + return + } + + val lock = isPrivateSpaceLocked(context) + lockPrivateSpace(context, !lock) + if (!lock) { + Toast.makeText( + context, + context.getString(R.string.toast_private_space_locked), + Toast.LENGTH_LONG + ).show() + } +} + 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 98496ce..c216911 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 @@ -30,6 +30,7 @@ import eu.jonahbauer.android.preference.annotations.Preferences; @Preference(name = "custom_names", type = HashMap.class, serializer = MapAppInfoStringPreferenceSerializer.class), @Preference(name = "hide_bound_apps", type = boolean.class, defaultValue = "false"), @Preference(name = "hide_paused_apps", type = boolean.class, defaultValue = "false"), + @Preference(name = "hide_private_space_apps", type = boolean.class, defaultValue = "false"), }), @PreferenceGroup(name = "list", prefix = "settings_list_", suffix = "_key", value = { @Preference(name = "layout", type = ListLayout.class, defaultValue = "DEFAULT") diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/ListActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/ListActivity.kt index 1fe5afb..902e561 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/ListActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/ListActivity.kt @@ -16,10 +16,14 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter import androidx.viewpager.widget.ViewPager import com.google.android.material.tabs.TabLayout +import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.R import de.jrpie.android.launcher.REQUEST_UNINSTALL import de.jrpie.android.launcher.actions.LauncherAction import de.jrpie.android.launcher.apps.AppFilter +import de.jrpie.android.launcher.apps.isPrivateSpaceLocked +import de.jrpie.android.launcher.apps.isPrivateSpaceSetUp +import de.jrpie.android.launcher.apps.togglePrivateSpaceLock import de.jrpie.android.launcher.databinding.ListBinding import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.UIObject @@ -30,6 +34,8 @@ import de.jrpie.android.launcher.ui.list.other.ListFragmentOther // TODO: Better solution for this intercommunication functionality (used in list-fragments) var intention = ListActivity.ListActivityIntention.VIEW var favoritesVisibility: AppFilter.Companion.AppSetVisibility = AppFilter.Companion.AppSetVisibility.VISIBLE +var privateSpaceVisibility: AppFilter.Companion.AppSetVisibility = + AppFilter.Companion.AppSetVisibility.VISIBLE var hiddenVisibility: AppFilter.Companion.AppSetVisibility = AppFilter.Companion.AppSetVisibility.HIDDEN var forGesture: String? = null @@ -44,6 +50,29 @@ class ListActivity : AppCompatActivity(), UIObject { private lateinit var binding: ListBinding + private fun updateLockIcon(locked: Boolean) { + binding.listLock.setImageDrawable( + getDrawable( + if (locked) { + R.drawable.baseline_lock_24 + } else { + R.drawable.baseline_lock_open_24 + } + ) + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + binding.listLock.tooltipText = getString( + if (locked) { + R.string.tooltip_unlock_private_space + } else { + R.string.tooltip_lock_private_space + } + ) + } + } + + + enum class ListActivityIntention(val titleResource: Int) { VIEW(R.string.list_title_view), /* view list of apps */ PICK(R.string.list_title_pick) /* choose app or action to associate to a gesture */ @@ -70,8 +99,10 @@ class ListActivity : AppCompatActivity(), UIObject { favoritesVisibility = bundle.getSerializable("favoritesVisibility") as? AppFilter.Companion.AppSetVisibility ?: favoritesVisibility + privateSpaceVisibility = bundle.getSerializable("privateSpaceVisibility") + as? AppFilter.Companion.AppSetVisibility ?: privateSpaceVisibility hiddenVisibility = bundle.getSerializable("hiddenVisibility") - as? AppFilter.Companion.AppSetVisibility ?: favoritesVisibility + as? AppFilter.Companion.AppSetVisibility ?: hiddenVisibility if (intention != ListActivityIntention.VIEW) forGesture = bundle.getString("forGesture") @@ -86,6 +117,31 @@ class ListActivity : AppCompatActivity(), UIObject { LauncherAction.SETTINGS.launch(this@ListActivity) } + binding.listLock.visibility = + if (intention != ListActivityIntention.VIEW) { + View.GONE + } else if (!isPrivateSpaceSetUp(this)) { + View.GONE + } else if (LauncherPreferences.apps().hidePrivateSpaceApps()) { + if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { + View.VISIBLE + } else { + View.GONE + } + } else { + View.VISIBLE + } + + if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { + isPrivateSpaceSetUp(this, showToast = true, launchSettings = true) + if (isPrivateSpaceLocked(this)) { + togglePrivateSpaceLock(this) + } + } + updateLockIcon(isPrivateSpaceLocked(this)) + + val privateSpaceLocked = (this.applicationContext as Application).privateSpaceLocked + privateSpaceLocked.observe(this) { updateLockIcon(it) } // android:windowSoftInputMode="adjustResize" doesn't work in full screen. // workaround from https://stackoverflow.com/a/57623505 @@ -144,6 +200,8 @@ class ListActivity : AppCompatActivity(), UIObject { if (intention == ListActivityIntention.VIEW) { titleResource = if (hiddenVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { R.string.list_title_hidden + } else if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { + R.string.list_title_private_space } else if (favoritesVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { R.string.list_title_favorite } else { @@ -161,6 +219,12 @@ class ListActivity : AppCompatActivity(), UIObject { override fun setOnClicks() { binding.listClose.setOnClickListener { finish() } + binding.listLock.setOnClickListener { + togglePrivateSpaceLock(this) + if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { + finish() + } + } } override fun adjustLayout() { 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 c52f951..3a6e403 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 @@ -19,6 +19,7 @@ import de.jrpie.android.launcher.ui.list.favoritesVisibility import de.jrpie.android.launcher.ui.list.forGesture import de.jrpie.android.launcher.ui.list.hiddenVisibility import de.jrpie.android.launcher.ui.list.intention +import de.jrpie.android.launcher.ui.list.privateSpaceVisibility import de.jrpie.android.launcher.ui.openSoftKeyboard @@ -72,6 +73,7 @@ class ListFragmentApps : Fragment(), UIObject { requireContext(), "", favoritesVisibility = favoritesVisibility, + privateSpaceVisibility = privateSpaceVisibility, hiddenVisibility = hiddenVisibility ), layout = LauncherPreferences.list().layout() diff --git a/app/src/main/res/drawable/baseline_lock_24px.xml b/app/src/main/res/drawable/baseline_lock_24.xml similarity index 100% rename from app/src/main/res/drawable/baseline_lock_24px.xml rename to app/src/main/res/drawable/baseline_lock_24.xml diff --git a/app/src/main/res/drawable/baseline_lock_open_24.xml b/app/src/main/res/drawable/baseline_lock_open_24.xml new file mode 100644 index 0000000..f0f6ea3 --- /dev/null +++ b/app/src/main/res/drawable/baseline_lock_open_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/list.xml b/app/src/main/res/layout/list.xml index 062cab4..4fd6962 100644 --- a/app/src/main/res/layout/list.xml +++ b/app/src/main/res/layout/list.xml @@ -53,8 +53,8 @@ android:text="@string/list_title_pick" android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title" android:textSize="30sp" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/list_lock" + app:layout_constraintStart_toEndOf="@id/list_settings" app:layout_constraintTop_toTopOf="parent" /> + apps.custom_names apps.hide_bound_apps apps.hide_paused_apps + apps.hide_private_space_apps list.layout general.select_launcher diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c07ed6..97d7384 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -150,6 +150,7 @@ Hidden apps Don\'t show apps that are bound to a gesture in the app list Hide paused apps + Hide private space from app list Layout of app list Default @@ -197,6 +198,7 @@ All Apps Favorite Apps Hidden Apps + Private Space Choose App Apps @@ -219,6 +221,7 @@ µLauncher Settings All Applications Favorite Applications + Private Space Toggle Private Space Lock Music: Louder Music: Quieter @@ -273,6 +276,8 @@ Private space unlocked Private space is not available µLauncher needs to be the default home screen to access private space. + Lock private space + Unlock private space Error: Locking the screen using accessibility is not supported on this device. Please use device admin instead. µLauncher - lock screen diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 314f85f..406f81a 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -151,6 +151,11 @@ android:title="@string/settings_apps_hide_paused_apps" android:defaultValue="false" /> + + Date: Sun, 9 Feb 2025 21:08:16 +0100 Subject: [PATCH 007/103] lint --- app/build.gradle | 2 +- .../java/de/jrpie/android/launcher/Functions.kt | 2 +- .../de/jrpie/android/launcher/actions/Gesture.kt | 2 +- .../android/launcher/actions/LauncherAction.kt | 3 ++- .../android/launcher/apps/DetailedAppInfo.kt | 2 +- .../launcher/preferences/legacy/Version1.kt | 8 ++++---- .../launcher/preferences/legacy/VersionUnknown.kt | 1 - .../serialization/PreferenceSerializers.kt | 4 ++-- .../de/jrpie/android/launcher/ui/HomeActivity.kt | 2 +- .../android/launcher/ui/list/ListActivity.kt | 4 +++- .../launcher/ui/list/apps/ContextMenuActions.kt | 2 +- .../ui/settings/meta/SettingsFragmentMeta.kt | 15 --------------- .../ui/tutorial/tabs/TutorialFragmentConcept.kt | 2 +- app/src/main/res/layout/list.xml | 4 ++-- .../res/layout/list_apps_row_variant_text.xml | 1 + app/src/main/res/layout/list_other_row.xml | 2 -- app/src/main/res/layout/settings.xml | 4 ++-- .../main/res/mipmap-anydpi-v26/ic_launcher.xml | 1 + .../res/mipmap-anydpi-v26/ic_launcher_round.xml | 1 + 19 files changed, 25 insertions(+), 37 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 23273af..76bd285 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -100,7 +100,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.2.0' implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.palette:palette-ktx:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.3.2' + implementation 'androidx.recyclerview:recyclerview:1.4.0' implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'com.google.android.material:material:1.12.0' implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") 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 7cc5c39..5f7e9c9 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -182,5 +182,5 @@ fun getDeviceInfo(): String { 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); + clipboardManager.setPrimaryClip(clipData) } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt b/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt index dba314d..34e053e 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt @@ -273,7 +273,7 @@ enum class Gesture( companion object { fun byId(id: String): Gesture? { - return Gesture.values().firstOrNull { it.id == id } + return Gesture.entries.firstOrNull { it.id == id } } } diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt index 0220fc2..4f89758 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt @@ -9,6 +9,7 @@ import android.os.Build import android.os.SystemClock import android.view.KeyEvent import android.widget.Toast +import androidx.appcompat.content.res.AppCompatResources import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.R import de.jrpie.android.launcher.apps.AppFilter @@ -134,7 +135,7 @@ enum class LauncherAction( } override fun getIcon(context: Context): Drawable? { - return context.getDrawable(icon) + return AppCompatResources.getDrawable(context, icon) } override fun isAvailable(context: Context): Boolean { diff --git a/app/src/main/java/de/jrpie/android/launcher/apps/DetailedAppInfo.kt b/app/src/main/java/de/jrpie/android/launcher/apps/DetailedAppInfo.kt index 1984d47..d77bf93 100644 --- a/app/src/main/java/de/jrpie/android/launcher/apps/DetailedAppInfo.kt +++ b/app/src/main/java/de/jrpie/android/launcher/apps/DetailedAppInfo.kt @@ -9,7 +9,7 @@ import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.preferences.LauncherPreferences /** - * Stores information used to create [AppsRecyclerAdapter] rows. + * Stores information used to create [de.jrpie.android.launcher.ui.list.apps.AppsRecyclerAdapter] rows. */ class DetailedAppInfo( val app: AppInfo, 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 d617127..a61980a 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 @@ -68,9 +68,9 @@ private fun Action.Companion.legacyFromPreference(id: String): Action? { val actionId = preferences.getString("$id.app", "")!! var u: Int? = preferences.getInt( "$id.user", - AppInfo.INVALID_USER + INVALID_USER ) - u = if (u == AppInfo.INVALID_USER) null else u + u = if (u == INVALID_USER) null else u return Action.legacyFromId(actionId, u) } @@ -80,9 +80,9 @@ private fun migrateAppInfoStringMap(key: String) { MapAppInfoStringPreferenceSerializer().serialize( preferences.getStringSet(key, setOf())?.mapNotNull { entry -> try { - val obj = JSONObject(entry); + val obj = JSONObject(entry) val info = AppInfo.legacyDeserialize(obj.getString("key")) - val value = obj.getString("value"); + val value = obj.getString("value") Pair(info, value) } catch (_: JSONException) { null 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 c61ca95..a33670b 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 @@ -4,7 +4,6 @@ import android.content.Context import android.content.SharedPreferences import android.util.Log import de.jrpie.android.launcher.preferences.LauncherPreferences -import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION import de.jrpie.android.launcher.preferences.theme.Background import de.jrpie.android.launcher.preferences.theme.ColorTheme 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 4a745a2..041fe4d 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 @@ -15,7 +15,7 @@ import kotlinx.serialization.json.Json class SetAppInfoPreferenceSerializer : PreferenceSerializer?, java.util.Set?> { @Throws(PreferenceSerializationException::class) - override fun serialize(value: java.util.Set?): java.util.Set? { + override fun serialize(value: java.util.Set?): java.util.Set { return value?.map(AppInfo::serialize)?.toHashSet() as java.util.Set } @@ -29,7 +29,7 @@ class SetAppInfoPreferenceSerializer : class MapAppInfoStringPreferenceSerializer : PreferenceSerializer?, java.util.Set?> { - @Serializable() + @Serializable private class MapEntry(val key: AppInfo, val value: String) @Throws(PreferenceSerializationException::class) 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 42dc01b..b41eff3 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 @@ -216,7 +216,7 @@ class HomeActivity : UIObject, AppCompatActivity(), if (e1 == null) return false - val displayMetrics: DisplayMetrics = DisplayMetrics() + val displayMetrics = DisplayMetrics() windowManager.defaultDisplay.getMetrics(displayMetrics) val width = displayMetrics.widthPixels diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/ListActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/ListActivity.kt index 902e561..c4ecded 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/ListActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/ListActivity.kt @@ -11,6 +11,7 @@ import android.view.View import android.widget.Toast import android.window.OnBackInvokedDispatcher import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter @@ -52,7 +53,8 @@ class ListActivity : AppCompatActivity(), UIObject { private fun updateLockIcon(locked: Boolean) { binding.listLock.setImageDrawable( - getDrawable( + AppCompatResources.getDrawable( + this, if (locked) { R.drawable.baseline_lock_24 } else { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ContextMenuActions.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ContextMenuActions.kt index e09111e..9636dc2 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ContextMenuActions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ContextMenuActions.kt @@ -33,7 +33,7 @@ fun AppInfo.openSettings( } fun AppInfo.uninstall(activity: android.app.Activity) { - val packageName = this.packageName.toString() + val packageName = this.packageName val userId = this.user Log.i(LOG_TAG, "uninstalling $this") 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 fd3a738..70a225d 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 @@ -45,21 +45,6 @@ class SettingsFragmentMeta : Fragment(), UIObject { super.onStart() } - // Rate App - // Just copied code from https://stackoverflow.com/q/10816757/12787264 - // that is how we write good software ^^ - - private fun rateIntentForUrl(url: String): Intent { - val intent = Intent( - Intent.ACTION_VIEW, - Uri.parse(String.format("%s?id=%s", url, this.requireContext().packageName)) - ) - var flags = Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_MULTIPLE_TASK - flags = flags or Intent.FLAG_ACTIVITY_NEW_DOCUMENT - intent.addFlags(flags) - return intent - } - override fun setOnClicks() { binding.settingsMetaButtonViewTutorial.setOnClickListener { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragmentConcept.kt b/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragmentConcept.kt index 26d4141..f0fd233 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragmentConcept.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragmentConcept.kt @@ -21,7 +21,7 @@ class TutorialFragmentConcept : Fragment(), UIObject { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val binding = TutorialConceptBinding.inflate(inflater, container, false) + binding = TutorialConceptBinding.inflate(inflater, container, false) binding.tutorialConceptBadgeVersion.text = BuildConfig.VERSION_NAME return binding.root } diff --git a/app/src/main/res/layout/list.xml b/app/src/main/res/layout/list.xml index 4fd6962..e68c895 100644 --- a/app/src/main/res/layout/list.xml +++ b/app/src/main/res/layout/list.xml @@ -32,7 +32,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:layout_marginLeft="8dp" + android:layout_marginEnd="8dp" android:contentDescription="@string/settings" android:gravity="center" android:paddingLeft="16sp" @@ -62,7 +62,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" - android:layout_marginRight="8dp" + android:layout_marginStart="8dp" android:gravity="center" android:includeFontPadding="true" android:paddingLeft="16sp" diff --git a/app/src/main/res/layout/list_apps_row_variant_text.xml b/app/src/main/res/layout/list_apps_row_variant_text.xml index 053568b..700dd7c 100644 --- a/app/src/main/res/layout/list_apps_row_variant_text.xml +++ b/app/src/main/res/layout/list_apps_row_variant_text.xml @@ -25,6 +25,7 @@ android:gravity="start|center_horizontal" android:padding="5dp" android:paddingStart="20dp" + android:paddingEnd="20dp" android:text="" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/list_other_row.xml b/app/src/main/res/layout/list_other_row.xml index aea51e1..530cc9e 100644 --- a/app/src/main/res/layout/list_other_row.xml +++ b/app/src/main/res/layout/list_other_row.xml @@ -22,9 +22,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="60sp" - android:layout_marginLeft="60sp" android:layout_marginEnd="5dp" - android:layout_marginRight="5dp" android:gravity="start" android:text="" android:textSize="20sp" diff --git a/app/src/main/res/layout/settings.xml b/app/src/main/res/layout/settings.xml index f7d5c6e..668358a 100644 --- a/app/src/main/res/layout/settings.xml +++ b/app/src/main/res/layout/settings.xml @@ -39,7 +39,7 @@ android:id="@+id/settings_close" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginRight="8dp" + android:layout_marginEnd="8dp" android:gravity="center" android:includeFontPadding="true" android:paddingLeft="16sp" @@ -55,7 +55,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:layout_marginLeft="8dp" + android:layout_marginEnd="8dp" android:gravity="center" android:includeFontPadding="true" android:paddingLeft="16sp" diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f2acb4..4dd969d 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6f2acb4..8b20aae 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file From 757486771d151495ba8d8f4a7ab5daa15fb6d99c Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Mon, 10 Feb 2025 21:57:34 +0100 Subject: [PATCH 008/103] reenable proguard --- .scripts/release.sh | 9 ++++++++- app/build.gradle | 4 ++-- app/proguard-rules.pro | 3 +++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.scripts/release.sh b/.scripts/release.sh index 4bba806..0c71f4a 100755 --- a/.scripts/release.sh +++ b/.scripts/release.sh @@ -9,7 +9,14 @@ KEYSTORE_ACCRESCENT_PASS=$(keepassxc-password "android_keys/launcher-accrescent" if [[ $(git status --porcelain) ]]; then echo "There are uncommitted changes." - exit 1 + + read -p "Continue anyway? (y/n) " -n 1 -r + echo # (optional) move to a new line + if ! [[ $REPLY =~ ^[Yy]$ ]] + then + exit 1 + fi + fi rm -rf "$OUTPUT_DIR" diff --git a/app/build.gradle b/app/build.gradle index 76bd285..53dc960 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,8 +43,8 @@ android { buildTypes { release { - // minifyEnabled true - // proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } debug { applicationIdSuffix = ".debug" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 329090c..9e3e326 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,4 +1,6 @@ # Add project specific ProGuard rules here. +-dontobfuscate +-dontoptimize # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # @@ -25,3 +27,4 @@ # This is generated automatically by the Android Gradle plugin. -dontwarn javax.annotation.processing.AbstractProcessor -dontwarn javax.annotation.processing.SupportedAnnotationTypes +-dontwarn javax.annotation.processing.SupportedSourceVersion From 012f13c8272151f42b5fdb3d2072f11d95607fc1 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Tue, 11 Feb 2025 18:06:06 +0100 Subject: [PATCH 009/103] update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 312 ++++++++++++++--------- gradlew.bat | 78 +++--- 4 files changed, 242 insertions(+), 151 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f6b961fd5a86aa5fbfe90f707c3138408be7c718..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch literal 43583 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 GZ~q4|mN%#X literal 54329 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

<5G2M1WZP5 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 F{|7XC?P&l2 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dd89564..df97d72 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Fri Aug 02 03:11:16 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index cccdd3d..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,130 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,92 +133,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done fi + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index e95643d..9b42019 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,25 +27,29 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -35,48 +57,36 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 0c0d90a35749cec150917c4167514b8097b32afc Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sat, 15 Feb 2025 03:08:18 +0100 Subject: [PATCH 010/103] improve gesture detection --- .../jrpie/android/launcher/ui/HomeActivity.kt | 144 ++----------- .../launcher/ui/TouchGestureDetector.kt | 199 ++++++++++++++++++ 2 files changed, 219 insertions(+), 124 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt 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 b41eff3..973e0ca 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 @@ -6,14 +6,11 @@ import android.content.res.Resources import android.os.Build import android.os.Bundle import android.util.DisplayMetrics -import android.view.GestureDetector import android.view.KeyEvent import android.view.MotionEvent import android.view.View -import android.view.ViewConfiguration import android.window.OnBackInvokedDispatcher import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.GestureDetectorCompat import androidx.core.view.isVisible import de.jrpie.android.launcher.R import de.jrpie.android.launcher.actions.Action @@ -23,13 +20,6 @@ import de.jrpie.android.launcher.databinding.HomeBinding import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.tutorial.TutorialActivity import java.util.Locale -import java.util.Timer -import kotlin.concurrent.fixedRateTimer -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min -import kotlin.math.tan - /** * [HomeActivity] is the actual application Launcher, @@ -43,10 +33,10 @@ import kotlin.math.tan * - Setting global variables (preferences etc.) * - Opening the [TutorialActivity] on new installations */ -class HomeActivity : UIObject, AppCompatActivity(), - GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener { +class HomeActivity : UIObject, AppCompatActivity() { private lateinit var binding: HomeBinding + private lateinit var touchGestureDetector: TouchGestureDetector private var sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> @@ -61,22 +51,29 @@ class HomeActivity : UIObject, AppCompatActivity(), } } - private var edgeWidth = 0.15f - - private var bufferedPointerCount = 1 // how many fingers on screen - private var pointerBufferTimer = Timer() - - private lateinit var mDetector: GestureDetectorCompat - - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) super.onCreate() + + val displayMetrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(displayMetrics) + + val width = displayMetrics.widthPixels + val height = displayMetrics.heightPixels + + touchGestureDetector = TouchGestureDetector( + this, + width, + height, + LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f + ) + // Initialise layout binding = HomeBinding.inflate(layoutInflater) setContentView(binding.root) + // Handle back key / gesture on Android 13+, cf. onKeyDown() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { onBackInvokedDispatcher.registerOnBackInvokedCallback( @@ -95,9 +92,6 @@ class HomeActivity : UIObject, AppCompatActivity(), override fun onStart() { super.onStart() - mDetector = GestureDetectorCompat(this, this) - mDetector.setOnDoubleTapListener(this) - super.onStart() LauncherPreferences.getSharedPreferences() @@ -172,7 +166,8 @@ class HomeActivity : UIObject, AppCompatActivity(), override fun onResume() { super.onResume() - edgeWidth = LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f + touchGestureDetector.edgeWidth = + LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f initClock() updateSettingsFallbackButtonVisibility() @@ -211,95 +206,8 @@ class HomeActivity : UIObject, AppCompatActivity(), return true } - override fun onFling(e1: MotionEvent?, e2: MotionEvent, dX: Float, dY: Float): Boolean { - - if (e1 == null) return false - - - val displayMetrics = DisplayMetrics() - windowManager.defaultDisplay.getMetrics(displayMetrics) - - val width = displayMetrics.widthPixels - val height = displayMetrics.heightPixels - - val diffX = e1.x - e2.x - val diffY = e1.y - e2.y - - val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe() - val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe() - - val threshold = ViewConfiguration.get(this).scaledTouchSlop - val angularThreshold = tan(Math.PI / 6) - - var gesture = if (angularThreshold * abs(diffX) > abs(diffY)) { // horizontal swipe - if (diffX > threshold) - Gesture.SWIPE_LEFT - else if (diffX < -threshold) - Gesture.SWIPE_RIGHT - else null - } else if (angularThreshold * abs(diffY) > abs(diffX)) { // vertical swipe - // Only open if the swipe was not from the phones top edge - // TODO: replace 100px by sensible dp value (e.g. twice the height of the status bar) - if (diffY < -threshold && e1.y > 100) - Gesture.SWIPE_DOWN - else if (diffY > threshold) - Gesture.SWIPE_UP - else null - } else null - - if (doubleActions && bufferedPointerCount > 1) { - gesture = gesture?.let(Gesture::getDoubleVariant) - } - - if (edgeActions) { - if (max(e1.x, e2.x) < edgeWidth * width) { - gesture = gesture?.getEdgeVariant(Gesture.Edge.LEFT) - } else if (min(e1.x, e2.x) > (1 - edgeWidth) * width) { - gesture = gesture?.getEdgeVariant(Gesture.Edge.RIGHT) - } - - if (max(e1.y, e2.y) < edgeWidth * height) { - gesture = gesture?.getEdgeVariant(Gesture.Edge.TOP) - } else if (min(e1.y, e2.y) > (1 - edgeWidth) * height) { - gesture = gesture?.getEdgeVariant(Gesture.Edge.BOTTOM) - } - } - gesture?.invoke(this) - - return true - } - - override fun onLongPress(event: MotionEvent) { - Gesture.LONG_CLICK(this) - } - - override fun onDoubleTap(event: MotionEvent): Boolean { - Gesture.DOUBLE_CLICK(this) - return false - } - - // Tooltip - override fun onSingleTapConfirmed(event: MotionEvent): Boolean { - - return false - } - override fun onTouchEvent(event: MotionEvent): Boolean { - - // Buffer / Debounce the pointer count - if (event.pointerCount > bufferedPointerCount) { - bufferedPointerCount = event.pointerCount - pointerBufferTimer = fixedRateTimer("pointerBufferTimer", true, 300, 1000) { - bufferedPointerCount = 1 - this.cancel() // a non-recurring timer - } - } - - return if (mDetector.onTouchEvent(event)) { - false - } else { - super.onTouchEvent(event) - } + return touchGestureDetector.onTouchEvent(event) || super.onTouchEvent(event) } override fun setOnClicks() { @@ -329,16 +237,4 @@ class HomeActivity : UIObject, AppCompatActivity(), override fun isHomeScreen(): Boolean { return true } - - - /* TODO: Remove those. For now they are necessary - * because this inherits from GestureDetector.OnGestureListener */ - override fun onDoubleTapEvent(event: MotionEvent): Boolean { return false } - override fun onDown(event: MotionEvent): Boolean { return false } - override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dX: Float, dY: Float): Boolean { return false } - override fun onShowPress(event: MotionEvent) {} - override fun onSingleTapUp(event: MotionEvent): Boolean { return false } - - - } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt new file mode 100644 index 0000000..6a462e9 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt @@ -0,0 +1,199 @@ +package de.jrpie.android.launcher.ui + +import android.content.Context +import android.view.MotionEvent +import android.view.ViewConfiguration +import de.jrpie.android.launcher.actions.Gesture +import de.jrpie.android.launcher.preferences.LauncherPreferences +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.tan + +class TouchGestureDetector( + private val context: Context, + val width: Int, + val height: Int, + var edgeWidth: Float +) { + private val ANGULAR_THRESHOLD = tan(Math.PI / 6) + private val TOUCH_SLOP: Int + private val TOUCH_SLOP_SQUARE: Int + private val DOUBLE_TAP_SLOP: Int + private val DOUBLE_TAP_SLOP_SQUARE: Int + private val LONG_PRESS_TIMEOUT: Int + private val TAP_TIMEOUT: Int + private val DOUBLE_TAP_TIMEOUT: Int + + + data class Vector(val x: Float, val y: Float) { + fun absSquared(): Float { + return this.x * this.x + this.y * this.y + } + fun plus(vector: Vector): Vector { + return Vector(this.x + vector.x, this.y + vector.y) + } + fun max(other: Vector): Vector { + return Vector(max(this.x, other.x), max(this.y, other.y)) + } + fun min(other: Vector): Vector { + return Vector(min(this.x, other.x), min(this.y, other.y)) + } + operator fun minus(vector: Vector): Vector { + return Vector(this.x - vector.x, this.y - vector.y) + } + } + + + class PointerPath( + val number: Int, + val start: Vector, + var last: Vector = start + ) { + var min = Vector(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY) + var max = Vector(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY) + fun sizeSquared(): Float { + return (max - min).absSquared() + } + fun getDirection(): Vector { + return last - start + } + fun update(vector: Vector) { + min = min.min(vector) + max = max.max(vector) + last = vector + } + } + private fun PointerPath.isTap(): Boolean { + return sizeSquared() < TOUCH_SLOP_SQUARE + } + + init { + val configuration = ViewConfiguration.get(context) + TOUCH_SLOP = configuration.scaledTouchSlop + TOUCH_SLOP_SQUARE = TOUCH_SLOP * TOUCH_SLOP + DOUBLE_TAP_SLOP = configuration.scaledDoubleTapSlop + DOUBLE_TAP_SLOP_SQUARE = DOUBLE_TAP_SLOP * DOUBLE_TAP_SLOP + + LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout() + TAP_TIMEOUT = ViewConfiguration.getTapTimeout() + DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout() + } + + private var paths = HashMap() + + private var lastTappedTime = 0L + private var lastTappedLocation: Vector? = null + + fun onTouchEvent(event: MotionEvent): Boolean { + val pointerIdToIndex = + (0.. abs(direction.y)) { // horizontal swipe + if (direction.x > TOUCH_SLOP) + Gesture.SWIPE_RIGHT + else if (direction.x < -TOUCH_SLOP) + Gesture.SWIPE_LEFT + else null + } else if (ANGULAR_THRESHOLD * abs(direction.y) > abs(direction.x)) { // vertical swipe + if (direction.y < -TOUCH_SLOP) + Gesture.SWIPE_UP + else if (direction.y > TOUCH_SLOP) + Gesture.SWIPE_DOWN + else null + } else null + } + + private fun classifyPaths(paths: Map, timeStart: Long, timeEnd: Long) { + val duration = timeEnd - timeStart + val pointerCount = paths.entries.size + if (paths.entries.isEmpty()) { + return + } + + val mainPointerPath = paths.entries.firstOrNull { it.value.number == 0 }?.value ?: return + + // Ignore swipes at the very top, since this interferes with the status bar. + // TODO: replace 100px by sensible dp value (e.g. twice the height of the status bar) + if (paths.entries.any { it.value.start.y < 100 }) { + return + } + + if (pointerCount == 1 && mainPointerPath.isTap()) { + // detect taps + + if (duration in 0..TAP_TIMEOUT) { + if (timeStart - lastTappedTime < DOUBLE_TAP_TIMEOUT && + lastTappedLocation?.let { + (mainPointerPath.last - it).absSquared() < DOUBLE_TAP_SLOP_SQUARE} == true + ) { + Gesture.DOUBLE_CLICK.invoke(context) + } else { + lastTappedTime = timeEnd + lastTappedLocation = mainPointerPath.last + } + } else if (duration > LONG_PRESS_TIMEOUT) { + // TODO: Don't wait until the finger is lifted. + // Instead set a timer to start long click as soon as LONG_PRESS_TIMEOUT is reached + Gesture.LONG_CLICK.invoke(context) + } + } else { + // detect swipes + + val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe() + val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe() + + var gesture = getGestureForDirection(mainPointerPath.getDirection()) + + if (doubleActions && pointerCount > 1) { + if (paths.entries.any { getGestureForDirection(it.value.getDirection()) != gesture }) { + // the directions of the pointers don't match + return + } + gesture = gesture?.let(Gesture::getDoubleVariant) + } + + if (edgeActions) { + if (mainPointerPath.max.x < edgeWidth * width) { + gesture = gesture?.getEdgeVariant(Gesture.Edge.LEFT) + } else if (mainPointerPath.min.x > (1 - edgeWidth) * width) { + gesture = gesture?.getEdgeVariant(Gesture.Edge.RIGHT) + } + + if (mainPointerPath.max.y < edgeWidth * height) { + gesture = gesture?.getEdgeVariant(Gesture.Edge.TOP) + } else if (mainPointerPath.min.y > (1 - edgeWidth) * height) { + gesture = gesture?.getEdgeVariant(Gesture.Edge.BOTTOM) + } + } + gesture?.invoke(context) + } + } +} \ No newline at end of file From 5669279c642aa58b6695f21463cacb49343b614e Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sun, 16 Feb 2025 14:38:41 +0100 Subject: [PATCH 011/103] =?UTF-8?q?add=20<,>,V,=CE=9B=20gestures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jrpie/android/launcher/actions/Gesture.kt | 50 ++++++++++++++++ .../launcher/ui/TouchGestureDetector.kt | 58 ++++++++++++++++--- app/src/main/res/values/defaults.xml | 2 + app/src/main/res/values/strings.xml | 18 ++++++ 4 files changed, 120 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt b/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt index 34e053e..a4f25b4 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/Gesture.kt @@ -1,6 +1,7 @@ package de.jrpie.android.launcher.actions import android.content.Context +import android.util.Log import de.jrpie.android.launcher.R import de.jrpie.android.launcher.preferences.LauncherPreferences @@ -169,6 +170,54 @@ enum class Gesture( R.array.default_double_right, R.anim.left_right ), + SWIPE_LARGER( + "action.larger", + R.string.settings_gesture_swipe_larger, + R.string.settings_gesture_description_swipe_larger, + R.array.no_default + ), + SWIPE_LARGER_REVERSE( + "action.larger_reverse", + R.string.settings_gesture_swipe_larger_reverse, + R.string.settings_gesture_description_swipe_larger_reverse, + R.array.no_default + ), + SWIPE_SMALLER( + "action.smaller", + R.string.settings_gesture_swipe_smaller, + R.string.settings_gesture_description_swipe_smaller, + R.array.no_default + ), + SWIPE_SMALLER_REVERSE( + "action.smaller_reverse", + R.string.settings_gesture_swipe_smaller_reverse, + R.string.settings_gesture_description_swipe_smaller_reverse, + R.array.no_default + ), + SWIPE_LAMBDA( + "action.lambda", + R.string.settings_gesture_swipe_lambda, + R.string.settings_gesture_description_swipe_lambda, + R.array.no_default + ), + SWIPE_LAMBDA_REVERSE( + "action.lambda_reverse", + R.string.settings_gesture_swipe_lambda_reverse, + R.string.settings_gesture_description_swipe_lambda_reverse, + R.array.no_default + ), + SWIPE_V( + "action.v", + R.string.settings_gesture_swipe_v, + R.string.settings_gesture_description_swipe_v, + R.array.no_default + ), + SWIPE_V_REVERSE( + "action.v_reverse", + R.string.settings_gesture_swipe_v_reverse, + R.string.settings_gesture_description_swipe_v_reverse, + R.array.no_default + ), BACK( "action.back", R.string.settings_gesture_back, @@ -267,6 +316,7 @@ enum class Gesture( } operator fun invoke(context: Context) { + Log.i("Launcher", "Detected gesture: $this") val action = Action.forGesture(this) Action.launch(action, context, this.animationIn, this.animationOut) } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt index 6a462e9..df633af 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt @@ -25,6 +25,8 @@ class TouchGestureDetector( private val TAP_TIMEOUT: Int private val DOUBLE_TAP_TIMEOUT: Int + private val MIN_TRIANGLE_HEIGHT = 250 + data class Vector(val x: Float, val y: Float) { fun absSquared(): Float { @@ -94,19 +96,24 @@ class TouchGestureDetector( } // add new pointers - (0.. { + if(startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) { + gesture = Gesture.SWIPE_LARGER + } else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) { + gesture = Gesture.SWIPE_SMALLER + } + } + Gesture.SWIPE_UP -> { + if(startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) { + gesture = Gesture.SWIPE_LARGER_REVERSE + } else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) { + gesture = Gesture.SWIPE_SMALLER_REVERSE + } + } + Gesture.SWIPE_RIGHT -> { + if(startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) { + gesture = Gesture.SWIPE_V + } else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) { + gesture = Gesture.SWIPE_LAMBDA + } + } + Gesture.SWIPE_LEFT -> { + if(startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) { + gesture = Gesture.SWIPE_V_REVERSE + } else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) { + gesture = Gesture.SWIPE_LAMBDA_REVERSE + } + } + else -> { } + } + if (edgeActions) { if (mainPointerPath.max.x < edgeWidth * width) { gesture = gesture?.getEdgeVariant(Gesture.Edge.LEFT) diff --git a/app/src/main/res/values/defaults.xml b/app/src/main/res/values/defaults.xml index 276651d..cee201e 100644 --- a/app/src/main/res/values/defaults.xml +++ b/app/src/main/res/values/defaults.xml @@ -2,6 +2,8 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 97d7384..b61c1b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,6 +59,24 @@ Swipe down at the left edge of the screen Down (Right Edge) Swipe down at the right edge of the screen + + ]]> + Top left -> mid right -> bottom left + (reverse)]]> + Bottom left -> mid right -> top left + + Top right -> mid left -> bottom right + + Bottom right -> mid left -> top right + V + Top left -> bottom mid -> top right + V (reverse) + Top right -> bottom mid -> top left + Λ + Bottom left -> top mid -> bottom right + Λ (reverse) + Bottom right -> top mid -> bottom left + Volume Up Press the volume up button Volume Down From 47ae0bf35f6f8fafe9f25f2b6c63300edf51e577 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sun, 16 Feb 2025 16:51:18 +0100 Subject: [PATCH 012/103] update README.md --- README.md | 71 ++++++++++++++++++++++----------------- BUILD.md => docs/build.md | 0 docs/launcher.md | 49 +++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 31 deletions(-) rename BUILD.md => docs/build.md (100%) create mode 100644 docs/launcher.md diff --git a/README.md b/README.md index b29b44d..016b362 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,6 @@ µLauncher is an Android home screen that lets you launch apps using swipe gestures and button presses. It is *minimal, efficient and free of distraction*. -Your home screen only displays the date, time and a wallpaper. -Pressing back or swiping up (this can be configured) opens a list -of all installed apps, which can be searched efficiently. - -This is a fork of [finnmglas's app Launcher][original-repo]. - Get it on F-Droid Get it on Accrescent @@ -51,6 +45,45 @@ You can also [get it on Google Play](https://play.google.com/store/apps/details? height="400"> +µLauncher is a fork of [finnmglas's app Launcher][original-repo]. +An incomplete list of changes can be found [here](docs/launcher.md). + +## Features + +µLauncher only displays the date, time and a wallpaper. +Pressing back or swiping up (this can be configured) opens a list +of all installed apps, which can be searched efficiently. + +The following gestures are available: + - volume up / down, + - swipe up / down / left / right, + - swipe with two fingers, + - swipe on the left / right resp. top / bottom edge, + - draw < / > / V / Λ + - click on date / time, + - double click, + - long click, + - back button. + +To every gesture you can bind one of the following actions: + - launch an app, + - open a list of all / favorite / private apps, + - open µLauncher settings, + - toggle private space lock, + - lock the screen, + - toggle the torch, + - volume up / down, + - go to previous / next audio track. + + + +µ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. + +By default the font is set to [Hack][hack-font], but other fonts can be selected. + + + ## Contributing There are several ways to contribute to this app: @@ -63,34 +96,10 @@ 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 [build.md](docs/build.md) for instructions how to build this project. The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds. Note that those are not signed. -## Notable changes compared to [Finn's Launcher][original-repo]: - -* 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. -* The home button now works as expected. - -### Visual -* This app uses the system wallpaper instead of a custom solution. -* The font has been changed to [Hack][hack-font]. -* Font Awesome Icons were replaced by Material icons. -* The gear button on the home screen was removed. Instead pressing back opens the list of applications and the app settings are accessible from there. - - -### 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 -* Small improvements to the gesture detection. -* Different apps set as default. -* Package name was changed to `de.jrpie.android.launcher` to avoid clashing with the original app. -* Dropped support for API < 21 (i.e. pre Lollypop) -* Some refactoring ---- --- [hack-font]: https://sourcefoundry.org/hack/ [original-repo]: https://github.com/finnmglas/Launcher diff --git a/BUILD.md b/docs/build.md similarity index 100% rename from BUILD.md rename to docs/build.md diff --git a/docs/launcher.md b/docs/launcher.md new file mode 100644 index 0000000..37b24a4 --- /dev/null +++ b/docs/launcher.md @@ -0,0 +1,49 @@ +# Notable changes compared to [Finn's Launcher][original-repo]: + +µLauncher is a fork of [finnmglas's app Launcher][original-repo]. +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. +- 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 +- Option to hide apps +- Favorite apps +- New actions: + - Toggle Torch + - Lock screen +- The home button now works as expected. +- Improved gesture detection. + +### 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 +- The search algorithm was modified to prefer matches at the beginning of the app name, i.e. when searching for `"te"`, `"termux"` is sorted before `"notes"`. +- The search bar was moved to the bottom of the screen. + +### Technical +- Improved gesture detection. +- Different apps set as default. +- Package name was changed to `de.jrpie.android.launcher` to avoid clashing with the original app. +- Dropped support for API < 21 (i.e. pre Lollypop) +- Fixed some bugs +- Some refactoring + + +The complete list of changes can be viewed [here](https://github.com/jrpie/launcher/compare/340ee731...master). + +--- + [original-repo]: https://github.com/finnmglas/Launcher From 7257d4ca35407aa51198e127cc04c54a7de431ef Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sun, 16 Feb 2025 23:58:42 +0100 Subject: [PATCH 013/103] fix bug in gesture detection logic --- .../jrpie/android/launcher/ui/TouchGestureDetector.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt index df633af..0ddbfd1 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt @@ -107,11 +107,16 @@ class TouchGestureDetector( ) } - for( i in 0.. Date: Mon, 17 Feb 2025 00:31:18 +0100 Subject: [PATCH 014/103] add action: media play / pause --- .../launcher/actions/LauncherAction.kt | 63 +++++++------------ .../res/drawable/baseline_play_arrow_24.xml | 12 ++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 37 insertions(+), 39 deletions(-) create mode 100644 app/src/main/res/drawable/baseline_play_arrow_24.xml diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt index 4f89758..1ed6473 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt @@ -82,22 +82,32 @@ enum class LauncherAction( VOLUME_UP( "volume_up", R.string.list_other_volume_up, - R.drawable.baseline_volume_up_24, ::audioVolumeUp + R.drawable.baseline_volume_up_24, + { context -> audioVolumeAdjust(context, true)} ), VOLUME_DOWN( "volume_down", R.string.list_other_volume_down, - R.drawable.baseline_volume_down_24, ::audioVolumeDown + R.drawable.baseline_volume_down_24, + { context -> audioVolumeAdjust(context, false)} + ), + TRACK_PLAY_PAUSE( + "play_pause_track", + R.string.list_other_track_play_pause, + R.drawable.baseline_play_arrow_24, + { context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)} ), TRACK_NEXT( "next_track", R.string.list_other_track_next, - R.drawable.baseline_skip_next_24, ::audioNextTrack + R.drawable.baseline_skip_next_24, + { context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_NEXT)} ), TRACK_PREV( "previous_track", R.string.list_other_track_previous, - R.drawable.baseline_skip_previous_24, ::audioPreviousTrack + R.drawable.baseline_skip_previous_24, + { context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PREVIOUS)} ), EXPAND_NOTIFICATIONS_PANEL( "expand_notifications_panel", @@ -155,56 +165,32 @@ enum class LauncherAction( /* Media player actions */ - -private fun audioNextTrack(context: Context) { - +private fun audioManagerPressKey(context: Context, key: Int) { val mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - val eventTime: Long = SystemClock.uptimeMillis() - val downEvent = - KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT, 0) + KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, key, 0) mAudioManager.dispatchMediaKeyEvent(downEvent) - - val upEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT, 0) + val upEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, key, 0) mAudioManager.dispatchMediaKeyEvent(upEvent) + } -private fun audioPreviousTrack(context: Context) { - val mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - - val eventTime: Long = SystemClock.uptimeMillis() - - val downEvent = - KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS, 0) - mAudioManager.dispatchMediaKeyEvent(downEvent) - - val upEvent = - KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS, 0) - mAudioManager.dispatchMediaKeyEvent(upEvent) -} - -private fun audioVolumeUp(context: Context) { +private fun audioVolumeAdjust(context: Context, louder: Boolean) { val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager.adjustStreamVolume( AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_RAISE, + if (louder) { + AudioManager.ADJUST_RAISE + } else { + AudioManager.ADJUST_LOWER + }, AudioManager.FLAG_SHOW_UI ) } -private fun audioVolumeDown(context: Context) { - val audioManager = - context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - - audioManager.adjustStreamVolume( - AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_LOWER, - AudioManager.FLAG_SHOW_UI - ) -} /* End media player actions */ private fun toggleTorch(context: Context) { @@ -320,5 +306,4 @@ private class LauncherActionSerializer : KSerializer { encodeSerializableElement(descriptor, 0, String.serializer(), value.id) } } - } \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_play_arrow_24.xml b/app/src/main/res/drawable/baseline_play_arrow_24.xml new file mode 100644 index 0000000..ca4e475 --- /dev/null +++ b/app/src/main/res/drawable/baseline_play_arrow_24.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b61c1b0..eb7cd8b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -245,6 +245,7 @@ Music: Quieter Music: Next Music: Previous + Music: Play / Pause Expand notifications panel Do nothing Lock Screen From 5792c7f38cfcbb254521ea96ac279c54a30160a7 Mon Sep 17 00:00:00 2001 From: Nicola Bortoletto Date: Tue, 4 Feb 2025 07:12:41 +0000 Subject: [PATCH 015/103] Translated using Weblate (Italian) Currently translated at 98.1% (214 of 218 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/ --- app/src/main/res/values-it/strings.xml | 54 ++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index afc92da..06555b0 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -4,7 +4,7 @@ Aspetto Scegliere l\'applicazione Tema - Questo launcher è progettato per essere minimale, efficiente e privo di distrazioni.\n\nNon contiene pagamenti, pubblicità o servizi di tracciamento. + Questo launcher è progettato per essere minimale, efficiente e privo di distrazioni. Non contiene pagamenti, pubblicità o servizi di tracciamento. Predefinito Non mostrare applicazioni collegate a gesti nella lista delle app Testo @@ -45,11 +45,11 @@ Scorri verso il basso con due dita Sinistra Scorrere verso sinistra - Doppio a sinistra + Due dita verso sinistra Scorrere verso sinistra con due dita Destra Scorri verso destra - Doppio a destra + Due dita verso destra Scorri a destra con due dita Destra (in alto) Scorri verso destra sul bordo superiore dello schermo @@ -125,12 +125,12 @@ Abbiamo impostato alcune app predefinite per te. Puoi modificarle ora se lo desideri: Puoi anche cambiare la tua selezione in seguito. Iniziamo! - Sei pronto per iniziare!\n\nSpero questa applicazione ti risulti preziosa!\n\n- Finn (che ha ideato il launcher)\n\t e Josia (che ha aggiunto qualche miglioramento e mantiene il fork μLauncher) + Sei pronto per iniziare! Spero questa applicazione ti risulti preziosa! - Finn (che ha ideato il launcher) \te Josia (che ha aggiunto qualche miglioramento 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.\nQuesta azione utilizza funzionalità non incluse nelle API Android pubbliche. Sfortunatamente, non sembra funzionare sul tuo dispositivo. + 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 @@ -182,7 +182,7 @@ Mostra Rinomina Le applicazioni selezionate sono state rimosse - Cerca tra le applicazioni + Cerca Impostazioni μLauncher Espandi il pannello notifiche Non fare niente @@ -191,11 +191,49 @@ Tutorial Prenditi qualche secondo per imparare ad usare questo launcher! Concetto - L\'app è open source (sotto licenza MIT) e disponibile su GitHub!\n\nVisita il nostro archivio! + L\'app è open source (sotto licenza MIT) e disponibile su GitHub! Visita il nostro archivio! Utilizzo La schermata principale contiene solo data e ora. Nessuna distrazione. - Questa funzione richiede Android 6.0 o successivi. + Questa funzione richiede Android 6 o successivi. Ok Rinomina %1$s Impossibile rimuovere l\'applicazione + Dinamico + Colore + 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. + Nascondi le app in pausa + Attiva/Disattiva Blocco Spazio Privato + Questa funzionalità richiede Android 15 o successivi. + Rosso + Trasparente + Blu + Verde + Ok + Colore + Scegli colore + 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 (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: + Copia negli appunti + Non segnalare pubblicamente le vulnerabilità di sicurezza su GitHub, ma utilizza invece: + Annulla + Premi spazio per disabilitare temporaneamente questa funzionalità. + 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. + Impossibile aprire l\'URL: nessun browser trovato. + Non è stata trovata un\'applicazione per gestire la ricerca. \ No newline at end of file From 18b4fca9337595ffb00b690a93604f99c5271f0b Mon Sep 17 00:00:00 2001 From: Xanadul Date: Wed, 5 Feb 2025 10:11:17 +0000 Subject: [PATCH 016/103] Translated using Weblate (German) Currently translated at 13.3% (2 of 15 strings) Translation: jrpie-Launcher/metadata Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/de/ --- fastlane/metadata/android/de-DE/title.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/de-DE/title.txt diff --git a/fastlane/metadata/android/de-DE/title.txt b/fastlane/metadata/android/de-DE/title.txt new file mode 100644 index 0000000..4305604 --- /dev/null +++ b/fastlane/metadata/android/de-DE/title.txt @@ -0,0 +1 @@ +µLauncher From 7841a99415a84ecd294b6e8ef1d3fae982abacd4 Mon Sep 17 00:00:00 2001 From: Vossa Excelencia Date: Wed, 5 Feb 2025 16:39:04 +0000 Subject: [PATCH 017/103] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (218 of 218 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 | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6a404eb..228d0f4 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -30,10 +30,10 @@ 2 dedos para esquerda Para direita 2 dedos para direita - Para direita (No topo) + Para direita (no topo) Para direita (em baixo) - Para esquerda (Em baixo) - Para esquerda (No topo) + Para esquerda (em baixo) + Para esquerda (no topo) Para cima (Borda esquerda) Para cima (Borda direita) Para baixo (Borda esquerda) @@ -164,7 +164,7 @@ Escolha um método de bloqueio Escolha um método de bloqueio da tela Configurações rápidas - Essa funcionalidade requer o Android 6.0 ou mais recente. + Essa funcionalidade requer o Android 6 ou mais recente. Nenhuma câmera com lanterna detectada. Erro: O bloqueio da tela via Serviço de acessibilidade não é compatível com este aparelho. Tente usar Administrador do dispositivo como método alternativo. Definindo µLauncher como Serviço de acessibilidade permite a ele bloquear a tela. Considere que é necessário conceder as permissões elevadas. Você nunca deveria autorizar essas permissões a qualquer aplicativo sem avaliação. O µLauncher usará o Serviço de acessibilidade somente para bloquear a tela. Você pode verificar o código-fonte para ter certeza. O bloqueio da tela também pode ser realizado dando ao µLauncher permissões de Administrador do dispositivo. Apesar de que esse método não funciona com impressão digital e desbloqueio facial. @@ -260,7 +260,7 @@ Licenças de código aberto Ocultar apps pausados Ativar o Espaço privado - Essa funcionalidade requer o Android 15.0 ou mais recente. + Essa funcionalidade requer o Android 15 ou mais recente. Espaço privado trancado Espaço privado liberado Espaço privado indisponível @@ -271,4 +271,7 @@ Criar relatório Relatar um bug Obrigado por ajudar a melhorar o µLauncher!\nConsidere adicionar as seguintes informações ao relatório de bug: + Toque no espaço para temporariamente desativar esta funcionalidade. + Não foi possível abrir a URL: nenhum navegador encontrado. + Nenhum app encontrado para efetuar a pesquisa. \ No newline at end of file From 958d4879f54311a5fa8b2ce7ae2e6cac789e5660 Mon Sep 17 00:00:00 2001 From: Nicola Bortoletto Date: Thu, 6 Feb 2025 12:57:53 +0000 Subject: [PATCH 018/103] Translated using Weblate (Italian) Currently translated at 99.0% (216 of 218 strings) Translation: jrpie-Launcher/Launcher Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/launcher/it/ --- app/src/main/res/values-it/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 06555b0..320bfc6 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -125,7 +125,7 @@ Abbiamo impostato alcune app predefinite per te. Puoi modificarle ora se lo desideri: Puoi anche cambiare la tua selezione in seguito. Iniziamo! - Sei pronto per iniziare! Spero questa applicazione ti risulti preziosa! - Finn (che ha ideato il launcher) \te Josia (che ha aggiunto qualche miglioramento e mantiene il fork μLauncher) + 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) Inizia Impostazioni Altre opzioni @@ -236,4 +236,5 @@ µLauncher deve essere la schermata iniziale predefinita per accedere allo spazio privato. Impossibile aprire l\'URL: nessun browser trovato. Non è stata trovata un\'applicazione per gestire la ricerca. + privilegi più ampi a µLauncher.
µLauncher utilizzerà questi privilegi solo per bloccare lo schermo. µLauncher non raccoglierà mai alcun dato. In particolare, µLauncher non usa il servizio di accessibilità per raccogliere nessun dato.]]>
\ No newline at end of file From e959e9d957c8a2a00190ef031e74ccfba0b91db0 Mon Sep 17 00:00:00 2001 From: Vossa Excelencia Date: Fri, 7 Feb 2025 13:36:03 +0000 Subject: [PATCH 019/103] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (220 of 220 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 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 228d0f4..0ec4a32 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -274,4 +274,6 @@ Toque no espaço para temporariamente desativar esta funcionalidade. Não foi possível abrir a URL: nenhum navegador encontrado. Nenhum app encontrado para efetuar a pesquisa. + Voltar + Botão Voltar / gesto de Voltar \ No newline at end of file From 4508e4ee5ca1d5938f9d3545cc4f219f16c90286 Mon Sep 17 00:00:00 2001 From: Vossa Excelencia Date: Fri, 7 Feb 2025 13:35:02 +0000 Subject: [PATCH 020/103] Translated using Weblate (Portuguese (Brazil)) Currently translated at 20.0% (3 of 15 strings) Translation: jrpie-Launcher/metadata Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/pt_BR/ --- .../android/pt-BR/full_description.txt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt index b3bfbce..e976080 100644 --- a/fastlane/metadata/android/pt-BR/full_description.txt +++ b/fastlane/metadata/android/pt-BR/full_description.txt @@ -1,23 +1,23 @@ -O µLauncher é uma tela inicial que permite abrir aplicativos através gestos de deslize e botões físicos. +O µLauncher é uma tela inicial que permite abrir apps através de gestos e botões físicos. O launcher é minimalista, eficiente e livre de distrações. -Sua tela inicial exibe apenas a data, hora e papel de parede. +A tela inicial mostra apenas a data, hora e papel de parede. -Ao usar Voltar ou deslize para cima (pode ser configurado depois) você abre a lista -com todos os aplicativos instalados, que podem ser encontrados rápido. +Ao usar botão Voltar ou gesto pra cima (pode ser redefinido depois) +você abre com facilidade e rapidez a lista com todos os apps instalados. -Esta é uma modificação do app Launcher -feita por Finn M Glas. +O app é uma modificação do Launcher +feito por Finn M Glas. Funcionalidades: * Você pode associar várias ações a 22 gestos diferentes. -* Também pode definir algumas das seguintes acções: - - Iniciar algum app +* Pode definir algumas das seguintes ações: + - Iniciar vários apps - Listar todos aplicativos - Listar apps favoritos - Aumentar / diminuir o volume - - Música: faixa anterior / seguinte + - Música: passar pra faixa anterior / seguinte - Bloquear a tela - Ligar a lanterna - Mostrar notificações / configurações rápidas -* Compatível com Perfil de trabalho, desta forma apps como Shelter podem ser usados. +* App é compatível com Perfil de trabalho e pode ser usado com apps tipo Shelter. From d0b0c27b2c01eee253502538fe7a0cc147ce630f Mon Sep 17 00:00:00 2001 From: Vossa Excelencia Date: Sun, 9 Feb 2025 15:23:17 +0000 Subject: [PATCH 021/103] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (220 of 220 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 | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 0ec4a32..fef6043 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -34,10 +34,10 @@ Para direita (em baixo) Para esquerda (em baixo) Para esquerda (no topo) - Para cima (Borda esquerda) - Para cima (Borda direita) - Para baixo (Borda esquerda) - Para baixo (Borda direita) + Para cima (borda esquerda) + Para cima (borda direita) + Para baixo (borda esquerda) + Para baixo (borda direita) Aumento de volume Diminuição de volume Toque duplo @@ -62,7 +62,7 @@ Mostrar Data Use formato de data localizado Inverter data e hora - Escolha um papel de parede + Escolha papel de parede Alterar papel de parede Exibição Manter a tela ligada @@ -121,11 +121,11 @@ O Launcher foi criado para ser minimalista, eficiente e livre de distrações. Ele é livre de pagamentos, anúncios e serviços de rastreamento. O app é de código aberto (licença MIT) e está disponível no GitHub! Não deixe de conferir o repositório! Uso - Sua tela inicial contém a data e hora local. Sem distração. - Você pode iniciar seus aplicativos com um toque único ou pressionando um botão. Escolha algumas ações no próximo slide. + Sua tela inicial contém a data e hora local. Sem distrações. + Você pode iniciar seus aplicativos com um gesto único ou apertando um botão. Escolha algumas ações no próximo slide. Configurar Selecionamos alguns aplicativos padrão para você. Se quiser, você pode alterá-los agora: - Você também pode alterar suas escolhas mais tarde. + Você pode alterar suas escolhas mais tarde. Vamos lá! Tá todo pronto para começar! Espero que isso seja de grande valor para você! - Finn (que criou o Launcher) \te Josia (que fez algumas melhorias e tb mantém o fork do μLauncher) Começar @@ -162,7 +162,7 @@ Usar o Serviço de acessibilidade Usar o Administrador do dispositivo Escolha um método de bloqueio - Escolha um método de bloqueio da tela + Escolha método de bloqueio da tela Configurações rápidas Essa funcionalidade requer o Android 6 ou mais recente. Nenhuma câmera com lanterna detectada. From bef38c2657063358e1bb92eac1c448c960563603 Mon Sep 17 00:00:00 2001 From: Vossa Excelencia Date: Tue, 11 Feb 2025 21:14:28 +0000 Subject: [PATCH 022/103] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (225 of 225 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 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index fef6043..0dd41ed 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -276,4 +276,9 @@ Nenhum app encontrado para efetuar a pesquisa. Voltar Botão Voltar / gesto de Voltar + Ocultar espaço privado na lista de apps + Espaço privado + Espaço privado + Trancar espaço privado + Liberar espaço privado \ No newline at end of file From 68b79724e80adc48f48799c93494483c03096cc4 Mon Sep 17 00:00:00 2001 From: Vossa Excelencia Date: Tue, 11 Feb 2025 21:19:15 +0000 Subject: [PATCH 023/103] Translated using Weblate (Portuguese (Brazil)) Currently translated at 26.6% (4 of 15 strings) Translation: jrpie-Launcher/metadata Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/pt_BR/ --- fastlane/metadata/android/pt-BR/short_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/pt-BR/short_description.txt b/fastlane/metadata/android/pt-BR/short_description.txt index e1c50b5..1fa66b5 100644 --- a/fastlane/metadata/android/pt-BR/short_description.txt +++ b/fastlane/metadata/android/pt-BR/short_description.txt @@ -1 +1 @@ -Uma tela inicial minimalista e sem distrações para Android. +Tela inicial minimalista e sem distrações para Android. From c448c51164ea25eb8d294cd4b9ecd638b88a4d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20=C3=87eliker?= Date: Sun, 16 Feb 2025 23:00:52 +0000 Subject: [PATCH 024/103] Translated using Weblate (Turkish) Currently translated at 86.6% (13 of 15 strings) Translation: jrpie-Launcher/metadata Translate-URL: https://toolate.othing.xyz/projects/jrpie-launcher/metadata/tr/ --- fastlane/metadata/android/tr-TR/changelogs/21.txt | 15 +++++++++++++++ fastlane/metadata/android/tr-TR/changelogs/24.txt | 8 ++++++++ fastlane/metadata/android/tr-TR/changelogs/25.txt | 4 ++++ fastlane/metadata/android/tr-TR/changelogs/26.txt | 1 + fastlane/metadata/android/tr-TR/changelogs/27.txt | 9 +++++++++ fastlane/metadata/android/tr-TR/changelogs/29.txt | 1 + fastlane/metadata/android/tr-TR/changelogs/32.txt | 11 +++++++++++ fastlane/metadata/android/tr-TR/changelogs/33.txt | 5 +++++ fastlane/metadata/android/tr-TR/changelogs/34.txt | 9 +++++++++ fastlane/metadata/android/tr-TR/changelogs/35.txt | 6 ++++++ fastlane/metadata/android/tr-TR/changelogs/36.txt | 6 ++++++ fastlane/metadata/android/tr-TR/changelogs/37.txt | 8 ++++++++ fastlane/metadata/android/tr-TR/title.txt | 1 + 13 files changed, 84 insertions(+) create mode 100644 fastlane/metadata/android/tr-TR/changelogs/21.txt create mode 100644 fastlane/metadata/android/tr-TR/changelogs/24.txt create mode 100644 fastlane/metadata/android/tr-TR/changelogs/25.txt create mode 100644 fastlane/metadata/android/tr-TR/changelogs/26.txt create mode 100644 fastlane/metadata/android/tr-TR/changelogs/27.txt create mode 100644 fastlane/metadata/android/tr-TR/changelogs/29.txt create mode 100644 fastlane/metadata/android/tr-TR/changelogs/32.txt create mode 100644 fastlane/metadata/android/tr-TR/changelogs/33.txt create mode 100644 fastlane/metadata/android/tr-TR/changelogs/34.txt create mode 100644 fastlane/metadata/android/tr-TR/changelogs/35.txt create mode 100644 fastlane/metadata/android/tr-TR/changelogs/36.txt create mode 100644 fastlane/metadata/android/tr-TR/changelogs/37.txt create mode 100644 fastlane/metadata/android/tr-TR/title.txt diff --git a/fastlane/metadata/android/tr-TR/changelogs/21.txt b/fastlane/metadata/android/tr-TR/changelogs/21.txt new file mode 100644 index 0000000..91667e5 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/21.txt @@ -0,0 +1,15 @@ +- Çince çeviri (teşekkürler, yzqzss!) +- Fransızca çeviri iyileştirildi (teşekkürler, toby-bro!) +- Almanca çeviri iyileştirildi + +Tüm Uygulamalar: +- Uygulama listesindeki üç nokta kaldırıldı (bunun yerine uzun tıklama kullanın) +- Enter'a basıldığında sorguyla eşleşen ilk uygulama açılıyor +- Klavyeyi tam ekran modunda açarken oluşan hata için geçici çözüm düzeltildi +- Sistem uygulamaları için kaldırma seçeneği kaldırıldı +- Ana Sayfa Düğmesi artık düzgün çalışıyor + +Ayarlar: +- Küçük ekranlar için ayarlar düzeltildi +- Hassasiyet ayarı kaldırıldı (herkes zaten maksimuma ayarlıyordu) +- Tarih ve saat ayarları yeniden düzenlendi diff --git a/fastlane/metadata/android/tr-TR/changelogs/24.txt b/fastlane/metadata/android/tr-TR/changelogs/24.txt new file mode 100644 index 0000000..63f5cde --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/24.txt @@ -0,0 +1,8 @@ +* Renk teması, yazı tipi, arka plan, monokrom simgeler için seçenekler eklendi +* Tarih ve saat için seçenekler eklendi +* Döndürmeye izin verme seçeneği eklendi +* İyileştirilmiş arama algoritması +* Brezilya Portekizcesine çeviri - teşekkürler, Jonatas de Almeida Barros! +* Ayarlardan seçilen uygulamaların kaybolmasına neden olan bir hata düzeltildi +* İyileştirilmiş kod kalitesi +* Güncellenmiş çeviriler diff --git a/fastlane/metadata/android/tr-TR/changelogs/25.txt b/fastlane/metadata/android/tr-TR/changelogs/25.txt new file mode 100644 index 0000000..6aa3565 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/25.txt @@ -0,0 +1,4 @@ +* Favori uygulamalar +* Uygulamaları gizleme seçeneği +* Birden fazla ana aktiviteye sahip uygulamalar için destek +* Küçük ekranlarda ayarların düzeni düzeltildi diff --git a/fastlane/metadata/android/tr-TR/changelogs/26.txt b/fastlane/metadata/android/tr-TR/changelogs/26.txt new file mode 100644 index 0000000..9174cd3 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/26.txt @@ -0,0 +1 @@ +hata düzeltme diff --git a/fastlane/metadata/android/tr-TR/changelogs/27.txt b/fastlane/metadata/android/tr-TR/changelogs/27.txt new file mode 100644 index 0000000..ec73839 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/27.txt @@ -0,0 +1,9 @@ +* Matrix ve Discord'da sohbet +* Hareket algılama iyileştirildi +* Hareket açıklamaları iyileştirildi +* Yeni eylem: Ekranı kilitle - teşekkürler, yzqzss! +* Yeni eylem: Meşaleyi aç +* Yeni eylem: Hızlı ayarları aç +* Fransızca çevirisi iyileştirildi - teşekkürler, toby-bro! +* Portekizce çevirisi iyileştirildi - teşekkürler, "Vossa Excelencia"! +* Bazı hatalar düzeltildi diff --git a/fastlane/metadata/android/tr-TR/changelogs/29.txt b/fastlane/metadata/android/tr-TR/changelogs/29.txt new file mode 100644 index 0000000..4f318eb --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/29.txt @@ -0,0 +1 @@ +Ekranı kilitlemek için Cihaz Yöneticisi yerine Erişilebilirlik Hizmetini kullanma seçeneği eklendi. diff --git a/fastlane/metadata/android/tr-TR/changelogs/32.txt b/fastlane/metadata/android/tr-TR/changelogs/32.txt new file mode 100644 index 0000000..0499004 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/32.txt @@ -0,0 +1,11 @@ +* Uygulamaları yeniden adlandırma seçeneği +* Uygulama listesinden hareketlere bağlı uygulamaları otomatik olarak gizleme seçeneği +* Uygulama listesinden µLauncher'ı varsayılan olarak gizle +* Varsayılan uygulamaların seçimi iyileştirildi +* Açık tema (deneysel) + +* Aramadaki hata düzeltildi +* Kilit ekranı iletişim kutusundaki hata düzeltildi (teşekkürler, yzqzss ve jeroen!) + +* Portekizce çeviri güncellendi (teşekkürler, "Vossa Excelencia"!) +* Almanca çeviri güncellendi diff --git a/fastlane/metadata/android/tr-TR/changelogs/33.txt b/fastlane/metadata/android/tr-TR/changelogs/33.txt new file mode 100644 index 0000000..64bea01 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/33.txt @@ -0,0 +1,5 @@ +* Uygulama listesi için alternatif düzenler (ızgara, metin) +* Hata düzeltildi: Uygulamaların yeniden adlandırılması artık düzgün çalışıyor + +* Çince çeviri güncellendi (teşekkürler, yzqzss!) +* Portekizce çeviri güncellendi (teşekkürler, "Vossa Excelencia"!) diff --git a/fastlane/metadata/android/tr-TR/changelogs/34.txt b/fastlane/metadata/android/tr-TR/changelogs/34.txt new file mode 100644 index 0000000..c1b80a1 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/34.txt @@ -0,0 +1,9 @@ +* Saat rengini seçme seçeneği +* Dinamik renk teması eklendi; açık tema kaldırıldı +* Erişilebilirlik hizmeti için onay iletişim kutusu eklendi + +* Türkçe çeviri eklendi (teşekkürler, Ahmet Çeliker!) +* İtalyanca çeviri eklendi (teşekkürler, Samantha!) +* Portekizce çeviri iyileştirildi (teşekkürler, "Vossa Excelencia"!) + +* Uygulama çekmecesinde hareketle gezinmeyle ilgili hata düzeltildi. Artık geri basıldığında çekmece hemen kapanıyor. diff --git a/fastlane/metadata/android/tr-TR/changelogs/35.txt b/fastlane/metadata/android/tr-TR/changelogs/35.txt new file mode 100644 index 0000000..cfd9c1e --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/35.txt @@ -0,0 +1,6 @@ +* Ayarlar için depolama biçimi yeniden düzenlendi. Eski biçim otomatik olarak dönüştürülecek. +* Uygulama listesinden web'de arama yapma seçeneği eklendi. +* Ekranı kilitlemek için tercih edilen yöntem olarak cihaz yöneticisini ayarlayın. +* Bir erişilebilirlik hizmetini etkinleştirmenin şifrelemeyle çakışabileceğine dair bir uyarı eklendi. +* Yeniden adlandırma iletişim kutusundaki bir hata düzeltildi. +* Kilit ekranı iletişim kutusu kaydırılabilir hale getirildi. diff --git a/fastlane/metadata/android/tr-TR/changelogs/36.txt b/fastlane/metadata/android/tr-TR/changelogs/36.txt new file mode 100644 index 0000000..a8b448e --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/36.txt @@ -0,0 +1,6 @@ +* yeni özellik: otomatik başlatmayı geçici olarak devre dışı bırakmak için boşluk içeren önek sorgusu +* iyileştirilmiş arama: sorguda görünmedikleri sürece diakritik işaretler artık yok sayılıyor. (Android 7+) +* açık kaynak lisanslarının listesi eklendi +* erişilebilirlik hizmeti uyarısı güncellendi +* iyileştirilmiş Fransızca çeviri (teşekkürler, Alexandre Ancel ve Nin Dan!) +* iyileştirilmiş Portekizce çeviri (teşekkürler, "Vossa Excelencia"!) diff --git a/fastlane/metadata/android/tr-TR/changelogs/37.txt b/fastlane/metadata/android/tr-TR/changelogs/37.txt new file mode 100644 index 0000000..914b7d5 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/37.txt @@ -0,0 +1,8 @@ +* private space için temel destek (Android 15) +* light tema yeniden tanıtıldı +* Portekizce çeviri iyileştirildi (teşekkürler, "Vossa Excelencia"!) +* Almanca çeviri iyileştirildi +* ayarlara sürüm adı eklendi +* hata raporları için iletişim kutusu +* saat performansı iyileştirildi +* bazı hatalar düzeltildi diff --git a/fastlane/metadata/android/tr-TR/title.txt b/fastlane/metadata/android/tr-TR/title.txt new file mode 100644 index 0000000..ad88d66 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/title.txt @@ -0,0 +1 @@ +µBaşlatıcı From befa3afc5d3cb15c19d08eaae8f2dc1c7be84436 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Mon, 17 Feb 2025 22:39:49 +0100 Subject: [PATCH 025/103] 0.0.22 --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/38.txt | 7 +++++++ fastlane/metadata/android/tr-TR/title.txt | 1 - 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/38.txt delete mode 100644 fastlane/metadata/android/tr-TR/title.txt diff --git a/app/build.gradle b/app/build.gradle index 53dc960..6656d5e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { minSdkVersion 21 targetSdkVersion 35 compileSdk 35 - versionCode 37 - versionName "0.0.21" + versionCode 38 + versionName "0.0.22" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/fastlane/metadata/android/en-US/changelogs/38.txt b/fastlane/metadata/android/en-US/changelogs/38.txt new file mode 100644 index 0000000..930481e --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/38.txt @@ -0,0 +1,7 @@ +* Fixed bug in detection of two finger gestures +* Added 8 new gestures: <,>,V,Λ and reversed versions +* Back is now treated as a regular gesture and can be assigned freely +* Improved support for private space +* Improved Portuguese translation (thank you, "Vossa Excelencia"!) +* Improved Turkish translation (thank you, Ahmet Çeliker!) +* Improved Italian translation (thank you, Nicola Bortoletto!) diff --git a/fastlane/metadata/android/tr-TR/title.txt b/fastlane/metadata/android/tr-TR/title.txt deleted file mode 100644 index ad88d66..0000000 --- a/fastlane/metadata/android/tr-TR/title.txt +++ /dev/null @@ -1 +0,0 @@ -µBaşlatıcı From 3aee137a3c4075fdae24b32fa9d35a1773827d3f Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Tue, 18 Feb 2025 04:20:27 +0100 Subject: [PATCH 026/103] basic support for pinned shortcuts (see #45) TODO: Show pinned shortcuts in app list --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 10 ++ .../de/jrpie/android/launcher/Application.kt | 3 + .../de/jrpie/android/launcher/Functions.kt | 33 +++++ .../launcher/actions/ShortcutAction.kt | 57 ++++++++ .../actions/shortcuts/PinnedShortcutInfo.kt | 60 ++++++++ .../launcher/ui/PinShortcutActivity.kt | 128 ++++++++++++++++++ .../main/res/drawable/baseline_close_24.xml | 1 - .../res/drawable/baseline_favorite_24.xml | 1 - .../drawable/baseline_favorite_border_24.xml | 1 - .../drawable/baseline_flashlight_on_24.xml | 1 - .../main/res/drawable/baseline_lock_24.xml | 9 +- .../res/drawable/baseline_lock_open_24.xml | 1 - .../main/res/drawable/baseline_menu_24.xml | 10 +- .../res/drawable/baseline_more_horiz_24.xml | 10 +- .../drawable/baseline_not_interested_24.xml | 1 - .../drawable/baseline_notifications_24.xml | 1 - .../main/res/drawable/baseline_search_24.xml | 1 - .../res/drawable/baseline_security_24.xml | 1 - .../res/drawable/baseline_settings_24.xml | 1 - .../baseline_settings_applications_24.xml | 1 - .../res/drawable/baseline_skip_next_24.xml | 1 - .../drawable/baseline_skip_previous_24.xml | 1 - .../res/drawable/baseline_volume_down_24.xml | 1 - .../res/drawable/baseline_volume_up_24.xml | 1 - .../main/res/layout/activity_pin_shortcut.xml | 110 +++++++++++++++ .../main/res/layout/dialog_select_gesture.xml | 27 ++++ .../res/layout/dialog_select_gesture_row.xml | 53 ++++++++ app/src/main/res/values/strings.xml | 5 + build.gradle | 4 +- 30 files changed, 509 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/actions/ShortcutAction.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/actions/shortcuts/PinnedShortcutInfo.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt create mode 100644 app/src/main/res/layout/activity_pin_shortcut.xml create mode 100644 app/src/main/res/layout/dialog_select_gesture.xml create mode 100644 app/src/main/res/layout/dialog_select_gesture_row.xml diff --git a/app/build.gradle b/app/build.gradle index 6656d5e..c81509c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -95,6 +95,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.activity:activity:1.8.0' implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.core:core-ktx:1.15.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3096d6d..93f6ce8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,16 @@ android:supportsRtl="true" android:theme="@style/launcherBaseTheme" tools:ignore="UnusedAttribute"> + + + + + + = VERSION_CODES.N_MR1) { + removeUnusedShortcuts(this) + } loadApps() } 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 5f7e9c9..8fc95a3 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -9,7 +9,9 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.content.pm.LauncherApps +import android.content.pm.LauncherApps.ShortcutQuery import android.content.pm.PackageManager +import android.content.pm.ShortcutInfo import android.net.Uri import android.os.Build import android.os.Bundle @@ -18,8 +20,11 @@ import android.os.UserManager import android.provider.Settings import android.util.Log import android.widget.Toast +import androidx.annotation.RequiresApi import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Gesture +import de.jrpie.android.launcher.actions.ShortcutAction +import de.jrpie.android.launcher.actions.shortcuts.PinnedShortcutInfo import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.apps.DetailedAppInfo import de.jrpie.android.launcher.apps.getPrivateSpaceUser @@ -81,6 +86,34 @@ fun getUserFromId(userId: Int?, context: Context): UserHandle { return profiles.firstOrNull { it.hashCode() == userId } ?: profiles[0] } +@RequiresApi(Build.VERSION_CODES.N_MR1) +fun removeUnusedShortcuts(context: Context) { + val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps + fun getShortcuts(profile: UserHandle): List? { + return launcherApps.getShortcuts( + ShortcutQuery().apply { + setQueryFlags(ShortcutQuery.FLAG_MATCH_PINNED) + }, + profile + ) + } + + val userManager = context.getSystemService(Service.USER_SERVICE) as UserManager + val boundActions: Set = + Gesture.entries.mapNotNull { Action.forGesture(it) as? ShortcutAction }.map { it.shortcut } + .toSet() + try { + userManager.userProfiles.filter { !userManager.isQuietModeEnabled(it) }.forEach { profile -> + getShortcuts(profile)?.groupBy { it.`package` }?.forEach { (p, shortcuts) -> + launcherApps.pinShortcuts(p, + shortcuts.filter { boundActions.contains(PinnedShortcutInfo(it)) } + .map { it.id }.toList(), + profile + ) + } + } + } catch (_: SecurityException) { } +} fun openInBrowser(url: String, context: Context) { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/ShortcutAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/ShortcutAction.kt new file mode 100644 index 0000000..8517b1a --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/actions/ShortcutAction.kt @@ -0,0 +1,57 @@ +package de.jrpie.android.launcher.actions + +import android.app.Service +import android.content.Context +import android.content.pm.LauncherApps +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.os.Build +import de.jrpie.android.launcher.actions.shortcuts.PinnedShortcutInfo +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("action:shortcut") +class ShortcutAction(val shortcut: PinnedShortcutInfo) : Action { + + override fun invoke(context: Context, rect: Rect?): Boolean { + val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + // TODO + return false + } + shortcut.getShortcutInfo(context)?.let { + launcherApps.startShortcut(it, rect, null) + } + + // TODO: handle null + return true + } + + override fun label(context: Context): String { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + return "?" + } + + return shortcut.getShortcutInfo(context)?.longLabel?.toString() ?: "?" + } + + override fun getIcon(context: Context): Drawable? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + return null + } + val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps + return shortcut.getShortcutInfo(context)?.let { launcherApps.getShortcutBadgedIconDrawable(it, 0) } + } + + override fun isAvailable(context: Context): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + return false + } + return shortcut.getShortcutInfo(context) != null + } + + override fun canReachSettings(): Boolean { + return false + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/shortcuts/PinnedShortcutInfo.kt b/app/src/main/java/de/jrpie/android/launcher/actions/shortcuts/PinnedShortcutInfo.kt new file mode 100644 index 0000000..796c737 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/actions/shortcuts/PinnedShortcutInfo.kt @@ -0,0 +1,60 @@ +package de.jrpie.android.launcher.actions.shortcuts + +import android.app.Service +import android.content.ComponentName +import android.content.Context +import android.content.pm.LauncherApps +import android.content.pm.LauncherApps.ShortcutQuery +import android.content.pm.ShortcutInfo +import android.os.Build +import androidx.annotation.RequiresApi +import de.jrpie.android.launcher.getUserFromId +import kotlinx.serialization.Serializable + + +@RequiresApi(Build.VERSION_CODES.N_MR1) +@Serializable +class PinnedShortcutInfo( + val id: String, + val packageName: String, + val activityName: String, + val user: Int +) { + + constructor(info: ShortcutInfo) : this(info.id, info.`package`, info.activity?.className ?: "", info.userHandle.hashCode()) + + fun getShortcutInfo(context: Context): ShortcutInfo? { + val launcherApps = context.getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps + + return launcherApps.getShortcuts( + ShortcutQuery().apply { + setQueryFlags(ShortcutQuery.FLAG_MATCH_PINNED) + setPackage(packageName) + setActivity(ComponentName(packageName, activityName)) + setShortcutIds(listOf(id)) + }, + getUserFromId(user, context) + )?.firstOrNull() + } + + override fun equals(other: Any?): Boolean { + return (other as? PinnedShortcutInfo)?.let { + packageName == this.packageName && + activityName == this.activityName && + id == this.id && + user == this.user + } ?: false + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + packageName.hashCode() + result = 31 * result + activityName.hashCode() + result = 31 * result + user + return result + } + + override fun toString(): String { + return "PinnedShortcutInfo { package=$packageName, activity=$activityName, user=$user, id=$id}" + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..d19fe04 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt @@ -0,0 +1,128 @@ +package de.jrpie.android.launcher.ui + +import android.app.AlertDialog +import android.app.Service +import android.content.Context +import android.content.pm.LauncherApps +import android.content.pm.LauncherApps.PinItemRequest +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.activity.enableEdgeToEdge +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.actions.Action +import de.jrpie.android.launcher.actions.Gesture +import de.jrpie.android.launcher.actions.ShortcutAction +import de.jrpie.android.launcher.actions.shortcuts.PinnedShortcutInfo +import de.jrpie.android.launcher.databinding.ActivityPinShortcutBinding +import de.jrpie.android.launcher.preferences.LauncherPreferences + +class PinShortcutActivity : AppCompatActivity(), UIObject { + private lateinit var binding: ActivityPinShortcutBinding + + private var isBound = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + super.onCreate() + enableEdgeToEdge() + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + finish() + return + } + + binding = ActivityPinShortcutBinding.inflate(layoutInflater) + setContentView(binding.root) + + val launcherApps = getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps + + val request = launcherApps.getPinItemRequest(intent) + if (request == null || request.requestType != PinItemRequest.REQUEST_TYPE_SHORTCUT) { + finish() + return + } + + binding.pinShortcutLabel.text = request.shortcutInfo!!.shortLabel ?: "?" + binding.pinShortcutLabel.setCompoundDrawables( + launcherApps.getShortcutBadgedIconDrawable(request.shortcutInfo, 0).also { + val size = (40 * resources.displayMetrics.density).toInt() + it.setBounds(0,0, size, size) + }, null, null, null) + + binding.pinShortcutButtonBind.setOnClickListener { + AlertDialog.Builder(this, R.style.AlertDialogCustom) + .setTitle(getString(R.string.pin_shortcut_button_bind)) + .setView(R.layout.dialog_select_gesture) + .setNegativeButton(android.R.string.cancel, null) + .create().also { it.show() }.let { dialog -> + val viewManager = LinearLayoutManager(dialog.context) + val viewAdapter = GestureRecyclerAdapter (dialog.context) { gesture -> + if (!isBound) { + isBound = true + request.accept() + } + val editor = LauncherPreferences.getSharedPreferences().edit() + ShortcutAction(PinnedShortcutInfo(request.shortcutInfo!!)).bindToGesture(editor, gesture.id) + editor.apply() + dialog.dismiss() + } + dialog.findViewById(R.id.dialog_select_gesture_recycler).apply { + setHasFixedSize(true) + layoutManager = viewManager + adapter = viewAdapter + } + } + } + + binding.pinShortcutClose.setOnClickListener { finish() } + } + + override fun onStart() { + super.onStart() + super.onStart() + } + + override fun getTheme(): Resources.Theme { + return modifyTheme(super.getTheme()) + } + + inner class GestureRecyclerAdapter(val context: Context, val onClick: (Gesture) -> Unit): RecyclerView.Adapter() { + val gestures = Gesture.entries.filter { it.isEnabled() }.toList() + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val label = itemView.findViewById(R.id.dialog_select_gesture_row_name) + val description = itemView.findViewById(R.id.dialog_select_gesture_row_description) + val icon = itemView.findViewById(R.id.dialog_select_gesture_row_icon) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view: View = inflater.inflate(R.layout.dialog_select_gesture_row, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val gesture = gestures[position] + holder.label.text = gesture.getLabel(context) + holder.description.text = gesture.getDescription(context) + holder.icon.setImageDrawable( + Action.forGesture(gesture)?.getIcon(context) + ) + holder.itemView.setOnClickListener { + onClick(gesture) + } + } + + override fun getItemCount(): Int { + return gestures.size + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_close_24.xml b/app/src/main/res/drawable/baseline_close_24.xml index 41350ac..2ab439d 100644 --- a/app/src/main/res/drawable/baseline_close_24.xml +++ b/app/src/main/res/drawable/baseline_close_24.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/drawable/baseline_favorite_24.xml b/app/src/main/res/drawable/baseline_favorite_24.xml index 4f9b020..5a612d2 100644 --- a/app/src/main/res/drawable/baseline_favorite_24.xml +++ b/app/src/main/res/drawable/baseline_favorite_24.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/drawable/baseline_favorite_border_24.xml b/app/src/main/res/drawable/baseline_favorite_border_24.xml index cecc9b0..14875dd 100644 --- a/app/src/main/res/drawable/baseline_favorite_border_24.xml +++ b/app/src/main/res/drawable/baseline_favorite_border_24.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/drawable/baseline_flashlight_on_24.xml b/app/src/main/res/drawable/baseline_flashlight_on_24.xml index e1326ae..16654cd 100644 --- a/app/src/main/res/drawable/baseline_flashlight_on_24.xml +++ b/app/src/main/res/drawable/baseline_flashlight_on_24.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/drawable/baseline_lock_24.xml b/app/src/main/res/drawable/baseline_lock_24.xml index 1e96180..8cb2d1f 100644 --- a/app/src/main/res/drawable/baseline_lock_24.xml +++ b/app/src/main/res/drawable/baseline_lock_24.xml @@ -2,9 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="960" - android:viewportHeight="960" - android:tint="?attr/colorControlNormal"> - + android:viewportHeight="960"> + diff --git a/app/src/main/res/drawable/baseline_lock_open_24.xml b/app/src/main/res/drawable/baseline_lock_open_24.xml index f0f6ea3..8d8e09b 100644 --- a/app/src/main/res/drawable/baseline_lock_open_24.xml +++ b/app/src/main/res/drawable/baseline_lock_open_24.xml @@ -1,7 +1,6 @@ + - + diff --git a/app/src/main/res/drawable/baseline_more_horiz_24.xml b/app/src/main/res/drawable/baseline_more_horiz_24.xml index a370298..061fae2 100644 --- a/app/src/main/res/drawable/baseline_more_horiz_24.xml +++ b/app/src/main/res/drawable/baseline_more_horiz_24.xml @@ -1,5 +1,11 @@ - + - + diff --git a/app/src/main/res/drawable/baseline_not_interested_24.xml b/app/src/main/res/drawable/baseline_not_interested_24.xml index 48ab05d..875f546 100644 --- a/app/src/main/res/drawable/baseline_not_interested_24.xml +++ b/app/src/main/res/drawable/baseline_not_interested_24.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/drawable/baseline_notifications_24.xml b/app/src/main/res/drawable/baseline_notifications_24.xml index b695693..ca969df 100644 --- a/app/src/main/res/drawable/baseline_notifications_24.xml +++ b/app/src/main/res/drawable/baseline_notifications_24.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/drawable/baseline_search_24.xml b/app/src/main/res/drawable/baseline_search_24.xml index ca9cbc0..9ba30e3 100644 --- a/app/src/main/res/drawable/baseline_search_24.xml +++ b/app/src/main/res/drawable/baseline_search_24.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/drawable/baseline_security_24.xml b/app/src/main/res/drawable/baseline_security_24.xml index 3c260ff..cd38b06 100644 --- a/app/src/main/res/drawable/baseline_security_24.xml +++ b/app/src/main/res/drawable/baseline_security_24.xml @@ -1,6 +1,5 @@ diff --git a/app/src/main/res/drawable/baseline_settings_24.xml b/app/src/main/res/drawable/baseline_settings_24.xml index 7cb5b17..4200acc 100644 --- a/app/src/main/res/drawable/baseline_settings_24.xml +++ b/app/src/main/res/drawable/baseline_settings_24.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/drawable/baseline_settings_applications_24.xml b/app/src/main/res/drawable/baseline_settings_applications_24.xml index f2d03cc..dd30af7 100644 --- a/app/src/main/res/drawable/baseline_settings_applications_24.xml +++ b/app/src/main/res/drawable/baseline_settings_applications_24.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/drawable/baseline_skip_next_24.xml b/app/src/main/res/drawable/baseline_skip_next_24.xml index 0091e03..9e203e0 100644 --- a/app/src/main/res/drawable/baseline_skip_next_24.xml +++ b/app/src/main/res/drawable/baseline_skip_next_24.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/drawable/baseline_skip_previous_24.xml b/app/src/main/res/drawable/baseline_skip_previous_24.xml index 0029a3e..832a188 100644 --- a/app/src/main/res/drawable/baseline_skip_previous_24.xml +++ b/app/src/main/res/drawable/baseline_skip_previous_24.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/drawable/baseline_volume_down_24.xml b/app/src/main/res/drawable/baseline_volume_down_24.xml index 78b51d3..1a34ad9 100644 --- a/app/src/main/res/drawable/baseline_volume_down_24.xml +++ b/app/src/main/res/drawable/baseline_volume_down_24.xml @@ -2,7 +2,6 @@ android:width="24dp" android:height="24dp" android:autoMirrored="true" - android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/baseline_volume_up_24.xml b/app/src/main/res/drawable/baseline_volume_up_24.xml index 6737fa6..f147499 100644 --- a/app/src/main/res/drawable/baseline_volume_up_24.xml +++ b/app/src/main/res/drawable/baseline_volume_up_24.xml @@ -2,7 +2,6 @@ android:width="24dp" android:height="24dp" android:autoMirrored="true" - android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/layout/activity_pin_shortcut.xml b/app/src/main/res/layout/activity_pin_shortcut.xml new file mode 100644 index 0000000..c401b42 --- /dev/null +++ b/app/src/main/res/layout/activity_pin_shortcut.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +