@@ -36,17 +36,19 @@ import com.amplifyframework.ui.liveness.model.LivenessCheckState
3636import com.amplifyframework.ui.liveness.ui.helper.VideoViewportSize
3737import java.util.Date
3838import java.util.Timer
39+ import java.util.TimerTask
3940import kotlin.concurrent.schedule
4041
4142internal data class InitialStreamFace (val faceRect : RectF , val timestamp : Long )
4243
4344internal data class LivenessState (
4445 val sessionId : String ,
4546 val context : Context ,
47+ val disableStartView : Boolean ,
4648 val onCaptureReady : () -> Unit ,
4749 val onFaceDistanceCheckPassed : () -> Unit ,
4850 val onSessionError : (FaceLivenessDetectionException , Boolean ) -> Unit ,
49- val onFinalEventsSent : () -> Unit
51+ val onFinalEventsSent : () -> Unit ,
5052) {
5153 var videoViewportSize: VideoViewportSize ? by mutableStateOf(null )
5254 var livenessCheckState = mutableStateOf<LivenessCheckState >(
@@ -58,13 +60,15 @@ internal data class LivenessState(
5860 var initialFaceDistanceCheckPassed by mutableStateOf(false )
5961 var initialLocalFaceFound by mutableStateOf(false )
6062
63+ var showingStartView by mutableStateOf(! disableStartView)
64+
6165 private var initialStreamFace: InitialStreamFace ? = null
6266 @VisibleForTesting
6367 var faceMatchOvalStart: Long? = null
6468 @VisibleForTesting
6569 var faceMatchOvalEnd: Long? = null
6670 private var initialFaceOvalIou = - 1f
67- private var faceOvalMatchTimerStarted = false
71+ private var faceOvalMatchTimer : TimerTask ? = null
6872 private var detectedFaceMatchedOval = false
6973
7074 @VisibleForTesting
@@ -85,6 +89,12 @@ internal data class LivenessState(
8589
8690 fun onError (stopLivenessSession : Boolean ) {
8791 livenessCheckState.value = LivenessCheckState .Error
92+ onDestroy(stopLivenessSession)
93+ }
94+
95+ // Cleans up state when challenge is completed or cancelled
96+ fun onDestroy (stopLivenessSession : Boolean ) {
97+ faceOvalMatchTimer?.cancel()
8898 readyForOval = false
8999 faceGuideRect = null
90100 runningFreshness = false
@@ -127,28 +137,44 @@ internal data class LivenessState(
127137 * @return true if FrameAnalyzer should continue processing the frame
128138 */
129139 fun onFrameAvailable (): Boolean {
130- val livenessCheckState = livenessCheckState.value
131- if (livenessCheckState == LivenessCheckState .Error ) return false
132- if (livenessCheckState !is LivenessCheckState .Success ) return true
133-
134- if (readyToSendFinalEvents) {
135- readyToSendFinalEvents = false
136-
137- livenessSessionInfo!! .sendChallengeResponseEvent(
138- FaceTargetChallengeResponse (
139- colorChallenge!! .challengeId,
140- livenessCheckState.faceGuideRect,
141- Date (faceMatchOvalStart!! ),
142- Date (faceMatchOvalEnd!! )
143- )
144- )
140+ if (showingStartView) return false
141+
142+ return when (val livenessCheckState = livenessCheckState.value) {
143+ is LivenessCheckState .Error -> false
144+ is LivenessCheckState .Initial , is LivenessCheckState .Running -> {
145+ /* *
146+ * Start freshness check if the face has matched oval (we know this if faceMatchOvalStart is not null)
147+ * We trigger this in onFrameAvailable instead of onFrameFaceUpdate in the event the user moved the face
148+ * away from the camera. We want to run this check on every frame if the challenge is in process.
149+ */
150+ if (! runningFreshness && colorChallenge?.challengeType ==
151+ ColorChallengeType .SEQUENTIAL &&
152+ faceMatchOvalStart?.let { (Date ().time - it) > 1000 } == true
153+ ) {
154+ runningFreshness = true
155+ }
156+ true
157+ }
158+ is LivenessCheckState .Success -> {
159+ if (readyToSendFinalEvents) {
160+ readyToSendFinalEvents = false
161+
162+ livenessSessionInfo!! .sendChallengeResponseEvent(
163+ FaceTargetChallengeResponse (
164+ colorChallenge!! .challengeId,
165+ livenessCheckState.faceGuideRect,
166+ Date (faceMatchOvalStart!! ),
167+ Date (faceMatchOvalEnd!! )
168+ )
169+ )
145170
146- // Send empty video event to signal we're done sending video
147- livenessSessionInfo!! .sendVideoEvent(VideoEvent (ByteArray (0 ), Date ()))
148- onFinalEventsSent()
171+ // Send empty video event to signal we're done sending video
172+ livenessSessionInfo!! .sendVideoEvent(VideoEvent (ByteArray (0 ), Date ()))
173+ onFinalEventsSent()
174+ }
175+ false
176+ }
149177 }
150-
151- return false
152178 }
153179
154180 fun onFrameFaceCountUpdate (faceCount : Int ) {
@@ -178,12 +204,19 @@ internal data class LivenessState(
178204 }
179205 }
180206
207+ /* *
208+ * returns true if face update inspect, false if thrown away
209+ */
181210 fun onFrameFaceUpdate (
182211 faceRect : RectF ,
183212 leftEye : FaceDetector .Landmark ,
184213 rightEye : FaceDetector .Landmark ,
185214 mouth : FaceDetector .Landmark
186- ) {
215+ ): Boolean {
216+ if (showingStartView) {
217+ return false
218+ }
219+
187220 if (! initialFaceDistanceCheckPassed) {
188221 val faceDistance = FaceDetector .calculateFaceDistance(
189222 leftEye, rightEye, mouth,
@@ -261,29 +294,25 @@ internal data class LivenessState(
261294
262295 // Start timer and then timeout if the detected face doesn't match
263296 // the oval after a period of time
264- if (! detectedFaceMatchedOval && ! faceOvalMatchTimerStarted) {
265- faceOvalMatchTimerStarted = true
266- Timer ().schedule(faceTargetChallenge!! .faceTargetMatching.ovalFitTimeout.toLong()) {
267- if (! detectedFaceMatchedOval && faceGuideRect != null ) {
268- readyForOval = false
269- val timeoutError =
270- FaceLivenessDetectionException (
271- " Face did not match oval within time limit."
272- )
273- onSessionError(timeoutError, true )
297+ if (! detectedFaceMatchedOval && faceOvalMatchTimer == null ) {
298+ faceOvalMatchTimer =
299+ Timer ().schedule(faceTargetChallenge!! .faceTargetMatching.ovalFitTimeout.toLong()) {
300+ if (! detectedFaceMatchedOval && faceGuideRect != null ) {
301+ readyForOval = false
302+ val timeoutError =
303+ FaceLivenessDetectionException (
304+ " Face did not match oval within time limit."
305+ )
306+ onSessionError(timeoutError, true )
307+ }
308+ cancel()
274309 }
275- faceOvalMatchTimerStarted = false
276- cancel()
277- }
278- }
279-
280- // Start freshness check if it's not already started and face is in oval
281- if (! runningFreshness && colorChallenge?.challengeType ==
282- ColorChallengeType .SEQUENTIAL &&
283- faceOvalPosition == FaceDetector .FaceOvalPosition .MATCHED
284- ) {
285- runningFreshness = true
286310 }
287311 }
312+ return true
313+ }
314+
315+ fun onStartViewComplete () {
316+ showingStartView = false
288317 }
289318}
0 commit comments