From 528fa217958e474c7881cf64f369c69327fbf200 Mon Sep 17 00:00:00 2001 From: jihanurrahman33 Date: Thu, 4 Sep 2025 03:17:24 +0600 Subject: [PATCH 01/11] CI: Lint + unit tests, build sample, and run translation check --- .github/workflows/android-ci.yml | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/android-ci.yml diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml new file mode 100644 index 00000000..7c033419 --- /dev/null +++ b/.github/workflows/android-ci.yml @@ -0,0 +1,50 @@ +name: Android CI + +on: + push: + branches: [ "master", "main" ] + pull_request: + branches: [ "*" ] + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Install SDK packages + shell: bash + run: | + sdkmanager --install \ + "platforms;android-34" \ + "build-tools;34.0.0" \ + "platform-tools" + yes | sdkmanager --licenses + + - name: Gradle version + run: ./gradlew --version + + - name: Lint and unit tests + run: ./gradlew :veridui:lint :veridui:test --no-daemon --stacktrace + + - name: Build library and sample + run: ./gradlew :veridui:assembleRelease :sample:assembleDebug --no-daemon --stacktrace + + - name: Translation checks + run: | + python3 test_translation.py veridui/src/main/assets/fr.xml + From 6246ce9f8a735ae53883b96a147404d30203ff7d Mon Sep 17 00:00:00 2001 From: jihanurrahman33 Date: Thu, 4 Sep 2025 03:17:52 +0600 Subject: [PATCH 02/11] CI: Add Gradle wrapper validation workflow --- .github/workflows/gradle-wrapper-validation.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/gradle-wrapper-validation.yml diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml new file mode 100644 index 00000000..285f2087 --- /dev/null +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -0,0 +1,13 @@ +name: Validate Gradle Wrapper + +on: + pull_request: + branches: [ "*" ] + +jobs: + validation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: gradle/wrapper-validation-action@v1 + From 26c168b4f107c33fd75cc4da20105af78eec29ea Mon Sep 17 00:00:00 2001 From: jihanurrahman33 Date: Thu, 4 Sep 2025 03:18:12 +0600 Subject: [PATCH 03/11] CI: Add dependency review action for PRs --- .github/workflows/dependency-review.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/dependency-review.yml diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..4bd9ffca --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,18 @@ +name: Dependency Review + +on: + pull_request: + branches: [ "master", "main" ] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Dependency Review + uses: actions/dependency-review-action@v4 + From 368f890f18292f49f90618e13067aa533f69224d Mon Sep 17 00:00:00 2001 From: jihanurrahman33 Date: Thu, 4 Sep 2025 03:18:31 +0600 Subject: [PATCH 04/11] Style: Add .editorconfig for consistent formatting --- .editorconfig | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..3f06acb1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{kt,kts,java}] +indent_style = space +indent_size = 4 +max_line_length = 120 + +[*.xml] +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + From 80ae6d3945ba59e98c83f54b74dbd7461f948f80 Mon Sep 17 00:00:00 2001 From: jihanurrahman33 Date: Thu, 4 Sep 2025 03:18:54 +0600 Subject: [PATCH 05/11] Docs: Add feature request issue template --- .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..9901d475 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature] " +labels: enhancement +assignees: '' +--- + +## Summary +Describe the problem this feature solves and the value. + +## Proposed solution +What should be added or changed? UI/UX, API shape, etc. + +## Alternatives considered +List any alternative solutions or workarounds. + +## Additional context +Screenshots, references, or links. + From d266fc21a059528bdab88432f88579ee4276e0b2 Mon Sep 17 00:00:00 2001 From: jihanurrahman33 Date: Thu, 4 Sep 2025 03:19:25 +0600 Subject: [PATCH 06/11] Docs: Add Android CI badge to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d5837e4d..768b51b3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ ![Maven Central](https://img.shields.io/maven-central/v/com.appliedrec.verid/ui2) +![Android CI](https://github.com/AppliedRecognition/Ver-ID-UI-Android/actions/workflows/android-ci.yml/badge.svg) # Ver-ID UI for Android From 2961178c576188cf07cd2122b666306896535f4f Mon Sep 17 00:00:00 2001 From: jihanurrahman33 Date: Thu, 4 Sep 2025 03:20:02 +0600 Subject: [PATCH 07/11] CI: Enable Dependabot for GitHub Actions updates (weekly) --- .github/dependabot.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..025ece5e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + From fe2c3ccbcf9f2ec1b8a637bae93a05be028648c4 Mon Sep 17 00:00:00 2001 From: jihanurrahman33 Date: Thu, 4 Sep 2025 03:20:44 +0600 Subject: [PATCH 08/11] Docs: Add README for the sample module --- sample/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 sample/README.md diff --git a/sample/README.md b/sample/README.md new file mode 100644 index 00000000..eac8f9e3 --- /dev/null +++ b/sample/README.md @@ -0,0 +1,15 @@ +Sample App + +- Module: `:sample` +- Builds a demo app using the Ver-ID UI library (`:veridui`). + +Build and run + +- From IDE: Open the project in Android Studio and run the `sample` configuration on a device. +- CLI: `./gradlew :sample:assembleDebug` + +Notes + +- The sample uses Compose and AndroidX; ensure you’re on Android Studio Giraffe or newer. +- Some features may require a device with a camera for full functionality. + From 7b1d056b28c8be0f295ab77d7856460587bfd55e Mon Sep 17 00:00:00 2001 From: jihanurrahman33 Date: Thu, 4 Sep 2025 03:21:35 +0600 Subject: [PATCH 09/11] Docs: Update translation scripts to Python 3 in docs --- Translating-Ver-ID-UI.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Translating-Ver-ID-UI.md b/Translating-Ver-ID-UI.md index 8cb07c1f..3268a3a5 100644 --- a/Translating-Ver-ID-UI.md +++ b/Translating-Ver-ID-UI.md @@ -2,12 +2,12 @@ Ver-ID UI allows you to supply a language translation when starting a Ver-ID session. -The Ver-ID-UI project provides [Python 2.7](https://www.python.org/download/releases/2.7/) scripts to generate an empty translation XML and to verify that a given file is not missing any translations. +The Ver-ID-UI project provides Python 3 scripts to generate an empty translation XML and to verify that a given file is not missing any translations. ## Generating a translation XML ~~~shell -python translation_xml.py +python3 translation_xml.py ~~~ The command will collect all strings used in the source code and output a string like this: @@ -76,14 +76,14 @@ Enter the translation of the string inside the `` tag in the ` es.xml +python3 translation_xml.py > es.xml ~~~ ## Checking that your translation is complete Once you translate all the strings in the XML run: ~~~shell -python test_translation.py es.xml +python3 test_translation.py es.xml ~~~ If all strings are translated the command will output: From cc4cb9cc2f54faf1029ca901b53b4eb3f0b11fba Mon Sep 17 00:00:00 2001 From: jihanurrahman33 Date: Thu, 4 Sep 2025 03:21:59 +0600 Subject: [PATCH 10/11] Docs: Add CONTRIBUTING guide with checks and tips --- CONTRIBUTING.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..8f3a5065 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +Contributing + +Thanks for your interest in contributing to Ver-ID UI for Android! + +Before you open a pull request: + +- Ensure `./gradlew :veridui:lint :veridui:test` passes +- Build `:veridui` and `:sample` locally (or let CI validate) +- Follow the PR template and keep changes focused and scoped + +Development tips + +- Use Android Studio Giraffe or newer with JDK 17 +- Run the sample app on a physical device for camera features + +Project scripts + +- Translation completeness check: `python3 test_translation.py veridui/src/main/assets/.xml` + From 894b7fa1a7ceccdd9ff5c44047f795baea3b3f1b Mon Sep 17 00:00:00 2001 From: jihanurrahman33 Date: Thu, 4 Sep 2025 03:27:09 +0600 Subject: [PATCH 11/11] Cleanup: Remove unused SessionController1 and fix incorrect Runnable import in SessionView --- .../verid/ui2/SessionController1.kt | 322 ------------------ .../com/appliedrec/verid/ui2/SessionView.kt | 3 +- 2 files changed, 1 insertion(+), 324 deletions(-) delete mode 100644 veridui/src/main/java/com/appliedrec/verid/ui2/SessionController1.kt diff --git a/veridui/src/main/java/com/appliedrec/verid/ui2/SessionController1.kt b/veridui/src/main/java/com/appliedrec/verid/ui2/SessionController1.kt deleted file mode 100644 index 24150067..00000000 --- a/veridui/src/main/java/com/appliedrec/verid/ui2/SessionController1.kt +++ /dev/null @@ -1,322 +0,0 @@ -package com.appliedrec.verid.ui2 - -import android.Manifest -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.Matrix -import android.media.ImageReader -import android.media.ImageReader.OnImageAvailableListener -import android.net.Uri -import android.view.Surface -import android.view.View -import androidx.core.content.ContextCompat -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import com.appliedrec.verid.core2.session.CoreSession -import com.appliedrec.verid.core2.session.FaceBounds -import com.appliedrec.verid.core2.session.FaceCapture -import com.appliedrec.verid.core2.session.FaceDetectionResult -import com.appliedrec.verid.core2.session.RegistrationSessionSettings -import com.appliedrec.verid.core2.session.VerIDSessionException -import com.appliedrec.verid.core2.session.VerIDSessionResult -import com.appliedrec.verid.core2.util.Log -import com.appliedrec.verid.ui2.ISessionView.SessionViewListener -import com.appliedrec.verid.ui2.SessionFailureDialogFactory.OnDismissAction -import kotlinx.coroutines.launch -import java.io.File -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicReference - -class SessionController1( - val activity: SessionActivity, - val sessionParameters: SessionParameters -) : CameraWrapper.Listener, SessionViewListener, OnImageAvailableListener, DefaultLifecycleObserver where T : View, T : ISessionView { - - private enum class State { - IDLE, - STARTING, - STARTED, - CLOSING - } - - val sessionView: T - - private val sessionPrompts: SessionPrompts - private val coreSession: CoreSession - private val cameraWrapper: CameraWrapper<*> - private val faceBounds: AtomicReference - get() = sessionView.faceBounds - private val exifOrientation: AtomicInteger - get() = coreSession.exifOrientation - private val isMirrored: AtomicBoolean - get() = coreSession.isMirrored - private val faceImageHeight: Int - get() = sessionView.capturedFaceImageHeight - private val isSessionRunning: AtomicBoolean = AtomicBoolean(false) - private val faceCaptureCount: AtomicInteger = AtomicInteger(0) - private val cameraState: AtomicReference = AtomicReference(State.IDLE) - private lateinit var camera: Camera - - init { - sessionView = sessionParameters.getSessionViewFactory().apply(activity) as T - sessionView.defaultFaceExtents = sessionParameters.sessionSettings.expectedFaceExtents - sessionView.setSessionSettings(sessionParameters.sessionSettings) - sessionView.addListener(this) - sessionPrompts = SessionPrompts(sessionParameters.stringTranslator) - coreSession = CoreSession( - sessionParameters.verID, - sessionParameters.sessionSettings, - faceBounds, - activity - ) - cameraWrapper = CameraWrapper( - activity, - sessionParameters.cameraLocation, - this, - exifOrientation, - isMirrored, - sessionParameters.videoRecorder.orElse(null) - ) - cameraWrapper.capturedImageMinimumArea = sessionParameters.minImageArea - cameraWrapper.setPreviewClass(sessionView.previewClass) - cameraWrapper.addListener(this) - coreSession.faceDetectionLiveData.observe( - activity, - { faceDetectionResult: FaceDetectionResult -> - this.onFaceDetection( - faceDetectionResult - ) - }) - coreSession.faceCaptureLiveData.observe( - activity, - { faceCapture: FaceCapture -> this.onFaceCapture(faceCapture) }) - coreSession.sessionResultLiveData.observe( - activity, - { result: VerIDSessionResult -> this.onSessionResult(result) }) - sessionParameters.videoRecorder.ifPresent({ videoRecorder: ISessionVideoRecorder? -> - activity.lifecycle.addObserver( - videoRecorder!! - ) - }) - } - - fun startSession() { - if (isSessionRunning.compareAndSet(false, true)) { - faceCaptureCount.set(0) - sessionView.onSessionStarted() - coreSession.start() - } else { - Log.v("SessionActivity.startSession: session already running") - } - } - - fun cancelSession() { - if (isSessionRunning.compareAndSet(true, false)) { - coreSession.cancel() - sessionParameters.onSessionCancelledRunnable.ifPresent { it.run() } - } else { - Log.v("SessionActivity.cancelSession: session not running") - } - cleanup() - } - - fun onCameraPermissionGranted() { - if (cameraState.get() == State.STARTING) { - cameraWrapper.start(sessionView.width, sessionView.height, sessionView.displayRotation) - } - } - - private fun cleanup() { - sessionView.removeListener(this) - cameraWrapper.removeListener(this) - } - - private fun startCamera() { - if (cameraState.compareAndSet(State.IDLE, State.STARTING)) { - if (!hasCameraPermission()) { - activity.requestPermissions(arrayOf(Manifest.permission.CAMERA), SessionActivity.REQUEST_CODE_CAMERA_PERMISSION) - return - } - cameraWrapper.start(sessionView.width, sessionView.height, sessionView.displayRotation) - } - } - - private fun stopCamera() { - if (cameraState.get() == State.STARTED || cameraState.get() == State.STARTING) { - cameraState.set(State.CLOSING) - cameraWrapper.stop() - } - } - - private fun hasCameraPermission(): Boolean { - return ContextCompat.checkSelfPermission( - activity, - Manifest.permission.CAMERA - ) == PackageManager.PERMISSION_GRANTED - } - - //region CameraWrapper.Listener - - override fun onCameraPreviewSize(width: Int, height: Int, sensorOrientation: Int) { - sessionView.setPreviewSize(width, height, sensorOrientation) - sessionView.setCameraPreviewMirrored(sessionParameters.cameraLocation == CameraLocation.FRONT) - } - - override fun onCameraError(error: VerIDSessionException) { - cameraState.set(State.IDLE) - if (isSessionRunning.compareAndSet(true, false)) { - cancelSession() - onSessionResult(VerIDSessionResult(error, 0, 0, null)) - } - } - - override fun onCameraStarted() { - cameraState.set(State.STARTED) - } - - override fun onCameraStopped() { - cameraState.set(State.IDLE) - } - - //endregion - - //region SessionViewListener - - override fun onPreviewSurfaceCreated(surface: Surface) { - cameraWrapper.setPreviewSurface(surface) - startCamera() - } - - override fun onPreviewSurfaceDestroyed() { - stopCamera() - } - - //endregion - - //region OnImageAvailableListener - - override fun onImageAvailable(imageReader: ImageReader?) { - if (isSessionRunning.get()) { - coreSession.onImageAvailable(imageReader) - } - } - - //endregion - - //region LiveData observers - - private fun onFaceDetection(faceDetectionResult: FaceDetectionResult) { - sessionParameters.faceDetectionResultObserver.ifPresent { observer -> - observer.onChanged(faceDetectionResult) - } - val prompt: String? = sessionPrompts.promptFromFaceDetectionResult(faceDetectionResult).orElse(null) - sessionView.setFaceDetectionResult(faceDetectionResult, prompt) - } - - private fun onFaceCapture(faceCapture: FaceCapture) { - sessionParameters.faceCaptureObserver.ifPresent { observer -> - observer.onChanged(faceCapture) - } - if (sessionParameters.sessionSettings !is RegistrationSessionSettings) { - return - } - activity.lifecycleScope.launch { - val targetHeight: Float = faceImageHeight.toFloat() - val scale: Float = targetHeight / faceCapture.faceImage.height.toFloat() - var bitmap: Bitmap = Bitmap.createScaledBitmap( - faceCapture.faceImage, - Math.round(faceCapture.faceImage.width.toFloat() * scale), - Math.round(faceCapture.faceImage.height.toFloat() * scale), - true - ) - if (sessionParameters.cameraLocation == CameraLocation.FRONT) { - val matrix: Matrix = Matrix() - matrix.setScale(-1f, 1f) - bitmap = Bitmap.createBitmap( - bitmap, - 0, - 0, - bitmap.width, - bitmap.height, - matrix, - false - ) - } - // TODO: Fix this -// activity.faceImages.add(bitmap) -// val drawables: List = activity.createFaceDrawables() -// withContext(Dispatchers.Main.immediate) { -// activity.drawFaces(drawables) -// } - } - } - - private fun onSessionResult(result: VerIDSessionResult) { - stopCamera() - if (!isSessionRunning.compareAndSet(true, false)) { - return - } - sessionParameters.videoRecorder.flatMap({ recorder: ISessionVideoRecorder -> - recorder.stop() - recorder.videoFile - }).ifPresent({ videoFile: File? -> result.setVideoUri(Uri.fromFile(videoFile)) }) - sessionParameters.sessionResultObserver.ifPresent { observer -> - observer.onChanged(result) - } - if (result.error.isPresent && sessionParameters.shouldRetryOnFailure().map { onFail -> onFail.apply(result.error.get()) }.orElse(false)) { - val alertDialog = sessionParameters.sessionFailureDialogFactory.makeDialog( - activity, - { onDismissAction: OnDismissAction? -> - if (onDismissAction != null) { - when (onDismissAction) { - OnDismissAction.RETRY -> { - startCamera() - startSession() - } - OnDismissAction.CANCEL -> { - sessionParameters.onSessionCancelledRunnable.ifPresent { it.run() } - cancelSession() - } - - OnDismissAction.SHOW_TIPS -> { - val tipsActivityIntent = sessionParameters.tipsIntentSupplier.apply(activity) - activity.startActivity(tipsActivityIntent) - } - } - } - }, - result.error.get(), - sessionParameters.stringTranslator - ) - if (alertDialog != null) { - alertDialog.show() - return - } - } - sessionView.willFinishWithResult(result) { - sessionParameters.onSessionFinishedRunnable.ifPresent { - it.run() - activity.finish() - } - } - } - - //endregion - - //region DefaultLifecycleObserver - - override fun onPause(owner: LifecycleOwner) { - super.onPause(owner) - stopCamera() - } - - override fun onDestroy(owner: LifecycleOwner) { - super.onDestroy(owner) - stopCamera() - cancelSession() - } - - //endregion -} \ No newline at end of file diff --git a/veridui/src/main/java/com/appliedrec/verid/ui2/SessionView.kt b/veridui/src/main/java/com/appliedrec/verid/ui2/SessionView.kt index a1d9e22b..62610722 100644 --- a/veridui/src/main/java/com/appliedrec/verid/ui2/SessionView.kt +++ b/veridui/src/main/java/com/appliedrec/verid/ui2/SessionView.kt @@ -71,7 +71,6 @@ import com.appliedrec.verid.ui2.ui.theme.SessionTheme import com.appliedrec.verid.ui2.ui.theme.VerIDTheme import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Runnable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -654,4 +653,4 @@ fun DirectionArrow( color = arrowColor, style = Stroke(width = strokeWidth, cap = StrokeCap.Round, join = StrokeJoin.Round)) } -} \ No newline at end of file +}