Refactor app shortcut handling (#1392)

This commit is contained in:
Harsh Shandilya 2021-04-21 18:07:35 +05:30 committed by GitHub
parent 53c3431ef0
commit 6ff01f5e1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 112 additions and 55 deletions

View File

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

View File

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