Skip to content
Merged
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
21 changes: 20 additions & 1 deletion app/src/main/java/com/getcode/Session.kt
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ data class SessionState(
UiElement.BALANCE
),
val tipCardConnected: Boolean = false,
val fullScreenLoading: Boolean = false,
)

sealed interface SessionEvent {
Expand Down Expand Up @@ -1463,16 +1464,34 @@ class Session @Inject constructor(
fun onImageSelected(
uri: Uri
) {
codeAnalyzer.onCodeScanned = { onCodeScan(it) }
var scanning = false
codeAnalyzer.onCodeScanned = {
scanning = false

uiFlow.update { state -> state.copy(fullScreenLoading = false) }
onCodeScan(it)
}
codeAnalyzer.onNoCodeFound = {
scanning = false
uiFlow.update { state -> state.copy(fullScreenLoading = false) }

TopBarManager.showMessage(
TopBarManager.TopBarMessage(
title = resources.getString(R.string.title_noCodeFound),
message = resources.getString(R.string.subtitle_noCodeFound)
)
)
}

codeAnalyzer.analyze(uri)
scanning = true

viewModelScope.launch {
delay(300)
if (scanning) {
uiFlow.update { it.copy(fullScreenLoading = true) }
}
}
}

fun onCodeScan(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.getcode.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.getcode.theme.CodeTheme
import com.getcode.ui.utils.swallowClicks

@Composable
fun FullScreenProgressSpinner(isLoading: Boolean, modifier: Modifier = Modifier) {
if (isLoading) {
Box(
modifier = modifier
.fillMaxSize()
.background(CodeTheme.colors.surface.copy(alpha = 0.32f))
.swallowClicks()
) {
CodeCircularProgressIndicator(
modifier = Modifier
.size(100.dp)
.align(Alignment.Center)
)
}
}
}
37 changes: 37 additions & 0 deletions app/src/main/java/com/getcode/ui/utils/KeepScreenOn.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.getcode.ui.utils

import android.view.WindowManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver

@Composable
fun KeepScreenOn(isEnabled: Boolean) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val window = (context as? androidx.activity.ComponentActivity)?.window

DisposableEffect(lifecycleOwner, isEnabled) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
if (isEnabled) {
// Keep screen on
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
// Allow screen to turn off
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
}

lifecycleOwner.lifecycle.addObserver(observer)

onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
}
2 changes: 0 additions & 2 deletions app/src/main/java/com/getcode/util/Bitmap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@ private fun Bitmap.getLuminanceData(): ByteArray {
for (x in 0 until width) {
val pixel = pixelData[y * width + x]

// Extract RGB values from the pixel
val r = (pixel shr 16) and 0xFF
val g = (pixel shr 8) and 0xFF
val b = pixel and 0xFF

// Calculate luminance (grayscale) using a common formula
val luminance = (0.299 * r + 0.587 * g + 0.114 * b).toInt()
luminanceData[y * width + x] = luminance.toByte()
}
Expand Down
1 change: 0 additions & 1 deletion app/src/main/java/com/getcode/view/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import com.getcode.LocalPhoneFormatter
import com.getcode.LocalSession
import com.getcode.R
import com.getcode.Session
import com.getcode.SessionState
import com.getcode.analytics.AnalyticsService
import com.getcode.network.TipController
import com.getcode.network.client.Client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ import com.getcode.navigation.screens.EnterTipModal
import com.getcode.navigation.screens.GetKinModal
import com.getcode.navigation.screens.GiveKinModal
import com.getcode.navigation.screens.ShareDownloadLinkModal
import com.getcode.ui.components.FullScreenProgressSpinner
import com.getcode.ui.components.OnLifecycleEvent
import com.getcode.ui.components.PermissionCheck
import com.getcode.ui.components.getPermissionLauncher
import com.getcode.ui.utils.AnimationUtils
import com.getcode.ui.utils.KeepScreenOn
import com.getcode.ui.utils.ModalAnimationSpeed
import com.getcode.ui.utils.measured
import com.getcode.view.login.notificationPermissionCheck
Expand Down Expand Up @@ -261,6 +263,9 @@ private fun ScannerContent(
onAction = { handleAction(it) },
)

FullScreenProgressSpinner(dataState.fullScreenLoading)
KeepScreenOn(dataState.fullScreenLoading)

OnLifecycleEvent { _, event ->
when (event) {
Lifecycle.Event.ON_START -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.kik.kikx.kikcodes.implementation
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Rect
import android.icu.text.DateFormat
import android.icu.text.SimpleDateFormat
import android.net.Uri
import android.os.Environment
import androidx.camera.core.ImageAnalysis
Expand All @@ -22,8 +24,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
Expand Down Expand Up @@ -99,7 +99,6 @@ class KikCodeAnalyzer @Inject constructor(

private suspend fun detectCodeInImage(
bitmap: Bitmap,
minSectionSize: Int = 100,
scan: suspend (Bitmap) -> Result<ScannableKikCode>
): Result<ScannableKikCode> = withContext(Dispatchers.Default) {
val destinationRoot =
Expand All @@ -111,75 +110,177 @@ class KikCodeAnalyzer @Inject constructor(
}

// Start the recursive division and scanning process
return@withContext divideAndScan(bitmap, destination, minSectionSize, scan)
return@withContext search(bitmap, destination, 100, scan)
}

private suspend fun divideAndScan(
private suspend fun search(
bitmap: Bitmap,
destination: File,
minSectionSize: Int,
scan: suspend (Bitmap) -> Result<ScannableKikCode>,
): Result<ScannableKikCode> {
// try scanning raw
val raw = scan(bitmap)
if (raw.isSuccess) {
debugPrint("Code found raw")
bitmap.recycle()
return raw
} else {
debugPrint("No Code found via raw")
}

// attempt quick lookup by recursively splitting image into quadrants, with increasing zoom levels
val zoomLevels = listOf(1.0, 2.0, 5.0, 10.0)
val recursiveSearch = processBitmapRecursively(
bitmap,
destination,
minSectionSize,
scan,
zoomLevels,
""
)

if (recursiveSearch.isSuccess) {
debugPrint("Code found via recursive lookup")
bitmap.recycle()
return recursiveSearch
} else {
debugPrint("No Code found via recursive lookup")
}

val result = slidingWindowSearch(
bitmap = bitmap,
windowSize = 300,
stepSize = 150,
scan = scan,
zoomLevels = zoomLevels
)

if (result.isSuccess) {
debugPrint("Code found via sliding window")
}

bitmap.recycle()
return result
}

private suspend fun slidingWindowSearch(
bitmap: Bitmap,
windowSize: Int,
stepSize: Int,
scan: suspend (Bitmap) -> Result<ScannableKikCode>,
zoomLevels: List<Double>
): Result<ScannableKikCode> {
val w = bitmap.width
val h = bitmap.height

debugPrint("search: original ${w}x${h}")

for (zoomLevel in zoomLevels) {
val windowWidth = (windowSize * zoomLevel).toInt()
val windowHeight = (windowSize * zoomLevel).toInt()

for (i in 0 until w step stepSize) {
for (j in 0 until h step stepSize) {
val x = i.coerceAtMost(w - windowWidth)
val y = j.coerceAtMost(h - windowHeight)
val width = windowWidth.coerceAtMost(w - x)
val height = windowHeight.coerceAtMost(h - y)
val windowBitmap = Bitmap.createBitmap(
bitmap,
x, y,
width, height
)

debugPrint("search: checking {x: $x, y: $y, w: $width, h: $height} @ $zoomLevel")
val result = scan(windowBitmap)
windowBitmap.recycle()

return processBitmapRecursively(bitmap, destination, minSectionSize, scan, zoomLevels)
if (result.isSuccess) {
debugPrint("search: SUCCESS in {x: $x, y: $y, w: $width, h: $height} @ $zoomLevel")
return result
}
}
}
}

return Result.failure(KikCodeScanner.NoKikCodeFoundException())
}

private suspend fun processBitmapRecursively(
bitmap: Bitmap,
destination: File,
minSectionSize: Int,
scan: suspend (Bitmap) -> Result<ScannableKikCode>,
zoomLevels: List<Double>
zoomLevels: List<Double>,
regionName: String
): Result<ScannableKikCode> {
val width = bitmap.width
val height = bitmap.height

// Base case: If the bitmap is smaller than the minimum section size, process it directly
if (width <= minSectionSize || height <= minSectionSize) {
return scanWithZoomLevels(bitmap, destination, scan, zoomLevels)
return scanWithZoomLevels(bitmap, destination, scan, zoomLevels, regionName)
}

// Scan the center section first
val centerRect = calculateCenterRect(width, height)
val centerBitmap = Bitmap.createBitmap(bitmap, centerRect.left, centerRect.top, centerRect.width(), centerRect.height())

val centerResult = scanWithZoomLevels(centerBitmap, destination, scan, zoomLevels)
val centerResult = scanWithZoomLevels(centerBitmap, destination, scan, zoomLevels, "center")
centerBitmap.recycle()

if (centerResult.isSuccess) {
return centerResult
}

// Divide the bitmap into left and right halves and process recursively
val leftHalf = Bitmap.createBitmap(bitmap, 0, 0, width / 2, height)
val rightHalf = Bitmap.createBitmap(bitmap, width / 2, 0, width / 2, height)
val quadrants = splitIntoQuadrants(bitmap)

val leftResult = processBitmapRecursively(leftHalf, destination, minSectionSize, scan, zoomLevels)
leftHalf.recycle()
// Process each quadrant recursively
for ((quadrantBitmap, name) in quadrants) {
val quadrantResult = processBitmapRecursively(quadrantBitmap, destination, minSectionSize, scan, zoomLevels, name)
quadrantBitmap.recycle()

if (leftResult.isSuccess) {
rightHalf.recycle()
return leftResult
if (quadrantResult.isSuccess) {
return quadrantResult
}
}

val rightResult = processBitmapRecursively(rightHalf, destination, minSectionSize, scan, zoomLevels)
rightHalf.recycle()
return Result.failure(KikCodeScanner.NoKikCodeFoundException())
}

return rightResult
private fun splitIntoQuadrants(bitmap: Bitmap): List<Pair<Bitmap, String>> {
val width = bitmap.width
val height = bitmap.height
val halfWidth = width / 2
val halfHeight = height / 2

val topLeft = Bitmap.createBitmap(bitmap, 0, 0, halfWidth, halfHeight)
val topRight = Bitmap.createBitmap(bitmap, halfWidth, 0, halfWidth, halfHeight)
val bottomLeft = Bitmap.createBitmap(bitmap, 0, halfHeight, halfWidth, halfHeight)
val bottomRight = Bitmap.createBitmap(bitmap, halfWidth, halfHeight, halfWidth, halfHeight)

return listOf(
topLeft to "topLeft",
topRight to "topRight",
bottomLeft to "bottomLeft",
bottomRight to "bottomRight"
)
}

private suspend fun scanWithZoomLevels(
bitmap: Bitmap,
destination: File,
scan: suspend (Bitmap) -> Result<ScannableKikCode>,
zoomLevels: List<Double>
zoomLevels: List<Double>,
regionName: String // Use the region name to give unique filenames
): Result<ScannableKikCode> {
for (zoomLevel in zoomLevels) {
val zoomedBitmap = zoomBitmap(bitmap, zoomLevel)
saveSegment(zoomedBitmap, destination) {
"section_${zoomedBitmap.width}x${zoomedBitmap.height}_zoom${zoomLevel}.png"
val prefix = regionName.ifEmpty { null }?.let { "${it}_"}
"$prefix${zoomedBitmap.width}x${zoomedBitmap.height}_zoom${zoomLevel}.png"
}

val result = scan(zoomedBitmap)

zoomedBitmap.recycle()
Expand Down Expand Up @@ -226,4 +327,8 @@ class KikCodeAnalyzer @Inject constructor(
}
}

private fun debugPrint(message: String) {
if (DEBUG) println(message)
}

private const val DEBUG = false