Skip to content

Commit b7fccb8

Browse files
tjleingThomas Leingtylerjroach
authored
Pass error codes on close to Amplify (#106)
Co-authored-by: Thomas Leing <[email protected]> Co-authored-by: tjroach <[email protected]>
1 parent 0ee534b commit b7fccb8

File tree

6 files changed

+65
-19
lines changed

6 files changed

+65
-19
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[versions]
22
agp = "8.1.4"
3-
amplify = "2.14.5"
3+
amplify = "2.14.8"
44
cameraX = "1.2.0"
55
compose = "1.5.4"
66
coroutines = "1.7.3"

liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import com.amplifyframework.ui.liveness.BuildConfig
4444
import com.amplifyframework.ui.liveness.model.FaceLivenessDetectionException
4545
import com.amplifyframework.ui.liveness.model.LivenessCheckState
4646
import com.amplifyframework.ui.liveness.state.LivenessState
47+
import com.amplifyframework.ui.liveness.util.WebSocketCloseCode
4748
import java.util.Date
4849
import java.util.Timer
4950
import java.util.concurrent.Executors
@@ -219,7 +220,12 @@ internal class LivenessCoordinator(
219220
faceLivenessException: FaceLivenessDetectionException,
220221
stopLivenessSession: Boolean
221222
) {
222-
livenessState.onError(stopLivenessSession)
223+
val webSocketCloseCode = when (faceLivenessException) {
224+
is FaceLivenessDetectionException.UserCancelledException -> WebSocketCloseCode.CANCELED
225+
is FaceLivenessDetectionException.FaceInOvalMatchExceededTimeLimitException -> WebSocketCloseCode.TIMEOUT
226+
else -> WebSocketCloseCode.RUNTIME_ERROR
227+
}
228+
livenessState.onError(stopLivenessSession, webSocketCloseCode)
223229
unbindCamera(context)
224230
onChallengeFailed.accept(faceLivenessException)
225231
}
@@ -270,12 +276,17 @@ internal class LivenessCoordinator(
270276
}
271277
}
272278

279+
/**
280+
* This is only called when onDispose is triggered from FaceLivenessDetector view.
281+
* If we begin calling destroy in other places, we should ensure we are still tracking the proper error code.
282+
*/
273283
fun destroy(context: Context) {
274284
// Destroy all resources so a new coordinator can safely be created
275285
encoder.stop {
276286
encoder.destroy()
277287
}
278-
livenessState.onDestroy(true)
288+
val webSocketCloseCode = if (!disconnectEventReceived) WebSocketCloseCode.DISPOSED else null
289+
livenessState.onDestroy(true, webSocketCloseCode)
279290
unbindCamera(context)
280291
analysisExecutor.shutdown()
281292
}

liveness/src/main/java/com/amplifyframework/ui/liveness/model/FaceLivenessDetectionException.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,11 @@ open class FaceLivenessDetectionException(
5050
recoverySuggestion: String = "Retry the face liveness check.",
5151
throwable: Throwable? = null
5252
) : FaceLivenessDetectionException(message, recoverySuggestion, throwable)
53+
54+
/**
55+
* This is not an error we have determined to publicly expose.
56+
* The error will come to the customer in onError, but only instance checked as FaceLivenessDetectionException.
57+
*/
58+
internal class FaceInOvalMatchExceededTimeLimitException :
59+
FaceLivenessDetectionException("Face did not match oval within time limit.")
5360
}

liveness/src/main/java/com/amplifyframework/ui/liveness/state/LivenessState.kt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import com.amplifyframework.ui.liveness.ml.FaceOval
3434
import com.amplifyframework.ui.liveness.model.FaceLivenessDetectionException
3535
import com.amplifyframework.ui.liveness.model.LivenessCheckState
3636
import com.amplifyframework.ui.liveness.ui.helper.VideoViewportSize
37+
import com.amplifyframework.ui.liveness.util.WebSocketCloseCode
3738
import java.util.Date
3839
import java.util.Timer
3940
import java.util.TimerTask
@@ -87,19 +88,21 @@ internal data class LivenessState(
8788
}
8889
}
8990

