-
Notifications
You must be signed in to change notification settings - Fork 629
/
CardScanFragment.kt
309 lines (270 loc) · 11.2 KB
/
CardScanFragment.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
package com.stripe.android.stripecardscan.cardscan
import android.annotation.SuppressLint
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.PointF
import android.os.Bundle
import android.util.Size
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.os.bundleOf
import androidx.core.view.updateMargins
import androidx.fragment.app.setFragmentResult
import com.stripe.android.camera.CameraPreviewImage
import com.stripe.android.camera.framework.Stats
import com.stripe.android.camera.scanui.ScanErrorListener
import com.stripe.android.camera.scanui.SimpleScanStateful
import com.stripe.android.camera.scanui.util.asRect
import com.stripe.android.camera.scanui.util.startAnimation
import com.stripe.android.stripecardscan.R
import com.stripe.android.stripecardscan.cardscan.exception.InvalidStripePublishableKeyException
import com.stripe.android.stripecardscan.cardscan.exception.UnknownScanException
import com.stripe.android.stripecardscan.cardscan.result.MainLoopAggregator
import com.stripe.android.stripecardscan.cardscan.result.MainLoopState
import com.stripe.android.stripecardscan.databinding.FragmentCardscanBinding
import com.stripe.android.stripecardscan.framework.api.dto.ScanStatistics
import com.stripe.android.stripecardscan.framework.api.uploadScanStatsOCR
import com.stripe.android.stripecardscan.framework.util.AppDetails
import com.stripe.android.stripecardscan.framework.util.Device
import com.stripe.android.stripecardscan.framework.util.ScanConfig
import com.stripe.android.stripecardscan.payment.card.ScannedCard
import com.stripe.android.stripecardscan.scanui.CancellationReason
import com.stripe.android.stripecardscan.scanui.ScanFragment
import com.stripe.android.stripecardscan.scanui.util.getColorByRes
import com.stripe.android.stripecardscan.scanui.util.getFloatResource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.min
import kotlin.math.roundToInt
private val MINIMUM_RESOLUTION = Size(1067, 600) // minimum size of OCR
const val CARD_SCAN_FRAGMENT_REQUEST_KEY = "CardScanRequestKey"
const val CARD_SCAN_FRAGMENT_BUNDLE_KEY = "CardScanBundleKey"
const val CARD_SCAN_FRAGMENT_PARAMS_KEY = "CardScanParamsKey"
class CardScanFragment : ScanFragment(), SimpleScanStateful<CardScanState> {
override val minimumAnalysisResolution = MINIMUM_RESOLUTION
private lateinit var viewBinding: FragmentCardscanBinding
override val instructionsText: TextView by lazy { viewBinding.instructions }
override val previewFrame: ViewGroup by lazy { viewBinding.previewFrame }
private val params: CardScanSheetParams by lazy {
arguments?.getParcelable(CARD_SCAN_FRAGMENT_PARAMS_KEY) ?: CardScanSheetParams("")
}
private val hasPreviousValidResult = AtomicBoolean(false)
override var scanState: CardScanState? = CardScanState.NotFound
override var scanStatePrevious: CardScanState? = null
override val scanErrorListener: ScanErrorListener = ScanErrorListener()
/**
* The listener which handles results from the scan.
*/
override val resultListener: CardScanResultListener =
object : CardScanResultListener {
override fun cardScanComplete(card: ScannedCard) {
setFragmentResult(
CARD_SCAN_FRAGMENT_REQUEST_KEY,
bundleOf(
CARD_SCAN_FRAGMENT_BUNDLE_KEY to CardScanSheetResult.Completed(
ScannedCard(
pan = card.pan
)
)
)
)
closeScanner()
}
override fun userCanceled(reason: CancellationReason) {
setFragmentResult(
CARD_SCAN_FRAGMENT_REQUEST_KEY,
bundleOf(
CARD_SCAN_FRAGMENT_BUNDLE_KEY to CardScanSheetResult.Canceled(reason)
)
)
}
override fun failed(cause: Throwable?) {
setFragmentResult(
CARD_SCAN_FRAGMENT_REQUEST_KEY,
bundleOf(
CARD_SCAN_FRAGMENT_BUNDLE_KEY to
CardScanSheetResult.Failed(
cause ?: UnknownScanException()
)
)
)
}
}
/**
* The flow used to scan an item.
*/
private val scanFlow: CardScanFlow by lazy {
object : CardScanFlow(scanErrorListener) {
/**
* A final result was received from the aggregator. Set the result from this activity.
*/
override suspend fun onResult(
result: MainLoopAggregator.FinalResult,
) {
launch(Dispatchers.Main) {
changeScanState(CardScanState.Correct)
activity?.let { cameraAdapter.unbindFromLifecycle(it) }
resultListener.cardScanComplete(ScannedCard(result.pan))
}.let { }
}
/**
* An interim result was received from the result aggregator.
*/
override suspend fun onInterimResult(
result: MainLoopAggregator.InterimResult,
) = launch(Dispatchers.Main) {
if (
result.state is MainLoopState.OcrFound &&
!hasPreviousValidResult.getAndSet(true)
) {
scanStat.trackResult("ocr_pan_observed")
}
when (result.state) {
is MainLoopState.Initial -> changeScanState(CardScanState.NotFound)
is MainLoopState.OcrFound -> changeScanState(CardScanState.Found)
is MainLoopState.Finished -> changeScanState(CardScanState.Correct)
}
}.let { }
override suspend fun onReset() = launch(Dispatchers.Main) {
changeScanState(CardScanState.NotFound)
}.let { }
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewBinding = FragmentCardscanBinding.inflate(inflater, container, false)
setupViewFinderConstraints()
viewBinding.closeButton.setOnClickListener {
userClosedScanner()
}
viewBinding.viewFinderBorder.setOnTouchListener { _, e ->
setFocus(
PointF(
e.x + viewBinding.viewFinderWindow.left,
e.y + viewBinding.viewFinderWindow.top
)
)
true
}
displayState(requireNotNull(scanState), scanStatePrevious)
return viewBinding.root
}
override fun onStart() {
super.onStart()
if (!ensureValidParams()) {
return
}
}
override fun onResume() {
super.onResume()
scanState = CardScanState.NotFound
}
override fun onDestroy() {
scanFlow.cancelFlow()
super.onDestroy()
}
/**
* Set up viewFinderWindowView and viewFinderBorderView centered with predefined margins
*/
private fun setupViewFinderConstraints() {
val screenSize = Resources.getSystem().displayMetrics.let {
Size(it.widthPixels, it.heightPixels)
}
val viewFinderMargin = (
min(screenSize.width, screenSize.height) *
(context?.getFloatResource(R.dimen.stripeViewFinderMargin) ?: 0F)
).roundToInt()
listOf(viewBinding.viewFinderWindow, viewBinding.viewFinderBorder).forEach { view ->
(view.layoutParams as ViewGroup.MarginLayoutParams)
.updateMargins(
viewFinderMargin, viewFinderMargin, viewFinderMargin, viewFinderMargin
)
}
viewBinding.viewFinderBackground.setViewFinderRect(viewBinding.viewFinderWindow.asRect())
}
override fun onFlashSupported(supported: Boolean) {}
override fun onSupportsMultipleCameras(supported: Boolean) {}
/**
* Prepare to start the camera. Once the camera is ready, [onCameraReady] must be called.
*/
override fun prepareCamera(onCameraReady: () -> Unit) {
viewBinding.previewFrame.post {
viewBinding.viewFinderBackground
.setViewFinderRect(viewBinding.viewFinderWindow.asRect())
onCameraReady()
}
}
/**
* Once the camera stream is available, start processing images.
*/
override suspend fun onCameraStreamAvailable(cameraStream: Flow<CameraPreviewImage<Bitmap>>) {
context?.let {
scanFlow.startFlow(
context = it,
imageStream = cameraStream,
viewFinder = viewBinding.viewFinderWindow.asRect(),
lifecycleOwner = this,
coroutineScope = this,
parameters = null
)
}
}
/**
* Called when the flashlight state has changed.
*/
override fun onFlashlightStateChanged(flashlightOn: Boolean) {}
private fun ensureValidParams() = when {
params.stripePublishableKey.isEmpty() -> {
scanFailure(InvalidStripePublishableKeyException("Missing publishable key"))
false
}
else -> true
}
override fun displayState(newState: CardScanState, previousState: CardScanState?) {
when (newState) {
is CardScanState.NotFound, CardScanState.Found -> {
context?.let {
viewBinding.viewFinderBackground
.setBackgroundColor(
it.getColorByRes(R.color.stripeNotFoundBackground)
)
}
viewBinding.viewFinderWindow
.setBackgroundResource(R.drawable.stripe_card_background_not_found)
viewBinding.viewFinderBorder
.startAnimation(R.drawable.stripe_paymentsheet_card_border_not_found)
}
is CardScanState.Correct -> {
context?.let {
viewBinding.viewFinderBackground
.setBackgroundColor(
it.getColorByRes(R.color.stripeCorrectBackground)
)
}
viewBinding.viewFinderWindow
.setBackgroundResource(R.drawable.stripe_card_background_correct)
viewBinding.viewFinderBorder.startAnimation(R.drawable.stripe_card_border_correct)
}
}
}
override fun closeScanner() {
uploadScanStatsOCR(
stripePublishableKey = params.stripePublishableKey,
instanceId = Stats.instanceId,
scanId = Stats.scanId,
device = Device.fromContext(context),
appDetails = AppDetails.fromContext(context),
scanStatistics = ScanStatistics.fromStats(),
scanConfig = ScanConfig(0),
)
super.closeScanner()
}
}