Refactor app shortcut handling (#1392)
This commit is contained in:
parent
53c3431ef0
commit
6ff01f5e1e
2 changed files with 112 additions and 55 deletions
|
@ -8,11 +8,6 @@ import android.Manifest
|
|||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
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.view.KeyEvent
|
||||
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.StartActivityForResult
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.SearchView.OnQueryTextListener
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
|
@ -40,6 +33,7 @@ import com.github.michaelbull.result.onFailure
|
|||
import com.github.michaelbull.result.runCatching
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.msfjarvis.aps.R
|
||||
import dev.msfjarvis.aps.data.password.PasswordItem
|
||||
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.GitSettings
|
||||
import dev.msfjarvis.aps.util.settings.PreferenceKeys
|
||||
import dev.msfjarvis.aps.util.shortcuts.ShortcutHandler
|
||||
import dev.msfjarvis.aps.util.viewmodel.SearchableRepositoryViewModel
|
||||
import java.io.File
|
||||
import java.lang.Character.UnicodeBlock
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -77,8 +73,10 @@ import org.eclipse.jgit.api.Git
|
|||
|
||||
const val PASSWORD_FRAGMENT_TAG = "PasswordsList"
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PasswordStore : BaseGitActivity() {
|
||||
|
||||
@Inject lateinit var shortcutHandler: ShortcutHandler
|
||||
private lateinit var searchItem: MenuItem
|
||||
private val settings by lazy { sharedPrefs }
|
||||
|
||||
|
@ -440,50 +438,7 @@ class PasswordStore : BaseGitActivity() {
|
|||
startActivity(decryptIntent)
|
||||
|
||||
// Adds shortcut
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
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()
|
||||
shortcutHandler.addDynamicShortcut(item, authDecryptIntent)
|
||||
}
|
||||
|
||||
private fun validateState(): Boolean {
|
||||
|
@ -693,10 +648,6 @@ class PasswordStore : BaseGitActivity() {
|
|||
|
||||
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"
|
||||
private fun isPrintable(c: Char): Boolean {
|
||||
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…
Reference in a new issue