mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-08-29 13:27:46 +00:00
Refactor app shortcut handling (#1392)
This commit is contained in:
parent
53c3431ef0
commit
6ff01f5e1e
@ -8,11 +8,6 @@ import android.Manifest
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ShortcutInfo
|
|
||||||
import android.content.pm.ShortcutInfo.Builder
|
|
||||||
import android.content.pm.ShortcutManager
|
|
||||||
import android.graphics.drawable.Icon
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@ -21,11 +16,9 @@ import android.view.MenuItem.OnActionExpandListener
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.appcompat.widget.SearchView.OnQueryTextListener
|
import androidx.appcompat.widget.SearchView.OnQueryTextListener
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
@ -40,6 +33,7 @@ import com.github.michaelbull.result.onFailure
|
|||||||
import com.github.michaelbull.result.runCatching
|
import com.github.michaelbull.result.runCatching
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.msfjarvis.aps.R
|
import dev.msfjarvis.aps.R
|
||||||
import dev.msfjarvis.aps.data.password.PasswordItem
|
import dev.msfjarvis.aps.data.password.PasswordItem
|
||||||
import dev.msfjarvis.aps.data.repo.PasswordRepository
|
import dev.msfjarvis.aps.data.repo.PasswordRepository
|
||||||
@ -67,9 +61,11 @@ import dev.msfjarvis.aps.util.extensions.sharedPrefs
|
|||||||
import dev.msfjarvis.aps.util.settings.AuthMode
|
import dev.msfjarvis.aps.util.settings.AuthMode
|
||||||
import dev.msfjarvis.aps.util.settings.GitSettings
|
import dev.msfjarvis.aps.util.settings.GitSettings
|
||||||
import dev.msfjarvis.aps.util.settings.PreferenceKeys
|
import dev.msfjarvis.aps.util.settings.PreferenceKeys
|
||||||
|
import dev.msfjarvis.aps.util.shortcuts.ShortcutHandler
|
||||||
import dev.msfjarvis.aps.util.viewmodel.SearchableRepositoryViewModel
|
import dev.msfjarvis.aps.util.viewmodel.SearchableRepositoryViewModel
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.Character.UnicodeBlock
|
import java.lang.Character.UnicodeBlock
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -77,8 +73,10 @@ import org.eclipse.jgit.api.Git
|
|||||||
|
|
||||||
const val PASSWORD_FRAGMENT_TAG = "PasswordsList"
|
const val PASSWORD_FRAGMENT_TAG = "PasswordsList"
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class PasswordStore : BaseGitActivity() {
|
class PasswordStore : BaseGitActivity() {
|
||||||
|
|
||||||
|
@Inject lateinit var shortcutHandler: ShortcutHandler
|
||||||
private lateinit var searchItem: MenuItem
|
private lateinit var searchItem: MenuItem
|
||||||
private val settings by lazy { sharedPrefs }
|
private val settings by lazy { sharedPrefs }
|
||||||
|
|
||||||
@ -440,50 +438,7 @@ class PasswordStore : BaseGitActivity() {
|
|||||||
startActivity(decryptIntent)
|
startActivity(decryptIntent)
|
||||||
|
|
||||||
// Adds shortcut
|
// Adds shortcut
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
shortcutHandler.addDynamicShortcut(item, authDecryptIntent)
|
||||||
addShortcut(item, authDecryptIntent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
|
||||||
private fun addShortcut(item: PasswordItem, intent: Intent) {
|
|
||||||
val shortcutManager: ShortcutManager = getSystemService() ?: return
|
|
||||||
val shortcut =
|
|
||||||
Builder(this, item.fullPathToParent)
|
|
||||||
.setShortLabel(item.toString())
|
|
||||||
.setLongLabel(item.fullPathToParent + item.toString())
|
|
||||||
.setIcon(Icon.createWithResource(this, R.drawable.ic_lock_open_24px))
|
|
||||||
.setIntent(intent)
|
|
||||||
.build()
|
|
||||||
val shortcuts = shortcutManager.dynamicShortcuts
|
|
||||||
// If we're above or equal to the maximum shortcuts allowed, drop the last item.
|
|
||||||
if (shortcuts.size >= MAX_SHORTCUT_COUNT) {
|
|
||||||
shortcuts.removeLast()
|
|
||||||
}
|
|
||||||
// Reverse the list so we can append our new shortcut at the 'end'.
|
|
||||||
shortcuts.reverse()
|
|
||||||
shortcuts.add(shortcut)
|
|
||||||
// Reverse it again, so the previous items are now in the correct order and our new item
|
|
||||||
// is at the front like it's supposed to.
|
|
||||||
shortcuts.reverse()
|
|
||||||
// Write back the new shortcuts.
|
|
||||||
shortcutManager.dynamicShortcuts = shortcuts.map(::rebuildShortcut)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes an existing [ShortcutInfo] and builds a fresh instance of [ShortcutInfo] with the same
|
|
||||||
* data, which ensures that the get/set dance in [addShortcut] does not cause invalidation of icon
|
|
||||||
* assets, resulting in invisible icons in all but the newest launcher shortcut.
|
|
||||||
*/
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
|
||||||
private fun rebuildShortcut(shortcut: ShortcutInfo): ShortcutInfo {
|
|
||||||
// Non-null assertions are fine since we know these values aren't null.
|
|
||||||
return Builder(this@PasswordStore, shortcut.id)
|
|
||||||
.setShortLabel(shortcut.shortLabel!!)
|
|
||||||
.setLongLabel(shortcut.longLabel!!)
|
|
||||||
.setIcon(Icon.createWithResource(this@PasswordStore, R.drawable.ic_lock_open_24px))
|
|
||||||
.setIntent(shortcut.intent!!)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateState(): Boolean {
|
private fun validateState(): Boolean {
|
||||||
@ -693,10 +648,6 @@ class PasswordStore : BaseGitActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
// The max shortcut count from the system is set to 15 for some godforsaken reason, which
|
|
||||||
// makes zero sense and is why our update logic just never worked. Capping it at 4 which is
|
|
||||||
// what most launchers seem to have agreed upon is the only reasonable solution.
|
|
||||||
private const val MAX_SHORTCUT_COUNT = 4
|
|
||||||
const val REQUEST_ARG_PATH = "PATH"
|
const val REQUEST_ARG_PATH = "PATH"
|
||||||
private fun isPrintable(c: Char): Boolean {
|
private fun isPrintable(c: Char): Boolean {
|
||||||
val block = UnicodeBlock.of(c)
|
val block = UnicodeBlock.of(c)
|
||||||
|
@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.msfjarvis.aps.util.shortcuts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ShortcutInfo
|
||||||
|
import android.content.pm.ShortcutManager
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import com.github.ajalt.timberkt.d
|
||||||
|
import dagger.Reusable
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dev.msfjarvis.aps.R
|
||||||
|
import dev.msfjarvis.aps.data.password.PasswordItem
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Reusable
|
||||||
|
class ShortcutHandler
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@ApplicationContext val context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
// The max shortcut count from the system is set to 15 for some godforsaken reason, which
|
||||||
|
// makes zero sense and is why our update logic just never worked. Capping it at 4 which is
|
||||||
|
// what most launchers seem to have agreed upon is the only reasonable solution.
|
||||||
|
private const val MAX_SHORTCUT_COUNT = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a
|
||||||
|
* [dynamic shortcut](https://developer.android.com/guide/topics/ui/shortcuts/creating-shortcuts#dynamic)
|
||||||
|
* that shows up with the app icon on long press. The list of items is capped to
|
||||||
|
* [MAX_SHORTCUT_COUNT] and older items are removed by a simple LRU sweep.
|
||||||
|
*/
|
||||||
|
fun addDynamicShortcut(item: PasswordItem, intent: Intent) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return
|
||||||
|
val shortcutManager: ShortcutManager = context.getSystemService() ?: return
|
||||||
|
val shortcut = buildShortcut(item, intent)
|
||||||
|
val shortcuts = shortcutManager.dynamicShortcuts
|
||||||
|
// If we're above or equal to the maximum shortcuts allowed, drop the last item.
|
||||||
|
if (shortcuts.size >= MAX_SHORTCUT_COUNT) {
|
||||||
|
shortcuts.removeLast()
|
||||||
|
}
|
||||||
|
// Reverse the list so we can append our new shortcut at the 'end'.
|
||||||
|
shortcuts.reverse()
|
||||||
|
shortcuts.add(shortcut)
|
||||||
|
// Reverse it again, so the previous items are now in the correct order and our new item
|
||||||
|
// is at the front like it's supposed to.
|
||||||
|
shortcuts.reverse()
|
||||||
|
// Write back the new shortcuts.
|
||||||
|
shortcutManager.dynamicShortcuts = shortcuts.map(::rebuildShortcut)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a
|
||||||
|
* [pinned shortcut](https://developer.android.com/guide/topics/ui/shortcuts/creating-shortcuts#pinned)
|
||||||
|
* which presents a UI to users, allowing manual placement on the launcher screen. This method is
|
||||||
|
* a no-op if the user's default launcher does not support pinned shortcuts.
|
||||||
|
*/
|
||||||
|
fun addPinnedShortcut(item: PasswordItem, intent: Intent) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||||
|
val shortcutManager: ShortcutManager = context.getSystemService() ?: return
|
||||||
|
if (!shortcutManager.isRequestPinShortcutSupported) {
|
||||||
|
d { "addPinnedShortcut: pin shortcuts unsupported" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val shortcut = buildShortcut(item, intent)
|
||||||
|
shortcutManager.requestPinShortcut(shortcut, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a [ShortcutInfo] from [item] and assigns [intent] to it. */
|
||||||
|
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private fun buildShortcut(item: PasswordItem, intent: Intent): ShortcutInfo {
|
||||||
|
return ShortcutInfo.Builder(context, item.fullPathToParent)
|
||||||
|
.setShortLabel(item.toString())
|
||||||
|
.setLongLabel(item.fullPathToParent + item.toString())
|
||||||
|
.setIcon(Icon.createWithResource(context, R.drawable.ic_lock_open_24px))
|
||||||
|
.setIntent(intent)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes an existing [ShortcutInfo] and builds a fresh instance of [ShortcutInfo] with the same
|
||||||
|
* data, which ensures that the get/set dance in [addDynamicShortcut] does not cause invalidation
|
||||||
|
* of icon assets, resulting in invisible icons in all but the newest launcher shortcut.
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private fun rebuildShortcut(shortcut: ShortcutInfo): ShortcutInfo {
|
||||||
|
// Non-null assertions are fine since we know these values aren't null.
|
||||||
|
return ShortcutInfo.Builder(context, shortcut.id)
|
||||||
|
.setShortLabel(shortcut.shortLabel!!)
|
||||||
|
.setLongLabel(shortcut.longLabel!!)
|
||||||
|
.setIcon(Icon.createWithResource(context, R.drawable.ic_lock_open_24px))
|
||||||
|
.setIntent(shortcut.intent!!)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user