Compare commits

...

5 commits

Author SHA1 Message Date
55af392706
add donate button
Some checks are pending
Android CI / build (push) Waiting to run
2025-03-14 15:40:06 +01:00
077ee4381a
lint 2025-03-14 15:27:26 +01:00
e250a58ef4
add new action: adjust volume 2025-03-14 13:37:41 +01:00
c7af387a94
implement #98 - hide lock icon when 'hide private space when locked' setting is set 2025-03-14 04:11:47 +01:00
6cd17343fc
show questionmark when unkown app or shortcut is bound to gesture 2025-03-14 04:09:28 +01:00
22 changed files with 211 additions and 113 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

@ -13,6 +13,7 @@ import androidx.appcompat.content.res.AppCompatResources
import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.apps.AppFilter import de.jrpie.android.launcher.apps.AppFilter
import de.jrpie.android.launcher.apps.hidePrivateSpaceWhenLocked
import de.jrpie.android.launcher.apps.isPrivateSpaceSupported import de.jrpie.android.launcher.apps.isPrivateSpaceSupported
import de.jrpie.android.launcher.apps.togglePrivateSpaceLock import de.jrpie.android.launcher.apps.togglePrivateSpaceLock
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
@ -66,7 +67,11 @@ enum class LauncherAction(
R.string.list_other_list_private_space, R.string.list_other_list_private_space,
R.drawable.baseline_security_24, R.drawable.baseline_security_24,
{ context -> { context ->
if ((context.applicationContext as Application).privateSpaceLocked.value != true
|| !hidePrivateSpaceWhenLocked(context)
) {
openAppsList(context, private = true) openAppsList(context, private = true)
}
}, },
available = { _ -> available = { _ ->
isPrivateSpaceSupported() isPrivateSpaceSupported()
@ -83,31 +88,37 @@ enum class LauncherAction(
"volume_up", "volume_up",
R.string.list_other_volume_up, R.string.list_other_volume_up,
R.drawable.baseline_volume_up_24, R.drawable.baseline_volume_up_24,
{ context -> audioVolumeAdjust(context, true)} { context -> audioVolumeAdjust(context, AudioManager.ADJUST_RAISE) }
), ),
VOLUME_DOWN( VOLUME_DOWN(
"volume_down", "volume_down",
R.string.list_other_volume_down, R.string.list_other_volume_down,
R.drawable.baseline_volume_down_24, R.drawable.baseline_volume_down_24,
{ context -> audioVolumeAdjust(context, false)} { context -> audioVolumeAdjust(context, AudioManager.ADJUST_LOWER) }
),
VOLUME_ADJUST(
"volume_adjust",
R.string.list_other_volume_adjust,
R.drawable.baseline_volume_adjust_24,
{ context -> audioVolumeAdjust(context, AudioManager.ADJUST_SAME) }
), ),
TRACK_PLAY_PAUSE( TRACK_PLAY_PAUSE(
"play_pause_track", "play_pause_track",
R.string.list_other_track_play_pause, R.string.list_other_track_play_pause,
R.drawable.baseline_play_arrow_24, R.drawable.baseline_play_arrow_24,
{ context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)} { context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) }
), ),
TRACK_NEXT( TRACK_NEXT(
"next_track", "next_track",
R.string.list_other_track_next, R.string.list_other_track_next,
R.drawable.baseline_skip_next_24, R.drawable.baseline_skip_next_24,
{ context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_NEXT)} { context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_NEXT) }
), ),
TRACK_PREV( TRACK_PREV(
"previous_track", "previous_track",
R.string.list_other_track_previous, R.string.list_other_track_previous,
R.drawable.baseline_skip_previous_24, R.drawable.baseline_skip_previous_24,
{ context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PREVIOUS)} { context -> audioManagerPressKey(context, KeyEvent.KEYCODE_MEDIA_PREVIOUS) }
), ),
EXPAND_NOTIFICATIONS_PANEL( EXPAND_NOTIFICATIONS_PANEL(
"expand_notifications_panel", "expand_notifications_panel",
@ -176,17 +187,13 @@ private fun audioManagerPressKey(context: Context, key: Int) {
} }
private fun audioVolumeAdjust(context: Context, louder: Boolean) { private fun audioVolumeAdjust(context: Context, direction: Int) {
val audioManager = val audioManager =
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
audioManager.adjustStreamVolume( audioManager.adjustStreamVolume(
AudioManager.STREAM_MUSIC, AudioManager.STREAM_MUSIC,
if (louder) { direction,
AudioManager.ADJUST_RAISE
} else {
AudioManager.ADJUST_LOWER
},
AudioManager.FLAG_SHOW_UI AudioManager.FLAG_SHOW_UI
) )
} }

View file

@ -95,6 +95,12 @@ fun lockPrivateSpace(context: Context, lock: Boolean) {
if (!isPrivateSpaceSupported()) { if (!isPrivateSpaceSupported()) {
return return
} }
// silently return when trying to unlock but hide when locked is set
if (!lock && hidePrivateSpaceWhenLocked(context)) {
return
}
val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
val privateSpaceUser = getPrivateSpaceUser(context) ?: return val privateSpaceUser = getPrivateSpaceUser(context) ?: return
userManager.requestQuietModeEnabled(lock, privateSpaceUser) userManager.requestQuietModeEnabled(lock, privateSpaceUser)
@ -116,3 +122,11 @@ fun togglePrivateSpaceLock(context: Context) {
} }
} }
fun hidePrivateSpaceWhenLocked(context: Context): Boolean {
// TODO: perhaps this should be cached
// https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Launcher3/src/com/android/launcher3/util/SettingsCache.java;l=61;drc=56bf7ad33bc9d5ed3c18e7abefeec5c177ec75d7
val key = "hide_privatespace_entry_point"
return Settings.Secure.getInt(context.contentResolver, key, 0) == 1
}

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) {
synchronized(this@TouchGestureDetector) {
paths = HashMap() 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

@ -22,6 +22,7 @@ import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.REQUEST_UNINSTALL import de.jrpie.android.launcher.REQUEST_UNINSTALL
import de.jrpie.android.launcher.actions.LauncherAction import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.apps.AppFilter import de.jrpie.android.launcher.apps.AppFilter
import de.jrpie.android.launcher.apps.hidePrivateSpaceWhenLocked
import de.jrpie.android.launcher.apps.isPrivateSpaceLocked import de.jrpie.android.launcher.apps.isPrivateSpaceLocked
import de.jrpie.android.launcher.apps.isPrivateSpaceSetUp import de.jrpie.android.launcher.apps.isPrivateSpaceSetUp
import de.jrpie.android.launcher.apps.togglePrivateSpaceLock import de.jrpie.android.launcher.apps.togglePrivateSpaceLock
@ -34,10 +35,12 @@ import de.jrpie.android.launcher.ui.list.other.ListFragmentOther
// TODO: Better solution for this intercommunication functionality (used in list-fragments) // TODO: Better solution for this intercommunication functionality (used in list-fragments)
var intention = ListActivity.ListActivityIntention.VIEW var intention = ListActivity.ListActivityIntention.VIEW
var favoritesVisibility: AppFilter.Companion.AppSetVisibility = AppFilter.Companion.AppSetVisibility.VISIBLE var favoritesVisibility: AppFilter.Companion.AppSetVisibility =
AppFilter.Companion.AppSetVisibility.VISIBLE
var privateSpaceVisibility: AppFilter.Companion.AppSetVisibility = var privateSpaceVisibility: AppFilter.Companion.AppSetVisibility =
AppFilter.Companion.AppSetVisibility.VISIBLE AppFilter.Companion.AppSetVisibility.VISIBLE
var hiddenVisibility: AppFilter.Companion.AppSetVisibility = AppFilter.Companion.AppSetVisibility.HIDDEN var hiddenVisibility: AppFilter.Companion.AppSetVisibility =
AppFilter.Companion.AppSetVisibility.HIDDEN
var forGesture: String? = null var forGesture: String? = null
/** /**
@ -52,6 +55,23 @@ class ListActivity : AppCompatActivity(), UIObject {
private fun updateLockIcon(locked: Boolean) { private fun updateLockIcon(locked: Boolean) {
if (
// only show lock for VIEW intention
(intention != ListActivityIntention.VIEW)
// hide lock when private space does not exist
|| !isPrivateSpaceSetUp(this)
// hide lock when private space apps are hidden from the main list and we are not in the private space list
|| (LauncherPreferences.apps().hidePrivateSpaceApps()
&& privateSpaceVisibility != AppFilter.Companion.AppSetVisibility.EXCLUSIVE)
// hide lock when private space is locked and the hidden when locked setting is set
|| (locked && hidePrivateSpaceWhenLocked(this))
) {
binding.listLock.visibility = View.GONE
return
}
binding.listLock.visibility = View.VISIBLE
binding.listLock.setImageDrawable( binding.listLock.setImageDrawable(
AppCompatResources.getDrawable( AppCompatResources.getDrawable(
this, this,
@ -74,7 +94,6 @@ class ListActivity : AppCompatActivity(), UIObject {
} }
enum class ListActivityIntention(val titleResource: Int) { enum class ListActivityIntention(val titleResource: Int) {
VIEW(R.string.list_title_view), /* view list of apps */ 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 */ PICK(R.string.list_title_pick) /* choose app or action to associate to a gesture */
@ -119,20 +138,6 @@ class ListActivity : AppCompatActivity(), UIObject {
LauncherAction.SETTINGS.launch(this@ListActivity) 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) { if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
isPrivateSpaceSetUp(this, showToast = true, launchSettings = true) isPrivateSpaceSetUp(this, showToast = true, launchSettings = true)
@ -200,7 +205,8 @@ class ListActivity : AppCompatActivity(), UIObject {
fun updateTitle() { fun updateTitle() {
var titleResource = intention.titleResource var titleResource = intention.titleResource
if (intention == ListActivityIntention.VIEW) { if (intention == ListActivityIntention.VIEW) {
titleResource = if (hiddenVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { titleResource =
if (hiddenVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
R.string.list_title_hidden R.string.list_title_hidden
} else if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) { } else if (privateSpaceVisibility == AppFilter.Companion.AppSetVisibility.EXCLUSIVE) {
R.string.list_title_private_space R.string.list_title_private_space

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

@ -11,6 +11,7 @@ import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -94,6 +95,8 @@ class SettingsFragmentActionsRecycler : Fragment(), UIObject {
class ActionsRecyclerAdapter(val activity: Activity) : class ActionsRecyclerAdapter(val activity: Activity) :
RecyclerView.Adapter<ActionsRecyclerAdapter.ViewHolder>() { RecyclerView.Adapter<ActionsRecyclerAdapter.ViewHolder>() {
private val drawableUnknown = AppCompatResources.getDrawable(activity, R.drawable.baseline_question_mark_24)
private val gesturesList: ArrayList<Gesture> = private val gesturesList: ArrayList<Gesture> =
Gesture.entries.filter(Gesture::isEnabled) as ArrayList<Gesture> Gesture.entries.filter(Gesture::isEnabled) as ArrayList<Gesture>
@ -115,15 +118,18 @@ class ActionsRecyclerAdapter(val activity: Activity) :
private fun updateViewHolder(gesture: Gesture, viewHolder: ViewHolder) { private fun updateViewHolder(gesture: Gesture, viewHolder: ViewHolder) {
val action = Action.forGesture(gesture) val action = Action.forGesture(gesture)
val drawable = action?.getIcon(activity)
if (action == null || drawable == null) { if (action == null) {
viewHolder.img.visibility = View.INVISIBLE viewHolder.img.visibility = View.INVISIBLE
viewHolder.removeAction.visibility = View.GONE viewHolder.removeAction.visibility = View.GONE
viewHolder.chooseButton.visibility = View.VISIBLE viewHolder.chooseButton.visibility = View.VISIBLE
return return
} }
// Use the unknown icon if there is an action, but we can't find its icon.
// Probably an app was uninstalled.
val drawable = action.getIcon(activity) ?: drawableUnknown
viewHolder.img.visibility = View.VISIBLE viewHolder.img.visibility = View.VISIBLE
viewHolder.removeAction.visibility = View.VISIBLE viewHolder.removeAction.visibility = View.VISIBLE
viewHolder.chooseButton.visibility = View.INVISIBLE viewHolder.chooseButton.visibility = View.INVISIBLE

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
@ -47,8 +48,17 @@ class SettingsFragmentMeta : Fragment(), UIObject {
override fun setOnClicks() { override fun setOnClicks() {
fun bindURL(view: View, urlRes: Int) {
view.setOnClickListener {
openInBrowser(
getString(urlRes),
requireContext()
)
}
}
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
@ -69,12 +79,7 @@ class SettingsFragmentMeta : Fragment(), UIObject {
// view code // view code
binding.settingsMetaButtonViewCode.setOnClickListener { bindURL(binding.settingsMetaButtonViewCode, R.string.settings_meta_link_github)
openInBrowser(
getString(R.string.settings_meta_link_github),
requireContext()
)
}
// report a bug // report a bug
binding.settingsMetaButtonReportBug.setOnClickListener { binding.settingsMetaButtonReportBug.setOnClickListener {
@ -110,37 +115,19 @@ class SettingsFragmentMeta : Fragment(), UIObject {
} }
// join chat // join chat
binding.settingsMetaButtonJoinChat.setOnClickListener { bindURL(binding.settingsMetaButtonJoinChat, R.string.settings_meta_chat_url)
openInBrowser(
getString(R.string.settings_meta_chat_url),
requireContext()
)
}
// contact developer // contact developer
binding.settingsMetaButtonContact.setOnClickListener { // bindURL(binding.settingsMetaButtonContact, R.string.settings_meta_contact_url)
openInBrowser(
getString(R.string.settings_meta_contact_url),
requireContext()
)
}
// contact fork developer // contact fork developer
binding.settingsMetaButtonForkContact.setOnClickListener { bindURL(binding.settingsMetaButtonForkContact, R.string.settings_meta_fork_contact_url)
openInBrowser(
getString(R.string.settings_meta_fork_contact_url), // donate
requireContext() bindURL(binding.settingsMetaButtonDonate, R.string.settings_meta_donate_url)
)
}
// privacy policy // privacy policy
binding.settingsMetaButtonPrivacy.setOnClickListener { bindURL(binding.settingsMetaButtonPrivacy, R.string.settings_meta_privacy_url)
openInBrowser(
getString(R.string.settings_meta_privacy_url),
requireContext()
)
}
// legal info // legal info
binding.settingsMetaButtonLicenses.setOnClickListener { binding.settingsMetaButtonLicenses.setOnClickListener {

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()

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11.07,12.85c0.77,-1.39 2.25,-2.21 3.11,-3.44c0.91,-1.29 0.4,-3.7 -2.18,-3.7c-1.69,0 -2.52,1.28 -2.87,2.34L6.54,6.96C7.25,4.83 9.18,3 11.99,3c2.35,0 3.96,1.07 4.78,2.41c0.7,1.15 1.11,3.3 0.03,4.9c-1.2,1.77 -2.35,2.31 -2.97,3.45c-0.25,0.46 -0.35,0.76 -0.35,2.24h-2.89C10.58,15.22 10.46,13.95 11.07,12.85zM14,20c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2S14,18.9 14,20z" />
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="m 3,9 v 6 h 4 l 5,5 V 4 L 7,9 Z m 13.5,3 C 16.5,10.23 15.48,8.71 14,7.97 v 8.05 c 1.48,-0.73 2.5,-2.25 2.5,-4.02 z" />
<path
android:fillAlpha="0.5"
android:fillColor="?android:textColor"
android:pathData="m 14,3.23 v 2.06 c 2.89,0.86 5,3.54 5,6.71 0,3.17 -2.11,5.85 -5,6.71 v 2.06 C 18.01,19.86 21,16.28 21,12 21,7.72 18.01,4.14 14,3.23 Z"
android:strokeAlpha="0.5" />
</vector>

View file

@ -59,12 +59,12 @@
android:text="@string/settings_meta_join_chat" android:text="@string/settings_meta_join_chat"
android:textAllCaps="false" /> android:textAllCaps="false" />
<Button <!--<Button
android:id="@+id/settings_meta_button_contact" android:id="@+id/settings_meta_button_contact"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/settings_meta_contact" android:text="@string/settings_meta_contact"
android:textAllCaps="false" /> android:textAllCaps="false" />-->
<Button <Button
android:id="@+id/settings_meta_button_fork_contact" android:id="@+id/settings_meta_button_fork_contact"
@ -73,6 +73,13 @@
android:text="@string/settings_meta_fork_contact" android:text="@string/settings_meta_fork_contact"
android:textAllCaps="false" /> android:textAllCaps="false" />
<Button
android:id="@+id/settings_meta_button_donate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_meta_donate"
android:textAllCaps="false" />
<Space <Space
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="64sp" /> android:layout_height="64sp" />

View file

@ -129,6 +129,7 @@
<string name="dialog_report_bug_create_report">Report erstellen</string> <string name="dialog_report_bug_create_report">Report erstellen</string>
<string name="settings_meta_fork_contact">Entwickler des Fork kontaktieren</string> <string name="settings_meta_fork_contact">Entwickler des Fork kontaktieren</string>
<string name="settings_meta_join_chat">Dem µLauncher-Chat beitreten</string> <string name="settings_meta_join_chat">Dem µLauncher-Chat beitreten</string>
<string name="settings_meta_donate">Spenden</string>
<string name="settings_meta_privacy">Datenschutzerklärung</string> <string name="settings_meta_privacy">Datenschutzerklärung</string>
<!-- <!--
- -
@ -152,8 +153,9 @@
<string name="list_other_list">Alle Anwendungen</string> <string name="list_other_list">Alle Anwendungen</string>
<string name="list_other_list_favorites">Favoriten</string> <string name="list_other_list_favorites">Favoriten</string>
<string name="list_other_toggle_private_space_lock">Privaten Bereich (ent)sperren</string> <string name="list_other_toggle_private_space_lock">Privaten Bereich (ent)sperren</string>
<string name="list_other_volume_up">Musik: Lauter</string> <string name="list_other_volume_up">Lauter</string>
<string name="list_other_volume_down">Musik: Leiser</string> <string name="list_other_volume_down">Leiser</string>
<string name="list_other_volume_adjust">Lautstärke ändern</string>
<string name="list_other_track_next">Musik: Weiter</string> <string name="list_other_track_next">Musik: Weiter</string>
<string name="list_other_track_previous">Musik: Zurück</string> <string name="list_other_track_previous">Musik: Zurück</string>
<string name="list_other_nop">Nichts tun</string> <string name="list_other_nop">Nichts tun</string>

View file

@ -164,6 +164,7 @@
<string name="settings_meta_privacy_url" translatable="false">https://s.jrpie.de/android-legal</string> <string name="settings_meta_privacy_url" translatable="false">https://s.jrpie.de/android-legal</string>
<string name="settings_meta_contact_url" translatable="false">https://www.finnmglas.com/contact/</string> <string name="settings_meta_contact_url" translatable="false">https://www.finnmglas.com/contact/</string>
<string name="settings_meta_chat_url" translatable="false">https://s.jrpie.de/launcher-chat</string> <string name="settings_meta_chat_url" translatable="false">https://s.jrpie.de/launcher-chat</string>
<string name="settings_meta_donate_url" translatable="false">https://s.jrpie.de/launcher-donate</string>
<!-- <!--
- -

View file

@ -210,6 +210,7 @@
<string name="settings_meta_fork_contact">Contact the developer of the fork</string> <string name="settings_meta_fork_contact">Contact the developer of the fork</string>
<string name="settings_meta_join_chat">Join µLauncher chat</string> <string name="settings_meta_join_chat">Join µLauncher chat</string>
<string name="settings_meta_donate">Donate</string>
<string name="settings_meta_privacy">Privacy Policy</string> <string name="settings_meta_privacy">Privacy Policy</string>
@ -250,8 +251,9 @@
<string name="list_other_list_favorites">Favorite Applications</string> <string name="list_other_list_favorites">Favorite Applications</string>
<string name="list_other_list_private_space">Private Space</string> <string name="list_other_list_private_space">Private Space</string>
<string name="list_other_toggle_private_space_lock">Toggle Private Space Lock</string> <string name="list_other_toggle_private_space_lock">Toggle Private Space Lock</string>
<string name="list_other_volume_up">Music: Louder</string> <string name="list_other_volume_up">Raise Volume</string>
<string name="list_other_volume_down">Music: Quieter</string> <string name="list_other_volume_down">Lower Volume</string>
<string name="list_other_volume_adjust">Adjust Volume</string>
<string name="list_other_track_next">Music: Next</string> <string name="list_other_track_next">Music: Next</string>
<string name="list_other_track_previous">Music: Previous</string> <string name="list_other_track_previous">Music: Previous</string>
<string name="list_other_track_play_pause">Music: Play / Pause</string> <string name="list_other_track_play_pause">Music: Play / Pause</string>