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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_select_gesture.xml b/app/src/main/res/layout/dialog_select_gesture.xml
new file mode 100644
index 0000000..e8f2a19
--- /dev/null
+++ b/app/src/main/res/layout/dialog_select_gesture.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_select_gesture_row.xml b/app/src/main/res/layout/dialog_select_gesture_row.xml
new file mode 100644
index 0000000..863df99
--- /dev/null
+++ b/app/src/main/res/layout/dialog_select_gesture_row.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b61c1b0..5e2ed75 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -250,6 +250,11 @@
Lock Screen
Toggle Torch
+
+ Add Shortcut
+ Bind to gesture
+ Show in app list
+