This commit is contained in:
Josia Pietsch 2025-03-14 15:27:26 +01:00
parent e250a58ef4
commit 077ee4381a
Signed by: jrpie
GPG key ID: E70B571D66986A2D
12 changed files with 75 additions and 36 deletions

View file

@ -10,6 +10,8 @@ import android.content.pm.ShortcutInfo
import android.os.AsyncTask import android.os.AsyncTask
import android.os.Build import android.os.Build
import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES
import android.os.Handler
import android.os.Looper
import android.os.UserHandle import android.os.UserHandle
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -108,12 +110,10 @@ class Application : android.app.Application() {
// Try to restore old preferences // Try to restore old preferences
migratePreferencesToNewVersion(this) migratePreferencesToNewVersion(this)
// First time opening the app: set defaults and start tutorial // First time opening the app: set defaults
// The tutorial is started from HomeActivity#onStart, as starting it here is blocked by android
if (!LauncherPreferences.internal().started()) { if (!LauncherPreferences.internal().started()) {
resetPreferences(this) resetPreferences(this)
LauncherPreferences.internal().started(true)
openTutorial(this)
} }
@ -134,7 +134,8 @@ class Application : android.app.Application() {
it.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) it.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
} }
} }
ContextCompat.registerReceiver(this, profileAvailabilityBroadcastReceiver, filter, ContextCompat.registerReceiver(
this, profileAvailabilityBroadcastReceiver, filter,
ContextCompat.RECEIVER_EXPORTED ContextCompat.RECEIVER_EXPORTED
) )
} }

View file

@ -135,9 +135,7 @@ fun openInBrowser(url: String, context: Context) {
} }
fun openTutorial(context: Context) { fun openTutorial(context: Context) {
context.startActivity(Intent(context, TutorialActivity::class.java).apply { context.startActivity(Intent(context, TutorialActivity::class.java))
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
} }

View file

@ -21,8 +21,10 @@ import eu.jonahbauer.android.preference.annotations.Preferences;
r = R.class, r = R.class,
value = { value = {
@PreferenceGroup(name = "internal", prefix = "settings_internal_", suffix = "_key", value = { @PreferenceGroup(name = "internal", prefix = "settings_internal_", suffix = "_key", value = {
// set after the user finished the tutorial
@Preference(name = "started", type = boolean.class, defaultValue = "false"), @Preference(name = "started", type = boolean.class, defaultValue = "false"),
@Preference(name = "started_time", type = long.class), @Preference(name = "started_time", type = long.class),
// see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt
@Preference(name = "version_code", type = int.class, defaultValue = "-1"), @Preference(name = "version_code", type = int.class, defaultValue = "-1"),
}), }),
@PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = { @PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = {

View file

@ -100,7 +100,7 @@ private fun migrateAppInfoStringMap(key: String) {
} }
}?.toMap(HashMap()) }?.toMap(HashMap())
)?.let { )?.let {
preferences.edit().putStringSet(key, it as Set<String>).apply() preferences.edit().putStringSet(key, it).apply()
} }
} }

View file

