Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get completion loop frame count from initialize client #5679

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
4 changes: 2 additions & 2 deletions stripecardscan/api/stripecardscan.api
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ public final class com/stripe/android/stripecardscan/cardimageverification/CardI
}

public abstract class com/stripe/android/stripecardscan/cardimageverification/CardImageVerificationSheet$Configuration$StrictModeFrameCount : android/os/Parcelable {
public synthetic fun <init> (ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getCount ()I
public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getCount ()Lkotlin/jvm/functions/Function1;
}

public final class com/stripe/android/stripecardscan/cardimageverification/CardImageVerificationSheet$Configuration$StrictModeFrameCount$High : com/stripe/android/stripecardscan/cardimageverification/CardImageVerificationSheet$Configuration$StrictModeFrameCount {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import android.util.Size
import androidx.test.filters.SmallTest
import com.stripe.android.stripecardscan.framework.util.AcceptedImageConfigs
import com.stripe.android.stripecardscan.framework.util.ImageFormat

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import com.stripe.android.stripecardscan.framework.util.decodeFromJson
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
Expand All @@ -22,18 +21,21 @@ class CardImageVerificationDetailsTest {
"image_size": [
1080,
1920
]
],
"image_count": 5,
"unknown_field": "test"
},
"format_settings": {
"heic": {
"compression_ratio": 0.5
"compression_ratio": 0.5,
"image_count": 3
},
"webp": {
"compression_ratio": 0.7
"image_size": [
2160,
1920
]
]
}
},
"preferred_formats": [
Expand All @@ -48,34 +50,24 @@ class CardImageVerificationDetailsTest {
}
}"""

val result = Json.decodeFromString<CardImageVerificationDetailsResult>(json)
val result = decodeFromJson(CardImageVerificationDetailsResult.serializer(), json)
assertNotNull(result.acceptedImageConfigs)

result.acceptedImageConfigs?.let {
val acceptedImageConfigs = AcceptedImageConfigs(it)
val heicSettings = acceptedImageConfigs.imageSettings(ImageFormat.HEIC)
assertNotNull(heicSettings)
val acceptedImageConfigs = AcceptedImageConfigs(result.acceptedImageConfigs)

heicSettings?.let {
assertEquals(it.first, 0.5)
assertEquals(it.second, Size(1080, 1920))
}
val heicSettings = acceptedImageConfigs.getImageSettings(ImageFormat.HEIC)
assertEquals(heicSettings.compressionRatio, 0.5)
assertEquals(heicSettings.imageSize, Size(1080, 1920))
assertEquals(heicSettings.imageCount, 3)

val jpegSettings = acceptedImageConfigs.imageSettings(ImageFormat.JPEG)
assertNotNull(jpegSettings)
val jpegSettings = acceptedImageConfigs.getImageSettings(ImageFormat.JPEG)
assertEquals(jpegSettings.compressionRatio, 0.8)
assertEquals(jpegSettings.imageSize, Size(1080, 1920))
assertEquals(jpegSettings.imageCount, 5)

jpegSettings?.let {
assertEquals(it.first, 0.8)
assertEquals(it.second, Size(1080, 1920))
}

val webpSettings = acceptedImageConfigs.imageSettings(ImageFormat.WEBP)
assertNotNull(webpSettings)

webpSettings?.let {
assertEquals(it.first, 0.7)
assertEquals(it.second, Size(2160, 1920))
}
}
val webpSettings = acceptedImageConfigs.getImageSettings(ImageFormat.WEBP)
assertEquals(webpSettings.compressionRatio, 0.7)
assertEquals(webpSettings.imageSize, Size(2160, 1920))
assertEquals(webpSettings.imageCount, 5)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,10 @@ internal open class CardImageVerificationActivity :
cameraAdapter.unbindFromLifecycle(this@CardImageVerificationActivity)
resultListener.cardReadyForVerification(
pan = result.pan,
frames = scanFlow.selectCompletionLoopFrames(result.savedFrames)
frames = scanFlow.selectCompletionLoopFrames(
result.savedFrames,
imageConfigs
)
)
}.let { }
}
Expand Down Expand Up @@ -366,7 +369,9 @@ internal open class CardImageVerificationActivity :
CardVerificationFlowParameters(
cardIssuer = getIssuerByDisplayName(expectedCard.issuer),
lastFour = expectedCard.lastFour,
strictModeFrames = params.configuration.strictModeFrames.count
strictModeFrames = params.configuration.strictModeFrames.count(
imageConfigs.getImageSettings().second.imageCount
)
)
} else {
launch(Dispatchers.Main) {
Expand Down Expand Up @@ -648,7 +653,9 @@ internal open class CardImageVerificationActivity :
appDetails = AppDetails.fromContext(this),
scanStatistics = ScanStatistics.fromStats(),
scanConfig = ScanConfig(
strictModeFrameCount = params.configuration.strictModeFrames.count
strictModeFrameCount = params.configuration.strictModeFrames.count(
imageConfigs.getImageSettings().second.imageCount
)
),
payloadInfo = currentScanPayloadInfo
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.stripe.android.camera.scanui.ScanFlow
import com.stripe.android.stripecardscan.cardimageverification.analyzer.MainLoopAnalyzer
import com.stripe.android.stripecardscan.cardimageverification.result.MainLoopAggregator
import com.stripe.android.stripecardscan.cardimageverification.result.MainLoopState
import com.stripe.android.stripecardscan.framework.util.AcceptedImageConfigs
import com.stripe.android.stripecardscan.payment.ml.CardDetect
import com.stripe.android.stripecardscan.payment.ml.CardDetectModelManager
import com.stripe.android.stripecardscan.payment.ml.SSDOcr
Expand All @@ -39,13 +40,6 @@ internal abstract class CardImageVerificationFlow(
) : ScanFlow<CardVerificationFlowParameters?, CameraPreviewImage<Bitmap>>,
AggregateResultListener<MainLoopAggregator.InterimResult, MainLoopAggregator.FinalResult> {

companion object {
/**
* The maximum number of frames to process
*/
const val MAX_COMPLETION_LOOP_FRAMES = 3
}

/**
* If this is true, do not start the flow.
*/
Expand Down Expand Up @@ -151,14 +145,15 @@ internal abstract class CardImageVerificationFlow(
* Select which frames to use in the completion loop.
*/
fun <SavedFrame> selectCompletionLoopFrames(
frames: Map<SavedFrameType, List<SavedFrame>>
frames: Map<SavedFrameType, List<SavedFrame>>,
imageConfigs: AcceptedImageConfigs
): Collection<SavedFrame> {
fun getFrames(frameType: SavedFrameType) = frames[frameType] ?: emptyList()

val cardAndPan = getFrames(SavedFrameType(hasCard = true, hasOcr = true))
val pan = getFrames(SavedFrameType(hasCard = false, hasOcr = true))
val card = getFrames(SavedFrameType(hasCard = true, hasOcr = false))

return (cardAndPan + pan + card).take(MAX_COMPLETION_LOOP_FRAMES)
return (cardAndPan + pan + card).take(imageConfigs.getImageSettings().second.imageCount)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.contract.ActivityResultContract
import androidx.fragment.app.Fragment
import com.stripe.android.stripecardscan.cardimageverification.CardImageVerificationFlow.Companion.MAX_COMPLETION_LOOP_FRAMES
import com.stripe.android.stripecardscan.cardimageverification.exception.UnknownScanException
import com.stripe.android.stripecardscan.payment.card.ScannedCard
import com.stripe.android.stripecardscan.scanui.CancellationReason
Expand Down Expand Up @@ -59,14 +58,14 @@ class CardImageVerificationSheet private constructor(
*/
val enableCannotScanButton: Boolean = true
) : Parcelable {
sealed class StrictModeFrameCount(val count: Int) : Parcelable {
@Parcelize object None : StrictModeFrameCount(0)
sealed class StrictModeFrameCount(val count: (maxCompletionLoopFrames: Int) -> Int) : Parcelable {
@Parcelize object None : StrictModeFrameCount({ 0 })

@Parcelize object Low : StrictModeFrameCount(1)
@Parcelize object Low : StrictModeFrameCount({ 1 })

@Parcelize object Medium : StrictModeFrameCount(MAX_COMPLETION_LOOP_FRAMES / 2)
@Parcelize object Medium : StrictModeFrameCount({ maxCompletionLoopFrames -> maxCompletionLoopFrames / 2 })

@Parcelize object High : StrictModeFrameCount(MAX_COMPLETION_LOOP_FRAMES)
@Parcelize object High : StrictModeFrameCount({ maxCompletionLoopFrames -> maxCompletionLoopFrames })
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ internal enum class CardImageVerificationDetailsFormat {
@Serializable
internal data class CardImageVerificationDetailsImageSettings(
@SerialName("compression_ratio") val compressionRatio: Double? = null,
@SerialName("image_size") val imageSize: DoubleArray? = null
@SerialName("image_size") val imageSize: List<Double>? = null,
@SerialName("image_count") val imageCount: Int? = null
)

@Serializable
Expand All @@ -52,5 +53,5 @@ internal data class CardImageVerificationDetailsAcceptedImageConfigs(
CardImageVerificationDetailsImageSettings?>? = null,

@SerialName("preferred_formats")
val preferredFormats: Array<CardImageVerificationDetailsFormat>? = null
val preferredFormats: List<CardImageVerificationDetailsFormat>? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,7 @@ internal data class ConfigurationStats(
internal data class PayloadInfo(
@SerialName("image_compression_type") val imageCompressionType: String,
@SerialName("image_compression_quality") val imageCompressionQuality: Float,
@SerialName("image_payload_size") val imagePayloadSizeInBytes: Int
@SerialName("image_payload_size") val imagePayloadSizeInBytes: Int,
@SerialName("image_payload_count") val imagePayloadCount: Int,
@SerialName("image_payload_max_count") val imagePayloadMaxCount: Int
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.stripe.android.stripecardscan.framework.image

import android.graphics.Bitmap
import android.graphics.Rect
import android.util.Size
import androidx.annotation.CheckResult
import com.stripe.android.camera.framework.image.constrainToSize
import com.stripe.android.camera.framework.image.crop
Expand All @@ -11,6 +10,7 @@ import com.stripe.android.camera.framework.image.toJpeg
import com.stripe.android.camera.framework.image.toWebP
import com.stripe.android.camera.framework.util.scaleAndCenterWithin
import com.stripe.android.stripecardscan.framework.util.ImageFormat
import com.stripe.android.stripecardscan.framework.util.ImageSettings

/**
* Convert a [Bitmap] to an [MLImage] for use in ML models.
Expand All @@ -27,10 +27,10 @@ internal fun Bitmap.toMLImage(mean: ImageTransformValues, std: ImageTransformVal

internal fun Bitmap.toImageFormat(
format: ImageFormat,
imageSettings: Pair<Double, Size>
imageSettings: ImageSettings
): Pair<ByteArray, Rect> {
// Size and crop the image per the settings.
val maxImageSize = imageSettings.second
val maxImageSize = imageSettings.imageSize

val cropRect = maxImageSize
.scaleAndCenterWithin(this.size())
Expand All @@ -40,7 +40,7 @@ internal fun Bitmap.toImageFormat(
.constrainToSize(maxImageSize)

// Now convert formats with the compression ratio from settings.
val compressionRatio = imageSettings.first
val compressionRatio = imageSettings.compressionRatio

// Convert to 0..100
val convertedRatio = compressionRatio.times(100.0).toInt()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,77 +13,70 @@ internal enum class ImageFormat(val string: String) {
companion object {
fun fromValue(value: CardImageVerificationDetailsFormat): ImageFormat =
when (value) {
CardImageVerificationDetailsFormat.HEIC -> ImageFormat.HEIC
CardImageVerificationDetailsFormat.JPEG -> ImageFormat.JPEG
CardImageVerificationDetailsFormat.WEBP -> ImageFormat.WEBP
CardImageVerificationDetailsFormat.HEIC -> HEIC
CardImageVerificationDetailsFormat.JPEG -> JPEG
CardImageVerificationDetailsFormat.WEBP -> WEBP
}
}
}

internal data class OptionalImageSettings(
val compressionRatio: Double?,
val imageSize: Size?,
val imageCount: Int?
) {
constructor(settings: CardImageVerificationDetailsImageSettings?) : this(
compressionRatio = settings?.compressionRatio,
imageSize = settings?.imageSize?.takeIf { it.size > 1 }?.let {
Size(it.first().toInt(), it.last().toInt())
},
imageCount = settings?.imageCount
)
}

internal data class ImageSettings(
var compressionRatio: Double? = null,
var imageSize: Size? = null
val compressionRatio: Double,
val imageSize: Size,
val imageCount: Int
Comment on lines +38 to +40
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer immutability

) {
companion object {
// These default values are what Android was using before the addition of a server config.
val DEFAULT = ImageSettings(0.92, Size(1080, 1920))
}

constructor(settings: CardImageVerificationDetailsImageSettings?) : this() {
settings?.let {
compressionRatio = it.compressionRatio ?: compressionRatio

it.imageSize?.takeIf { it.size > 1 }?.let {
var pendingSize = imageSize ?: ImageSettings.DEFAULT.imageSize!!
val width = it.first().toInt() ?: pendingSize.width
val height = it.last().toInt() ?: pendingSize.height
imageSize = Size(width, height)
}
}
val DEFAULT = ImageSettings(0.92, Size(1080, 1920), 3)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jaimepark-stripe these are the default values

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh woops! didnt see this. ty!

}
}

internal data class AcceptedImageConfigs(
private var defaultSettings: ImageSettings? = ImageSettings.DEFAULT,
private var formatSettings: HashMap<ImageFormat, ImageSettings?>? = null,
var preferredFormats: Array<ImageFormat>? = Array<ImageFormat>(1) { ImageFormat.JPEG }
private val defaultSettings: OptionalImageSettings?,
private val formatSettings: Map<ImageFormat, OptionalImageSettings?>? = null,
val preferredFormats: List<ImageFormat>? = listOf(ImageFormat.JPEG)
) {
constructor(configs: CardImageVerificationDetailsAcceptedImageConfigs?) : this() {
configs?.let {
it.formatSettings?.takeIf { it.size > 0 }.also { formatSettings = HashMap() }?.forEach {
val value = ImageSettings(it.value)
val key = ImageFormat.fromValue(it.key)
formatSettings?.put(key, value)
}

val mappedFormats = it.preferredFormats?.map { ImageFormat.fromValue(it) }
?.filter { isformatSupport(it) }
?.takeIf { it.count() > 0 }
?.let { preferredFormats = it.toTypedArray() }

it.defaultSettings?.let { defaultSettings = ImageSettings(it) }
}
companion object {
internal fun isFormatSupported(format: ImageFormat) =
format == ImageFormat.JPEG || format == ImageFormat.WEBP
}

internal fun isformatSupport(format: ImageFormat) =
format == ImageFormat.JPEG || format == ImageFormat.WEBP

fun imageSettings(format: ImageFormat): Pair<Double, Size> {
// Default to client default settings
var result = ImageSettings.DEFAULT

// Override with server default settings
defaultSettings?.let {
result.compressionRatio = it.compressionRatio ?: result.compressionRatio
result.imageSize = it.imageSize ?: result.imageSize
constructor(configs: CardImageVerificationDetailsAcceptedImageConfigs? = null) : this(
defaultSettings = configs?.defaultSettings?.let { OptionalImageSettings(it) },
formatSettings = configs?.formatSettings?.takeIf { it.isNotEmpty() }?.map { entry ->
ImageFormat.fromValue(entry.key) to OptionalImageSettings(entry.value)
}
?.toMap(),
preferredFormats = configs?.preferredFormats?.map { ImageFormat.fromValue(it) }
?.filter { isFormatSupported(it) }
?.takeIf { it.isNotEmpty() }
)

// Take format specific settings
formatSettings?.get(format)?.let {
result.compressionRatio = it.compressionRatio ?: result.compressionRatio
result.imageSize = it.imageSize ?: result.imageSize
}
fun getImageSettings(format: ImageFormat): ImageSettings = ImageSettings(
compressionRatio = formatSettings?.get(format)?.compressionRatio
?: defaultSettings?.compressionRatio ?: ImageSettings.DEFAULT.compressionRatio,
imageSize = formatSettings?.get(format)?.imageSize ?: defaultSettings?.imageSize
?: ImageSettings.DEFAULT.imageSize,
imageCount = formatSettings?.get(format)?.imageCount ?: defaultSettings?.imageCount
?: ImageSettings.DEFAULT.imageCount
)

return Pair(result.compressionRatio!!, result.imageSize!!)
fun getImageSettings(): Pair<ImageFormat, ImageSettings> {
val format = preferredFormats?.first() ?: ImageFormat.JPEG
return format to getImageSettings(format)
}
}
Loading