Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@
</intent-filter>
</activity>

<activity
android:name=".CardShortcutActivity"
android:exported="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:taskAffinity=""
android:launchMode="singleInstance">
</activity>

<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui" />
Expand Down
113 changes: 113 additions & 0 deletions app/src/main/java/de/pawcode/cardstore/CardShortcutActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package de.pawcode.cardstore

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import de.pawcode.cardstore.data.database.entities.CardEntity
import de.pawcode.cardstore.data.database.repositories.CardRepository
import de.pawcode.cardstore.data.managers.PreferencesManager
import de.pawcode.cardstore.data.managers.ShortcutManager
import de.pawcode.cardstore.data.services.BiometricAuthService
import de.pawcode.cardstore.ui.components.BiometricPlaceholder
import de.pawcode.cardstore.ui.sheets.ViewCardSheet
import de.pawcode.cardstore.ui.theme.CardStoreTheme
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch

class CardShortcutActivity : FragmentActivity() {
private var cardRepository: CardRepository? = null
private var preferencesManager: PreferencesManager? = null
private var card by mutableStateOf<CardEntity?>(null)
private var isAuthenticated by mutableStateOf(false)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

preferencesManager = PreferencesManager(applicationContext)
cardRepository = CardRepository(applicationContext)

val cardId = intent.getStringExtra(ShortcutManager.EXTRA_CARD_ID)

if (cardId == null) {
finish()
return
}

lifecycleScope.launch {
val cardWithLabels = cardRepository?.getCardById(cardId)?.first()
card = cardWithLabels?.card
if (card == null) {
finish()
return@launch
}

// Increment usage count
card?.let { currentCard ->
val updatedCard =
currentCard.copy(useCount = currentCard.useCount + 1, lastUsed = System.currentTimeMillis())
cardRepository?.updateCard(updatedCard)
}
}

checkAuthentication()

setContent {
CardStoreTheme {
Box(
modifier =
Modifier.fillMaxSize()
.background(Color.Black.copy(alpha = 0.5f))
.pointerInput(Unit) { detectTapGestures(onTap = { finish() }) },
contentAlignment = Alignment.Center,
) {
Box(
modifier =
Modifier.padding(16.dp).pointerInput(Unit) {
detectTapGestures(onTap = {
// Prevent tap from propagating to parent
})
}
) {
if (isAuthenticated) {
card?.let { ViewCardSheet(it) }
} else {
BiometricPlaceholder(onRetry = { checkAuthentication() })
}
}
}
}
}
}

private fun checkAuthentication() {
lifecycleScope.launch {
val biometricEnabled = preferencesManager?.biometricEnabled?.first() ?: false
if (biometricEnabled && BiometricAuthService.isBiometricAvailable(this@CardShortcutActivity)) {
BiometricAuthService.authenticate(
activity = this@CardShortcutActivity,
title = getString(R.string.biometric_auth_title),
subtitle = getString(R.string.biometric_auth_subtitle),
onSuccess = { isAuthenticated = true },
onError = { finish() },
)
} else {
isAuthenticated = true
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package de.pawcode.cardstore.data.managers

import android.content.Context
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import de.pawcode.cardstore.CardShortcutActivity
import de.pawcode.cardstore.R
import de.pawcode.cardstore.data.database.entities.CardEntity
import de.pawcode.cardstore.data.enums.SortAttribute
import de.pawcode.cardstore.utils.calculateCardScore

object ShortcutManager {
private const val MAX_SHORTCUTS = 4
const val EXTRA_CARD_ID = "card_id"

fun updateShortcuts(context: Context, cards: List<CardEntity>, sortAttribute: SortAttribute) {
val topCards = getTopCards(cards, sortAttribute).take(MAX_SHORTCUTS)

val shortcuts =
topCards.mapIndexed { index, card ->
val intent =
Intent(context, CardShortcutActivity::class.java).apply {
action = Intent.ACTION_VIEW
putExtra(EXTRA_CARD_ID, card.cardId)
}

ShortcutInfoCompat.Builder(context, "card_${card.cardId}")
.setShortLabel(card.storeName)
.setLongLabel(card.storeName)
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher))
.setIntent(intent)
.setRank(index)
.build()
}

ShortcutManagerCompat.removeAllDynamicShortcuts(context)
ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts)
}

fun clearShortcuts(context: Context) {
ShortcutManagerCompat.removeAllDynamicShortcuts(context)
}

private fun getTopCards(cards: List<CardEntity>, sortAttribute: SortAttribute): List<CardEntity> {
return when (sortAttribute) {
SortAttribute.INTELLIGENT -> cards.sortedByDescending { calculateCardScore(it) }
SortAttribute.ALPHABETICALLY -> cards.sortedBy { it.storeName }
SortAttribute.RECENTLY_USED -> cards.sortedByDescending { it.lastUsed }
SortAttribute.MOST_USED -> cards.sortedByDescending { it.useCount }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import de.pawcode.cardstore.data.database.entities.EXAMPLE_LABEL_LIST
import de.pawcode.cardstore.data.database.entities.LabelEntity
import de.pawcode.cardstore.data.enums.SortAttribute
import de.pawcode.cardstore.data.managers.PreferencesManager
import de.pawcode.cardstore.data.managers.ShortcutManager
import de.pawcode.cardstore.data.services.DeeplinkService
import de.pawcode.cardstore.data.services.ReviewService
import de.pawcode.cardstore.data.services.SnackbarService
Expand Down Expand Up @@ -137,6 +138,7 @@ fun CardListScreenComponent(
onSortChange: (SortAttribute) -> Unit,
onShowAbout: () -> Unit,
) {
val context = LocalContext.current
val cards by cardsFlow.collectAsState(initial = emptyList())
val labels by labelsFlow.collectAsState(initial = emptyList())
val sortBy by sortByFlow.collectAsState(initial = null)
Expand Down Expand Up @@ -183,6 +185,14 @@ fun CardListScreenComponent(

LaunchedEffect(sortBy, selectedLabel) { listState.scrollToItem(0) }

// Update app shortcuts when cards or sort order changes
val currentContext = context
LaunchedEffect(cardsFiltered, sortBy) {
sortBy?.let { currentSortBy ->
ShortcutManager.updateShortcuts(currentContext, cardsFiltered, currentSortBy)
}
}

Scaffold(
topBar = {
AppBar(
Expand Down