@ -11,6 +11,7 @@ import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPre
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.util.HashSet import java.util.HashSet
import androidx.core.content.edit
/** /**
* Migrate preferences from version 3 (used until version 0.0.23) to the current format * Migrate preferences from version 3 (used until version 0.0.23) to the current format
@ -42,6 +43,7 @@ private fun migrateSetAppInfo(key: String, preferences: SharedPreferences, edito
deserializeSet(preferences.getStringSet(key, null))?.let { deserializeSet(preferences.getStringSet(key, null))?.let {
set.addAll(it) set.addAll(it)
} }
@Suppress("UNCHECKED_CAST")
editor.putStringSet( editor.putStringSet(
key, key,
serializer.serialize(set as java.util.Set<AbstractAppInfo>) as Set<String>? serializer.serialize(set as java.util.Set<AbstractAppInfo>) as Set<String>?
@ -60,6 +62,7 @@ private fun migrateMapAppInfoString(key: String, preferences: SharedPreferences,
deserializeMap(preferences.getStringSet(key, null))?.let { deserializeMap(preferences.getStringSet(key, null))?.let {
map.putAll(it) map.putAll(it)
} }
@Suppress("UNCHECKED_CAST")
editor.putStringSet(key, serializer.serialize(map) as Set<String>?) editor.putStringSet(key, serializer.serialize(map) as Set<String>?)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@ -72,14 +75,11 @@ fun migratePreferencesFromVersion3() {
assert(LauncherPreferences.internal().versionCode() == 3) assert(LauncherPreferences.internal().versionCode() == 3)
val preferences = LauncherPreferences.getSharedPreferences() val preferences = LauncherPreferences.getSharedPreferences()
val editor = preferences.edit() preferences.edit {
migrateSetAppInfo(LauncherPreferences.apps().keys().favorites(), preferences, editor) migrateSetAppInfo(LauncherPreferences.apps().keys().favorites(), preferences, this)
migrateSetAppInfo(LauncherPreferences.apps().keys().hidden(), preferences, editor) migrateSetAppInfo(LauncherPreferences.apps().keys().hidden(), preferences, this)
migrateMapAppInfoString(LauncherPreferences.apps().keys().customNames(), preferences, editor) migrateMapAppInfoString(LauncherPreferences.apps().keys().customNames(), preferences, this)
}
editor.apply()
LauncherPreferences.internal().versionCode(4) LauncherPreferences.internal().versionCode(4)
} }

View file

@ -9,9 +9,6 @@ import android.util.DisplayMetrics
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.Window
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.window.OnBackInvokedDispatcher import android.window.OnBackInvokedDispatcher
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -20,6 +17,7 @@ import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.databinding.HomeBinding import de.jrpie.android.launcher.databinding.HomeBinding
import de.jrpie.android.launcher.openTutorial
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
import java.util.Locale import java.util.Locale
@ -58,7 +56,6 @@ class HomeActivity : UIObject, AppCompatActivity() {
super<AppCompatActivity>.onCreate(savedInstanceState) super<AppCompatActivity>.onCreate(savedInstanceState)
super<UIObject>.onCreate() super<UIObject>.onCreate()
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics) windowManager.defaultDisplay.getMetrics(displayMetrics)
@ -88,8 +85,6 @@ class HomeActivity : UIObject, AppCompatActivity() {
binding.buttonFallbackSettings.setOnClickListener { binding.buttonFallbackSettings.setOnClickListener {
LauncherAction.SETTINGS.invoke(this) LauncherAction.SETTINGS.invoke(this)
} }
} }
override fun onStart() { override fun onStart() {
@ -97,6 +92,11 @@ class HomeActivity : UIObject, AppCompatActivity() {
super<UIObject>.onStart() super<UIObject>.onStart()
// If the tutorial was not finished, start it
if (!LauncherPreferences.internal().started()) {
openTutorial(this)
}
LauncherPreferences.getSharedPreferences() LauncherPreferences.getSharedPreferences()
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener) .registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
@ -220,7 +220,8 @@ class HomeActivity : UIObject, AppCompatActivity() {
} }
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
return touchGestureDetector.onTouchEvent(event) || super.onTouchEvent(event) touchGestureDetector.onTouchEvent(event)
return true
} }
override fun setOnClicks() { override fun setOnClicks() {

View file

@ -1,6 +1,8 @@
package de.jrpie.android.launcher.ui package de.jrpie.android.launcher.ui
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ViewConfiguration import android.view.ViewConfiguration
import de.jrpie.android.launcher.actions.Gesture import de.jrpie.android.launcher.actions.Gesture
@ -27,6 +29,8 @@ class TouchGestureDetector(
private val MIN_TRIANGLE_HEIGHT = 250 private val MIN_TRIANGLE_HEIGHT = 250
private val longPressHandler = Handler(Looper.getMainLooper())
data class Vector(val x: Float, val y: Float) { data class Vector(val x: Float, val y: Float) {
fun absSquared(): Float { fun absSquared(): Float {
@ -83,16 +87,28 @@ class TouchGestureDetector(
} }
private var paths = HashMap<Int, PointerPath>() private var paths = HashMap<Int, PointerPath>()
private var gestureIsLongClick = false
private var lastTappedTime = 0L private var lastTappedTime = 0L
private var lastTappedLocation: Vector? = null private var lastTappedLocation: Vector? = null
fun onTouchEvent(event: MotionEvent): Boolean { fun onTouchEvent(event: MotionEvent) {
val pointerIdToIndex = val pointerIdToIndex =
(0..<event.pointerCount).associateBy { event.getPointerId(it) } (0..<event.pointerCount).associateBy { event.getPointerId(it) }
if (event.actionMasked == MotionEvent.ACTION_DOWN) { if (event.actionMasked == MotionEvent.ACTION_DOWN) {
paths = HashMap() synchronized(this@TouchGestureDetector) {
paths = HashMap()
gestureIsLongClick = false
}
longPressHandler.postDelayed({
synchronized(this@TouchGestureDetector) {
if (paths.entries.size == 1 && paths.entries.firstOrNull()?.value?.isTap() == true) {
gestureIsLongClick = true
Gesture.LONG_CLICK.invoke(context)
}
}
}, LONG_PRESS_TIMEOUT.toLong())
} }
// add new pointers // add new pointers
@ -122,9 +138,17 @@ class TouchGestureDetector(
} }
if (event.actionMasked == MotionEvent.ACTION_UP) { if (event.actionMasked == MotionEvent.ACTION_UP) {
synchronized(this@TouchGestureDetector) {
// if the long press handler is still running, kill it
longPressHandler.removeCallbacksAndMessages(null)
// if the gesture was already detected as a long click, there is nothing to do
if (gestureIsLongClick) {
return
}
}
classifyPaths(paths, event.downTime, event.eventTime) classifyPaths(paths, event.downTime, event.eventTime)
} }
return true return
} }
private fun getGestureForDirection(direction: Vector): Gesture? { private fun getGestureForDirection(direction: Vector): Gesture? {
@ -171,10 +195,6 @@ class TouchGestureDetector(
lastTappedTime = timeEnd lastTappedTime = timeEnd
lastTappedLocation = mainPointerPath.last 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 { } else {
// detect swipes // detect swipes

View file

@ -67,7 +67,7 @@ class AppsRecyclerAdapter(
override fun onClick(v: View) { override fun onClick(v: View) {
val rect = Rect() val rect = Rect()
img.getGlobalVisibleRect(rect) img.getGlobalVisibleRect(rect)
selectItem(adapterPosition, rect) selectItem(bindingAdapterPosition, rect)
} }
init { init {

View file

@ -22,6 +22,7 @@ import de.jrpie.android.launcher.apps.DetailedAppInfo
import de.jrpie.android.launcher.apps.PinnedShortcutInfo import de.jrpie.android.launcher.apps.PinnedShortcutInfo
import de.jrpie.android.launcher.getUserFromId import de.jrpie.android.launcher.getUserFromId
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import androidx.core.net.toUri
private const val LOG_TAG = "AppContextMenu" private const val LOG_TAG = "AppContextMenu"
@ -44,7 +45,7 @@ fun AbstractAppInfo.uninstall(activity: Activity) {
Log.i(LOG_TAG, "uninstalling $this") Log.i(LOG_TAG, "uninstalling $this")
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE) val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE)
intent.data = Uri.parse("package:$packageName") intent.data = "package:$packageName".toUri()
getUserFromId(userId, activity).let { user -> getUserFromId(userId, activity).let { user ->
intent.putExtra(Intent.EXTRA_USER, user) intent.putExtra(Intent.EXTRA_USER, user)
} }

View file

@ -33,7 +33,7 @@ class OtherRecyclerAdapter(val activity: Activity) :
override fun onClick(v: View) { override fun onClick(v: View) {
val pos = adapterPosition val pos = bindingAdapterPosition
val content = othersList[pos] val content = othersList[pos]
forGesture?.let { returnChoiceIntent(it, content) } forGesture?.let { returnChoiceIntent(it, content) }

View file

@ -16,6 +16,7 @@ import de.jrpie.android.launcher.copyToClipboard
import de.jrpie.android.launcher.databinding.SettingsMetaBinding import de.jrpie.android.launcher.databinding.SettingsMetaBinding
import de.jrpie.android.launcher.getDeviceInfo import de.jrpie.android.launcher.getDeviceInfo
import de.jrpie.android.launcher.openInBrowser import de.jrpie.android.launcher.openInBrowser
import de.jrpie.android.launcher.openTutorial
import de.jrpie.android.launcher.preferences.resetPreferences import de.jrpie.android.launcher.preferences.resetPreferences
import de.jrpie.android.launcher.ui.LegalInfoActivity import de.jrpie.android.launcher.ui.LegalInfoActivity
import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.UIObject
@ -48,7 +49,7 @@ class SettingsFragmentMeta : Fragment(), UIObject {
override fun setOnClicks() { override fun setOnClicks() {
binding.settingsMetaButtonViewTutorial.setOnClickListener { binding.settingsMetaButtonViewTutorial.setOnClickListener {
startActivity(Intent(this.context, TutorialActivity::class.java)) openTutorial(requireContext())
} }
// prompting for settings-reset confirmation // prompting for settings-reset confirmation

View file

@ -2,7 +2,9 @@ package de.jrpie.android.launcher.ui.tutorial
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.window.OnBackInvokedDispatcher
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
@ -33,6 +35,19 @@ class TutorialActivity : AppCompatActivity(), UIObject {
super<AppCompatActivity>.onCreate(savedInstanceState) super<AppCompatActivity>.onCreate(savedInstanceState)
super<UIObject>.onCreate() super<UIObject>.onCreate()
// Handle back key / gesture on Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_OVERLAY
) {
// prevent going back when the tutorial is shown for the first time
if (!LauncherPreferences.internal().started()) {
return@registerOnBackInvokedCallback
}
finish()
}
}
// Initialise layout // Initialise layout
setContentView(R.layout.tutorial) setContentView(R.layout.tutorial)
@ -60,7 +75,7 @@ class TutorialActivity : AppCompatActivity(), UIObject {
} }
} }
// Default: prevent going back, allow if viewed again later // prevent going back when the tutorial is shown for the first time
override fun onBackPressed() { override fun onBackPressed() {
if (LauncherPreferences.internal().started()) if (LauncherPreferences.internal().started())
super.onBackPressed() super.onBackPressed()