From 6ff01f5e1ee90c6203bc9bd0189eea33c42d6db0 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Wed, 21 Apr 2021 18:07:35 +0530 Subject: Refactor app shortcut handling (#1392) --- .../msfjarvis/aps/ui/passwords/PasswordStore.kt | 61 ++---------- .../aps/util/shortcuts/ShortcutHandler.kt | 106 +++++++++++++++++++++ 2 files changed, 112 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/dev/msfjarvis/aps/util/shortcuts/ShortcutHandler.kt diff --git a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt b/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt index 8717d199..3339fc04 100644 --- a/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt +++ b/app/src/main/java/dev/msfjarvis/aps/ui/passwords/PasswordStore.kt @@ -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) diff --git a/app/src/main/java/dev/msfjarvis/aps/util/shortcuts/ShortcutHandler.kt b/app/src/main/java/dev/msfjarvis/aps/util/shortcuts/ShortcutHandler.kt new file mode 100644 index 00000000..8e36c9af --- /dev/null +++ b/app/src/main/java/dev/msfjarvis/aps/util/shortcuts/ShortcutHandler.kt @@ -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() + } +} -- cgit v1.2.3