90-
fun onError(stopLivenessSession: Boolean) {
91+
fun onError(stopLivenessSession: Boolean, webSocketCloseCode: WebSocketCloseCode) {
9192
livenessCheckState.value = LivenessCheckState.Error
92-
onDestroy(stopLivenessSession)
93+
onDestroy(stopLivenessSession, webSocketCloseCode)
9394
}
9495

95-
// Cleans up state when challenge is completed or cancelled
96-
fun onDestroy(stopLivenessSession: Boolean) {
96+
// Cleans up state when challenge is completed or cancelled.
97+
// We only send webSocketCloseCode if error encountered.
98+
fun onDestroy(stopLivenessSession: Boolean, webSocketCloseCode: WebSocketCloseCode? = null) {
99+
livenessCheckState.value = LivenessCheckState.Error
97100
faceOvalMatchTimer?.cancel()
98101
readyForOval = false
99102
faceGuideRect = null
100103
runningFreshness = false
101104
if (stopLivenessSession) {
102-
livenessSessionInfo?.stopSession()
105+
livenessSessionInfo?.stopSession(webSocketCloseCode?.code)
103106
}
104107
}
105108

@@ -300,9 +303,7 @@ internal data class LivenessState(
300303
if (!detectedFaceMatchedOval && faceGuideRect != null) {
301304
readyForOval = false
302305
val timeoutError =
303-
FaceLivenessDetectionException(
304-
"Face did not match oval within time limit."
305-
)
306+
FaceLivenessDetectionException.FaceInOvalMatchExceededTimeLimitException()
306307
onSessionError(timeoutError, true)
307308
}
308309
cancel()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.amplifyframework.ui.liveness.util
2+
3+
internal enum class WebSocketCloseCode(val code: Int) {
4+
TIMEOUT(4001),
5+
CANCELED(4003),
6+
RUNTIME_ERROR(4005),
7+
DISPOSED(4008)
8+
}

liveness/src/test/java/com/amplifyframework/ui/liveness/state/LivenessStateTest.kt

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.amplifyframework.predictions.models.VideoEvent
2828
import com.amplifyframework.ui.liveness.ml.FaceDetector
2929
import com.amplifyframework.ui.liveness.model.FaceLivenessDetectionException
3030
import com.amplifyframework.ui.liveness.model.LivenessCheckState
31+
import com.amplifyframework.ui.liveness.util.WebSocketCloseCode
3132
import io.mockk.mockk
3233
import io.mockk.verify
3334
import org.junit.Assert.assertEquals
@@ -131,26 +132,44 @@ internal class LivenessStateTest {
131132

132133
@Test
133134
fun `state is error after on error`() {
134-
livenessState.onError(true)
135+
livenessState.onError(true, WebSocketCloseCode.RUNTIME_ERROR)
135136
assertTrue(livenessState.livenessCheckState.value is LivenessCheckState.Error)
136137
}
137138

138139
@Test
139140
fun `session is stopped when stopLivenessSession is true and error occurs`() {
140141
val challenges = mockk<List<FaceLivenessSessionChallenge>>(relaxed = true)
141-
val stopSession = mockk<() -> Unit>(relaxed = true)
142+
val stopSession = mockk<(Int?) -> Unit>(relaxed = true)
142143
livenessState.livenessSessionInfo = FaceLivenessSession(challenges, { }, { }, stopSession)
143-
livenessState.onError(true)
144-
verify(exactly = 1) { stopSession() }
144+
livenessState.onError(true, WebSocketCloseCode.RUNTIME_ERROR)
145+
verify(exactly = 1) { stopSession(WebSocketCloseCode.RUNTIME_ERROR.code) }
146+
}
147+
148+
@Test
149+
fun `proper code is sent when provided in onDestroy`() {
150+
val challenges = mockk<List<FaceLivenessSessionChallenge>>(relaxed = true)
151+
val stopSession = mockk<(Int?) -> Unit>(relaxed = true)
152+
livenessState.livenessSessionInfo = FaceLivenessSession(challenges, { }, { }, stopSession)
153+
livenessState.onDestroy(true, WebSocketCloseCode.DISPOSED)
154+
verify(exactly = 1) { stopSession(WebSocketCloseCode.DISPOSED.code) }
155+
}
156+
157+
@Test
158+
fun `null close code is sent when no close code provided in onDestroy`() {
159+
val challenges = mockk<List<FaceLivenessSessionChallenge>>(relaxed = true)
160+
val stopSession = mockk<(Int?) -> Unit>(relaxed = true)
161+
livenessState.livenessSessionInfo = FaceLivenessSession(challenges, { }, { }, stopSession)
162+
livenessState.onDestroy(true, null)
163+
verify(exactly = 1) { stopSession(null) }
145164
}
146165

147166
@Test
148167
fun `session is not stopped when stopLivenessSession is false and error occurs`() {
149168
val challenges = mockk<List<FaceLivenessSessionChallenge>>(relaxed = true)
150-
val stopSession = mockk<() -> Unit>(relaxed = true)
169+
val stopSession = mockk<(Int?) -> Unit>(relaxed = true)
151170
livenessState.livenessSessionInfo = FaceLivenessSession(challenges, { }, { }, stopSession)
152-
livenessState.onError(false)
153-
verify(exactly = 0) { stopSession() }
171+
livenessState.onError(false, WebSocketCloseCode.RUNTIME_ERROR)
172+
verify(exactly = 0) { stopSession(any()) }
154173
}
155174

156175
@Test
@@ -205,7 +224,7 @@ internal class LivenessStateTest {
205224
@Test
206225
fun `state is error after freshness completes and an error occurs`() {
207226
livenessState.faceGuideRect = mockk(relaxed = true)
208-
livenessState.onError(false)
227+
livenessState.onError(false, WebSocketCloseCode.RUNTIME_ERROR)
209228
livenessState.onFreshnessComplete()
210229
assertTrue(livenessState.livenessCheckState.value is LivenessCheckState.Error)
211230
}

0 commit comments

Comments
 (0)