diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ad77196c36..442c447298 100755
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -323,7 +323,6 @@ dependencies {
implementation(projects.layouteditor)
implementation(projects.idetooltips)
- implementation(projects.cvImageToXml)
implementation(projects.composePreview)
implementation(projects.gitCore)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e1f73811c1..91ca818848 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -25,6 +25,7 @@
+
+
+
+
+
+
+
+ Log.e(TAG, "Failed to instantiate plugin fragment: $fragmentClassName", error)
+ finish()
+ return
+ }
+
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.plugin_screen_container, fragment, TAG_PLUGIN_SCREEN)
+ .commit()
+ }
+
+ override fun onDestroy() {
+ val pluginId = pluginId
+ val fragmentClassName = fragmentClassName
+
+ if (!pluginId.isNullOrBlank() && !fragmentClassName.isNullOrBlank()) {
+ PluginFragmentFactory.unregisterPluginClassLoader(pluginId, listOf(fragmentClassName))
+ }
+
+ super.onDestroy()
+ }
+
+ companion object {
+ private const val TAG = "PluginScreenActivity"
+ private const val TAG_PLUGIN_SCREEN = "plugin_screen"
+ }
+}
diff --git a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
index 3369b0edc3..e2d50f51d3 100755
--- a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
+++ b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
@@ -46,7 +46,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
-import org.appdevforall.codeonthego.computervision.di.computerVisionModule
import org.jetbrains.kotlin.cli.jvm.compiler.setupIdeaStandaloneExecution
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.GlobalContext
@@ -198,7 +197,7 @@ class IDEApplication :
runCatching { GlobalContext.get() }.getOrNull()?.let { return }
startKoin {
androidContext(this@IDEApplication)
- modules(coreModule, pluginModule, computerVisionModule)
+ modules(coreModule, pluginModule)
}
}
diff --git a/app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt b/app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt
index 0a3e898137..0376e3c57c 100644
--- a/app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt
+++ b/app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt
@@ -58,7 +58,6 @@ import com.itsaky.androidide.actions.text.RedoAction
import com.itsaky.androidide.actions.text.UndoAction
import com.itsaky.androidide.actions.PluginActionItem
import com.itsaky.androidide.actions.build.PluginBuildActionItem
-import com.itsaky.androidide.actions.etc.GenerateXMLAction
import com.itsaky.androidide.plugins.extensions.UIExtension
import com.itsaky.androidide.plugins.manager.build.PluginBuildActionManager
import com.itsaky.androidide.plugins.manager.core.PluginManager
@@ -101,9 +100,6 @@ class EditorActivityActions {
registry.registerAction(FindInProjectAction(context, order++))
registry.registerAction(LaunchAppAction(context, order++))
registry.registerAction(DisconnectLogSendersAction(context, order++))
- if (FeatureFlags.isExperimentsEnabled) {
- registry.registerAction(action = GenerateXMLAction(context, order = order++))
- }
// Plugin contributions
order = registerPluginActions(context, registry, order)
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
new file mode 100644
index 0000000000..76a5a4393e
--- /dev/null
+++ b/app/src/main/res/values/ids.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/cv-image-to-xml/build.gradle.kts b/cv-image-to-xml/build.gradle.kts
deleted file mode 100644
index 6c3860fedb..0000000000
--- a/cv-image-to-xml/build.gradle.kts
+++ /dev/null
@@ -1,57 +0,0 @@
-plugins {
- id("com.android.library")
- id("org.jetbrains.kotlin.android")
-}
-
-android {
- namespace = "org.appdevforall.codeonthego.computervision"
-
- defaultConfig {
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- }
-
- buildFeatures {
- viewBinding = true
- }
-}
-
-dependencies {
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.appcompat)
- implementation(libs.google.material)
- implementation(libs.androidx.activity.ktx)
- implementation(libs.androidx.constraintlayout)
-
- implementation(libs.androidx.lifecycle.viewmodel.ktx)
- implementation(libs.androidx.lifecycle.runtime.ktx)
- implementation(libs.common.kotlin.coroutines.android)
-
- implementation(libs.koin.android)
-
- implementation("com.google.ai.edge.litert:litert:1.0.1")
- implementation("com.google.ai.edge.litert:litert-support:1.0.1")
- implementation("com.google.ai.edge.litert:litert-gpu:1.0.1")
-
- implementation("com.google.mlkit:text-recognition:16.0.1")
-
- implementation(libs.composite.fuzzysearch)
-
- testImplementation(libs.tests.junit)
- androidTestImplementation(libs.tests.androidx.junit)
- androidTestImplementation(libs.tests.androidx.espresso.core)
- implementation(projects.commonUi)
-
- implementation(platform(libs.firebase.bom))
- implementation(libs.firebase.analytics)
- api(libs.google.gson)
-}
diff --git a/cv-image-to-xml/src/androidTest/java/com/example/images/ExampleInstrumentedTest.kt b/cv-image-to-xml/src/androidTest/java/com/example/images/ExampleInstrumentedTest.kt
deleted file mode 100644
index 5c5450ad20..0000000000
--- a/cv-image-to-xml/src/androidTest/java/com/example/images/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.example.images
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.example.images", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/AndroidManifest.xml b/cv-image-to-xml/src/main/AndroidManifest.xml
deleted file mode 100644
index 46e2df2dbf..0000000000
--- a/cv-image-to-xml/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/assets/best_float32.tflite b/cv-image-to-xml/src/main/assets/best_float32.tflite
deleted file mode 100644
index 6593e433a9..0000000000
Binary files a/cv-image-to-xml/src/main/assets/best_float32.tflite and /dev/null differ
diff --git a/cv-image-to-xml/src/main/assets/labels.txt b/cv-image-to-xml/src/main/assets/labels.txt
deleted file mode 100644
index 04ea3c3e61..0000000000
--- a/cv-image-to-xml/src/main/assets/labels.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-generic_box
-dropdown_symbol
-image_placeholder
-button
-checkbox_checked
-checkbox_unchecked
-radio_button_unchecked
-radio_button_checked
-slider
-switch_off
-switch_on
-widget_tag
-margin_metadata
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/repository/DrawableImportHelper.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/repository/DrawableImportHelper.kt
deleted file mode 100644
index 6a3298176b..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/repository/DrawableImportHelper.kt
+++ /dev/null
@@ -1,174 +0,0 @@
-package org.appdevforall.codeonthego.computervision.data.repository
-
-import android.content.ContentResolver
-import android.net.Uri
-import android.provider.OpenableColumns
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import java.io.File
-import java.util.Locale
-
-/**
- * Helper class responsible for importing and managing image files within an Android project's
- * resource directory (e.g., res/drawable).
- *
- * @property contentResolver The ContentResolver used to read data from content URIs.
- */
-class DrawableImportHelper(
- private val contentResolver: ContentResolver
-) {
-
- /**
- * Imports an image from a given URI into the 'res/drawable' directory associated with
- * the provided layout file.
- *
- * @param sourceUri The URI of the image to import.
- * @param layoutFilePath The absolute path to the current layout XML file. Used to locate the 'res' directory.
- * @param fallbackName A name to use if the original file name cannot be resolved.
- * @return A [Result] containing an [ImportedDrawable] if successful, or an exception on failure.
- */
- suspend fun importDrawable(
- sourceUri: Uri,
- layoutFilePath: String?,
- fallbackName: String
- ): Result = withContext(Dispatchers.IO) {
- runCatching {
- requireNotNull(layoutFilePath) { "Layout file path is not available." }
-
- val drawableDir = getOrCreateDrawableDirectory(layoutFilePath)
- val extension = resolveSupportedExtension(sourceUri, fallbackName)
- val baseName = sanitizeResourceName(resolveDisplayName(sourceUri) ?: fallbackName)
- val destinationFile = resolveAvailableFile(drawableDir, baseName, extension)
-
- copyImageToDestination(sourceUri, destinationFile)
-
- ImportedDrawable(
- resourceName = destinationFile.nameWithoutExtension,
- drawableReference = "@drawable/${destinationFile.nameWithoutExtension}",
- file = destinationFile
- )
- }
- }
-
- /**
- * Deletes an imported drawable file from the filesystem.
- *
- * @param layoutFilePath The absolute path to the current layout XML file. Used to locate the 'res' directory.
- * @param resourceName The sanitized name of the resource to delete (without extension).
- * @return A [Result] indicating success (true if deleted, false if file did not exist) or failure.
- */
- suspend fun deleteDrawable(
- layoutFilePath: String?,
- resourceName: String
- ): Result = withContext(Dispatchers.IO) {
- runCatching {
- requireNotNull(layoutFilePath) { "Layout file path is not available." }
-
- val drawableDir = resolveDrawableDir(File(layoutFilePath))
- if (!drawableDir.exists()) return@runCatching false
-
- val targetFile = findFileByResourceName(drawableDir, resourceName)
-
- targetFile?.delete() ?: false
- }
- }
-
- private fun getOrCreateDrawableDirectory(layoutFilePath: String): File {
- val layoutFile = File(layoutFilePath)
- val drawableDir = resolveDrawableDir(layoutFile)
- check(drawableDir.exists() || drawableDir.mkdirs()) {
- "Could not create drawable directory: ${drawableDir.absolutePath}"
- }
- return drawableDir
- }
-
- private fun copyImageToDestination(sourceUri: Uri, destinationFile: File) {
- contentResolver.openInputStream(sourceUri)?.use { input ->
- destinationFile.outputStream().use(input::copyTo)
- } ?: error("Could not open selected image.")
- }
-
- private fun findFileByResourceName(directory: File, resourceName: String): File? {
- return directory.listFiles()?.firstOrNull {
- it.nameWithoutExtension == resourceName
- }
- }
-
- private fun resolveDrawableDir(layoutFile: File): File {
- val resDir = generateSequence(layoutFile.parentFile) { it.parentFile }
- .firstOrNull { it.name == "res" }
- ?: throw IllegalStateException("Could not resolve res directory from: ${layoutFile.absolutePath}")
-
- return File(resDir, "drawable")
- }
-
- private fun resolveDisplayName(uri: Uri): String? {
- return contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)
- ?.use { cursor ->
- val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
- if (index >= 0 && cursor.moveToFirst()) cursor.getString(index) else null
- }
- }
-
- private fun resolveSupportedExtension(uri: Uri, fallbackName: String): String {
- val mimeType = contentResolver.getType(uri)?.lowercase(Locale.US)
- var extension = when (mimeType) {
- "image/png" -> "png"
- "image/jpeg", "image/jpg" -> "jpg"
- "image/webp" -> "webp"
- else -> null
- }
-
- if (extension == null) {
- val nameToUse = resolveDisplayName(uri) ?: fallbackName
- extension = nameToUse
- .substringAfterLast('.', missingDelimiterValue = "")
- .lowercase(Locale.US)
- .takeIf { it.isNotBlank() }
- }
-
- return when (extension) {
- "png", "jpg", "jpeg", "webp" -> extension
- else -> throw IllegalArgumentException("Unsupported image format. Use PNG, JPG, JPEG, or WEBP.")
- }
- }
-
- private fun sanitizeResourceName(rawName: String): String {
- val nameWithoutExtension = rawName.substringBeforeLast('.')
- val normalized = nameWithoutExtension
- .lowercase(Locale.US)
- .replace(Regex("[^a-z0-9_]"), "_")
- .replace(Regex("_+"), "_")
- .trim('_')
-
- val safeName = normalized.ifBlank { "imported_image" }
-
- return if (safeName.first().isDigit()) {
- "img_$safeName"
- } else {
- safeName
- }
- }
-
- private fun resolveAvailableFile(
- drawableDir: File,
- baseName: String,
- extension: String
- ): File {
- var candidate = File(drawableDir, "$baseName.$extension")
- var index = 1
-
- while (!candidate.createNewFile()) {
- candidate = File(drawableDir, "${baseName}_$index.$extension")
- index++
- }
-
- return candidate
- }
-}
-
-data class ImportedDrawable(
- val resourceName: String,
- val drawableReference: String,
- val file: File
-)
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/repository/VisionRepository.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/repository/VisionRepository.kt
deleted file mode 100644
index c4713f1c51..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/repository/VisionRepository.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.appdevforall.codeonthego.computervision.data.repository
-
-import android.graphics.Bitmap
-import com.google.mlkit.vision.text.Text
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-
-/**
- * Abstract contract for computer vision data sources.
- * Handles raw interactions with machine learning models (YOLO, MLKit)
- * without leaking domain logic.
- */
-interface VisionRepository {
- suspend fun initModel(): Result
- suspend fun detectWidgets(bitmap: Bitmap): Result>
- suspend fun recognizeText(bitmap: Bitmap): Result>
- fun isInitialized(): Boolean
- fun release()
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/repository/VisionRepositoryImpl.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/repository/VisionRepositoryImpl.kt
deleted file mode 100644
index 305e55ab1d..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/repository/VisionRepositoryImpl.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package org.appdevforall.codeonthego.computervision.data.repository
-
-import android.content.res.AssetManager
-import android.graphics.Bitmap
-import com.google.mlkit.vision.text.Text
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import org.appdevforall.codeonthego.computervision.data.source.OcrSource
-import org.appdevforall.codeonthego.computervision.data.source.YoloModelSource
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-
-class VisionRepositoryImpl(
- private val assetManager: AssetManager,
- private val yoloModelSource: YoloModelSource,
- private val ocrSource: OcrSource
-) : VisionRepository {
-
- override suspend fun initModel(): Result = withContext(Dispatchers.IO) {
- runCatching {
- yoloModelSource.initialize(assetManager)
- }
- }
-
- override suspend fun detectWidgets(bitmap: Bitmap): Result> =
- withContext(Dispatchers.Default) {
- runCatching { yoloModelSource.runInference(bitmap) }
- }
-
- override suspend fun recognizeText(bitmap: Bitmap): Result> =
- withContext(Dispatchers.Default) {
- ocrSource.recognizeText(bitmap)
- }
-
- override fun isInitialized(): Boolean = yoloModelSource.isInitialized()
-
- override fun release() {
- yoloModelSource.release()
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/source/OcrSource.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/source/OcrSource.kt
deleted file mode 100644
index 4eac4d0378..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/source/OcrSource.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.appdevforall.codeonthego.computervision.data.source
-
-import android.graphics.Bitmap
-import android.util.Log
-import com.google.mlkit.vision.common.InputImage
-import com.google.mlkit.vision.text.Text
-import com.google.mlkit.vision.text.TextRecognition
-import com.google.mlkit.vision.text.latin.TextRecognizerOptions
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlin.coroutines.resume
-
-class OcrSource {
-
- private val textRecognizer by lazy {
- TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
- }
-
- suspend fun recognizeText(bitmap: Bitmap): Result> =
- suspendCancellableCoroutine { continuation ->
- val inputImage = InputImage.fromBitmap(bitmap, 0)
-
- textRecognizer.process(inputImage)
- .addOnSuccessListener { visionText ->
- continuation.resume(Result.success(visionText.textBlocks))
- }
- .addOnFailureListener { exception ->
- continuation.resume(Result.failure(exception))
- }
- }
-}
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/source/YoloModelSource.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/source/YoloModelSource.kt
deleted file mode 100644
index 6c305eb79a..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/data/source/YoloModelSource.kt
+++ /dev/null
@@ -1,156 +0,0 @@
-package org.appdevforall.codeonthego.computervision.data.source
-
-import android.content.res.AssetManager
-import android.graphics.Bitmap
-import android.graphics.RectF
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-import org.tensorflow.lite.DataType
-import org.tensorflow.lite.Interpreter
-import org.tensorflow.lite.support.common.ops.CastOp
-import org.tensorflow.lite.support.common.ops.NormalizeOp
-import org.tensorflow.lite.support.image.ImageProcessor
-import org.tensorflow.lite.support.image.TensorImage
-import org.tensorflow.lite.support.image.ops.ResizeOp
-import org.tensorflow.lite.support.tensorbuffer.TensorBuffer
-import java.io.FileInputStream
-import java.io.IOException
-import java.nio.MappedByteBuffer
-import java.nio.channels.FileChannel
-
-class YoloModelSource {
-
- private var interpreter: Interpreter? = null
- private var labels: List = emptyList()
-
- companion object {
- private const val MODEL_INPUT_WIDTH = 640
- private const val MODEL_INPUT_HEIGHT = 640
- private const val CONFIDENCE_THRESHOLD = 0.2f
- private const val NMS_THRESHOLD = 0.45f
- }
-
- @Throws(IOException::class)
- fun initialize(assetManager: AssetManager, modelPath: String = "best_float32.tflite", labelsPath: String = "labels.txt") {
- interpreter = Interpreter(loadModelFile(assetManager, modelPath))
- labels = assetManager.open(labelsPath).bufferedReader()
- .useLines { lines -> lines.map { it.trim() }.toList() }
- }
-
- fun isInitialized(): Boolean = interpreter != null && labels.isNotEmpty()
-
- fun runInference(bitmap: Bitmap): List {
- val interp = interpreter ?: throw IllegalStateException("Model not initialized")
-
- val imageProcessor = ImageProcessor.Builder()
- .add(ResizeOp(MODEL_INPUT_HEIGHT, MODEL_INPUT_WIDTH, ResizeOp.ResizeMethod.BILINEAR))
- .add(NormalizeOp(0.0f, 255.0f))
- .add(CastOp(DataType.FLOAT32))
- .build()
-
- val tensorImage = imageProcessor.process(TensorImage.fromBitmap(bitmap))
- val outputShape = interp.getOutputTensor(0).shape()
- val outputBuffer = TensorBuffer.createFixedSize(outputShape, DataType.FLOAT32)
-
- interp.run(tensorImage.buffer, outputBuffer.buffer.rewind())
-
- return processYoloOutput(outputBuffer, bitmap.width, bitmap.height)
- }
-
- fun release() {
- interpreter?.close()
- interpreter = null
- }
-
- private fun processYoloOutput(buffer: TensorBuffer, imageWidth: Int, imageHeight: Int): List {
- val shape = buffer.shape
- val numProperties = shape[1]
- val numPredictions = shape[2]
- val numClasses = numProperties - 4
- val floatArray = buffer.floatArray
-
- val transposedArray = FloatArray(shape[0] * numPredictions * numProperties)
- for (i in 0 until numPredictions) {
- for (j in 0 until numProperties) {
- transposedArray[i * numProperties + j] = floatArray[j * numPredictions + i]
- }
- }
-
- val allDetections = mutableListOf()
- for (i in 0 until numPredictions) {
- val offset = i * numProperties
- var maxClassScore = 0f
- var classId = -1
- for (j in 0 until numClasses) {
- val classScore = transposedArray[offset + 4 + j]
- if (classScore > maxClassScore) {
- maxClassScore = classScore
- classId = j
- }
- }
- if (maxClassScore > CONFIDENCE_THRESHOLD) {
- val x = transposedArray[offset + 0]
- val y = transposedArray[offset + 1]
- val w = transposedArray[offset + 2]
- val h = transposedArray[offset + 3]
-
- val left = (x - w / 2) * imageWidth
- val top = (y - h / 2) * imageHeight
- val right = (x + w / 2) * imageWidth
- val bottom = (y + h / 2) * imageHeight
-
- val label = labels.getOrElse(classId) { "Unknown" }
- allDetections.add(DetectionResult(RectF(left, top, right, bottom), label, maxClassScore))
- }
- }
-
- return applyNms(allDetections)
- }
-
- private fun applyNms(detections: List): List {
- val finalDetections = mutableListOf()
- val groupedByLabel = detections.groupBy { it.label }
-
- for ((_, group) in groupedByLabel) {
- val sortedDetections = group.sortedByDescending { it.score }
- val remaining = sortedDetections.toMutableList()
-
- while (remaining.isNotEmpty()) {
- val bestDetection = remaining.first()
- finalDetections.add(bestDetection)
- remaining.remove(bestDetection)
-
- val iterator = remaining.iterator()
- while (iterator.hasNext()) {
- val detection = iterator.next()
- if (calculateIoU(bestDetection.boundingBox, detection.boundingBox) > NMS_THRESHOLD) {
- iterator.remove()
- }
- }
- }
- }
-
- return finalDetections
- }
-
- private fun calculateIoU(box1: RectF, box2: RectF): Float {
- val xA = maxOf(box1.left, box2.left)
- val yA = maxOf(box1.top, box2.top)
- val xB = minOf(box1.right, box2.right)
- val yB = minOf(box1.bottom, box2.bottom)
-
- val intersectionArea = maxOf(0f, xB - xA) * maxOf(0f, yB - yA)
- val box1Area = box1.width() * box1.height()
- val box2Area = box2.width() * box2.height()
- val unionArea = box1Area + box2Area - intersectionArea
-
- return if (unionArea == 0f) 0f else intersectionArea / unionArea
- }
-
- @Throws(IOException::class)
- private fun loadModelFile(assetManager: AssetManager, modelPath: String): MappedByteBuffer {
- val fileDescriptor = assetManager.openFd(modelPath)
- return FileInputStream(fileDescriptor.fileDescriptor).use { inputStream ->
- inputStream.channel.map(FileChannel.MapMode.READ_ONLY, fileDescriptor.startOffset, fileDescriptor.declaredLength)
- }
- }
-}
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/di/ComputerVisionModule.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/di/ComputerVisionModule.kt
deleted file mode 100644
index bd77aa56f3..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/di/ComputerVisionModule.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package org.appdevforall.codeonthego.computervision.di
-
-import org.appdevforall.codeonthego.computervision.data.repository.DrawableImportHelper
-import org.appdevforall.codeonthego.computervision.data.repository.VisionRepository
-import org.appdevforall.codeonthego.computervision.data.repository.VisionRepositoryImpl
-import org.appdevforall.codeonthego.computervision.data.source.OcrSource
-import org.appdevforall.codeonthego.computervision.data.source.YoloModelSource
-import org.appdevforall.codeonthego.computervision.domain.GenericBoxResolver
-import org.appdevforall.codeonthego.computervision.domain.RegionOcrProcessor
-import org.appdevforall.codeonthego.computervision.domain.usecase.GenerateXmlUC
-import org.appdevforall.codeonthego.computervision.domain.usecase.ImportPlaceholderImageUC
-import org.appdevforall.codeonthego.computervision.domain.usecase.PrepareImageUC
-import org.appdevforall.codeonthego.computervision.domain.usecase.RemovePlaceholderImageUC
-import org.appdevforall.codeonthego.computervision.domain.usecase.RunVisionUC
-import org.appdevforall.codeonthego.computervision.ui.viewmodel.ComputerVisionViewModel
-import org.koin.android.ext.koin.androidContext
-import org.koin.core.module.dsl.viewModel
-import org.koin.dsl.module
-
-val computerVisionModule = module {
-
- single { YoloModelSource() }
- single { OcrSource() }
- single { RegionOcrProcessor(ocrSource = get()) }
- single { GenericBoxResolver() }
-
- single {
- VisionRepositoryImpl(
- assetManager = androidContext().assets,
- yoloModelSource = get(),
- ocrSource = get()
- )
- }
-
- single { DrawableImportHelper(contentResolver = androidContext().contentResolver) }
- single { GenerateXmlUC() }
- single { ImportPlaceholderImageUC(drawableImportHelper = get()) }
- single { PrepareImageUC(contentResolver = androidContext().contentResolver) }
- single { RemovePlaceholderImageUC(drawableImportHelper = get()) }
- single { RunVisionUC(repository = get(), boxResolver = get(), regionOcrProcessor = get()) }
-
- viewModel { (layoutFilePath: String?, layoutFileName: String?) ->
- ComputerVisionViewModel(
- repository = get(),
- prepareImageUC = get(),
- runVisionUC = get(),
- generateXmlUC = get(),
- importPlaceholderImageUC = get(),
- removePlaceholderImageUC = get(),
- layoutFilePath = layoutFilePath,
- layoutFileName = layoutFileName
- )
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/DetectionMerger.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/DetectionMerger.kt
deleted file mode 100644
index 1878eac88d..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/DetectionMerger.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain
-
-import android.graphics.RectF
-import com.google.mlkit.vision.text.Text
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-import org.appdevforall.codeonthego.computervision.utils.OcrTextAssembler.joinElementsWithTolerance
-
-class DetectionMerger(
- private val enrichedComponents: List,
- private val remainingYoloDetections: List,
- private val fullImageTextBlocks: List
-) {
-
- private val containerLabels = setOf("card", "toolbar")
-
- fun merge(): List {
- val finalDetections = mutableListOf()
- val usedTextBlocks = mutableSetOf()
-
- finalDetections.addAll(enrichedComponents)
- finalDetections.addAll(remainingYoloDetections)
-
- val containers = remainingYoloDetections.filter { it.label in containerLabels }
- for (container in containers) {
- val candidates = fullImageTextBlocks.filter { it !in usedTextBlocks }
- for (textBlock in candidates) {
- val textBox = textBlock.boundingBox?.let { RectF(it) } ?: continue
- if (container.boundingBox.contains(textBox)) {
- finalDetections.add(
- DetectionResult(
- boundingBox = textBox,
- label = "text",
- score = 0.99f,
- text = textBlock.text.replace("\n", " "),
- isYolo = false
- )
- )
- usedTextBlocks.add(textBlock)
- }
- }
- }
-
- val orphanDetections = fullImageTextBlocks
- .filter { it !in usedTextBlocks }
- .flatMap { it.lines }
- .mapNotNull { line ->
- line.boundingBox?.let { box ->
- DetectionResult(
- boundingBox = RectF(box),
- label = "text",
- score = 0.99f,
- text = joinElementsWithTolerance(line),
- isYolo = false
- )
- }
- }
-
- finalDetections.addAll(orphanDetections)
-
- return finalDetections
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/DetectionScaler.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/DetectionScaler.kt
deleted file mode 100644
index 5f0808731d..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/DetectionScaler.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain
-
-import android.graphics.Rect
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
-import kotlin.math.max
-import kotlin.math.roundToInt
-
-/**
- * Scales the normalized YOLO coordinates (0.0 to 1.0) to the target dimensions
- * in DP (e.g., 360x640) of the Android screen.
- */
-object DetectionScaler {
- private const val MIN_W_ANY = 8
- private const val MIN_H_ANY = 8
-
- fun scale(
- detection: DetectionResult, sourceWidth: Int, sourceHeight: Int, targetW: Int, targetH: Int
- ): ScaledBox {
- if (sourceWidth == 0 || sourceHeight == 0) {
- return ScaledBox(detection.label, detection.text, 0, 0, MIN_W_ANY, MIN_H_ANY, MIN_W_ANY / 2, MIN_H_ANY / 2, Rect(0, 0, MIN_W_ANY, MIN_H_ANY))
- }
- val rect = detection.boundingBox
- val normCx = ((rect.left + rect.right) / 2f) / sourceWidth.toFloat()
- val normCy = ((rect.top + rect.bottom) / 2f) / sourceHeight.toFloat()
- val normW = (rect.right - rect.left) / sourceWidth.toFloat()
- val normH = (rect.bottom - rect.top) / sourceHeight.toFloat()
-
- val x = max(0, ((normCx - normW / 2f) * targetW).roundToInt())
- val y = max(0, ((normCy - normH / 2f) * targetH).roundToInt())
- val w = max(MIN_W_ANY, (normW * targetW).roundToInt())
- val h = max(MIN_H_ANY, (normH * targetH).roundToInt())
-
- return ScaledBox(
- detection.label, detection.text, x, y, w, h, x + w / 2, y + h / 2, Rect(x, y, x + w, y + h)
- )
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/GenericBoxResolver.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/GenericBoxResolver.kt
deleted file mode 100644
index bcb16e7071..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/GenericBoxResolver.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain
-
-import android.graphics.RectF
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-import kotlin.math.hypot
-
-class GenericBoxResolver {
-
- fun resolve(detections: List): List {
- val dropdownSymbols = detections.filter { it.label == "dropdown_symbol" }
-
- return detections.mapNotNull { det ->
- when (det.label) {
- "dropdown_symbol" -> null
- "generic_box" -> {
- val hasSymbolNearby = dropdownSymbols.any { symbol ->
- isNearby(det.boundingBox, symbol.boundingBox, 0.8f)
- }
- det.copy(label = if (hasSymbolNearby) "dropdown" else "text_entry_box")
- }
- else -> det
- }
- }
- }
-
- private fun isNearby(box1: RectF, box2: RectF, thresholdFactor: Float = 1.5f): Boolean {
- val avgDim1 = (box1.width() + box1.height()) / 2f
- val distanceThreshold = thresholdFactor * avgDim1
-
- val distance = hypot(
- (box1.centerX() - box2.centerX()).toDouble(),
- (box1.centerY() - box2.centerY()).toDouble()
- ).toFloat()
-
- return distance < distanceThreshold
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/LayoutTreeBuilder.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/LayoutTreeBuilder.kt
deleted file mode 100644
index 0877095b6d..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/LayoutTreeBuilder.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain
-
-import org.appdevforall.codeonthego.computervision.domain.model.LayoutItem
-import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
-import kotlin.math.abs
-import kotlin.math.max
-
-/**
- * Analyzes the spatial distribution of the detected boxes and builds
- * a logical visual hierarchy (tree of Layouts, Rows, RadioGroups, etc.)
- * based on vertical alignment and overlap.
- */
-object LayoutTreeBuilder {
- private const val OVERLAP_THRESHOLD = 0.6
- private const val VERTICAL_ALIGN_THRESHOLD = 20
-
- fun buildLayoutTree(boxes: List): List {
- val rows = groupIntoRows(boxes)
- val items = mutableListOf()
- val verticalRadioRun = mutableListOf()
- val verticalCheckboxRun = mutableListOf()
-
- fun flushRuns() {
- if (verticalRadioRun.isNotEmpty()) {
- items.add(LayoutItem.RadioGroup(verticalRadioRun.toList(), "vertical"))
- verticalRadioRun.clear()
- }
- if (verticalCheckboxRun.isNotEmpty()) {
- items.add(LayoutItem.CheckboxGroup(verticalCheckboxRun.toList(), "vertical"))
- verticalCheckboxRun.clear()
- }
- }
-
- rows.forEach { row ->
- val isRadioRow = row.all { isRadioButton(it) }
- val isCheckboxRow = row.all { isCheckbox(it) }
-
- if (!isRadioRow && verticalRadioRun.isNotEmpty()) flushRuns()
- if (!isCheckboxRow && verticalCheckboxRun.isNotEmpty()) flushRuns()
-
- when {
- isRadioRow && row.size == 1 -> verticalRadioRun.add(row.first())
- isRadioRow -> items.add(LayoutItem.RadioGroup(row, "horizontal"))
- isCheckboxRow && row.size == 1 -> verticalCheckboxRun.add(row.first())
- isCheckboxRow -> items.add(LayoutItem.CheckboxGroup(row, "horizontal"))
- else -> {
- flushRuns()
- if (row.size == 1) {
- items.add(LayoutItem.SimpleView(row.first()))
- } else {
- items.add(LayoutItem.HorizontalRow(row))
- }
- }
- }
- }
- flushRuns()
-
- return items
- }
-
- private fun groupIntoRows(boxes: List): List> {
- val rows = mutableListOf()
-
- boxes.sortedWith(compareBy({ it.y }, { it.x })).forEach { box ->
- val row = rows.firstOrNull { it.accepts(box) }
- if (row == null) {
- rows.add(LayoutRow(box))
- } else {
- row.add(box)
- }
- }
-
- return rows.sortedBy { it.top }.map { it.boxes.sortedBy(ScaledBox::x) }
- }
-
- private fun isRadioButton(box: ScaledBox): Boolean =
- box.label == "radio_button_unchecked" || box.label == "radio_button_checked"
-
- private fun isCheckbox(box: ScaledBox): Boolean =
- box.label == "checkbox_unchecked" || box.label == "checkbox_checked"
-
- private class LayoutRow(initialBox: ScaledBox) {
- private val _boxes = mutableListOf(initialBox)
- val boxes: List get() = _boxes
-
- var top: Int = initialBox.y
- private set
- var bottom: Int = initialBox.y + initialBox.h
- private set
-
- val height: Int get() = bottom - top
- val centerY: Int get() = top + height / 2
-
- fun add(box: ScaledBox) {
- _boxes.add(box)
- top = minOf(top, box.y)
- bottom = maxOf(bottom, box.y + box.h)
- }
-
- fun accepts(box: ScaledBox): Boolean {
- val verticalOverlap = minOf(box.y + box.h, bottom) - maxOf(box.y, top)
- val minHeight = minOf(box.h, height).coerceAtLeast(1)
- val overlapRatio = verticalOverlap.toFloat() / minHeight.toFloat()
- val centerDelta = abs(box.centerY - centerY)
- val centerThreshold = max(VERTICAL_ALIGN_THRESHOLD, minHeight / 2)
-
- return overlapRatio >= OVERLAP_THRESHOLD || centerDelta <= centerThreshold
- }
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/MarginAnnotationParser.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/MarginAnnotationParser.kt
deleted file mode 100644
index 02c33c8470..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/MarginAnnotationParser.kt
+++ /dev/null
@@ -1,231 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain
-
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-import org.appdevforall.codeonthego.computervision.utils.MetadataDetector
-import kotlin.math.abs
-
-object MarginAnnotationParser {
-
- /**
- * Extracts canvas UI elements and parses margin annotations, linking them together.
- * * @param detections The full list of detections from YOLO and OCR.
- * @param imageWidth The width of the source image in pixels.
- * @param leftGuidePct The percentage (0.0 to 1.0) defining the left margin boundary.
- * @param rightGuidePct The percentage (0.0 to 1.0) defining the right margin boundary.
- * @return A Pair containing the valid canvas UI detections and a mapped dictionary of [Widget Tag -> Annotation Text].
- */
- fun parse(
- detections: List,
- imageWidth: Int,
- leftGuidePct: Float,
- rightGuidePct: Float
- ): Pair, Map> {
- val sanitizedDetections = detections.filterNot { MetadataDetector.isMetadataLabel(it.label) }
-
- val distribution = distributeDetections(sanitizedDetections, imageWidth, leftGuidePct, rightGuidePct)
- val canvasTags = extractCanvasTags(distribution.canvas)
-
- val annotationMap = parseMarginsGlobally(
- leftMargin = distribution.leftMargin,
- rightMargin = distribution.rightMargin,
- canvasTags = canvasTags
- )
-
- return Pair(distribution.canvas, annotationMap)
- }
-
- /**
- * Distributes raw detections into three zones: left margin, right margin, and the main canvas.
- * Metadata detections invading the canvas are forcefully pushed back to the margins.
- */
- private fun distributeDetections(
- detections: List,
- imageWidth: Int,
- leftGuidePct: Float,
- rightGuidePct: Float
- ): DetectionDistribution {
- val leftMarginPx = imageWidth * leftGuidePct
- val rightMarginPx = imageWidth * rightGuidePct
-
- val canvas = mutableListOf()
- val leftMargin = mutableListOf()
- val rightMargin = mutableListOf()
-
- for (detection in detections) {
- val isMetadata = MetadataDetector.isCanvasMetadata(detection.text)
- val centerX = centerX(detection)
-
- when {
- isMetadata && centerX < (imageWidth / 2f) -> leftMargin.add(detection)
- isMetadata && centerX >= (imageWidth / 2f) -> rightMargin.add(detection)
- centerX > leftMarginPx && centerX < rightMarginPx -> canvas.add(detection)
- centerX <= leftMarginPx -> leftMargin.add(detection)
- else -> rightMargin.add(detection)
- }
- }
-
- return DetectionDistribution(canvas, leftMargin, rightMargin)
- }
-
- /**
- * Identifies and extracts valid widget tags from the canvas detections.
- */
- private fun extractCanvasTags(canvasDetections: List): List> {
- return canvasDetections.mapNotNull { det ->
- WidgetTagParser.extractTag(det.text)?.let { (tag, _) -> tag to det }
- }
- }
-
- /**
- * Processes both margins simultaneously to prevent cross-margin collisions.
- * Gathers all explicit annotations first, then resolves all implicit blocks
- * against a shared pool of remaining tags.
- */
- private fun parseMarginsGlobally(
- leftMargin: List,
- rightMargin: List,
- canvasTags: List>
- ): Map {
- val leftBlocks = extractBlocks(leftMargin.sortedBy { it.boundingBox.top })
- val rightBlocks = extractBlocks(rightMargin.sortedBy { it.boundingBox.top })
-
- val globalExplicitAnnotations = mergeAnnotations(
- leftBlocks.explicitAnnotations,
- rightBlocks.explicitAnnotations
- )
-
- val allImplicitBlocks = leftBlocks.implicitBlocks + rightBlocks.implicitBlocks
-
- val resolvedImplicitAnnotations = resolveImplicitBlocks(
- implicitBlocks = allImplicitBlocks,
- canvasTags = canvasTags,
- existingAnnotations = globalExplicitAnnotations
- )
-
- return mergeAnnotations(globalExplicitAnnotations, resolvedImplicitAnnotations)
- }
-
- /**
- * Merges multiple annotation maps. If a tag exists in multiple maps,
- * their values are combined separated by " | ".
- */
- private fun mergeAnnotations(vararg maps: Map): MutableMap {
- return maps.flatMap { it.toList() }
- .groupBy({ it.first }, { it.second })
- .mapValues { (_, values) -> values.joinToString(" | ") }
- .toMutableMap()
- }
-
- /**
- * Reads vertically through margin detections and groups them into text blocks.
- * Blocks starting with an explicit tag become explicit annotations, while untagged blocks are stored as implicit.
- * Side-based prefix heuristics are intentionally not applied here because OCR can place
- * a valid explicit tag on the opposite margin from its detected canvas tag.
- */
- private fun extractBlocks(
- sortedDetections: List
- ): GroupedBlocks {
- val blocks = GroupedBlocks()
- var currentTag: String? = null
- var currentText = StringBuilder()
- var blockStartY = 0f
-
- fun saveCurrentBlock() {
- if (currentTag != null) {
- blocks.explicitAnnotations[currentTag!!] = currentText.toString().trim()
- } else if (currentText.isNotBlank()) {
- blocks.implicitBlocks.add(ParsedBlock(currentText.toString().trim(), blockStartY))
- }
- }
-
- for (det in sortedDetections) {
- val text = det.text.trim().trimStart('|', ':', ';', '.', ',', '_')
- val extraction = WidgetTagParser.extractTag(text)
-
- val isExplicitTag = extraction != null
-
- if (isExplicitTag) {
- saveCurrentBlock()
-
- currentTag = extraction.first
- currentText = StringBuilder()
- blockStartY = centerY(det)
-
- val trailing = extraction.second?.trim()
- if (!trailing.isNullOrBlank() && WidgetTagParser.normalizeTagText(trailing) != currentTag) {
- currentText.append(trailing).append(" ")
- }
- } else {
- if (currentText.isEmpty()) blockStartY = centerY(det)
- currentText.append(text).append(" ")
- }
- }
- saveCurrentBlock()
-
- return blocks
- }
-
- /**
- * Resolves implicit (untagged) text blocks by associating them with the nearest vertical canvas tag
- * of the most appropriate prefix.
- */
- private fun resolveImplicitBlocks(
- implicitBlocks: List,
- canvasTags: List>,
- existingAnnotations: Map
- ): Map {
- val resolvedAnnotations = mutableMapOf()
-
- val canvasTagsByPrefix = canvasTags
- .groupBy { (tag, _) -> tag.substringBefore('-') }
- .mapValues { (_, tags) -> tags.sortedBy { (_, det) -> centerY(det) } }
-
- val unresolvedTagsByPrefix = canvasTagsByPrefix
- .mapValues { (_, tags) ->
- tags.map { it.first }
- .filter { tag -> tag !in existingAnnotations }
- .sortedBy { tag -> WidgetTagParser.extractOrdinal(tag) ?: Int.MAX_VALUE }
- .toMutableList()
- }.toMutableMap()
-
- for (block in implicitBlocks.sortedBy { it.centerY }) {
- val closestPrefix = unresolvedTagsByPrefix
- .filterValues { it.isNotEmpty() }
- .minByOrNull { (prefix, remainingTags) ->
- val nearestTagY = canvasTagsByPrefix[prefix]
- ?.firstOrNull { (tag, _) -> tag == remainingTags.firstOrNull() }
- ?.second?.let { centerY(it) } ?: Float.MAX_VALUE
- abs(nearestTagY - block.centerY)
- }?.key ?: continue
-
- val assignedTag = unresolvedTagsByPrefix[closestPrefix]?.removeFirstOrNull() ?: continue
- resolvedAnnotations[assignedTag] = block.annotationText
- }
-
- return resolvedAnnotations
- }
-
- private fun centerX(detection: DetectionResult): Float {
- return (detection.boundingBox.left + detection.boundingBox.right) / 2f
- }
-
- private fun centerY(detection: DetectionResult): Float {
- return (detection.boundingBox.top + detection.boundingBox.bottom) / 2f
- }
-
- private data class DetectionDistribution(
- val canvas: List,
- val leftMargin: List,
- val rightMargin: List
- )
-
- private data class GroupedBlocks(
- val explicitAnnotations: MutableMap = mutableMapOf(),
- val implicitBlocks: MutableList = mutableListOf()
- )
-
- private data class ParsedBlock(
- val annotationText: String,
- val centerY: Float
- )
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/RegionOcrProcessor.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/RegionOcrProcessor.kt
deleted file mode 100644
index 72653ba6ef..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/RegionOcrProcessor.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain
-
-import android.graphics.Bitmap
-import android.graphics.RectF
-import com.google.mlkit.vision.text.Text
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
-import org.appdevforall.codeonthego.computervision.data.source.OcrSource
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-import org.appdevforall.codeonthego.computervision.utils.BitmapUtils
-import org.appdevforall.codeonthego.computervision.utils.OcrTextAssembler
-
-class RegionOcrProcessor(
- private val ocrSource: OcrSource,
- private val componentPadding: Int = 10
-) {
-
- private val interactiveLabels = setOf(
- "button",
- "switch_on",
- "switch_off",
- "text_entry_box",
- "dropdown",
- "radio_button_checked",
- "radio_button_unchecked",
- "slider",
- "image_placeholder",
- "widget_tag"
- )
-
- data class RegionOcrResult(
- val enrichedDetections: List,
- val remainingDetections: List,
- val marginDetections: List,
- val fullImageTextBlocks: List
- )
-
- suspend fun process(
- originalBitmap: Bitmap,
- yoloDetections: List,
- leftGuidePct: Float,
- rightGuidePct: Float
- ): RegionOcrResult = coroutineScope {
- val interactive = yoloDetections.filter { it.label in interactiveLabels }
- val remaining = yoloDetections.filter { it.label !in interactiveLabels }
-
- val widgetOcrDeferred = async { runWidgetOcr(originalBitmap, interactive) }
- val marginOcrDeferred = async { runMarginOcr(originalBitmap, leftGuidePct, rightGuidePct) }
- val fullImageOcrDeferred = async { runFullImageOcr(originalBitmap) }
-
- RegionOcrResult(
- enrichedDetections = widgetOcrDeferred.await(),
- remainingDetections = remaining,
- marginDetections = marginOcrDeferred.await(),
- fullImageTextBlocks = fullImageOcrDeferred.await()
- )
- }
-
- private suspend fun runWidgetOcr(
- bitmap: Bitmap,
- components: List
- ): List = coroutineScope {
- components.map { component ->
- async {
- var crop: Bitmap? = null
- var preprocessed: Bitmap? = null
- try {
- crop = BitmapUtils.cropRegion(bitmap, component.boundingBox, componentPadding)
- preprocessed = BitmapUtils.preprocessForOcr(crop)
- val textBlocks = ocrSource.recognizeText(preprocessed).getOrNull()
- val text = textBlocks?.let { OcrTextAssembler.extractTextWithTolerance(it) } ?: ""
- component.copy(text = text)
- } finally {
- preprocessed?.recycle()
- if (crop != null && crop !== bitmap) crop.recycle()
- }
- }
- }.awaitAll()
- }
-
- private suspend fun runMarginOcr(
- bitmap: Bitmap,
- leftGuidePct: Float,
- rightGuidePct: Float
- ): List {
- val width = bitmap.width.toFloat()
- val height = bitmap.height.toFloat()
- val results = mutableListOf()
-
- val leftRect = RectF(0f, 0f, width * leftGuidePct, height)
- results.addAll(ocrCroppedRegion(bitmap, leftRect, 0f))
-
- val rightOffsetX = width * rightGuidePct
- val rightRect = RectF(rightOffsetX, 0f, width, height)
- results.addAll(ocrCroppedRegion(bitmap, rightRect, rightOffsetX))
-
- return results
- }
-
- private suspend fun ocrCroppedRegion(
- bitmap: Bitmap,
- rect: RectF,
- offsetX: Float
- ): List {
- val crop = BitmapUtils.cropRegion(bitmap, rect)
- if (crop === bitmap) return emptyList()
- return try {
- val textBlocks = ocrSource.recognizeText(crop).getOrNull() ?: emptyList()
- textBlocks.flatMap { block ->
- block.lines.mapNotNull { line ->
- line.boundingBox?.let { box ->
- DetectionResult(
- boundingBox = RectF(
- box.left + offsetX,
- box.top + rect.top,
- box.right + offsetX,
- box.bottom + rect.top
- ),
- label = "text",
- score = 0.99f,
- text = OcrTextAssembler.joinElementsWithTolerance(line),
- isYolo = false
- )
- }
- }
- }
- } finally {
- crop.recycle()
- }
- }
-
- private suspend fun runFullImageOcr(bitmap: Bitmap): List {
- return ocrSource.recognizeText(bitmap).getOrNull() ?: emptyList()
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/TextAssociator.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/TextAssociator.kt
deleted file mode 100644
index b1eac0d4d0..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/TextAssociator.kt
+++ /dev/null
@@ -1,106 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain
-
-import android.graphics.Rect
-import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
-import org.appdevforall.codeonthego.computervision.utils.TextCleaner.cleanTextPreservingLeadingO
-import org.appdevforall.codeonthego.computervision.utils.TextCleaner.cleanTextStrippingLeadingO
-import kotlin.math.abs
-import kotlin.math.max
-
-/**
- * Applies spatial proximity and intersection heuristics to associate
- * loose text blocks (OCR) with their corresponding visual widget (YOLO).
- */
-object TextAssociator {
- private const val OVERLAP_THRESHOLD = 0.6
-
- fun assignTextToParents(parents: List, texts: List, allBoxes: List): List {
- val consumedTexts = mutableSetOf()
- val updatedParents = mutableMapOf()
-
- for (parent in parents) {
- texts.firstOrNull { text ->
- !consumedTexts.contains(text) &&
- Rect(parent.rect).let { intersection ->
- intersection.intersect(text.rect) &&
- (intersection.width() * intersection.height()).let { intersectionArea ->
- val textArea = text.w * text.h
- textArea > 0 && (intersectionArea.toFloat() / textArea.toFloat()) > OVERLAP_THRESHOLD
- }
- }
- }?.let {
- updatedParents[parent] = parent.copy(text = it.text)
- consumedTexts.add(it)
- }
- }
-
- return allBoxes.mapNotNull { box ->
- when {
- consumedTexts.contains(box) -> null
- updatedParents.containsKey(box) -> updatedParents[box]
- else -> box
- }
- }
- }
-
- fun assignNearbyTextToWidgets(boxes: List, availableTexts: List): List {
- val consumedTexts = mutableSetOf()
- val updatedWidgets = mutableMapOf()
-
- val labelableWidgets = boxes.filter { isLabelableWidget(it) }.sortedWith(compareBy({ it.y }, { it.x }))
-
- for (widget in labelableWidgets) {
- val nearbyText = availableTexts
- .asSequence()
- .filter { it !in consumedTexts }
- .filter { text -> widget.isVerticallyAlignedWith(text, tolerance = max(widget.h * 2.5, 40.0)) }
- .minByOrNull { text -> widget.calculateProximityScoreTo(text) }
-
- if (nearbyText != null) {
- val finalText = cleanWidgetText(widget, nearbyText.text)
- updatedWidgets[widget] = widget.copy(text = finalText)
- consumedTexts.add(nearbyText)
- }
- }
-
- return boxes.mapNotNull { box ->
- when (box) {
- in consumedTexts -> null
- in updatedWidgets -> updatedWidgets[box]
- else -> box
- }
- }
- }
-
- private fun isLabelableWidget(box: ScaledBox): Boolean {
- return box.label in setOf(
- "radio_button_unchecked", "radio_button_checked",
- "checkbox_unchecked", "checkbox_checked",
- "switch_on", "switch_off"
- )
- }
-
- private fun cleanWidgetText(widget: ScaledBox, rawText: String): String {
- return if (widget.label.contains("radio", ignoreCase = true)) {
- cleanTextStrippingLeadingO(rawText)
- } else {
- cleanTextPreservingLeadingO(rawText)
- }
- }
-
- private fun ScaledBox.isVerticallyAlignedWith(other: ScaledBox, tolerance: Double): Boolean {
- return abs(this.centerY - other.centerY) < tolerance
- }
-
- private fun ScaledBox.calculateProximityScoreTo(other: ScaledBox): Double {
- val dx = this.rect.horizontalDistanceTo(other.rect).toDouble()
- val dy = abs(this.centerY - other.centerY).toDouble()
- return (dx * dx) + (dy * dy * 5)
- }
-
- private fun Rect.horizontalDistanceTo(other: Rect): Int = when {
- this.right < other.left -> other.left - this.right
- this.left > other.right -> this.left - other.right
- else -> 0
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/WidgetAnnotationMatcher.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/WidgetAnnotationMatcher.kt
deleted file mode 100644
index 105e49a6a5..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/WidgetAnnotationMatcher.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain
-
-import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
-
-class WidgetAnnotationMatcher {
- internal fun matchAnnotationsToElements(
- canvasTags: List,
- uiElements: List,
- annotations: Map
- ): Map {
- val finalAnnotations = mutableMapOf()
- val claimedWidgets = mutableSetOf()
-
- val deduplicatedTags = canvasTags
- .distinctBy { WidgetTagParser.normalizeTagText(it.text) }
-
- val tagsByWidgetType = annotations
- .mapNotNull { (tagText, annotationText) ->
- val normalizedTag = WidgetTagParser.normalizeTagText(tagText)
- val widgetType = getTagType(normalizedTag) ?: return@mapNotNull null
-
- val matchingTagBox = deduplicatedTags.find { WidgetTagParser.normalizeTagText(it.text) == normalizedTag }
-
- TaggedAnnotation(
- normalizedTag = normalizedTag,
- widgetType = widgetType,
- annotation = annotationText,
- tagBox = matchingTagBox
- )
- }
- .groupBy { it.widgetType }
-
- val widgetsByType = uiElements.groupBy { normalizeWidgetType(it.label) }
-
- for ((widgetType, taggedAnnotations) in tagsByWidgetType) {
- val candidateWidgets = widgetsByType[widgetType]
- ?.sortedWith(compareBy({ it.y }, { it.x }))
- ?: continue
-
- val sortedTags = taggedAnnotations.sortedWith(
- compareBy(
- { WidgetTagParser.extractOrdinal(it.normalizedTag) ?: Int.MAX_VALUE },
- { it.tagBox?.y ?: Int.MAX_VALUE },
- { it.tagBox?.x ?: Int.MAX_VALUE }
- )
- )
-
- for (taggedAnnotation in sortedTags) {
- val ordinal = WidgetTagParser.extractOrdinal(taggedAnnotation.normalizedTag)
- val matchedWidget = findWidgetByOrdinalOrFallback(
- ordinal = ordinal,
- tagBox = taggedAnnotation.tagBox,
- candidates = candidateWidgets,
- claimedWidgets = claimedWidgets
- ) ?: continue
-
- finalAnnotations[matchedWidget] = taggedAnnotation.annotation
- claimedWidgets.add(matchedWidget)
- }
- }
-
- return finalAnnotations
- }
-
- internal fun isTag(text: String): Boolean = WidgetTagParser.isTag(text)
-
- private fun getTagType(tag: String): String? {
- return when {
- tag.startsWith("B-") -> "button"
- tag.startsWith("P-") -> "image_placeholder"
- tag.startsWith("D-") -> "dropdown"
- tag.startsWith("T-") -> "text_entry_box"
- tag.startsWith("C-") -> "checkbox"
- tag.startsWith("R-") -> "radio"
- tag.startsWith("SW-") -> "switch"
- tag.startsWith("S-") -> "slider"
- else -> null
- }
- }
-
- private fun normalizeWidgetType(label: String): String = when {
- label.startsWith("text_entry_box") -> "text_entry_box"
- label.startsWith("button") -> "button"
- label.startsWith("switch") -> "switch"
- label.startsWith("checkbox") -> "checkbox"
- label.startsWith("radio") -> "radio"
- label.startsWith("dropdown") -> "dropdown"
- label.startsWith("slider") -> "slider"
- label.startsWith("image_placeholder") || label.startsWith("icon") -> "image_placeholder"
- else -> label
- }
-
- private fun findWidgetByOrdinalOrFallback(
- ordinal: Int?,
- tagBox: ScaledBox?,
- candidates: List,
- claimedWidgets: Set
- ): ScaledBox? {
- val available = candidates.filter { it !in claimedWidgets }
- if (available.isEmpty()) return null
-
- if (ordinal != null) {
- val oneBasedMatch = candidates.getOrNull(ordinal - 1)
- if (oneBasedMatch != null && oneBasedMatch !in claimedWidgets) {
- return oneBasedMatch
- }
-
- val zeroBasedMatch = candidates.getOrNull(ordinal)
- if (zeroBasedMatch != null && zeroBasedMatch !in claimedWidgets) {
- return zeroBasedMatch
- }
- }
-
- if (tagBox != null) {
- return available.minByOrNull { candidate ->
- val verticalDistance = kotlin.math.abs(tagBox.centerY - candidate.centerY)
- val horizontalDistance = kotlin.math.abs(tagBox.centerX - candidate.centerX)
- (verticalDistance * 2) + horizontalDistance
- }
- }
-
- return available.minByOrNull { it.y }
- }
-
- private data class TaggedAnnotation(
- val normalizedTag: String,
- val widgetType: String,
- val annotation: String,
- val tagBox: ScaledBox?
- )
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/WidgetTagParser.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/WidgetTagParser.kt
deleted file mode 100644
index 1b8fa879ea..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/WidgetTagParser.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain
-
-/**
- * Parses and normalizes raw OCR text into standardized Android widget tags.
- * Handles common OCR misreads and formatting inconsistencies.
- */
-internal object WidgetTagParser {
- private val tagRegex = Regex("^(?i)(B|P|D|T|C|R|SW|S)-[A-Z0-9_]+$")
- private val tagExtractRegex = Regex("^(?i)(B|P|D|T|C|R|SW|S|8|8W|S8)([\\s\\-_.,|/]*)([A-Z0-9_\\-]+)")
- private val VALID_PREFIXES = setOf("B", "P", "D", "T", "C", "R", "SW", "S")
-
- fun isTag(text: String): Boolean {
- val cleaned = text.trim().trimEnd('.', ',', ';', ':', '_', '|')
- val match = tagExtractRegex.find(cleaned) ?: return false
-
- if (!isValidTagMatch(match)) return false
-
- val trailingText = cleaned.substring(match.range.last + 1).trim()
- if (trailingText.isNotBlank() && trailingText.any { it.isLetterOrDigit() }) return false
-
- return normalizeTagText(cleaned).matches(tagRegex)
- }
-
- private fun parseTagParts(match: MatchResult): Pair? {
- val rawPrefix = match.groupValues[1]
- val prefix = normalizePrefix(rawPrefix)
-
- if (prefix !in VALID_PREFIXES) return null
-
- var tokenRaw = match.groupValues[3].trim('-')
-
- val upperToken = tokenRaw.uppercase()
- val remainder = upperToken.removePrefix(prefix)
-
- when {
- upperToken.startsWith("$prefix-") || upperToken.startsWith("${prefix}_") -> {
- tokenRaw = tokenRaw.substring(prefix.length + 1).trim('-')
- }
- upperToken.startsWith(prefix) && remainder.isNotEmpty() && remainder.all(::isNumericLikeOcrChar) -> {
- tokenRaw = remainder
- }
- }
-
- val token = normalizeTagToken(tokenRaw)
- return prefix to token
- }
-
- fun normalizeTagText(text: String): String {
- val cleaned = text.trim().trimEnd('.', ',', ';', ':', '_', '|')
- val match = tagExtractRegex.find(cleaned) ?: return cleaned.uppercase()
-
- if (!isValidTagMatch(match)) return cleaned.uppercase()
-
- val parts = parseTagParts(match) ?: return cleaned.uppercase()
-
- return "${parts.first}-${parts.second}"
- }
-
- fun extractTag(text: String): Pair? {
- val cleaned = text.trim().trimEnd('.', ',', ';', ':', '_', '|')
- val match = tagExtractRegex.find(cleaned) ?: return null
-
- if (!isValidTagMatch(match)) return null
-
- val parts = parseTagParts(match) ?: return null
-
- val finalTag = "${parts.first}-${parts.second}"
-
- if (!finalTag.matches(tagRegex)) return null
-
- val trailingText = cleaned.substring(match.range.last + 1).trim().takeIf { it.isNotBlank() }
- return finalTag to trailingText
- }
-
- private fun isValidTagMatch(match: MatchResult): Boolean {
- val separator = match.groupValues[2]
- val rawToken = match.groupValues[3]
-
- if (separator.isNotEmpty()) return true
- return rawToken.all(::isNumericLikeOcrChar)
- }
-
- private fun normalizePrefix(rawPrefix: String): String {
- return rawPrefix.uppercase()
- .replace(Regex("\\s+"), "")
- .replace(Regex("^8$"), "B")
- .replace(Regex("^(8W|S8)$"), "SW")
- }
-
- /**
- * Extracts the numeric or alphanumeric identifier part of the tag (the part after the hyphen).
- */
- fun extractOrdinal(tag: String): Int? = tag.substringAfter('-', "").toIntOrNull()
-
- /**
- * Cleans up the token suffix. If the token consists entirely of numbers or OCR artifacts,
- * it converts those artifacts back to digits.
- */
- private fun normalizeTagToken(rawToken: String): String {
- if (rawToken.isBlank()) return rawToken
-
- val uppercaseToken = rawToken.uppercase().replace('-', '_')
- return if (uppercaseToken.all(::isNumericLikeOcrChar)) {
- normalizeOcrDigits(uppercaseToken)
- } else {
- uppercaseToken.replace(Regex("[^A-Z0-9_]"), "_")
- }
- }
-
- /**
- * Replaces characters that are commonly misread by OCR with their intended numeric values.
- */
- private fun normalizeOcrDigits(raw: String): String =
- raw.replace('I', '1')
- .replace('L', '1')
- .replace('!', '1')
- .replace('O', '0')
- .replace('Z', '2')
- .replace('S', '5')
- .replace('B', '6')
-
- /**
- * Determines whether a character is a digit or a letter frequently confused with a digit by OCR.
- */
- private fun isNumericLikeOcrChar(char: Char): Boolean {
- return char.isDigit() || char.uppercaseChar() in setOf('O', 'I', 'L', 'Z', 'S', 'B', '!')
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/YoloToXmlConverter.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/YoloToXmlConverter.kt
deleted file mode 100644
index 9a7204415f..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/YoloToXmlConverter.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain
-
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
-import org.appdevforall.codeonthego.computervision.domain.xml.AndroidXmlGenerator
-import org.appdevforall.codeonthego.computervision.utils.MetadataDetector
-import org.appdevforall.codeonthego.computervision.utils.buildPlaceholderOverrides
-import kotlin.comparisons.compareBy
-
-class YoloToXmlConverter(
- private val annotationMatcher: WidgetAnnotationMatcher,
- private val xmlGenerator: AndroidXmlGenerator
-) {
-
- fun generateXmlLayout(
- detections: List,
- annotations: Map,
- selectedImagesByPlaceholderId: Map,
- sourceImageWidth: Int,
- sourceImageHeight: Int,
- targetDpWidth: Int,
- targetDpHeight: Int,
- wrapInScroll: Boolean = true
- ): Pair {
- // 1. Filter and prepare base UI candidates
- val uiCandidates = extractUiCandidates(detections)
-
- // 2. Scale detections to target DP dimensions
- val scaledBoxes = scaleDetections(uiCandidates, sourceImageWidth, sourceImageHeight, targetDpWidth, targetDpHeight)
-
- // 3. Associate isolated text detections to their respective UI widgets
- val associatedBoxes = associateTextToWidgets(scaledBoxes)
-
- // 4. Clean up and finalize the UI elements list
- val uiElements = finalizeUiElements(associatedBoxes)
-
- // 5. Extract and scale reference tags (e.g., T-1, B-1) from the canvas
- val canvasTags = extractCanvasTags(detections, sourceImageWidth, sourceImageHeight, targetDpWidth, targetDpHeight)
-
- // 6. Match margin annotations with the extracted UI elements
- val finalAnnotations = annotationMatcher.matchAnnotationsToElements(canvasTags, uiElements, annotations)
-
- // 7. Sort boxes top-to-bottom, left-to-right for sequential XML rendering
- val sortedBoxes = uiElements.sortedWith(compareBy({ it.y }, { it.x }))
-
- // 8. Prepare local drawable resources overrides for image placeholders
- val selectedImageOverrides = uiElements.buildPlaceholderOverrides(selectedImagesByPlaceholderId)
-
- // 9. Generate final XML output
- return xmlGenerator.buildXml(
- boxes = sortedBoxes,
- annotations = finalAnnotations,
- selectedImageOverrides = selectedImageOverrides,
- targetDpHeight = targetDpHeight,
- wrapInScroll = wrapInScroll
- )
- }
-
- private fun extractUiCandidates(detections: List): List {
- return detections
- .filter { (it.isYolo || it.label == "text") && it.label != "widget_tag" }
- .filterNot { MetadataDetector.isMetadataDetection(it.label, it.text) }
- .distinctBy {
- if (it.label.startsWith("switch")) {
- // Deduplicate switches by grouping them within a 50px vertical band
- "${((it.boundingBox.top + it.boundingBox.bottom) / 2f).toInt() / 50}"
- } else {
- // Exact coordinate deduplication for other widgets
- "${it.label}:${it.boundingBox.left}:${it.boundingBox.top}:${it.boundingBox.right}:${it.boundingBox.bottom}"
- }
- }
- }
-
- private fun scaleDetections(
- candidates: List,
- sourceWidth: Int,
- sourceHeight: Int,
- targetWidth: Int,
- targetHeight: Int
- ): List {
- return candidates.map {
- DetectionScaler.scale(it, sourceWidth, sourceHeight, targetWidth, targetHeight)
- }
- }
-
- private fun associateTextToWidgets(scaledBoxes: List): List {
- val parents = scaledBoxes.filter { it.label != "text" }
- val initialTexts = scaledBoxes.filter { it.label == "text" && !annotationMatcher.isTag(it.text) }
-
- val textAssignedBoxes = TextAssociator.assignTextToParents(parents, initialTexts, scaledBoxes)
- val remainingTexts = textAssignedBoxes.filter { it.label == "text" && !annotationMatcher.isTag(it.text) }
-
- return TextAssociator.assignNearbyTextToWidgets(textAssignedBoxes, remainingTexts)
- }
-
- private fun finalizeUiElements(boxes: List): List {
- return boxes.filter {
- // Keep the widget if it's not pure text, or if it is text but not recognized as a tag.
- (it.label != "text" || !annotationMatcher.isTag(it.text)) &&
- !MetadataDetector.isMetadataDetection(it.label, it.text)
- }
- }
-
- private fun extractCanvasTags(
- detections: List,
- sourceWidth: Int,
- sourceHeight: Int,
- targetWidth: Int,
- targetHeight: Int
- ): List {
- val widgetTags = detections.filter {
- it.label == "widget_tag" || (!it.isYolo && annotationMatcher.isTag(it.text))
- }
- return widgetTags.map {
- DetectionScaler.scale(it, sourceWidth, sourceHeight, targetWidth, targetHeight)
- }
- }
-
- companion object {
- fun generateXmlLayout(
- detections: List,
- annotations: Map,
- selectedImagesByPlaceholderId: Map = emptyMap(),
- sourceImageWidth: Int,
- sourceImageHeight: Int,
- targetDpWidth: Int,
- targetDpHeight: Int,
- wrapInScroll: Boolean = true
- ): Pair {
- val matcher = WidgetAnnotationMatcher()
- val generator = AndroidXmlGenerator()
-
- val converter = YoloToXmlConverter(matcher, generator)
-
- return converter.generateXmlLayout(
- detections = detections,
- annotations = annotations,
- selectedImagesByPlaceholderId = selectedImagesByPlaceholderId,
- sourceImageWidth = sourceImageWidth,
- sourceImageHeight = sourceImageHeight,
- targetDpWidth = targetDpWidth,
- targetDpHeight = targetDpHeight,
- wrapInScroll = wrapInScroll
- )
- }
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/grammar/AttributeValidator.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/grammar/AttributeValidator.kt
deleted file mode 100644
index 9ee62dd607..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/grammar/AttributeValidator.kt
+++ /dev/null
@@ -1,158 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.grammar
-
-
-import com.itsaky.androidide.fuzzysearch.FuzzySearch
-import org.appdevforall.codeonthego.computervision.utils.extractOcrEntries
-
-interface AttributeValidator {
- fun validate(rawValue: String): String?
-}
-
-internal fun matchCategoricalValue(rawValue: String, allowedValues: List, threshold: Int = 70): String? {
- val result = FuzzySearch.extractOne(rawValue, allowedValues)
- return if (result.score >= threshold) result.string else null
-}
-
-object PassThroughValidator : AttributeValidator {
- override fun validate(rawValue: String): String = rawValue.trim()
-}
-
-object BooleanValidator : AttributeValidator {
- private val allowedValues = listOf("true", "false")
-
- override fun validate(rawValue: String): String? {
- return matchCategoricalValue(rawValue.trim().lowercase(), allowedValues, threshold = 85)
- }
-}
-
-object DimensionValidator : AttributeValidator {
- private val dimensionValues = listOf("match_parent", "wrap_content")
-
- override fun validate(rawValue: String): String? {
- val trimmed = rawValue.trim()
- if (trimmed.endsWith("dp") || trimmed.endsWith("sp") || trimmed.endsWith("px")) {
- return trimmed
- }
- return matchCategoricalValue(trimmed, dimensionValues)
- }
-}
-
-class SpDimensionRangeValidator(
- private val minSp: Int,
- private val maxSp: Int
-) : AttributeValidator {
- private val spRegex = Regex("^(\\d+(?:\\.\\d+)?)sp$")
-
- override fun validate(rawValue: String): String? {
- val trimmed = rawValue.trim()
-
- val match = spRegex.matchEntire(trimmed) ?: return null
- val value = match.groupValues[1].toFloatOrNull() ?: return null
-
- return trimmed.takeIf { value >= minSp && value <= maxSp }
- }
-}
-
-class CategoricalValidator(private val allowedValues: List) : AttributeValidator {
- override fun validate(rawValue: String): String? {
- return matchCategoricalValue(rawValue.trim(), allowedValues)
- }
-}
-
-object SliderStyleValidator : AttributeValidator {
- private val sliderStyles = listOf("continuous", "discrete", "material", "thick")
- private val styleResourceMapping = mapOf(
- "continuous" to "@style/Widget.MaterialComponents.Slider",
- "discrete" to "@style/Widget.MaterialComponents.Slider.Discrete",
- "material" to "@style/Widget.MaterialComponents.Slider",
- "thick" to "@style/Widget.App.Slider.Thick"
- )
-
- override fun validate(rawValue: String): String? {
- val matchedCategory = matchCategoricalValue(rawValue.trim(), sliderStyles)
- return matchedCategory?.let {
- styleResourceMapping[it] ?: "@style/Slider.${it.replaceFirstChar { c -> c.uppercase() }}"
- }
- }
-}
-
-object EntriesValidator : AttributeValidator {
- override fun validate(rawValue: String): String? {
- val trimmed = rawValue.trim()
- if (trimmed.startsWith("@")) return trimmed
-
- val content = trimmed.removeSurrounding("[", "]")
- val rawItems = content.extractOcrEntries()
-
- val isNumericArray = isEntireArrayLikelyNumeric(rawItems)
-
- val cleanedItems = rawItems.map { item ->
- val cleanItem = item.trim()
- if (isNumericArray) {
- cleanNumberArtifacts(cleanItem)
- } else {
- cleanTextArtifacts(cleanItem)
- }
- }
-
- return cleanedItems.joinToString(",")
- }
-
- private fun isEntireArrayLikelyNumeric(items: List): Boolean {
- if (items.isEmpty()) return false
- var hasAtLeastOneDigit = false
-
- for (item in items) {
- val cleanItem = item.trim()
- if (cleanItem.isEmpty()) continue
-
- if (!cleanItem.matches(Regex("^[0-9oOlIzZsSbB\\s]+$"))) {
- return false
- }
- if (cleanItem.any { it.isDigit() }) {
- hasAtLeastOneDigit = true
- }
- }
-
- return hasAtLeastOneDigit
- }
-
- private fun cleanNumberArtifacts(text: String): String {
- return text
- .replace(Regex("[oO]"), "0")
- .replace(Regex("[lI]"), "1")
- .replace(Regex("[zZ]"), "2")
- .replace(Regex("[sS]"), "5")
- .replace(Regex("[bB]"), "6")
- .replace(Regex("\\s+"), "")
- }
-
- private fun cleanTextArtifacts(text: String): String {
- return text.replace(Regex("\\s+"), " ")
- }
-}
-
-class FlagsCategoricalValidator(
- private val allowedValues: List,
- private val separator: String = "|",
- private val threshold: Int = 70
-) : AttributeValidator {
- override fun validate(rawValue: String): String? {
- val flags = rawValue.split(separator)
-
- val validFlags = flags.mapNotNull { flag ->
- val trimmedFlag = flag.trim()
- if (trimmedFlag.isEmpty()) {
- null
- } else {
- matchCategoricalValue(trimmedFlag, allowedValues, threshold)
- }
- }
-
- return if (validFlags.isNotEmpty()) {
- validFlags.distinct().joinToString(separator)
- } else {
- null
- }
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/grammar/UiGrammarValidator.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/grammar/UiGrammarValidator.kt
deleted file mode 100644
index b464b5fd33..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/grammar/UiGrammarValidator.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.grammar
-
-class UiGrammarValidator {
- private val registry: Map = listOf(
- SpinnerGrammar,
- ImageViewGrammar,
- EditTextGrammar,
- RadioButtonGrammar,
- CheckBoxGrammar,
- SwitchGrammar,
- RadioGroupGrammar,
- SliderGrammar,
- ButtonGrammar,
- TextViewGrammar,
- ).associateBy { it.tag }
-
- fun enforceGrammar(rawParsedAttributes: Map, tag: String): Map {
- val grammar = registry[tag] ?: return rawParsedAttributes
- val filteredMap = mutableMapOf()
-
- for ((key, rawValue) in rawParsedAttributes) {
- val validator = grammar.attributes[key]
- if (validator != null) {
- val validValue = validator.validate(rawValue)
- if (validValue != null) {
- filteredMap[key] = validValue
- }
- }
- }
- return filteredMap
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/grammar/WidgetGrammar.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/grammar/WidgetGrammar.kt
deleted file mode 100644
index 481e488731..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/grammar/WidgetGrammar.kt
+++ /dev/null
@@ -1,123 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.grammar
-
-import org.appdevforall.codeonthego.computervision.domain.parser.AttributeKey
-import org.appdevforall.codeonthego.computervision.domain.parser.GravityValueSet
-import org.appdevforall.codeonthego.computervision.domain.parser.InputTypeValueSet
-import org.appdevforall.codeonthego.computervision.domain.parser.VisibilityValueSet
-
-interface WidgetGrammar {
- val tag: String
- val attributes: Map
- get() = mapOf(
- AttributeKey.WIDTH.xmlName to DimensionValidator,
- AttributeKey.HEIGHT.xmlName to DimensionValidator,
- AttributeKey.ID.xmlName to PassThroughValidator
- )
-}
-
-interface LayoutGrammar : WidgetGrammar {
- override val attributes: Map
- get() = super.attributes + mapOf(
- AttributeKey.LAYOUT_MARGIN.xmlName to DimensionValidator,
- AttributeKey.LAYOUT_MARGIN_TOP.xmlName to DimensionValidator,
- AttributeKey.LAYOUT_MARGIN_BOTTOM.xmlName to DimensionValidator,
- AttributeKey.LAYOUT_MARGIN_START.xmlName to DimensionValidator,
- AttributeKey.LAYOUT_MARGIN_END.xmlName to DimensionValidator,
- AttributeKey.LAYOUT_GRAVITY.xmlName to CategoricalValidator(GravityValueSet.values),
- AttributeKey.GRAVITY.xmlName to CategoricalValidator(GravityValueSet.values),
- AttributeKey.LAYOUT_WEIGHT.xmlName to PassThroughValidator,
- AttributeKey.PADDING.xmlName to DimensionValidator,
- AttributeKey.VISIBILITY.xmlName to CategoricalValidator(VisibilityValueSet.values),
- AttributeKey.BACKGROUND.xmlName to PassThroughValidator,
- AttributeKey.BACKGROUND_TINT.xmlName to PassThroughValidator
- )
-}
-
-interface TextGrammar : LayoutGrammar {
- override val attributes: Map
- get() = super.attributes + mapOf(
- AttributeKey.TEXT_COLOR.xmlName to PassThroughValidator,
- AttributeKey.TEXT_SIZE.xmlName to PassThroughValidator,
- AttributeKey.TEXT_STYLE.xmlName to PassThroughValidator,
- AttributeKey.TEXT_ALIGNMENT.xmlName to PassThroughValidator,
- AttributeKey.FONT_FAMILY.xmlName to PassThroughValidator
- )
-}
-
-interface CompoundButtonGrammar : TextGrammar {
- override val attributes: Map
- get() = super.attributes + mapOf(
- AttributeKey.TEXT.xmlName to PassThroughValidator,
- AttributeKey.CHECKED.xmlName to BooleanValidator,
- AttributeKey.TEXT_SIZE.xmlName to SpDimensionRangeValidator(minSp = 8, maxSp = 32)
- )
-}
-
-
-object SpinnerGrammar : LayoutGrammar {
- override val tag = "Spinner"
- override val attributes = super.attributes + mapOf(
- AttributeKey.TEXT.xmlName to PassThroughValidator,
- AttributeKey.ENTRIES.xmlName to EntriesValidator
- )
-}
-
-object ImageViewGrammar : LayoutGrammar {
- override val tag = "ImageView"
-
- override val attributes = super.attributes + mapOf(
- AttributeKey.SRC.xmlName to PassThroughValidator
- )
-}
-
-object EditTextGrammar : TextGrammar {
- override val tag = "EditText"
-
- override val attributes = super.attributes + mapOf(
- AttributeKey.TEXT.xmlName to PassThroughValidator,
- AttributeKey.INPUT_TYPE.xmlName to FlagsCategoricalValidator(InputTypeValueSet.values),
- AttributeKey.HINT.xmlName to PassThroughValidator
- )
-}
-
-object RadioButtonGrammar : CompoundButtonGrammar {
- override val tag = "RadioButton"
-}
-
-object CheckBoxGrammar : CompoundButtonGrammar {
- override val tag = "CheckBox"
-}
-
-object SwitchGrammar : CompoundButtonGrammar {
- override val tag = "Switch"
-}
-
-object RadioGroupGrammar : TextGrammar {
- override val tag = "RadioGroup"
- override val attributes = super.attributes + mapOf(
- AttributeKey.ORIENTATION.xmlName to CategoricalValidator(listOf("horizontal", "vertical")),
- AttributeKey.TEXT_SIZE.xmlName to SpDimensionRangeValidator(minSp = 8, maxSp = 32)
- )
-}
-
-object SliderGrammar : LayoutGrammar {
- override val tag = "com.google.android.material.slider.Slider"
- override val attributes = super.attributes + mapOf(
- AttributeKey.TEXT.xmlName to PassThroughValidator,
- AttributeKey.STYLE.xmlName to SliderStyleValidator
- )
-}
-
-object TextViewGrammar : TextGrammar {
- override val tag = "TextView"
- override val attributes = super.attributes + mapOf(
- AttributeKey.TEXT.xmlName to PassThroughValidator
- )
-}
-
-object ButtonGrammar : TextGrammar {
- override val tag = "Button"
- override val attributes = super.attributes + mapOf(
- AttributeKey.TEXT.xmlName to PassThroughValidator
- )
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/model/DetectionResult.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/model/DetectionResult.kt
deleted file mode 100644
index d837aa16e7..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/model/DetectionResult.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.model
-
-import android.graphics.RectF
-
-data class DetectionResult(
- val boundingBox: RectF,
- val label: String,
- val score: Float,
- var text: String = "",
- val isYolo: Boolean = true
-)
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/model/LayoutItem.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/model/LayoutItem.kt
deleted file mode 100644
index 5234ef0711..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/model/LayoutItem.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.model
-
-sealed interface LayoutItem {
- data class SimpleView(val box: ScaledBox) : LayoutItem
- data class HorizontalRow(val row: List) : LayoutItem
- data class RadioGroup(val boxes: List, val orientation: String) : LayoutItem
- data class CheckboxGroup(val boxes: List, val orientation: String) : LayoutItem
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/model/ScaledBox.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/model/ScaledBox.kt
deleted file mode 100644
index d90a18a2cc..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/model/ScaledBox.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.model
-
-import android.graphics.Rect
-
-data class ScaledBox(
- val label: String,
- val text: String,
- val x: Int,
- val y: Int,
- val w: Int,
- val h: Int,
- val centerX: Int,
- val centerY: Int,
- val rect: Rect
-)
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/AttributeModels.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/AttributeModels.kt
deleted file mode 100644
index de1ffffeee..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/AttributeModels.kt
+++ /dev/null
@@ -1,178 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.parser
-
-interface AttributeValueSet {
- val values: List
-}
-
-object GravityValueSet : AttributeValueSet {
- override val values = listOf(
- "top",
- "bottom",
- "left",
- "right",
- "center",
- "center_vertical",
- "center_horizontal",
- "start",
- "end"
- )
-}
-
-object DimensionValueSet : AttributeValueSet {
- const val WRAP_CONTENT = "wrap_content"
- const val MATCH_PARENT = "match_parent"
-
- override val values = listOf(WRAP_CONTENT, MATCH_PARENT)
-
- val matchKeywords = setOf("match", "parent")
- val wrapKeywords = setOf("wrap", "content", "wrapcan")
-
- val allKeywords = matchKeywords + wrapKeywords
-}
-
-object VisibilityValueSet : AttributeValueSet {
- override val values = listOf(
- "visible",
- "invisible",
- "gone"
- )
-}
-
-object InputTypeValueSet : AttributeValueSet {
- override val values = listOf(
- "text",
- "textPassword",
- "number",
- "numberDecimal",
- "textEmailAddress",
- "textUri",
- "phone",
- "textVisiblePassword",
- "textPersonName",
- "textCapSentences",
- "textCapWords",
- "textMultiLine",
- "textNoSuggestions",
- "date",
- "time",
- "datetime"
- )
-}
-
-enum class ValueType {
- RAW,
- TEXT_CONTENT,
- DIMENSION,
- SP_DIMENSION,
- COLOR,
- ID,
- DRAWABLE,
- INTEGER,
- FLOAT,
- TEXT_STYLE
-}
-
-enum class AttributeKey(
- val xmlName: String,
- val aliases: List,
- val valueType: ValueType = ValueType.RAW
-) {
- WIDTH("android:layout_width", listOf("layout_width", "width"), ValueType.DIMENSION),
- HEIGHT("android:layout_height", listOf("layout_height", "height"), ValueType.DIMENSION),
- ID("android:id", listOf("id"), ValueType.ID),
- TEXT("android:text", listOf("text"), ValueType.TEXT_CONTENT),
- HINT("android:hint", listOf("hint"), ValueType.TEXT_CONTENT),
- BACKGROUND("android:background", listOf("background", "bg"), ValueType.COLOR),
- BACKGROUND_TINT("app:backgroundTint", listOf("backgroundtint", "background_tint", "bg_tint"), ValueType.COLOR),
- SRC("android:src", listOf("src", "scr", "sre", "5rc"), ValueType.DRAWABLE),
- CONTENT_DESCRIPTION("android:contentDescription", listOf("contentdescription", "content_description")),
-
- TEXT_SIZE("android:textSize", listOf("textsize", "text_size"), ValueType.SP_DIMENSION),
- TEXT_COLOR("android:textColor", listOf("textcolor", "text_color", "color", "text_colar", "textcolar"), ValueType.COLOR),
- TEXT_STYLE("android:textStyle", listOf("textstyle", "text_style"), ValueType.TEXT_STYLE),
- TEXT_ALIGNMENT("android:textAlignment", listOf("textalignment", "text_alignment")),
- TEXT_ALL_CAPS("android:textAllCaps", listOf("textallcaps", "text_all_caps")),
- FONT_FAMILY("android:fontFamily", listOf("fontfamily", "font_family", "font")),
- MAX_LINES("android:maxLines", listOf("maxlines", "max_lines"), ValueType.INTEGER),
- MIN_LINES("android:minLines", listOf("minlines", "min_lines"), ValueType.INTEGER),
- LINES("android:lines", listOf("lines"), ValueType.INTEGER),
- SINGLE_LINE("android:singleLine", listOf("singleline", "single_line")),
- ELLIPSIZE("android:ellipsize", listOf("ellipsize")),
- LINE_SPACING_EXTRA("android:lineSpacingExtra", listOf("linespacingextra", "line_spacing_extra"), ValueType.SP_DIMENSION),
- LETTER_SPACING("android:letterSpacing", listOf("letterspacing", "letter_spacing")),
- HINT_TEXT_COLOR("android:textColorHint", listOf("hinttextcolor", "hint_text_color", "textcolorhint", "text_color_hint"), ValueType.COLOR),
- IME_OPTIONS("android:imeOptions", listOf("imeoptions", "ime_options")),
-
- INPUT_TYPE("android:inputType", listOf("inputtype", "input_type")),
- MAX_LENGTH("android:maxLength", listOf("maxlength", "max_length"), ValueType.INTEGER),
-
- VISIBILITY("android:visibility", listOf("visibility")),
- ENABLED("android:enabled", listOf("enabled")),
- CLICKABLE("android:clickable", listOf("clickable")),
- FOCUSABLE("android:focusable", listOf("focusable")),
- ALPHA("android:alpha", listOf("alpha")),
- ELEVATION("android:elevation", listOf("elevation"), ValueType.DIMENSION),
- ROTATION("android:rotation", listOf("rotation")),
-
- PADDING("android:padding", listOf("padding"), ValueType.DIMENSION),
- PADDING_TOP("android:paddingTop", listOf("paddingtop", "padding_top"), ValueType.DIMENSION),
- PADDING_BOTTOM("android:paddingBottom", listOf("paddingbottom", "padding_bottom"), ValueType.DIMENSION),
- PADDING_START("android:paddingStart", listOf("paddingstart", "padding_start"), ValueType.DIMENSION),
- PADDING_END("android:paddingEnd", listOf("paddingend", "padding_end"), ValueType.DIMENSION),
- PADDING_LEFT("android:paddingLeft", listOf("paddingleft", "padding_left"), ValueType.DIMENSION),
- PADDING_RIGHT("android:paddingRight", listOf("paddingright", "padding_right"), ValueType.DIMENSION),
-
- LAYOUT_MARGIN("android:layout_margin", listOf("layout_margin", "margin"), ValueType.DIMENSION),
- LAYOUT_MARGIN_TOP("android:layout_marginTop", listOf("layout_margintop", "layout_margin_top", "margin_top", "margintop"), ValueType.DIMENSION),
- LAYOUT_MARGIN_BOTTOM("android:layout_marginBottom", listOf("layout_marginbottom", "layout_margin_bottom", "margin_bottom", "marginbottom"), ValueType.DIMENSION),
- LAYOUT_MARGIN_START("android:layout_marginStart", listOf("layout_marginstart", "layout_margin_start", "margin_start", "marginstart"), ValueType.DIMENSION),
- LAYOUT_MARGIN_END("android:layout_marginEnd", listOf("layout_marginend", "layout_margin_end", "margin_end", "marginend"), ValueType.DIMENSION),
- LAYOUT_MARGIN_LEFT("android:layout_marginLeft", listOf("layout_marginleft", "layout_margin_left", "margin_left"), ValueType.DIMENSION),
- LAYOUT_MARGIN_RIGHT("android:layout_marginRight", listOf("layout_marginright", "layout_margin_right", "margin_right"), ValueType.DIMENSION),
-
- LAYOUT_WEIGHT("android:layout_weight", listOf("layout_weight", "weight"), ValueType.FLOAT),
- LAYOUT_GRAVITY("android:layout_gravity", listOf("layout_gravity", "layaut_gravity")),
- GRAVITY("android:gravity", listOf("gravity")),
- ORIENTATION("android:orientation", listOf("orientation")),
-
- MIN_WIDTH("android:minWidth", listOf("minwidth", "min_width"), ValueType.DIMENSION),
- MIN_HEIGHT("android:minHeight", listOf("minheight", "min_height"), ValueType.DIMENSION),
- MAX_WIDTH("android:maxWidth", listOf("maxwidth", "max_width"), ValueType.DIMENSION),
- MAX_HEIGHT("android:maxHeight", listOf("maxheight", "max_height"), ValueType.DIMENSION),
-
- SCALE_TYPE("android:scaleType", listOf("scaletype", "scale_type")),
- ADJUST_VIEW_BOUNDS("android:adjustViewBounds", listOf("adjustviewbounds", "adjust_view_bounds")),
- TINT("android:tint", listOf("tint"), ValueType.COLOR),
-
- STYLE("style", listOf("style")),
- ENTRIES("android:entries", listOf("entries")),
- CHECKED("android:checked", listOf("checked")),
-
- CARD_CORNER_RADIUS("app:cardCornerRadius", listOf("cardcornerradius", "card_corner_radius", "cornerradius", "corner_radius"), ValueType.DIMENSION),
- CARD_ELEVATION("app:cardElevation", listOf("cardelevation", "card_elevation"), ValueType.DIMENSION),
- CARD_BACKGROUND_COLOR("app:cardBackgroundColor", listOf("cardbackgroundcolor", "card_background_color"), ValueType.COLOR),
- STROKE_COLOR("app:strokeColor", listOf("strokecolor", "stroke_color"), ValueType.COLOR),
- STROKE_WIDTH("app:strokeWidth", listOf("strokewidth", "stroke_width"), ValueType.DIMENSION),
-
- PROGRESS("android:progress", listOf("progress"), ValueType.INTEGER),
- MAX("android:max", listOf("max"), ValueType.INTEGER),
- MIN("android:min", listOf("min"), ValueType.INTEGER),
- VALUE_FROM("app:valueFrom", listOf("valuefrom", "value_from")),
- VALUE_TO("app:valueTo", listOf("valueto", "value_to")),
- STEP_SIZE("app:stepSize", listOf("stepsize", "step_size")),
- TRACK_COLOR("app:trackColor", listOf("trackcolor", "track_color"), ValueType.COLOR),
- THUMB_COLOR("app:thumbTint", listOf("thumbcolor", "thumb_color", "thumbtint", "thumb_tint"), ValueType.COLOR),
-
- FOREGROUND("android:foreground", listOf("foreground"), ValueType.COLOR),
- SPINNER_MODE("android:spinnerMode", listOf("spinnermode", "spinner_mode")),
- DRAWABLE_START("android:drawableStart", listOf("drawablestart", "drawable_start"), ValueType.DRAWABLE),
- DRAWABLE_END("android:drawableEnd", listOf("drawableend", "drawable_end"), ValueType.DRAWABLE),
- DRAWABLE_PADDING("android:drawablePadding", listOf("drawablepadding", "drawable_padding"), ValueType.DIMENSION);
-
- companion object {
- val allAliases: List by lazy { entries.flatMap { it.aliases } }
-
- fun findByAlias(alias: String): AttributeKey? =
- entries.firstOrNull { key -> key.aliases.any { it == alias } }
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/FuzzyAttributeParser.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/FuzzyAttributeParser.kt
deleted file mode 100644
index 6ac4c917f6..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/FuzzyAttributeParser.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.parser
-
-import com.itsaky.androidide.fuzzysearch.FuzzySearch
-import org.appdevforall.codeonthego.computervision.domain.grammar.UiGrammarValidator
-import org.appdevforall.codeonthego.computervision.domain.parser.sanitizer.OcrSanitizerFactory
-import java.lang.StringBuilder
-
-object FuzzyAttributeParser {
- private val grammarValidator = UiGrammarValidator()
- private const val PIPE_DELIMITER = "|"
- private val multipleUnderscoresRegex = Regex("_+")
- private val inputTypeValues = InputTypeValueSet.values.map { it.lowercase() }.toSet()
- private val sanitizer = OcrSanitizerFactory.createDefaultSanitizer()
-
- private val cleaners = mapOf(
- ValueType.TEXT_CONTENT to TextContentCleaner,
- ValueType.DIMENSION to DimensionCleaner,
- ValueType.SP_DIMENSION to SpDimensionCleaner,
- ValueType.COLOR to ColorCleaner,
- ValueType.ID to IdCleaner,
- ValueType.DRAWABLE to DrawableCleaner,
- ValueType.INTEGER to NumberCleaner,
- ValueType.FLOAT to FloatCleaner,
- ValueType.TEXT_STYLE to TextStyleCleaner,
- ValueType.RAW to ValueCleaner { it }
- )
-
- private val numericTypes = setOf(
- ValueType.DIMENSION,
- ValueType.SP_DIMENSION,
- ValueType.INTEGER,
- ValueType.FLOAT
- )
-
- fun parse(annotation: String?, tag: String): Map {
- if (annotation.isNullOrBlank()) return emptyMap()
-
- val normalizedInput = annotation.replace(Regex("\\s+:"), ":")
- val tokens = tokenizeAnnotation(normalizedInput)
-
- val rawAttributes = mapTokensToAttributes(tokens, tag)
- val finalAttributes = grammarValidator.enforceGrammar(rawAttributes, tag)
-
- return finalAttributes
- }
-
- private fun tokenizeAnnotation(annotation: String): List {
- val sanitized = sanitizer.sanitize(annotation)
-
- return if (sanitized.contains(PIPE_DELIMITER)) {
- sanitized.split(PIPE_DELIMITER).map { it.trim() }.filter { it.isNotEmpty() }
- } else {
- sanitized.split(Regex("[:;]|\\s+")).map { it.trim() }.filter { it.isNotEmpty() }
- }
- }
-
- private fun mapTokensToAttributes(tokens: List, tag: String): Map {
- val result = mutableMapOf()
- var currentKey: AttributeKey? = null
- val currentValue = StringBuilder()
-
- for (token in tokens) {
- val matchedKey = if (shouldTreatTokenAsValue(token, currentKey)) {
- null
- } else {
- fuzzyMatchKey(token)
- }
-
- if (matchedKey != null) {
- flushAttribute(currentKey, currentValue.toString(), tag, result)
- currentKey = matchedKey
- currentValue.clear()
- } else {
- currentValue.append(token).append(" ")
- }
- }
-
- flushAttribute(currentKey, currentValue.toString(), tag, result)
- return result
- }
-
- private fun shouldTreatTokenAsValue(token: String, currentKey: AttributeKey?): Boolean {
- val lowerToken = token.trim().lowercase()
-
- return when {
- currentKey == AttributeKey.INPUT_TYPE && lowerToken in inputTypeValues -> true
- currentKey?.valueType == ValueType.COLOR && isColorToken(lowerToken) -> true
- currentKey?.valueType == ValueType.DIMENSION && DimensionValueSet.allKeywords.any { it in lowerToken } -> true
- currentKey?.valueType in numericTypes -> lowerToken.any { it.isDigit() }
- else -> false
- }
- }
-
- private fun isColorToken(token: String): Boolean {
- return token.startsWith("#") || token.startsWith("@") || token in ColorCleaner.colorMap
- }
-
- private fun flushAttribute(key: AttributeKey?, rawValue: String, tag: String, destination: MutableMap) {
- if (key == null || rawValue.isBlank()) return
-
- val cleaner = cleaners[key.valueType] ?: ValueCleaner { it }
- val cleanedValue = cleaner.clean(rawValue.trim())
-
- if (cleanedValue.isNotEmpty()) {
- val (xmlAttr, finalValue) = resolveXmlAttribute(key, cleanedValue, tag)
- if (!destination.containsKey(xmlAttr)) {
- destination[xmlAttr] = finalValue
- }
- }
- }
-
- private fun fuzzyMatchKey(rawKey: String): AttributeKey? {
- val normalizedKey = rawKey.lowercase()
- .replace("-", "_")
- .replace(".", "_")
- .replace(multipleUnderscoresRegex, "_")
- .replace(Regex("lay[ao0]ut"), "layout")
- .replace(Regex("(?<=^|_)[lt]d(?=$|_)"), "id")
-
- val exactMatch = AttributeKey.findByAlias(normalizedKey)
- if (exactMatch != null) return exactMatch
-
- if (normalizedKey.length < 2) return null
-
- val threshold = when {
- normalizedKey.length <= 3 -> 65
- normalizedKey.length <= 6 -> 75
- else -> 80
- }
-
- val result = FuzzySearch.extractOne(normalizedKey, AttributeKey.allAliases)
-
- return if (result.score >= threshold) AttributeKey.findByAlias(result.string) else null
- }
-
- private fun resolveXmlAttribute(key: AttributeKey, value: String, tag: String): Pair {
- if (key == AttributeKey.BACKGROUND && tag == "Button") return "app:backgroundTint" to value
- if (key == AttributeKey.ID) return key.xmlName to value.replace(" ", "_")
- return key.xmlName to value
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/ValueCleaner.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/ValueCleaner.kt
deleted file mode 100644
index db5cc17304..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/ValueCleaner.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.parser
-
-fun interface ValueCleaner {
- fun clean(rawValue: String): String
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/ValueCleanersImpl.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/ValueCleanersImpl.kt
deleted file mode 100644
index dc12fef7bb..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/ValueCleanersImpl.kt
+++ /dev/null
@@ -1,178 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.parser
-
-import com.itsaky.androidide.fuzzysearch.FuzzySearch
-
-internal object TextContentCleaner : ValueCleaner {
- private val trailingWidgetTagRegex = Regex(
- "\\s+(?:[A-Z]{1,2}\\s+)?(?:B|P|D|T|C|R|SW|S)\\s*-\\s*[A-Z0-9_]+\\s*$",
- RegexOption.IGNORE_CASE
- )
- private val trailingRepeatedPrefixRegex = Regex(
- "\\s+(?:B|P|D|T|C|R|SW|S)\\s+(?=(?:B|P|D|T|C|R|SW|S)\\s*-\\s*[A-Z0-9_]+\\s*$)",
- RegexOption.IGNORE_CASE
- )
- private val multipleWhitespaceRegex = Regex("\\s+")
-
- override fun clean(rawValue: String): String {
- return rawValue
- .replace(trailingRepeatedPrefixRegex, " ")
- .replace(trailingWidgetTagRegex, "")
- .replace(multipleWhitespaceRegex, " ")
- .trim()
- }
-}
-
-
-internal object NumberCleaner : ValueCleaner {
- private val ocrCharMap = mapOf(
- 'O' to '0', 'A' to '0', '@' to '0', 'Q' to '0',
- 'L' to '1', 'I' to '1', '|' to '1', '!' to '1', '/' to '1', '\\' to '1',
- '(' to '1', ')' to '1', '[' to '1', ']' to '1',
- 'Z' to '2', 'S' to '5', 'B' to '6'
- )
-
- override fun clean(rawValue: String): String {
- val translated = rawValue.map { ocrCharMap[it.uppercaseChar()] ?: it }.joinToString("")
- return Regex("-?\\d+").find(translated)?.value ?: rawValue
- }
-}
-
-internal object DimensionCleaner : ValueCleaner {
- private val leadingNumberRegex = Regex("^-?\\d+")
-
- override fun clean(rawValue: String): String {
- val trimmedValue = rawValue.trim().lowercase()
- val normalized = trimmedValue.replace(" ", "_")
-
- if (DimensionValueSet.matchKeywords.any { it in normalized }) return DimensionValueSet.MATCH_PARENT
- if (DimensionValueSet.wrapKeywords.any { it in normalized }) return DimensionValueSet.WRAP_CONTENT
-
- val fuzzyResult = FuzzySearch.extractOne(normalized, DimensionValueSet.values)
- if (fuzzyResult.score >= 60) return fuzzyResult.string
-
- val unitMatch = Regex("(dp|sp|px|in|mm|pt)$").find(trimmedValue)
- val originalUnit = unitMatch?.value ?: "dp"
-
- val firstToken = trimmedValue.substringBefore(" ")
- val rawNumber = firstToken.removeSuffix(originalUnit).trim()
- val numericPart = NumberCleaner.clean(rawNumber)
-
- val numMatch = leadingNumberRegex.find(numericPart)?.value
- ?: return trimmedValue
- val correctedNum = removeOcrTrailingZero(numMatch)
-
- return "$correctedNum$originalUnit"
- }
-
- private fun removeOcrTrailingZero(num: String): String {
- val isOcrArtifact = num.endsWith("0") && (num.toLongOrNull() ?: 0L) >= 1000L
- return if (isOcrArtifact) num.dropLast(1) else num
- }
-}
-
-internal object SpDimensionCleaner : ValueCleaner {
- override fun clean(rawValue: String): String {
- val normalized = rawValue.lowercase().replace(" ", "").replace(Regex("(sp|5p)$"), "")
- val numericPart = NumberCleaner.clean(normalized.replace("_", ""))
- return if (numericPart != normalized) "${numericPart}sp" else rawValue
- }
-}
-
-internal object ColorCleaner : ValueCleaner {
- val colorMap = mapOf(
- "red" to "#FF0000", "rel" to "#FF0000", "rad" to "#FF0000", "reo" to "#FF0000",
- "green" to "#00FF00",
- "blue" to "#0000FF", "ine" to "#0000FF", "hne" to "#0000FF", "hlue" to "#0000FF", "ane" to "#0000FF", "lne" to "#0000FF",
- "black" to "#000000", "white" to "#FFFFFF", "gray" to "#808080",
- "grey" to "#808080", "dark_gray" to "#A9A9A9", "yellow" to "#FFFF00",
- "cyan" to "#00FFFF", "magenta" to "#FF00FF", "purple" to "#800080",
- "orange" to "#FFA500", "brown" to "#A52A2A", "pink" to "#FFC0CB",
- "light_gray" to "#D3D3D3", "dark_blue" to "#00008B", "dark_green" to "#006400",
- "dark_red" to "#8B0000", "teal" to "#008080", "navy" to "#000080",
- "transparent" to "@android:color/transparent"
- )
-
- override fun clean(rawValue: String): String {
- if (rawValue.startsWith("#") || rawValue.startsWith("@")) return rawValue
-
- val normalizedValue = rawValue.lowercase().replace(Regex("[^a-z_]"), "").replace(" ", "_")
-
- val exactColor = colorMap[normalizedValue]
- if (exactColor != null) return exactColor
-
- val result = FuzzySearch.extractOne(normalizedValue, colorMap.keys.toList())
- return if (result.score >= 70) colorMap[result.string] ?: rawValue else rawValue
- }
-}
-
-internal object IdCleaner : ValueCleaner {
- private val ID_VOCABULARY = listOf("cb", "rb", "group", "checkbox", "radio", "btn", "button", "text", "view", "img", "image", "input")
- private val nonAlphanumericRegex = Regex("[^a-z0-9_]")
-
- override fun clean(rawValue: String): String {
- val firstWord = rawValue.trim().split(Regex("\\s+")).firstOrNull() ?: rawValue
-
- val cleaned = firstWord.lowercase()
- .replace(Regex("inm|rn|wm|nm")) { m -> if (m.value == "inm") "im" else "m" }
- .replace(nonAlphanumericRegex, "_")
- .replace(Regex("_+"), "_")
- .trim('_')
-
- return normalizeKnownIdVocabulary(cleaned)
- }
-
- private fun normalizeKnownIdVocabulary(identifier: String): String {
- if (identifier.isBlank()) return identifier
- return identifier.split('_').filter { it.isNotBlank() }
- .flatMap(::normalizeIdToken).joinToString("_")
- }
-
- private fun normalizeIdToken(token: String): List {
- if (token.isBlank()) return emptyList()
- if (token.all(Char::isDigit)) return listOf(token)
-
- val exactMatch = FuzzySearch.extractOne(token, ID_VOCABULARY)
- if (exactMatch.score >= 80 && kotlin.math.abs(token.length - exactMatch.string.length) <= 2) {
- return listOf(exactMatch.string)
- }
- return listOf(token)
- }
-}
-
-internal object DrawableCleaner : ValueCleaner {
- override fun clean(rawValue: String): String {
- if (rawValue.startsWith("@drawable/")) return rawValue
-
- val cleaned = rawValue.lowercase()
- .replace(Regex("\\.(png|jpg|jpeg|webp|xml|svg)$"), "")
- .replace(Regex("inm|rn|wm|nm")) { m -> if (m.value == "inm") "im" else "m" }
- .replace(Regex("[^a-z0-9_]"), "_")
- .replace(Regex("_+"), "_")
- .trim('_')
-
- val finalCleaned = cleaned
- .replace("im_age", "image")
- .replace(Regex("(^|_)im($|_)"), "$1image$2")
- .replace(Regex("_+"), "_")
- .trim('_')
- return if (finalCleaned.isEmpty()) rawValue else "@drawable/$finalCleaned"
- }
-}
-
-internal object TextStyleCleaner : ValueCleaner {
- private val TEXT_STYLE_VALUES = listOf("normal", "bold", "italic", "bold|italic")
-
- override fun clean(rawValue: String): String {
- val normalizedValue = rawValue.lowercase().replace(" ", "_")
- if (normalizedValue in TEXT_STYLE_VALUES) return normalizedValue
-
- val result = FuzzySearch.extractOne(normalizedValue, TEXT_STYLE_VALUES)
- return if (result.score >= 60) result.string else rawValue
- }
-}
-
-internal object FloatCleaner : ValueCleaner {
- override fun clean(rawValue: String): String {
- return Regex("-?\\d+\\.?\\d*").find(rawValue)?.value ?: rawValue
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/CompositeOcrSanitizer.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/CompositeOcrSanitizer.kt
deleted file mode 100644
index b26d0619de..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/CompositeOcrSanitizer.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.parser.sanitizer
-
-
-class CompositeOcrSanitizer(
- private val sanitizers: List
-) : OcrSanitizer {
- override fun sanitize(input: String): String {
- return sanitizers.fold(input) { acc, sanitizer ->
- sanitizer.sanitize(acc)
- }
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/OcrSanitizer.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/OcrSanitizer.kt
deleted file mode 100644
index 56504e8027..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/OcrSanitizer.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.parser.sanitizer
-
-
-interface OcrSanitizer {
- fun sanitize(input: String): String
-}
-
-abstract class DictionaryRegexSanitizer : OcrSanitizer {
- protected abstract val rawRules: Map
-
- private val compiledRules: List> by lazy {
- rawRules.map { (pattern, replacement) ->
- Regex(pattern, RegexOption.IGNORE_CASE) to replacement
- }
- }
-
- override fun sanitize(input: String): String {
- return compiledRules.fold(input) { acc, (regex, replacement) ->
- acc.replace(regex, replacement)
- }
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/OcrSanitizerFactory.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/OcrSanitizerFactory.kt
deleted file mode 100644
index ebd48f3fd1..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/OcrSanitizerFactory.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.parser.sanitizer
-
-
-object OcrSanitizerFactory {
- fun createDefaultSanitizer(): OcrSanitizer {
- return CompositeOcrSanitizer(
- listOf(
- ColorSanitizer(),
- TextAttributeSanitizer(),
- DimensionSanitizer(),
- MarginPaddingSanitizer(),
- StructureSanitizer()
- )
- )
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/OcrSanitizerRules.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/OcrSanitizerRules.kt
deleted file mode 100644
index ea5d1b1f22..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/OcrSanitizerRules.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.parser.sanitizer
-
-
-class ColorSanitizer : DictionaryRegexSanitizer() {
- override val rawRules = mapOf(
- "backgroundired" to "background: red",
- "backgroundred" to "background: red",
- "\\bback[a-z]*[-_.]?\\s*[:;]\\s*" to "background: "
- )
-}
-
-class TextAttributeSanitizer : DictionaryRegexSanitizer() {
- override val rawRules = mapOf(
- "text\\s*st[yj]l?e?" to "text_style"
- )
-}
-
-class DimensionSanitizer : DictionaryRegexSanitizer() {
- override val rawRules = mapOf(
- "[il]ay[a-z]*[-_.\\s]*w[a-z0-9]*\\.?\\s*[:;]\\s*" to "layout_width: ",
- "[il]ay[a-z]*[-_.\\s]*hei[a-z0-9]*\\.?\\s*[:;]\\s*" to "layout_height: ",
- "m?w?at[ce]h[-_\\s]?p[ar]+ent" to "match_parent"
- )
-}
-
-class MarginPaddingSanitizer : DictionaryRegexSanitizer() {
- override val rawRules = mapOf(
- "layout_margin\\s+(top|bottom|start|end|left|right)" to "layout_margin_$1",
- "padding\\s+(top|bottom|start|end|left|right)" to "padding_$1"
- )
-}
-
-class StructureSanitizer : DictionaryRegexSanitizer() {
- override val rawRules = mapOf(
- "horizontal\\s+gravity\\s*:\\s*center\\s+layout" to "layout_gravity: center_horizontal",
- "\\b[ilL][dl]\\b\\s*[:;]?" to "id: ",
- "\\bS[ec][rt]\\b\\s*[:;]?" to "src: "
- )
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/GenerateXmlUC.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/GenerateXmlUC.kt
deleted file mode 100644
index 54570595d4..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/GenerateXmlUC.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.usecase
-
-import org.appdevforall.codeonthego.computervision.domain.YoloToXmlConverter
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-import kotlinx.coroutines.CancellationException
-
-/**
- * Use case responsible for generating the final Android XML layout string
- * and the corresponding strings.xml resource based on the detected UI elements.
- */
-class GenerateXmlUC {
- operator fun invoke(
- detections: List,
- annotations: Map,
- selectedImagesByPlaceholderId: Map,
- sourceImageWidth: Int,
- sourceImageHeight: Int,
- targetDpWidth: Int,
- targetDpHeight: Int
- ): Result> = runCatching {
- YoloToXmlConverter.generateXmlLayout(
- detections = detections,
- annotations = annotations,
- selectedImagesByPlaceholderId = selectedImagesByPlaceholderId,
- sourceImageWidth = sourceImageWidth,
- sourceImageHeight = sourceImageHeight,
- targetDpWidth = targetDpWidth,
- targetDpHeight = targetDpHeight,
- wrapInScroll = true
- )
- }.onFailure {
- if (it is CancellationException) throw it
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/ImportPlaceholderImageUC.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/ImportPlaceholderImageUC.kt
deleted file mode 100644
index 1208ca4e01..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/ImportPlaceholderImageUC.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.usecase
-
-import android.net.Uri
-import org.appdevforall.codeonthego.computervision.data.repository.DrawableImportHelper
-import org.appdevforall.codeonthego.computervision.data.repository.ImportedDrawable
-
-/**
- * Use case that handles copying a user-selected image from the gallery
- * into the project's local drawable resources folder.
- */
-class ImportPlaceholderImageUC(private val drawableImportHelper: DrawableImportHelper) {
- suspend operator fun invoke(
- uri: Uri,
- layoutFilePath: String?,
- placeholderId: String
- ): Result {
- return drawableImportHelper.importDrawable(
- sourceUri = uri,
- layoutFilePath = layoutFilePath,
- fallbackName = "imported_image_$placeholderId"
- )
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/PrepareImageUC.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/PrepareImageUC.kt
deleted file mode 100644
index 26817610ec..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/PrepareImageUC.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.usecase
-
-import android.content.ContentResolver
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import android.graphics.Matrix
-import android.net.Uri
-import androidx.exifinterface.media.ExifInterface
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import org.appdevforall.codeonthego.computervision.utils.SmartBoundaryDetector
-import java.io.IOException
-
-/**
- * Use case responsible for decoding an image URI, correcting its EXIF rotation,
- * and estimating the initial left and right canvas boundaries.
- */
-class PrepareImageUC(private val contentResolver: ContentResolver) {
- data class PreparedImage(val bitmap: Bitmap, val leftPct: Float, val rightPct: Float)
-
- suspend operator fun invoke(uri: Uri): Result = withContext(Dispatchers.Default) {
- runCatching {
- val bitmap = uriToBitmap(uri) ?: throw IllegalStateException("Failed to decode image from URI")
- val rotatedBitmap = handleImageRotation(uri, bitmap)
- val (leftBoundPx, rightBoundPx) = SmartBoundaryDetector.detectSmartBoundaries(rotatedBitmap)
-
- val widthFloat = rotatedBitmap.width.toFloat()
- PreparedImage(
- bitmap = rotatedBitmap,
- leftPct = leftBoundPx / widthFloat,
- rightPct = rightBoundPx / widthFloat
- )
- }.onFailure {
- if (it is CancellationException) throw it
- }
- }
-
- private fun uriToBitmap(uri: Uri): Bitmap? {
- return contentResolver.openFileDescriptor(uri, "r")?.use {
- BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
- }
- }
-
- private fun handleImageRotation(uri: Uri, bitmap: Bitmap): Bitmap {
- val orientation = try {
- contentResolver.openInputStream(uri)?.use { stream ->
- ExifInterface(stream).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
- } ?: ExifInterface.ORIENTATION_NORMAL
- } catch (_: IOException) {
- ExifInterface.ORIENTATION_NORMAL
- }
-
- val matrix = Matrix().apply {
- when (orientation) {
- ExifInterface.ORIENTATION_ROTATE_90 -> postRotate(90f)
- ExifInterface.ORIENTATION_ROTATE_180 -> postRotate(180f)
- ExifInterface.ORIENTATION_ROTATE_270 -> postRotate(270f)
- else -> return bitmap
- }
- }
- return try {
- val rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
- if (rotated != bitmap) bitmap.recycle()
- rotated
- } catch (_: OutOfMemoryError) {
- bitmap
- }
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/RemovePlaceholderImageUC.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/RemovePlaceholderImageUC.kt
deleted file mode 100644
index 8d3e9d2630..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/RemovePlaceholderImageUC.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.usecase
-
-import org.appdevforall.codeonthego.computervision.data.repository.DrawableImportHelper
-
-/**
- * Use case that handles deleting a previously imported placeholder image
- * from the project's local drawable resources folder.
- */
-class RemovePlaceholderImageUC(private val drawableImportHelper: DrawableImportHelper) {
- suspend operator fun invoke(
- layoutFilePath: String?,
- resourceName: String
- ): Result {
- return drawableImportHelper.deleteDrawable(
- layoutFilePath = layoutFilePath,
- resourceName = resourceName
- )
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/RunVisionUC.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/RunVisionUC.kt
deleted file mode 100644
index 0bce67c0c8..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/usecase/RunVisionUC.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.usecase
-
-import android.graphics.Bitmap
-import kotlinx.coroutines.CancellationException
-import org.appdevforall.codeonthego.computervision.data.repository.VisionRepository
-import org.appdevforall.codeonthego.computervision.domain.DetectionMerger
-import org.appdevforall.codeonthego.computervision.domain.GenericBoxResolver
-import org.appdevforall.codeonthego.computervision.domain.MarginAnnotationParser
-import org.appdevforall.codeonthego.computervision.domain.RegionOcrProcessor
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-import org.appdevforall.codeonthego.computervision.ui.CvOperation
-
-/**
- * Main use case that orchestrates the complete computer vision pipeline:
- * 1. YOLO Detection -> 2. Region OCR -> 3. Merging -> 4. Metadata Parsing.
- */
-class RunVisionUC(
- private val repository: VisionRepository,
- private val boxResolver: GenericBoxResolver,
- private val regionOcrProcessor: RegionOcrProcessor
-) {
- data class VisionResult(
- val detections: List,
- val annotations: Map
- )
-
- suspend operator fun invoke(
- bitmap: Bitmap,
- leftPct: Float,
- rightPct: Float,
- onProgress: (CvOperation) -> Unit
- ): Result = runCatching {
-
- onProgress(CvOperation.RunningYolo)
- val rawDetections = repository.detectWidgets(bitmap).getOrThrow()
- val resolvedDetections = boxResolver.resolve(rawDetections)
-
- onProgress(CvOperation.RunningOcr)
- val ocrResult = regionOcrProcessor.process(bitmap, resolvedDetections, leftPct, rightPct)
-
- onProgress(CvOperation.MergingDetections)
- val mergedDetections = DetectionMerger(
- ocrResult.enrichedDetections,
- ocrResult.remainingDetections,
- ocrResult.fullImageTextBlocks
- ).merge()
-
- val leftBound = bitmap.width * leftPct
- val rightBound = bitmap.width * rightPct
- val canvasOnlyMerged = mergedDetections.filter { detection ->
- detection.isYolo || detection.boundingBox.centerX() in leftBound..rightBound
- }
-
- val allDetections = canvasOnlyMerged + ocrResult.marginDetections
-
- val (canvasDetections, annotationMap) = MarginAnnotationParser.parse(
- detections = allDetections,
- imageWidth = bitmap.width,
- leftGuidePct = leftPct,
- rightGuidePct = rightPct
- )
-
- VisionResult(canvasDetections, annotationMap)
- }.onFailure {
- if (it is CancellationException) throw it
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/AndroidConstants.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/AndroidConstants.kt
deleted file mode 100644
index 9cf33fca19..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/AndroidConstants.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.xml
-
-
-object AndroidConstants {
- const val MATCH_PARENT = "match_parent"
- const val WRAP_CONTENT = "wrap_content"
-
- const val ORIENTATION_HORIZONTAL = "horizontal"
-
- const val TRUE = "true"
- const val FALSE = "false"
-
- const val DEFAULT_TEXT_SIZE = "16sp"
-}
-
-object AndroidWidgetTags {
- const val LINEAR_LAYOUT = "LinearLayout"
- const val RADIO_GROUP = "RadioGroup"
-
- const val TEXT_VIEW = "TextView"
- const val BUTTON = "Button"
- const val IMAGE_VIEW = "ImageView"
- const val CHECK_BOX = "CheckBox"
- const val RADIO_BUTTON = "RadioButton"
- const val SWITCH = "Switch"
- const val EDIT_TEXT = "EditText"
- const val SPINNER = "Spinner"
- const val SEEK_BAR = "SeekBar"
- const val VIEW = "View"
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/AndroidWidget.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/AndroidWidget.kt
deleted file mode 100644
index 265fd5b32f..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/AndroidWidget.kt
+++ /dev/null
@@ -1,347 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.xml
-
-import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
-import org.appdevforall.codeonthego.computervision.domain.parser.AttributeKey
-import org.appdevforall.codeonthego.computervision.utils.extractOcrEntries
-
-sealed class AndroidWidget(
- protected open val box: ScaledBox?,
- protected val parsedAttrs: Map
-) {
- abstract val tag: String
- var idOverride: String? = null
- var extraAttrs: Map = emptyMap()
-
- protected open fun fallbackIdLabel() = box?.label ?: tag.lowercase()
- protected open fun defaultWidth() = AndroidConstants.WRAP_CONTENT
- protected open fun defaultHeight() = AndroidConstants.WRAP_CONTENT
- protected open fun getChildren(): List = emptyList()
-
- protected abstract fun specificAttributes(): Map
-
- protected open fun processAttributes(context: XmlContext, id: String, attrs: Map): Map {
- return attrs.mapValues { it.value.escapeXmlAttr() }
- }
-
- fun render(context: XmlContext, indent: String) {
- val resolvedId = resolveWidgetId(context)
- val finalAttributes = assembleAttributes(context, resolvedId)
- writeXml(context, indent, finalAttributes)
- }
-
- protected open fun resolveWidgetId(context: XmlContext): String {
- val requestedId = idOverride ?: parsedAttrs[AttributeKey.ID.xmlName]?.substringAfterLast('/')
- return context.resolveId(requestedId, fallbackIdLabel())
- }
-
- private fun assembleAttributes(context: XmlContext, resolvedId: String): Map {
- val width = parsedAttrs[AttributeKey.WIDTH.xmlName] ?: extraAttrs[AttributeKey.WIDTH.xmlName] ?: defaultWidth()
- val height = parsedAttrs[AttributeKey.HEIGHT.xmlName] ?: extraAttrs[AttributeKey.HEIGHT.xmlName] ?: defaultHeight()
-
- val assembledAttrs = mutableMapOf(
- AttributeKey.ID.xmlName to "@+id/${resolvedId.escapeXmlAttr()}",
- AttributeKey.WIDTH.xmlName to width.escapeXmlAttr(),
- AttributeKey.HEIGHT.xmlName to height.escapeXmlAttr()
- )
-
- specificAttributes().forEach { (k, v) -> assembledAttrs[k] = v.escapeXmlAttr() }
-
- val mergedAttrs = parsedAttrs + extraAttrs
- val processedAttrs = processAttributes(context, resolvedId, mergedAttrs)
-
- processedAttrs.forEach { (key, value) ->
- assembledAttrs.putIfAbsent(key, value)
- }
-
- return assembledAttrs
- }
-
- private fun writeXml(context: XmlContext, indent: String, attributes: Map) {
- context.append("$indent<$tag\n")
-
- attributes.forEach { (key, value) ->
- context.append("$indent $key=\"$value\"\n")
- }
-
- val childWidgets = getChildren()
- if (childWidgets.isEmpty()) {
- context.append("$indent/>")
- } else {
- context.append("$indent>\n")
- childWidgets.forEach { child ->
- child.render(context, "$indent ")
- context.appendLine()
- }
- context.append("$indent$tag>")
- }
- }
-
- companion object {
- private val nonAlphanumericRegex = Regex("[^a-z0-9_]")
- private val multipleUnderscoresRegex = Regex("_+")
-
- fun create(box: ScaledBox, parsedAttrs: Map): AndroidWidget {
- return when (box.label) {
- "text", "button", "radio_button_unchecked", "radio_button_checked" ->
- TextBasedWidget(box, parsedAttrs, getTagFor(box.label))
- "checkbox_unchecked", "checkbox_checked" -> CheckBoxWidget(box, parsedAttrs)
- "switch_off", "switch_on" -> SwitchWidget(box, parsedAttrs)
- "text_entry_box" -> InputWidget(box, parsedAttrs)
- "image_placeholder", "icon" -> ImageWidget(box, parsedAttrs)
- "dropdown" -> SpinnerWidget(box, parsedAttrs)
- else -> GenericWidget(box, parsedAttrs, getTagFor(box.label))
- }
- }
-
- fun getTagFor(label: String): String = when (label) {
- "text" -> AndroidWidgetTags.TEXT_VIEW
- "button" -> AndroidWidgetTags.BUTTON
- "image_placeholder", "icon" -> AndroidWidgetTags.IMAGE_VIEW
- "checkbox_unchecked", "checkbox_checked" -> AndroidWidgetTags.CHECK_BOX
- "radio_button_unchecked", "radio_button_checked" -> AndroidWidgetTags.RADIO_BUTTON
- "switch_off", "switch_on" -> AndroidWidgetTags.SWITCH
- "text_entry_box" -> AndroidWidgetTags.EDIT_TEXT
- "dropdown" -> AndroidWidgetTags.SPINNER
- "slider" -> AndroidWidgetTags.SEEK_BAR
- else -> AndroidWidgetTags.VIEW
- }
-
- internal fun sanitizeResourceName(raw: String): String {
- return raw
- .lowercase()
- .replace('-', '_')
- .replace(nonAlphanumericRegex, "_")
- .replace(multipleUnderscoresRegex, "_")
- .trim('_')
- }
- }
-}
-
-class SpinnerWidget(
- override val box: ScaledBox, parsedAttrs: Map
-) : AndroidWidget(box, parsedAttrs) {
- companion object {
- private val placeholderEntries = setOf("year", "month", "day", "select", "choose", "dropdown")
- }
-
- override val tag = AndroidWidgetTags.SPINNER
- override fun fallbackIdLabel(): String {
- val normalizedLabel = sanitizeResourceName(box.text.normalizedDropdownLabel())
- return normalizedLabel.takeIf { it.isNotBlank() }?.let { "dd_$it" } ?: "spinner"
- }
- override fun specificAttributes() = emptyMap()
-
- override fun resolveWidgetId(context: XmlContext): String {
- val requestedId = idOverride ?: parsedAttrs[AttributeKey.ID.xmlName]?.substringAfterLast('/')
- if (requestedId != null) return context.resolveId(requestedId, fallbackIdLabel())
-
- val derivedId = fallbackIdLabel()
- return derivedId
- .takeUnless { it == "spinner" }
- ?.let { context.resolveId(it, "spinner") }
- ?: context.nextId("spinner")
- }
-
- override fun processAttributes(context: XmlContext, id: String, attrs: Map): Map {
- val processed = mutableMapOf()
- val rawEntries = attrs[AttributeKey.ENTRIES.xmlName]
- ?: attrs[AttributeKey.TEXT.xmlName]
- ?: box.text.takeIf { it.isMeaningfulDropdownText() }
-
- when {
- rawEntries == null -> Unit
- rawEntries.trimStart().startsWith("@") -> {
- processed[AttributeKey.ENTRIES.xmlName] = rawEntries.trim().escapeXmlAttr()
- }
- else -> rawEntries
- .toSpinnerEntries()
- .takeIf { items -> items.isNotEmpty() && !items.isSinglePlaceholderEntry() }
- ?.let { items ->
- val arrayName = "${id}_array"
- context.stringArrays[arrayName] = items
- processed[AttributeKey.ENTRIES.xmlName] = "@array/$arrayName"
- }
- }
-
- attrs.forEach { (key, value) ->
- when {
- key == AttributeKey.ENTRIES.xmlName || key == AttributeKey.TEXT.xmlName -> Unit
- else -> processed[key] = value.escapeXmlAttr()
- }
- }
- return processed
- }
-
- private fun List.isSinglePlaceholderEntry(): Boolean {
- if (size != 1) return false
- return first().normalizedDropdownLabel().lowercase() in placeholderEntries
- }
-
- private fun String.toSpinnerEntries(): List {
- return this.removeTrailingDropdownGlyph().extractOcrEntries()
- }
-
- private fun String.removeTrailingDropdownGlyph(): String {
- return trim()
- .replace(Regex("\\s*[▼▽▾▿⌄˅∨]$|\\s+[vV]$"), "")
- .trim()
- }
-
- private fun String.removeLeadingDropdownHint(): String {
- return trim()
- .replace(Regex("^[vV]\\s+"), "")
- .trim()
- }
-
- private fun String.normalizedDropdownLabel(): String {
- return removeTrailingDropdownGlyph()
- .removeLeadingDropdownHint()
- .trim()
- }
-
- private fun String.isMeaningfulDropdownText(): Boolean {
- val cleaned = normalizedDropdownLabel()
- return cleaned.isNotBlank() && !cleaned.equals("dropdown", ignoreCase = true)
- }
-}
-
-class TextBasedWidget(
- override val box: ScaledBox, parsedAttrs: Map, override val tag: String
-) : AndroidWidget(box, parsedAttrs) {
- private val widgetTags = setOf(AndroidWidgetTags.SWITCH, AndroidWidgetTags.CHECK_BOX, AndroidWidgetTags.RADIO_BUTTON)
-
- override fun specificAttributes(): Map {
- val attrs = mutableMapOf()
- val rawViewText = parsedAttrs[AttributeKey.TEXT.xmlName]
- ?: box.text.takeIf { it.isNotEmpty() && it != box.label }
- ?: if (tag in widgetTags) tag else box.label
-
- attrs[AttributeKey.TEXT.xmlName] = rawViewText
- attrs["tools:ignore"] = "HardcodedText"
-
- if (tag == AndroidWidgetTags.TEXT_VIEW || tag in widgetTags) {
- attrs[AttributeKey.TEXT_SIZE.xmlName] = parsedAttrs[AttributeKey.TEXT_SIZE.xmlName] ?: AndroidConstants.DEFAULT_TEXT_SIZE
- }
- if (box.label.contains("_checked") || box.label.contains("_on")) {
- attrs[AttributeKey.CHECKED.xmlName] = parsedAttrs[AttributeKey.CHECKED.xmlName] ?: AndroidConstants.TRUE
- }
- return attrs
- }
-
- override fun fallbackIdLabel(): String {
- return if (tag == AndroidWidgetTags.RADIO_BUTTON) "radio_button" else super.fallbackIdLabel()
- }
-}
-
-class CheckBoxWidget(
- override val box: ScaledBox, parsedAttrs: Map
-) : AndroidWidget(box, parsedAttrs) {
- override val tag = AndroidWidgetTags.CHECK_BOX
-
- override fun specificAttributes(): Map {
- val attrs = mutableMapOf()
- val rawViewText = box.text.takeIf { it.isNotEmpty() && it != box.label }
- ?: parsedAttrs[AttributeKey.TEXT.xmlName]
- ?: AndroidWidgetTags.CHECK_BOX
-
- attrs[AttributeKey.TEXT.xmlName] = rawViewText
- attrs["tools:ignore"] = "HardcodedText"
-
- if (box.label.contains("_checked")) {
- attrs[AttributeKey.CHECKED.xmlName] = parsedAttrs[AttributeKey.CHECKED.xmlName] ?: AndroidConstants.TRUE
- }
- return attrs
- }
-
- override fun fallbackIdLabel(): String = "checkbox"
-}
-
-class SwitchWidget(
- override val box: ScaledBox, parsedAttrs: Map
-) : AndroidWidget(box, parsedAttrs) {
- override val tag = AndroidWidgetTags.SWITCH
- override fun fallbackIdLabel(): String = "switch"
-
- override fun specificAttributes(): Map {
- val attrs = mutableMapOf()
- val switchText = parsedAttrs[AttributeKey.TEXT.xmlName] ?: box.text.trim().takeIf { it.isNotEmpty() && it != box.label } ?: AndroidWidgetTags.SWITCH
-
- attrs[AttributeKey.TEXT.xmlName] = switchText
- attrs["tools:ignore"] = "HardcodedText"
-
- if (box.label.contains("_on")) {
- attrs[AttributeKey.CHECKED.xmlName] = parsedAttrs[AttributeKey.CHECKED.xmlName] ?: AndroidConstants.TRUE
- }
- return attrs
- }
-}
-
-class InputWidget(
- override val box: ScaledBox, parsedAttrs: Map
-) : AndroidWidget(box, parsedAttrs) {
- override val tag = AndroidWidgetTags.EDIT_TEXT
-
- override fun specificAttributes(): Map {
- val resolvedHint = parsedAttrs[AttributeKey.HINT.xmlName] ?: box.text.ifEmpty { "Enter text..." }
- val resolvedInputType = parsedAttrs[AttributeKey.INPUT_TYPE.xmlName] ?: "text"
-
- return mapOf(
- AttributeKey.HINT.xmlName to resolvedHint,
- AttributeKey.INPUT_TYPE.xmlName to resolvedInputType,
- "tools:ignore" to "HardcodedText"
- )
- }
-}
-
-class ImageWidget(
- override val box: ScaledBox, parsedAttrs: Map
-) : AndroidWidget(box, parsedAttrs) {
- override val tag = AndroidWidgetTags.IMAGE_VIEW
- override fun specificAttributes(): Map = mapOf(
- AttributeKey.CONTENT_DESCRIPTION.xmlName to (parsedAttrs[AttributeKey.CONTENT_DESCRIPTION.xmlName] ?: box.label),
- )
-}
-
-class GenericWidget(
- override val box: ScaledBox, parsedAttrs: Map, override val tag: String
-) : AndroidWidget(box, parsedAttrs) {
- override fun specificAttributes() = emptyMap()
-}
-
-abstract class AndroidViewGroup(
- parsedAttrs: Map,
- protected val childWidgets: List
-) : AndroidWidget(null, parsedAttrs) {
- override fun getChildren() = childWidgets
-}
-
-class HorizontalRowWidget(
- childWidgets: List
-) : AndroidViewGroup(emptyMap(), childWidgets) {
- override val tag = AndroidWidgetTags.LINEAR_LAYOUT
- override fun fallbackIdLabel() = "linear_layout"
- override fun defaultWidth() = AndroidConstants.MATCH_PARENT
- override fun specificAttributes() = mapOf(
- "android:orientation" to AndroidConstants.ORIENTATION_HORIZONTAL,
- "android:baselineAligned" to AndroidConstants.FALSE
- )
-}
-
-class RadioGroupWidget(
- parsedAttrs: Map,
- childWidgets: List,
- private val orientation: String,
- private val checkedId: String?
-) : AndroidViewGroup(parsedAttrs, childWidgets) {
- override val tag = AndroidWidgetTags.RADIO_GROUP
- override fun fallbackIdLabel() = "radio_group"
- override fun defaultWidth() = AndroidConstants.MATCH_PARENT
-
- override fun specificAttributes(): Map {
- val attrs = mutableMapOf("android:orientation" to orientation)
- if (checkedId != null) {
- attrs["android:checkedButton"] = "@id/$checkedId"
- }
- return attrs
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/AndroidXmlGenerator.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/AndroidXmlGenerator.kt
deleted file mode 100644
index 2f949bd89b..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/AndroidXmlGenerator.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.xml
-
-import org.appdevforall.codeonthego.computervision.domain.LayoutTreeBuilder
-import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
-
-class AndroidXmlGenerator {
- internal fun buildXml(
- boxes: List,
- annotations: Map,
- selectedImageOverrides: Map,
- targetDpHeight: Int,
- wrapInScroll: Boolean
- ): Pair {
- val context = XmlContext()
- val maxBottom = boxes.maxOfOrNull { it.y + it.h } ?: 0
- val needScroll = wrapInScroll && maxBottom > targetDpHeight
-
- appendHeaders(context, needScroll)
-
- val layoutItems = LayoutTreeBuilder.buildLayoutTree(boxes)
- val renderer = LayoutRenderer(context, annotations, selectedImageOverrides = selectedImageOverrides)
-
- layoutItems.forEach { item -> renderer.render(item, " ") }
-
- appendFooters(context, needScroll)
-
- val layoutXml = context.toString()
- val stringsXml = generateStringsResourceXml(context)
-
- return Pair(layoutXml, stringsXml)
- }
-
- private fun generateStringsResourceXml(context: XmlContext): String {
- if (context.stringArrays.isEmpty()) return ""
-
- val builder = StringBuilder()
- context.stringArrays.forEach { (name, items) ->
- builder.appendLine(" ")
- items.forEach { item ->
- builder.appendLine(" - ${item.escapeXmlAttr()}
")
- }
- builder.appendLine(" ")
- }
-
- return builder.toString().trimEnd()
- }
-
- private fun appendHeaders(context: XmlContext, needScroll: Boolean) {
- val namespaces = """xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools""""
- context.appendLine("")
-
- if (needScroll) {
- context.appendLine("")
- context.appendLine(" ")
- } else {
- context.appendLine("")
- }
- context.appendLine()
- }
-
- private fun appendFooters(context: XmlContext, needScroll: Boolean) {
- context.appendLine(if (needScroll) " \n" else "")
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/LayoutRenderer.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/LayoutRenderer.kt
deleted file mode 100644
index 35b558c441..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/LayoutRenderer.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.xml
-
-import org.appdevforall.codeonthego.computervision.domain.model.LayoutItem
-import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
-
-class LayoutRenderer(
- private val context: XmlContext,
- annotations: Map,
- selectedImageOverrides: Map = emptyMap()
-) {
- private val widgetFactory = WidgetFactory(context, annotations, selectedImageOverrides)
-
- fun render(item: LayoutItem, indent: String = " ") {
- val widgets = widgetFactory.createWidgets(item)
-
- widgets.forEach { widget ->
- widget.render(context, indent)
- context.appendLine()
- }
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/WidgetFactory.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/WidgetFactory.kt
deleted file mode 100644
index 2fd1210782..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/WidgetFactory.kt
+++ /dev/null
@@ -1,193 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.xml
-
-import org.appdevforall.codeonthego.computervision.domain.model.LayoutItem
-import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
-import org.appdevforall.codeonthego.computervision.domain.parser.AttributeKey
-import org.appdevforall.codeonthego.computervision.domain.parser.FuzzyAttributeParser
-
-class WidgetFactory(
- private val context: XmlContext,
- private val annotations: Map,
- private val selectedImageOverrides: Map = emptyMap()
-) {
- private val checkboxGroupIdPattern = Regex("^cb_group_\\d+$")
- private val radioChildGroupIdPatterns = listOf(
- Regex("^rb_group(?:_\\d+)?(?:_|$).*"),
- Regex("^radio_group(?:_\\d+)?(?:_|$).*")
- )
-
- fun createWidgets(item: LayoutItem): List = when (item) {
- is LayoutItem.SimpleView -> createWidgetsForBox(item.box)
- is LayoutItem.HorizontalRow -> createHorizontalRow(item)
- is LayoutItem.RadioGroup -> createRadioGroup(item)
- is LayoutItem.CheckboxGroup -> createCheckboxGroup(item)
- }
-
- private fun createHorizontalRow(item: LayoutItem.HorizontalRow): List {
- val children = item.row.flatMapIndexed { index, box ->
- val extraAttrs = getMarginEndForHorizontalGap(item.row, index)
- createWidgetsForBox(box, extraAttrs = extraAttrs)
- }
- return listOf(HorizontalRowWidget(children))
- }
-
- private fun createWidgetsForBox(
- box: ScaledBox,
- extraAttrs: Map = emptyMap(),
- idOverride: String? = null,
- parsedAttrsOverride: Map? = null
- ): List {
- val widgets = mutableListOf()
-
- val dropdownTitle = box.text
- .takeIf { box.label == "dropdown" }
- ?.let(::extractDropdownTitle)
-
- if (dropdownTitle != null) {
- val titleBox = box.copy(label = "text", text = dropdownTitle)
-
- val titleAttrs = mapOf(
- AttributeKey.WIDTH.xmlName to AndroidConstants.WRAP_CONTENT,
- AttributeKey.HEIGHT.xmlName to AndroidConstants.WRAP_CONTENT,
- AttributeKey.LAYOUT_MARGIN_BOTTOM.xmlName to "4dp",
- AttributeKey.TEXT.xmlName to dropdownTitle,
- AttributeKey.TEXT_STYLE.xmlName to "bold"
- )
- widgets.add(createSimpleWidget(titleBox, parsedAttrsOverride = titleAttrs))
-
- val baseParsedAttrs = parsedAttrsOverride?.toMutableMap()
- ?: FuzzyAttributeParser.parse(annotations[box], AndroidWidget.getTagFor(box.label)).toMutableMap()
- baseParsedAttrs.remove(AttributeKey.TEXT.xmlName)
-
- val spinnerBox = box.copy(text = "")
- widgets.add(createSimpleWidget(spinnerBox, extraAttrs, idOverride, baseParsedAttrs))
-
- return widgets
- }
-
- widgets.add(createSimpleWidget(box, extraAttrs, idOverride, parsedAttrsOverride))
- return widgets
- }
-
- private fun createRadioGroup(item: LayoutItem.RadioGroup): List {
- val groupAnnotation = item.boxes.firstNotNullOfOrNull { annotations[it] }
- val fullGroupAttrs = FuzzyAttributeParser.parse(groupAnnotation, "RadioGroup")
-
- val groupId = resolveRadioGroupId(fullGroupAttrs["android:id"]?.substringAfterLast('/'))
-
- val groupStructuralAttrs = setOf("android:id", "android:layout_width", "android:layout_height", "android:orientation")
- val sharedAttrs = fullGroupAttrs.filterKeys { it !in groupStructuralAttrs }
-
- var checkedId: String? = null
-
- val children = item.boxes.mapIndexed { index, box ->
- val parsedAttrs = (sharedAttrs + FuzzyAttributeParser.parse(annotations[box], "RadioButton")).toMutableMap()
-
- if (parsedAttrs["android:id"] == fullGroupAttrs["android:id"]) {
- parsedAttrs.remove("android:id")
- }
-
- val requestedId = parsedAttrs["android:id"]?.substringAfterLast('/')
- val childId = if (requestedId != null && radioChildGroupIdPatterns.any { it.matches(requestedId) }) {
- context.nextId("radio_button")
- } else {
- context.resolveId(requestedId, "radio_button")
- }
-
- val isChecked = box.label == "radio_button_checked" || parsedAttrs["android:checked"]?.equals("true", ignoreCase = true) == true
- if (isChecked) {
- checkedId = childId
- parsedAttrs["android:checked"] = "true"
- } else {
- parsedAttrs["android:checked"] = "false"
- }
-
- val extraAttrs = if (item.orientation == "horizontal") {
- getMarginEndForHorizontalGap(item.boxes, index)
- } else emptyMap()
-
- createSimpleWidget(box, parsedAttrsOverride = parsedAttrs, idOverride = childId, extraAttrs = extraAttrs)
- }
-
- val textStyleAttrs = setOf("android:textColor", "android:textSize", "android:textStyle", "android:fontFamily")
- val groupFinalAttrs = fullGroupAttrs.filterKeys { it !in textStyleAttrs }.toMutableMap()
- groupFinalAttrs["android:id"] = groupId
-
- return listOf(RadioGroupWidget(groupFinalAttrs, children, item.orientation, checkedId))
- }
-
- private fun createCheckboxGroup(item: LayoutItem.CheckboxGroup): List {
- val groupAnnotation = item.boxes.firstNotNullOfOrNull { annotations[it] }
- val parsedAttrs = FuzzyAttributeParser.parse(groupAnnotation, "CheckBox")
-
- val requestedId = parsedAttrs["android:id"]?.substringAfterLast('/')
- val baseId = if (requestedId != null && checkboxGroupIdPattern.matches(requestedId)) {
- context.resolveId(requestedId, "cb_group")
- } else {
- context.nextId("cb_group", initialIndex = 1)
- }
-
- return item.boxes.mapIndexed { index, box ->
- val suffix = ('a' + index).toString()
- val childId = "${baseId}_$suffix"
-
- val safeAttrs = parsedAttrs.toMutableMap()
- safeAttrs.remove("android:id")
-
- val extraAttrs = if (item.orientation == "horizontal") {
- getMarginEndForHorizontalGap(item.boxes, index)
- } else emptyMap()
-
- createSimpleWidget(box, parsedAttrsOverride = safeAttrs, idOverride = childId, extraAttrs = extraAttrs)
- }
- }
-
- private fun createSimpleWidget(
- box: ScaledBox,
- extraAttrs: Map = emptyMap(),
- idOverride: String? = null,
- parsedAttrsOverride: Map? = null
- ): AndroidWidget {
- val tag = AndroidWidget.getTagFor(box.label)
- val parsedAttrs = parsedAttrsOverride?.toMutableMap()
- ?: FuzzyAttributeParser.parse(annotations[box], tag).toMutableMap()
-
- selectedImageOverrides[box]?.let { drawableReference ->
- parsedAttrs["android:src"] = drawableReference
- }
-
- return AndroidWidget.create(box, parsedAttrs).apply {
- this.idOverride = idOverride
- this.extraAttrs = extraAttrs
- }
- }
-
- private fun extractDropdownTitle(rawText: String): String? {
- val cleaned = rawText.trim()
- .replace(Regex("\\s*[▼▽▾▿⌄˅∨]$|\\s+[vV]$"), "")
- .replace(Regex("^[vV]\\s+"), "")
- .trim()
-
- return cleaned.takeIf { it.isNotBlank() && !it.equals("dropdown", ignoreCase = true) }
- }
-
- private fun getMarginEndForHorizontalGap(boxes: List, currentIndex: Int): Map {
- if (currentIndex >= boxes.lastIndex) return emptyMap()
- val currentBox = boxes[currentIndex]
- val nextBox = boxes[currentIndex + 1]
- val gap = maxOf(0, nextBox.x - (currentBox.x + currentBox.w))
- return mapOf("android:layout_marginEnd" to "${gap}dp")
- }
-
- private fun resolveRadioGroupId(requestedId: String?): String {
- var cleanId = requestedId
- if (requestedId != null) {
- val normalizedId = requestedId.lowercase()
- when {
- normalizedId.startsWith("radio_grou") -> cleanId = "radio_group"
- normalizedId.startsWith("rb_grou") || normalizedId.startsWith("rb_group") -> cleanId = "rb_group"
- }
- }
- return context.resolveId(cleanId, "radio_group")
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/XmlContext.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/XmlContext.kt
deleted file mode 100644
index 737e60f4a2..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/domain/xml/XmlContext.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.xml
-
-class XmlContext(
- val builder: StringBuilder = StringBuilder(),
- private val counters: MutableMap = mutableMapOf()
-) {
- private val usedIds = mutableSetOf()
- val stringArrays = mutableMapOf>()
-
- fun nextId(label: String, initialIndex: Int = 0): String {
- val safeLabel = label.replace(Regex("[^a-zA-Z0-9_]"), "_")
-
- var count = counters.getOrDefault(safeLabel, initialIndex - 1)
- var newId: String
-
- do {
- count++
- newId = "${safeLabel}_$count"
- } while (usedIds.contains(newId))
-
- counters[safeLabel] = count
- usedIds.add(newId)
-
- return newId
- }
-
- fun registerId(id: String) {
- usedIds.add(id)
- }
-
- fun resolveId(requestedId: String?, fallbackLabel: String): String {
- return if (requestedId != null) {
- registerId(requestedId)
- requestedId
- } else {
- nextId(fallbackLabel)
- }
- }
-
- fun appendLine(text: String = "") {
- builder.appendLine(text)
- }
-
- fun append(text: String) {
- builder.append(text)
- }
-
- override fun toString(): String = builder.toString()
-}
-
-fun String.escapeXmlAttr(): String = this.trim()
- .replace("&", "&")
- .replace("<", "<")
- .replace(">", ">")
- .replace("\"", """)
- .replace("'", "'")
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/ComputerVisionActivity.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/ComputerVisionActivity.kt
deleted file mode 100644
index 773682763c..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/ComputerVisionActivity.kt
+++ /dev/null
@@ -1,235 +0,0 @@
-package org.appdevforall.codeonthego.computervision.ui
-
-import android.Manifest
-import android.content.ContentValues
-import android.content.Intent
-import android.os.Bundle
-import android.provider.MediaStore
-import android.widget.Toast
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.itsaky.androidide.FeedbackButtonManager
-import kotlinx.coroutines.launch
-import org.appdevforall.codeonthego.computervision.R
-import org.appdevforall.codeonthego.computervision.databinding.ActivityComputerVisionBinding
-import org.appdevforall.codeonthego.computervision.ui.viewmodel.ComputerVisionViewModel
-import org.appdevforall.codeonthego.computervision.utils.DetectionVisualizer
-import org.appdevforall.codeonthego.computervision.utils.XmlFileManager
-import org.koin.androidx.viewmodel.ext.android.viewModel
-import org.koin.core.parameter.parametersOf
-
-class ComputerVisionActivity : AppCompatActivity() {
-
- private lateinit var binding: ActivityComputerVisionBinding
- private var feedbackButtonManager: FeedbackButtonManager? = null
-
- private val detectionVisualizer by lazy { DetectionVisualizer(this) }
- private val xmlFileManager by lazy { XmlFileManager(this) }
-
- private val viewModel: ComputerVisionViewModel by viewModel {
- parametersOf(
- intent.getStringExtra(EXTRA_LAYOUT_FILE_PATH),
- intent.getStringExtra(EXTRA_LAYOUT_FILE_NAME)
- )
- }
-
- private var currentCameraUri: android.net.Uri? = null
-
- private val pickImageLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
- uri?.let { viewModel.onEvent(ComputerVisionEvent.ImageSelected(it)) }
- ?: Toast.makeText(this, R.string.msg_no_image_selected, Toast.LENGTH_SHORT).show()
- }
-
- private val takePictureLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
- currentCameraUri?.let { uri ->
- viewModel.onEvent(ComputerVisionEvent.ImageCaptured(uri, success))
- }
- }
-
- private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
- if (granted) launchCamera()
- else Toast.makeText(this, R.string.msg_camera_permission_required, Toast.LENGTH_LONG).show()
- }
-
- private val pickPlaceholderImageLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
- uri?.let { viewModel.onEvent(ComputerVisionEvent.PlaceholderImageSelected(it)) }
- ?: Toast.makeText(this, R.string.msg_no_image_selected, Toast.LENGTH_SHORT).show()
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityComputerVisionBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- setupToolbar()
- setupClickListeners()
- observeViewModel()
- setupFeedbackButton()
- setupGuidelines()
- }
-
- private fun setupToolbar() {
- setSupportActionBar(binding.toolbar)
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- binding.toolbar.setNavigationOnClickListener { finish() }
- }
-
- private fun setupClickListeners() {
- with(binding) {
- imageView.setOnClickListener { viewModel.onEvent(ComputerVisionEvent.OpenImagePicker) }
- detectButton.setOnClickListener { viewModel.onEvent(ComputerVisionEvent.RunDetection) }
- updateButton.setOnClickListener { viewModel.onEvent(ComputerVisionEvent.UpdateLayoutFile) }
- saveButton.setOnClickListener { viewModel.onEvent(ComputerVisionEvent.SaveToDownloads) }
- imageView.onImageTapListener = ::handleImageTap
- }
- }
-
- private fun observeViewModel() {
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.onScreenStarted()
- launch { viewModel.uiState.collect { updateUi(it) } }
- launch { viewModel.uiEffect.collect { handleEffect(it) } }
- }
- }
- }
-
- private fun setupFeedbackButton(){
- feedbackButtonManager = FeedbackButtonManager(activity = this, feedbackFab = binding.fabFeedback)
- feedbackButtonManager?.setupDraggableFab()
- }
-
- private fun setupGuidelines() {
- binding.imageView.onMatrixChangeListener = { matrix ->
- binding.guidelinesView.updateMatrix(matrix)
- }
- binding.guidelinesView.onGuidelinesChanged = { left, right ->
- viewModel.onEvent(ComputerVisionEvent.UpdateGuides(left, right))
- }
- }
-
- private fun updateUi(state: ComputerVisionUiState) {
- val displayBitmap = if (state.hasDetections && state.currentBitmap != null) {
- detectionVisualizer.visualize(
- bitmap = state.currentBitmap,
- detections = state.detections,
- selectedPlaceholderIds = state.selectedImagesByPlaceholderId.keys
- )
- } else {
- detectionVisualizer.clearCache()
- state.currentBitmap
- }
-
- binding.imageView.setImageBitmap(displayBitmap)
- state.currentBitmap?.let {
- binding.guidelinesView.setImageDimensions(it.width, it.height)
- }
- binding.guidelinesView.updateGuidelines(state.leftGuidePct, state.rightGuidePct)
-
- binding.detectButton.isEnabled = state.canRunDetection
- binding.updateButton.isEnabled = state.canGenerateXml
- binding.saveButton.isEnabled = state.canGenerateXml
- }
-
- private fun handleEffect(effect: ComputerVisionEffect) {
- when (effect) {
- ComputerVisionEffect.OpenImagePicker -> pickImageLauncher.launch("image/*")
- ComputerVisionEffect.RequestCameraPermission ->
- requestPermissionLauncher.launch(Manifest.permission.CAMERA)
- is ComputerVisionEffect.LaunchCamera -> {
- currentCameraUri = effect.outputUri
- takePictureLauncher.launch(effect.outputUri)
- }
- is ComputerVisionEffect.ShowToast ->
- Toast.makeText(this, effect.messageResId, Toast.LENGTH_SHORT).show()
- is ComputerVisionEffect.ShowError ->
- Toast.makeText(this, effect.message, Toast.LENGTH_LONG).show()
- is ComputerVisionEffect.ShowConfirmDialog ->
- showUpdateConfirmationDialog(effect.fileName)
- is ComputerVisionEffect.ReturnXmlResult -> returnXmlResult(effect.layoutXml, effect.stringsXml)
- ComputerVisionEffect.NavigateBack -> finish()
- ComputerVisionEffect.OpenPlaceholderImagePicker ->
- pickPlaceholderImageLauncher.launch("image/*")
- is ComputerVisionEffect.FileSaved -> saveXmlFile(effect.fileName)
- }
- }
-
- /**
- * Handles tap events on the image view, determining whether the user tapped
- * a delete action or a general placeholder, and routes the event to the ViewModel.
- *
- * @param imageX The X coordinate of the tap on the original image.
- * @param imageY The Y coordinate of the tap on the original image.
- * @return True if the tap was handled, false otherwise.
- */
- private fun handleImageTap(imageX: Float, imageY: Float): Boolean {
- val tappedDeleteId = detectionVisualizer.getTappedDeleteIconId(imageX, imageY)
- if (tappedDeleteId != null) {
- viewModel.onEvent(ComputerVisionEvent.RemovePlaceholderImage(tappedDeleteId))
- return true
- }
-
- if (!viewModel.isImagePlaceholderAt(imageX, imageY)) return false
-
- viewModel.onEvent(ComputerVisionEvent.ImagePlaceholderTapped(imageX, imageY))
- return true
- }
-
- private fun saveXmlFile(xmlString: String) {
- val result = xmlFileManager.saveXmlToDownloads(xmlString)
- result.onSuccess { fileName ->
- Toast.makeText(this, getString(R.string.msg_saved_to_downloads, fileName), Toast.LENGTH_LONG).show()
- }.onFailure { error ->
- Toast.makeText(this, getString(R.string.msg_error_saving_file, error.message), Toast.LENGTH_SHORT).show()
- }
- }
-
- private fun showUpdateConfirmationDialog(fileName: String) {
- MaterialAlertDialogBuilder(this)
- .setTitle(R.string.title_update_layout)
- .setMessage(getString(R.string.msg_overwrite_layout, fileName))
- .setNegativeButton(R.string.no, null)
- .setPositiveButton(R.string.yes) { dialog, _ ->
- dialog.dismiss()
- viewModel.onEvent(ComputerVisionEvent.ConfirmUpdate)
- }
- .setCancelable(false)
- .show()
- }
-
- private fun returnXmlResult(layoutXml: String, stringsXml: String) {
- setResult(RESULT_OK, Intent().apply {
- putExtra(RESULT_GENERATED_XML, layoutXml)
- putExtra(RESULT_GENERATED_STRINGS, stringsXml)
- putExtra(EXTRA_LAYOUT_FILE_PATH, intent.getStringExtra(EXTRA_LAYOUT_FILE_PATH))
- })
- finish()
- }
-
- private fun launchCamera() {
- val values = ContentValues().apply {
- put(MediaStore.Images.Media.TITLE, getString(R.string.camera_picture_title))
- put(MediaStore.Images.Media.DESCRIPTION, getString(R.string.camera_picture_description))
- }
- contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)?.let { uri ->
- currentCameraUri = uri
- takePictureLauncher.launch(uri)
- }
- }
-
- override fun onResume() {
- super.onResume()
- feedbackButtonManager?.loadFabPosition()
- }
-
- companion object {
- const val EXTRA_LAYOUT_FILE_PATH = "com.example.images.LAYOUT_FILE_PATH"
- const val EXTRA_LAYOUT_FILE_NAME = "com.example.images.LAYOUT_FILE_NAME"
- const val RESULT_GENERATED_XML = "ide.uidesigner.generatedXml"
- const val RESULT_GENERATED_STRINGS = "ide.uidesigner.generatedStrings"
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/ComputerVisionEvent.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/ComputerVisionEvent.kt
deleted file mode 100644
index 857a4d5ca2..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/ComputerVisionEvent.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.appdevforall.codeonthego.computervision.ui
-
-import android.net.Uri
-
-sealed class ComputerVisionEvent {
- data class ImageSelected(val uri: Uri) : ComputerVisionEvent()
- data class ImageCaptured(val uri: Uri, val success: Boolean) : ComputerVisionEvent()
- object RunDetection : ComputerVisionEvent()
- object UpdateLayoutFile : ComputerVisionEvent()
- object ConfirmUpdate : ComputerVisionEvent()
- object SaveToDownloads : ComputerVisionEvent()
- object OpenImagePicker : ComputerVisionEvent()
- object RequestCameraPermission : ComputerVisionEvent()
- data class UpdateGuides(val leftPct: Float, val rightPct: Float) : ComputerVisionEvent()
- data class ImagePlaceholderTapped(val imageX: Float, val imageY: Float) : ComputerVisionEvent()
- data class PlaceholderImageSelected(val uri: Uri) : ComputerVisionEvent()
- data class RemovePlaceholderImage(val placeholderId: String) : ComputerVisionEvent()
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/ComputerVisionUiState.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/ComputerVisionUiState.kt
deleted file mode 100644
index 69f010b23d..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/ComputerVisionUiState.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package org.appdevforall.codeonthego.computervision.ui
-
-import android.graphics.Bitmap
-import android.net.Uri
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-
-data class ComputerVisionUiState(
- val currentBitmap: Bitmap? = null,
- val imageUri: Uri? = null,
- val detections: List = emptyList(),
- val visualizedBitmap: Bitmap? = null,
- val layoutFilePath: String? = null,
- val layoutFileName: String? = null,
- val isModelInitialized: Boolean = false,
- val currentOperation: CvOperation = CvOperation.Idle,
- val leftGuidePct: Float = 0.2f,
- val rightGuidePct: Float = 0.8f,
- val parsedAnnotations: Map = emptyMap(), // Replaced old marginAnnotations
- val pendingImagePlaceholderId: String? = null,
- val selectedImagesByPlaceholderId: Map = emptyMap()
-) {
- val hasImage: Boolean
- get() = currentBitmap != null
-
- val hasDetections: Boolean
- get() = detections.isNotEmpty()
-
- val canRunDetection: Boolean
- get() = hasImage && isModelInitialized && currentOperation == CvOperation.Idle
-
- val canGenerateXml: Boolean
- get() = hasDetections && currentOperation == CvOperation.Idle
-}
-
-sealed class CvOperation {
- object Idle : CvOperation()
- object InitializingModel : CvOperation()
- object RunningYolo : CvOperation()
- object RunningOcr : CvOperation()
- object MergingDetections : CvOperation()
- object GeneratingXml : CvOperation()
- object SavingFile : CvOperation()
-}
-
-sealed class ComputerVisionEffect {
- object OpenImagePicker : ComputerVisionEffect()
- object RequestCameraPermission : ComputerVisionEffect()
- data class LaunchCamera(val outputUri: Uri) : ComputerVisionEffect()
- data class ShowToast(val messageResId: Int) : ComputerVisionEffect()
- data class ShowError(val message: String) : ComputerVisionEffect()
- data class ShowConfirmDialog(val fileName: String) : ComputerVisionEffect()
- data class ReturnXmlResult(val layoutXml: String, val stringsXml: String) : ComputerVisionEffect()
- data class FileSaved(val fileName: String) : ComputerVisionEffect()
- object NavigateBack : ComputerVisionEffect()
- object OpenPlaceholderImagePicker : ComputerVisionEffect()
-}
-
-data class SelectedImportedImage(
- val resourceName: String,
- val drawableReference: String
-)
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/GuidelinesView.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/GuidelinesView.kt
deleted file mode 100644
index d1ea9ffdda..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/GuidelinesView.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-package org.appdevforall.codeonthego.computervision.ui
-
-import android.content.Context
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.Matrix
-import android.graphics.Paint
-import android.graphics.RectF
-import android.util.AttributeSet
-import android.view.MotionEvent
-import android.view.View
-
-class GuidelinesView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0
-) : View(context, attrs, defStyleAttr) {
-
- private val linePaint = Paint().apply {
- color = Color.RED
- strokeWidth = 5f
- alpha = 180
- }
-
- private val textPaint = Paint().apply {
- color = Color.WHITE
- textSize = 40f
- textAlign = Paint.Align.CENTER
- setShadowLayer(5.0f, 0f, 0f, Color.BLACK)
- }
-
- private var leftGuidelinePct = 0.2f
- private var rightGuidelinePct = 0.8f
- private var draggingLine: Int = -1 // -1: none, 0: left, 1: right
- private val minDistancePct = 0.05f
-
- private val viewMatrix = Matrix()
- private val imageRect = RectF()
-
- var onGuidelinesChanged: ((Float, Float) -> Unit)? = null
-
- fun updateMatrix(matrix: Matrix) {
- viewMatrix.set(matrix)
- invalidate()
- }
-
- fun setImageDimensions(width: Int, height: Int) {
- imageRect.set(0f, 0f, width.toFloat(), height.toFloat())
- invalidate()
- }
-
- fun updateGuidelines(leftPct: Float, rightPct: Float) {
- leftGuidelinePct = leftPct
- rightGuidelinePct = rightPct
- invalidate()
- }
-
- override fun onDraw(canvas: Canvas) {
- super.onDraw(canvas)
- if (imageRect.isEmpty) return
-
- // 1. Define line X coordinates in the image's coordinate system
- val leftLineImageX = imageRect.width() * leftGuidelinePct
- val rightLineImageX = imageRect.width() * rightGuidelinePct
-
- // We only need the X coordinates for mapping
- val linePoints = floatArrayOf(leftLineImageX, 0f, rightLineImageX, 0f)
-
- // 2. Map image X coordinates to screen X coordinates
- viewMatrix.mapPoints(linePoints)
- val leftLineScreenX = linePoints[0]
- val rightLineScreenX = linePoints[2]
-
- // 3. Draw the lines across the full height of the view (screen)
- canvas.drawLine(leftLineScreenX, 0f, leftLineScreenX, height.toFloat(), linePaint)
- canvas.drawLine(rightLineScreenX, 0f, rightLineScreenX, height.toFloat(), linePaint)
-
- // 4. Draw the labels at the bottom of the screen
- val labelY = height - 60f
- canvas.drawText("Left Margin", leftLineScreenX, labelY, textPaint)
- canvas.drawText("Right Margin", rightLineScreenX, labelY, textPaint)
- }
-
- override fun onTouchEvent(event: MotionEvent): Boolean {
- if (imageRect.isEmpty) return false
-
- // Map screen touch coordinates to image coordinates for dragging
- val points = floatArrayOf(event.x, event.y)
- val invertedMatrix = Matrix()
- viewMatrix.invert(invertedMatrix)
- invertedMatrix.mapPoints(points)
- val mappedX = points[0]
-
- when (event.action) {
- MotionEvent.ACTION_DOWN -> {
- val leftLineImageX = imageRect.width() * leftGuidelinePct
- val rightLineImageX = imageRect.width() * rightGuidelinePct
-
- val screenPointLeft = mapImageCoordsToScreenCoords(leftLineImageX, imageRect.centerY())
- val screenPointRight = mapImageCoordsToScreenCoords(rightLineImageX, imageRect.centerY())
-
- if (isCloseTo(event.x, screenPointLeft[0])) {
- draggingLine = 0
- return true
- } else if (isCloseTo(event.x, screenPointRight[0])) {
- draggingLine = 1
- return true
- }
- }
- MotionEvent.ACTION_MOVE -> {
- if (draggingLine != -1) {
- val newPct = (mappedX / imageRect.width()).coerceIn(0f, 1f)
- if (draggingLine == 0) {
- if (newPct < rightGuidelinePct - minDistancePct) {
- leftGuidelinePct = newPct
- }
- } else { // draggingLine == 1
- if (newPct > leftGuidelinePct + minDistancePct) {
- rightGuidelinePct = newPct
- }
- }
- onGuidelinesChanged?.invoke(leftGuidelinePct, rightGuidelinePct)
- invalidate()
- return true
- }
- }
- MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
- draggingLine = -1
- }
- }
- return false
- }
-
- private fun mapImageCoordsToScreenCoords(imageX: Float, imageY: Float): FloatArray {
- val point = floatArrayOf(imageX, imageY)
- viewMatrix.mapPoints(point)
- return point
- }
-
- private fun isCloseTo(x: Float, lineX: Float, threshold: Float = 40f): Boolean {
- return Math.abs(x - lineX) < threshold
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/ZoomableImageView.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/ZoomableImageView.kt
deleted file mode 100644
index 0f1b0aab69..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/ZoomableImageView.kt
+++ /dev/null
@@ -1,206 +0,0 @@
-package org.appdevforall.codeonthego.computervision.ui
-
-import android.content.Context
-import android.graphics.Matrix
-import android.graphics.PointF
-import android.graphics.RectF
-import android.graphics.drawable.Drawable
-import android.util.AttributeSet
-import android.view.MotionEvent
-import android.view.ScaleGestureDetector
-import androidx.appcompat.widget.AppCompatImageView
-import kotlin.math.abs
-
-class ZoomableImageView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0
-) : AppCompatImageView(context, attrs, defStyleAttr) {
-
- private val currentMatrix = Matrix()
- private val matrixValues = FloatArray(9)
- private val scaleGestureDetector: ScaleGestureDetector
- private var last = PointF()
- private var start = PointF()
- private var minScale = 1f
- private var maxScale = 4f
- private var mode = NONE
-
- var onMatrixChangeListener: ((Matrix) -> Unit)? = null
- var onImageTapListener: ((Float, Float) -> Boolean)? = null
-
- init {
- super.setClickable(true)
- scaleGestureDetector = ScaleGestureDetector(context, ScaleListener())
- scaleType = ScaleType.MATRIX
- imageMatrix = currentMatrix
- }
-
- override fun onTouchEvent(event: MotionEvent): Boolean {
- scaleGestureDetector.onTouchEvent(event)
- val curr = PointF(event.x, event.y)
- when (event.action) {
- MotionEvent.ACTION_DOWN -> {
- last.set(curr)
- start.set(last)
- mode = DRAG
- }
- MotionEvent.ACTION_MOVE -> if (mode == DRAG) {
- val deltaX = curr.x - last.x
- val deltaY = curr.y - last.y
- val fixTransX = getFixDragTrans(deltaX, width.toFloat(), origWidth * scale)
- val fixTransY = getFixDragTrans(deltaY, height.toFloat(), origHeight * scale)
- currentMatrix.postTranslate(fixTransX, fixTransY)
- fixTrans()
- last.set(curr.x, curr.y)
- }
- MotionEvent.ACTION_UP -> {
- mode = NONE
- val xDiff = abs(curr.x - start.x).toInt()
- val yDiff = abs(curr.y - start.y).toInt()
- if (xDiff < CLICK && yDiff < CLICK) {
- val mappedPoint = mapViewPointToImage(event.x, event.y)
- val consumed = mappedPoint?.let {
- onImageTapListener?.invoke(it.x, it.y)
- } ?: false
-
- if (!consumed) {
- performClick()
- }
- }
- }
- MotionEvent.ACTION_POINTER_UP -> mode = NONE
- }
- imageMatrix = currentMatrix
- onMatrixChangeListener?.invoke(currentMatrix)
- return true
- }
-
- fun mapViewPointToImage(x: Float, y: Float): PointF? {
- val drawable = drawable ?: return null
- val points = floatArrayOf(x, y)
- val inverseMatrix = Matrix()
-
- if (!currentMatrix.invert(inverseMatrix)) return null
- inverseMatrix.mapPoints(points)
-
- val imageX = points[0]
- val imageY = points[1]
-
- return PointF(imageX, imageY).takeIf {
- it.x in 0f..drawable.intrinsicWidth.toFloat() &&
- it.y in 0f..drawable.intrinsicHeight.toFloat()
- }
- }
-
- override fun performClick(): Boolean {
- super.performClick()
- return true
- }
-
- override fun setImageDrawable(drawable: Drawable?) {
- super.setImageDrawable(drawable)
- fitToScreen()
- }
-
- override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
- super.onSizeChanged(w, h, oldw, oldh)
- fitToScreen()
- }
-
- private fun fitToScreen() {
- if (drawable == null || width == 0 || height == 0) {
- return
- }
-
- val drawableWidth = origWidth
- val drawableHeight = origHeight
-
- val viewRect = RectF(0f, 0f, width.toFloat(), height.toFloat())
- val drawableRect = RectF(0f, 0f, drawableWidth, drawableHeight)
-
- currentMatrix.setRectToRect(drawableRect, viewRect, Matrix.ScaleToFit.CENTER)
- imageMatrix = currentMatrix
- onMatrixChangeListener?.invoke(currentMatrix) // Notify listener of the change
-
- currentMatrix.getValues(matrixValues)
- minScale = matrixValues[Matrix.MSCALE_X]
- maxScale = minScale * 4
- }
-
- private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
- override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
- mode = ZOOM
- return true
- }
-
- override fun onScale(detector: ScaleGestureDetector): Boolean {
- var mScaleFactor = detector.scaleFactor
- val origScale = scale
- var currentScale = origScale * mScaleFactor
- if (currentScale > maxScale) {
- mScaleFactor = maxScale / origScale
- } else if (currentScale < minScale) {
- mScaleFactor = minScale / origScale
- }
- currentScale = origScale * mScaleFactor
- if (origWidth * currentScale <= width || origHeight * currentScale <= height) {
- currentMatrix.postScale(mScaleFactor, mScaleFactor, width / 2f, height / 2f)
- } else {
- currentMatrix.postScale(mScaleFactor, mScaleFactor, detector.focusX, detector.focusY)
- }
- fixTrans()
- return true
- }
- }
-
- private fun fixTrans() {
- currentMatrix.getValues(matrixValues)
- val transX = matrixValues[Matrix.MTRANS_X]
- val transY = matrixValues[Matrix.MTRANS_Y]
- val fixTransX = getFixTrans(transX, width.toFloat(), origWidth * scale)
- val fixTransY = getFixTrans(transY, height.toFloat(), origHeight * scale)
- if (fixTransX != 0f || fixTransY != 0f) {
- currentMatrix.postTranslate(fixTransX, fixTransY)
- }
- }
-
- private fun getFixTrans(trans: Float, viewSize: Float, contentSize: Float): Float {
- val minTrans: Float
- val maxTrans: Float
- if (contentSize <= viewSize) {
- minTrans = 0f
- maxTrans = viewSize - contentSize
- } else {
- minTrans = viewSize - contentSize
- maxTrans = 0f
- }
- if (trans < minTrans) return -trans + minTrans
- return if (trans > maxTrans) -trans + maxTrans else 0f
- }
-
- private fun getFixDragTrans(delta: Float, viewSize: Float, contentSize: Float): Float {
- return if (contentSize <= viewSize) {
- 0f
- } else delta
- }
-
- private val scale: Float
- get() {
- currentMatrix.getValues(matrixValues)
- return matrixValues[Matrix.MSCALE_X]
- }
-
- private val origWidth: Float
- get() = drawable?.intrinsicWidth?.toFloat() ?: 0f
-
- private val origHeight: Float
- get() = drawable?.intrinsicHeight?.toFloat() ?: 0f
-
- companion object {
- private const val NONE = 0
- private const val DRAG = 1
- private const val ZOOM = 2
- private const val CLICK = 3
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/viewmodel/ComputerVisionViewModel.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/viewmodel/ComputerVisionViewModel.kt
deleted file mode 100644
index e581860c61..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/ui/viewmodel/ComputerVisionViewModel.kt
+++ /dev/null
@@ -1,313 +0,0 @@
-package org.appdevforall.codeonthego.computervision.ui.viewmodel
-
-import android.net.Uri
-import android.util.Log
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import org.appdevforall.codeonthego.computervision.R
-import org.appdevforall.codeonthego.computervision.data.repository.DrawableImportHelper
-import org.appdevforall.codeonthego.computervision.data.repository.VisionRepository
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-import org.appdevforall.codeonthego.computervision.domain.usecase.GenerateXmlUC
-import org.appdevforall.codeonthego.computervision.domain.usecase.ImportPlaceholderImageUC
-import org.appdevforall.codeonthego.computervision.domain.usecase.PrepareImageUC
-import org.appdevforall.codeonthego.computervision.domain.usecase.RemovePlaceholderImageUC
-import org.appdevforall.codeonthego.computervision.domain.usecase.RunVisionUC
-import org.appdevforall.codeonthego.computervision.ui.ComputerVisionEffect
-import org.appdevforall.codeonthego.computervision.ui.ComputerVisionEvent
-import org.appdevforall.codeonthego.computervision.ui.ComputerVisionUiState
-import org.appdevforall.codeonthego.computervision.ui.CvOperation
-import org.appdevforall.codeonthego.computervision.ui.SelectedImportedImage
-import org.appdevforall.codeonthego.computervision.utils.CvAnalyticsUtil
-import org.appdevforall.codeonthego.computervision.utils.getSortedPlaceholders
-
-class ComputerVisionViewModel(
- private val repository: VisionRepository,
- private val prepareImageUC: PrepareImageUC,
- private val runVisionUC: RunVisionUC,
- private val generateXmlUC: GenerateXmlUC,
- private val importPlaceholderImageUC: ImportPlaceholderImageUC,
- private val removePlaceholderImageUC: RemovePlaceholderImageUC,
- layoutFilePath: String?,
- layoutFileName: String?
-) : ViewModel() {
-
- private val _uiState = MutableStateFlow(
- ComputerVisionUiState(
- layoutFilePath = layoutFilePath,
- layoutFileName = layoutFileName
- )
- )
- val uiState: StateFlow = _uiState.asStateFlow()
-
- private val _uiEffect = Channel()
- val uiEffect = _uiEffect.receiveAsFlow()
-
- init {
- initModel()
- }
-
- fun onEvent(event: ComputerVisionEvent) {
- when (event) {
- is ComputerVisionEvent.ImageSelected -> {
- CvAnalyticsUtil.trackImageSelected(fromCamera = false)
- loadImageFromUri(event.uri)
- }
- is ComputerVisionEvent.ImageCaptured -> handleCameraResult(event.uri, event.success)
- ComputerVisionEvent.RunDetection -> runDetection()
- ComputerVisionEvent.UpdateLayoutFile -> showUpdateConfirmation()
- ComputerVisionEvent.ConfirmUpdate -> performLayoutUpdate()
- ComputerVisionEvent.SaveToDownloads -> saveXmlToDownloads()
- ComputerVisionEvent.OpenImagePicker -> viewModelScope.launch { _uiEffect.send(ComputerVisionEffect.OpenImagePicker) }
- ComputerVisionEvent.RequestCameraPermission -> viewModelScope.launch { _uiEffect.send(ComputerVisionEffect.RequestCameraPermission) }
- is ComputerVisionEvent.UpdateGuides -> updateGuides(event.leftPct, event.rightPct)
- is ComputerVisionEvent.ImagePlaceholderTapped -> handleImagePlaceholderTap(event.imageX, event.imageY)
- is ComputerVisionEvent.PlaceholderImageSelected -> handlePlaceholderImageSelected(event.uri)
- is ComputerVisionEvent.RemovePlaceholderImage -> removePlaceholderImage(event.placeholderId)
- }
- }
-
- private fun initModel() {
- viewModelScope.launch {
- _uiState.update { it.copy(currentOperation = CvOperation.InitializingModel) }
- repository.initModel()
- .onSuccess { _uiState.update { it.copy(isModelInitialized = true, currentOperation = CvOperation.Idle) } }
- .onFailure { exception ->
- Log.e(TAG, "Model initialization failed", exception)
- _uiState.update { it.copy(currentOperation = CvOperation.Idle) }
- _uiEffect.send(ComputerVisionEffect.ShowError("Model initialization failed: ${exception.message}"))
- }
- }
- }
-
- fun onScreenStarted() {
- CvAnalyticsUtil.trackScreenOpened()
- }
-
- private fun loadImageFromUri(uri: Uri) {
- viewModelScope.launch {
- prepareImageUC(uri).onSuccess { prepared ->
- _uiState.update {
- it.copy(
- currentBitmap = prepared.bitmap,
- imageUri = uri,
- detections = emptyList(),
- visualizedBitmap = null,
- leftGuidePct = prepared.leftPct,
- rightGuidePct = prepared.rightPct,
- parsedAnnotations = emptyMap(),
- pendingImagePlaceholderId = null,
- selectedImagesByPlaceholderId = emptyMap()
- )
- }
- }.onFailure { e ->
- Log.e(TAG, "Error loading image", e)
- _uiEffect.send(ComputerVisionEffect.ShowError("Failed to load image: ${e.message}"))
- }
- }
- }
-
- private fun handleCameraResult(uri: Uri, success: Boolean) {
- if (success) {
- CvAnalyticsUtil.trackImageSelected(fromCamera = true)
- loadImageFromUri(uri)
- } else {
- viewModelScope.launch { _uiEffect.send(ComputerVisionEffect.ShowToast(R.string.msg_image_capture_cancelled)) }
- }
- }
-
- private fun runDetection() {
- val state = _uiState.value
- val bitmap = state.currentBitmap ?: run {
- viewModelScope.launch { _uiEffect.send(ComputerVisionEffect.ShowToast(R.string.msg_select_image_first)) }
- return
- }
-
- viewModelScope.launch {
- CvAnalyticsUtil.trackDetectionStarted()
- val startTime = System.currentTimeMillis()
-
- runVisionUC(bitmap, state.leftGuidePct, state.rightGuidePct) { operation ->
- _uiState.update { it.copy(currentOperation = operation) }
- }.onSuccess { result ->
- CvAnalyticsUtil.trackDetectionCompleted(true, result.detections.size, System.currentTimeMillis() - startTime)
- _uiState.update {
- it.copy(
- detections = result.detections,
- parsedAnnotations = result.annotations,
- currentOperation = CvOperation.Idle,
- pendingImagePlaceholderId = null,
- selectedImagesByPlaceholderId = emptyMap()
- )
- }
- }.onFailure { exception ->
- Log.e(TAG, "Detection failed", exception)
- CvAnalyticsUtil.trackDetectionCompleted(false, 0, System.currentTimeMillis() - startTime)
- _uiState.update { it.copy(currentOperation = CvOperation.Idle) }
- _uiEffect.send(ComputerVisionEffect.ShowError("Detection failed: ${exception.message}"))
- }
- }
- }
-
- private fun showUpdateConfirmation() {
- val state = _uiState.value
- if (!state.hasDetections || state.currentBitmap == null) {
- viewModelScope.launch { _uiEffect.send(ComputerVisionEffect.ShowToast(R.string.msg_run_detection_first)) }
- return
- }
- viewModelScope.launch { _uiEffect.send(ComputerVisionEffect.ShowConfirmDialog(state.layoutFileName ?: "layout.xml")) }
- }
-
- private fun performLayoutUpdate() {
- val state = _uiState.value
- if (!state.hasDetections || state.currentBitmap == null) return
-
- viewModelScope.launch {
- _uiState.update { it.copy(currentOperation = CvOperation.GeneratingXml) }
-
- generateXml(state)
- .onSuccess { (layoutXml, stringsXml) ->
- CvAnalyticsUtil.trackXmlGenerated(state.detections.size)
- CvAnalyticsUtil.trackXmlExported(toDownloads = false)
- _uiState.update { it.copy(currentOperation = CvOperation.Idle) }
- _uiEffect.send(ComputerVisionEffect.ReturnXmlResult(layoutXml, stringsXml))
- }.onFailure { exception ->
- Log.e(TAG, "XML generation failed", exception)
- _uiState.update { it.copy(currentOperation = CvOperation.Idle) }
- _uiEffect.send(ComputerVisionEffect.ShowError("XML generation failed: ${exception.message}"))
- }
- }
- }
-
- private fun saveXmlToDownloads() {
- val state = _uiState.value
- if (!state.hasDetections || state.currentBitmap == null) {
- viewModelScope.launch { _uiEffect.send(ComputerVisionEffect.ShowToast(R.string.msg_run_detection_first)) }
- return
- }
-
- viewModelScope.launch {
- _uiState.update { it.copy(currentOperation = CvOperation.GeneratingXml) }
-
- generateXml(state).onSuccess { (layoutXml, _) ->
- CvAnalyticsUtil.trackXmlGenerated(state.detections.size)
- CvAnalyticsUtil.trackXmlExported(toDownloads = true)
- _uiState.update { it.copy(currentOperation = CvOperation.SavingFile) }
- _uiState.update { it.copy(currentOperation = CvOperation.Idle) }
-
- _uiEffect.send(ComputerVisionEffect.FileSaved(layoutXml))
- }.onFailure { exception ->
- Log.e(TAG, "XML generation failed", exception)
- _uiState.update { it.copy(currentOperation = CvOperation.Idle) }
- _uiEffect.send(ComputerVisionEffect.ShowError("XML generation failed: ${exception.message}"))
- }
- }
- }
-
- private fun updateGuides(leftPct: Float, rightPct: Float) {
- val clampedLeft = leftPct.coerceIn(0f, 1f)
- val clampedRight = rightPct.coerceIn(0f, 1f)
-
- _uiState.update {
- it.copy(
- leftGuidePct = minOf(clampedLeft, clampedRight),
- rightGuidePct = maxOf(clampedLeft, clampedRight)
- )
- }
- }
-
- private fun generateXml(state: ComputerVisionUiState): Result> {
- val bitmap = state.currentBitmap ?: return Result.failure(IllegalStateException("No bitmap available"))
- return generateXmlUC(
- detections = state.detections,
- annotations = state.parsedAnnotations,
- selectedImagesByPlaceholderId = state.selectedImagesByPlaceholderId.mapValues { it.value.drawableReference },
- sourceImageWidth = bitmap.width,
- sourceImageHeight = bitmap.height,
- targetDpWidth = TARGET_DP_WIDTH,
- targetDpHeight = TARGET_DP_HEIGHT
- )
- }
-
- private fun handleImagePlaceholderTap(imageX: Float, imageY: Float) {
- val placeholder = findImagePlaceholderAt(imageX, imageY) ?: return
- val placeholderId = resolvePlaceholderId(placeholder)
-
- _uiState.update { it.copy(pendingImagePlaceholderId = placeholderId) }
- viewModelScope.launch { _uiEffect.send(ComputerVisionEffect.OpenPlaceholderImagePicker) }
- }
-
- private fun handlePlaceholderImageSelected(uri: Uri) {
- val state = _uiState.value
- val placeholderId = state.pendingImagePlaceholderId ?: return
-
- viewModelScope.launch {
- importPlaceholderImageUC(uri, state.layoutFilePath, placeholderId)
- .onSuccess { importedDrawable ->
- _uiState.update { currentState ->
- currentState.copy(
- pendingImagePlaceholderId = null,
- selectedImagesByPlaceholderId = currentState.selectedImagesByPlaceholderId +
- (placeholderId to SelectedImportedImage(importedDrawable.resourceName, importedDrawable.drawableReference))
- )
- }
- _uiEffect.send(ComputerVisionEffect.ShowToast(R.string.msg_placeholder_image_selected))
- }.onFailure { exception ->
- _uiState.update { it.copy(pendingImagePlaceholderId = null) }
- _uiEffect.send(ComputerVisionEffect.ShowError("Image import failed: ${exception.message}"))
- }
- }
- }
-
- private fun removePlaceholderImage(placeholderId: String) {
- val state = _uiState.value
- val importedImageInfo = state.selectedImagesByPlaceholderId[placeholderId] ?: return
-
- viewModelScope.launch {
- removePlaceholderImageUC(state.layoutFilePath, importedImageInfo.resourceName)
- .onSuccess {
- _uiState.update { currentState ->
- currentState.copy(selectedImagesByPlaceholderId = currentState.selectedImagesByPlaceholderId - placeholderId)
- }
- _uiEffect.send(ComputerVisionEffect.ShowToast(R.string.msg_image_removed))
- }.onFailure { exception ->
- _uiEffect.send(ComputerVisionEffect.ShowError("Failed to clean up image file: ${exception.message}"))
- }
- }
- }
-
- private fun resolvePlaceholderId(detection: DetectionResult): String {
- val index = _uiState.value.detections.getSortedPlaceholders().indexOf(detection)
- return "ph_${index.coerceAtLeast(0)}"
- }
-
- fun isImagePlaceholderAt(imageX: Float, imageY: Float): Boolean {
- return findImagePlaceholderAt(imageX, imageY) != null
- }
-
- private fun findImagePlaceholderAt(imageX: Float, imageY: Float): DetectionResult? {
- return _uiState.value.detections
- .getSortedPlaceholders()
- .firstOrNull { it.boundingBox.contains(imageX, imageY) }
- }
-
- override fun onCleared() {
- super.onCleared()
- repository.release()
- }
-
- companion object {
- private const val TAG = "ComputerVisionViewModel"
-
- /** Standard Android phone viewport in dp used as the XML layout target size. */
- private const val TARGET_DP_WIDTH = 360
- private const val TARGET_DP_HEIGHT = 640
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/BitmapUtils.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/BitmapUtils.kt
deleted file mode 100644
index 2f34757d71..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/BitmapUtils.kt
+++ /dev/null
@@ -1,167 +0,0 @@
-package org.appdevforall.codeonthego.computervision.utils
-
-import android.graphics.Bitmap
-import android.graphics.Color
-import android.graphics.RectF
-import kotlin.math.abs
-import kotlin.math.exp
-import kotlin.math.roundToInt
-
-object BitmapUtils {
-
- private const val EDGE_DETECTION_THRESHOLD = 30
-
- fun preprocessForOcr(bitmap: Bitmap, blockSize: Int = 31, c: Int = 15): Bitmap {
- val width = bitmap.width
- val height = bitmap.height
- val pixels = IntArray(width * height)
- bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
-
- val gray = toGrayscale(pixels)
- normalize(gray)
- val blurred = gaussianBlur(gray, width, height, blockSize)
- val binary = adaptiveThreshold(gray, blurred, width, height, c)
- medianFilter(binary, width, height)
-
- val outputPixels = IntArray(width * height)
- for (i in binary.indices) {
- outputPixels[i] = if (binary[i] > 0) Color.WHITE else Color.BLACK
- }
- return Bitmap.createBitmap(outputPixels, width, height, Bitmap.Config.ARGB_8888)
- }
-
- fun cropRegion(bitmap: Bitmap, rect: RectF, padding: Int = 0): Bitmap {
- val left = maxOf(0, (rect.left - padding).toInt())
- val top = maxOf(0, (rect.top - padding).toInt())
- val right = minOf(bitmap.width, (rect.right + padding).toInt())
- val bottom = minOf(bitmap.height, (rect.bottom + padding).toInt())
- val w = right - left
- val h = bottom - top
- if (w <= 0 || h <= 0) return bitmap
- return Bitmap.createBitmap(bitmap, left, top, w, h)
- }
-
- fun calculateVerticalProjection(bitmap: Bitmap): FloatArray {
- val width = bitmap.width
- val height = bitmap.height
- val pixels = IntArray(width * height)
- bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
-
- val projection = FloatArray(width)
- if (width < 3 || height == 0) {
- return projection
- }
-
- for (y in 0 until height) {
- val rowOffset = y * width
- for (x in 1 until width - 1) {
- val leftPixel = pixels[rowOffset + x - 1]
- val rightPixel = pixels[rowOffset + x + 1]
-
- val rLeft = (leftPixel shr 16) and 0xFF
- val rRight = (rightPixel shr 16) and 0xFF
-
- val diff = abs(rLeft - rRight)
- if (diff > EDGE_DETECTION_THRESHOLD) {
- projection[x] += 1f
- }
- }
- }
- return projection
- }
-
- private fun toGrayscale(pixels: IntArray): IntArray {
- val gray = IntArray(pixels.size)
- for (i in pixels.indices) {
- val p = pixels[i]
- val r = (p shr 16) and 0xFF
- val g = (p shr 8) and 0xFF
- val b = p and 0xFF
- gray[i] = (0.299 * r + 0.587 * g + 0.114 * b).toInt()
- }
- return gray
- }
-
- private fun normalize(gray: IntArray) {
- var min = 255
- var max = 0
- for (v in gray) {
- if (v < min) min = v
- if (v > max) max = v
- }
- val range = max - min
- if (range == 0) return
- for (i in gray.indices) {
- gray[i] = ((gray[i] - min) * 255) / range
- }
- }
-
- private fun gaussianBlur(gray: IntArray, width: Int, height: Int, blockSize: Int): IntArray {
- val sigma = 0.3 * ((blockSize - 1) * 0.5 - 1) + 0.8
- val halfKernel = blockSize / 2
- val kernel = FloatArray(blockSize)
- var kernelSum = 0.0f
- for (i in 0 until blockSize) {
- val x = i - halfKernel
- kernel[i] = exp(-(x * x) / (2.0 * sigma * sigma)).toFloat()
- kernelSum += kernel[i]
- }
- for (i in kernel.indices) kernel[i] /= kernelSum
-
- val temp = IntArray(width * height)
- for (y in 0 until height) {
- for (x in 0 until width) {
- var sum = 0.0f
- for (k in 0 until blockSize) {
- val sx = (x + k - halfKernel).coerceIn(0, width - 1)
- sum += gray[y * width + sx] * kernel[k]
- }
- temp[y * width + x] = sum.roundToInt()
- }
- }
-
- val blurred = IntArray(width * height)
- for (x in 0 until width) {
- for (y in 0 until height) {
- var sum = 0.0f
- for (k in 0 until blockSize) {
- val sy = (y + k - halfKernel).coerceIn(0, height - 1)
- sum += temp[sy * width + x] * kernel[k]
- }
- blurred[y * width + x] = sum.roundToInt()
- }
- }
- return blurred
- }
-
- private fun adaptiveThreshold(
- gray: IntArray,
- blurred: IntArray,
- width: Int,
- height: Int,
- c: Int
- ): IntArray {
- val binary = IntArray(width * height)
- for (i in gray.indices) {
- binary[i] = if (gray[i] > blurred[i] - c) 255 else 0
- }
- return binary
- }
-
- private fun medianFilter(pixels: IntArray, width: Int, height: Int) {
- val copy = pixels.copyOf()
- val window = IntArray(9)
- for (y in 1 until height - 1) {
- for (x in 1 until width - 1) {
- var idx = 0
- for (dy in -1..1) {
- for (dx in -1..1) {
- window[idx++] = copy[(y + dy) * width + (x + dx)]
- }
- }
- window.sort()
- pixels[y * width + x] = window[4]
- }
- }
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/CvAnalyticsUtils.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/CvAnalyticsUtils.kt
deleted file mode 100644
index af15034e0e..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/CvAnalyticsUtils.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-package org.appdevforall.codeonthego.computervision.utils
-
-import android.os.Bundle
-import android.util.Log
-import com.google.firebase.analytics.ktx.analytics
-import com.google.firebase.ktx.Firebase
-import org.appdevforall.codeonthego.computervision.BuildConfig
-import org.appdevforall.codeonthego.computervision.utils.CvAnalyticsUtil.EventNames.CV_DETECTION_COMPLETED
-import org.appdevforall.codeonthego.computervision.utils.CvAnalyticsUtil.EventNames.CV_DETECTION_STARTED
-import org.appdevforall.codeonthego.computervision.utils.CvAnalyticsUtil.EventNames.CV_IMAGE_SELECTED
-import org.appdevforall.codeonthego.computervision.utils.CvAnalyticsUtil.EventNames.CV_SCREEN_OPENED
-import org.appdevforall.codeonthego.computervision.utils.CvAnalyticsUtil.EventNames.CV_XML_EXPORTED
-import org.appdevforall.codeonthego.computervision.utils.CvAnalyticsUtil.EventNames.CV_XML_GENERATED
-
-object CvAnalyticsUtil {
- private const val TAG = "CvAnalyticsUtil"
-
-
- private val analytics by lazy {
- try {
- Firebase.analytics
- } catch (e: Exception) {
- Log.w(TAG, "Firebase Analytics not available", e)
- null
- }
- }
-
- private fun logEvent(eventName: String, params: Bundle) {
- if (BuildConfig.DEBUG) {
- Log.i(TAG, "skipping analytics event on Debug build")
- return
- }
- try {
- analytics?.logEvent(eventName, params)
- } catch (e: Exception) {
- Log.w(TAG, "Failed to log event: $eventName", e)
- }
- }
-
- fun trackScreenOpened() {
- logEvent(CV_SCREEN_OPENED, Bundle().apply {
- putLong("timestamp", System.currentTimeMillis())
- })
- }
-
- fun trackImageSelected(fromCamera: Boolean) {
- logEvent(CV_IMAGE_SELECTED, Bundle().apply {
- putString("source", if (fromCamera) "camera" else "gallery")
- putLong("timestamp", System.currentTimeMillis())
- })
- }
-
- fun trackDetectionStarted() {
- logEvent(CV_DETECTION_STARTED, Bundle().apply {
- putLong("timestamp", System.currentTimeMillis())
- })
- }
-
- fun trackDetectionCompleted(success: Boolean, detectionCount: Int, durationMs: Long) {
- logEvent(CV_DETECTION_COMPLETED, Bundle().apply {
- putBoolean("success", success)
- putInt("detection_count", detectionCount)
- putLong("duration_ms", durationMs)
- putLong("timestamp", System.currentTimeMillis())
- })
- }
-
- fun trackXmlGenerated(componentCount: Int) {
- logEvent(CV_XML_GENERATED, Bundle().apply {
- putInt("component_count", componentCount)
- putLong("timestamp", System.currentTimeMillis())
- })
- }
-
- fun trackXmlExported(toDownloads: Boolean) {
- logEvent(CV_XML_EXPORTED, Bundle().apply {
- putString("export_method", if (toDownloads) "save_downloads" else "update_layout")
- putLong("timestamp", System.currentTimeMillis())
- })
- }
-
- private object EventNames {
- const val CV_SCREEN_OPENED = "cv_screen_opened"
- const val CV_IMAGE_SELECTED = "cv_image_selected"
- const val CV_DETECTION_STARTED = "cv_detection_started"
- const val CV_DETECTION_COMPLETED = "cv_detection_completed"
- const val CV_XML_GENERATED = "cv_xml_generated"
- const val CV_XML_EXPORTED = "cv_xml_exported"
- }
-}
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/DetectionVisualizer.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/DetectionVisualizer.kt
deleted file mode 100644
index 2ef2ace815..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/DetectionVisualizer.kt
+++ /dev/null
@@ -1,227 +0,0 @@
-package org.appdevforall.codeonthego.computervision.utils
-
-import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.Paint
-import android.graphics.RectF
-import android.graphics.drawable.Drawable
-import androidx.appcompat.content.res.AppCompatResources
-import androidx.core.graphics.toColorInt
-import org.appdevforall.codeonthego.computervision.R
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-
-
-/**
- * Utility class responsible for visualizing computer vision detection results.
- * It handles drawing bounding boxes, text labels, and interactive visual hints
- * directly onto image bitmaps.
- *
- * @property context The context used to retrieve resources such as drawables.
- */
-class DetectionVisualizer(private val context: Context) {
-
- private val boundingBoxPaint by lazy {
- Paint().apply {
- color = Color.GREEN
- style = Paint.Style.STROKE
- strokeWidth = 5.0f
- alpha = 200
- }
- }
-
- private val imagePlaceholderPaint by lazy {
- Paint().apply {
- color = "#FF8A00".toColorInt()
- style = Paint.Style.STROKE
- strokeWidth = 7.0f
- alpha = 230
- }
- }
-
- private val imagePlaceholderFillPaint by lazy {
- Paint().apply {
- color = "#FF8A00".toColorInt()
- style = Paint.Style.FILL
- alpha = 40
- }
- }
-
- private val textRecognitionBoxPaint by lazy {
- Paint().apply {
- color = Color.BLUE
- style = Paint.Style.STROKE
- strokeWidth = 3.0f
- alpha = 200
- }
- }
-
- private val textPaint by lazy {
- Paint().apply {
- color = Color.WHITE
- style = Paint.Style.FILL
- textSize = 40.0f
- setShadowLayer(5.0f, 0f, 0f, Color.BLACK)
- }
- }
-
- private val imagePlaceholderUploadDrawable: Drawable? by lazy {
- AppCompatResources.getDrawable(context, R.drawable.ic_placeholder_upload)?.mutate()?.apply {
- setTint(Color.WHITE)
- }
- }
-
- private val imagePlaceholderDeleteDrawable: Drawable? by lazy {
- AppCompatResources.getDrawable(context, R.drawable.ic_placeholder_delete)?.mutate()?.apply {
- setTint(Color.WHITE)
- }
- }
-
- private val imagePlaceholderBadgePaint by lazy {
- Paint(Paint.ANTI_ALIAS_FLAG).apply {
- color = "#CC111111".toColorInt()
- style = Paint.Style.FILL
- }
- }
-
- private val deleteBadgeBackgroundPaint by lazy {
- Paint(Paint.ANTI_ALIAS_FLAG).apply {
- color = "#CCB3261E".toColorInt()
- style = Paint.Style.FILL
- }
- }
-
- private val deleteIconClickableAreas = mutableMapOf()
-
- /**
- * Draws detection bounding boxes, labels, and interactive placeholder hints on a given bitmap.
- *
- * @param bitmap The original image on which detections were performed.
- * @param detections A list of [DetectionResult] objects containing the bounding boxes and labels.
- * @param selectedPlaceholderIds A set of IDs representing image placeholders that have been selected/filled.
- * @return A new [Bitmap] instance with the visualized detections drawn over it.
- */
- fun visualize(
- bitmap: Bitmap,
- detections: List,
- selectedPlaceholderIds: Set
- ): Bitmap {
- val mutableBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
- val canvas = Canvas(mutableBitmap)
-
- deleteIconClickableAreas.clear()
-
- val placeholderIdsByDetection = mapPlaceholderDetections(detections)
-
- for (result in detections) {
- if (result.label == IMAGE_PLACEHOLDER_LABEL) {
- val placeholderId = placeholderIdsByDetection[result] ?: continue
- val hasSelectedImage = selectedPlaceholderIds.contains(placeholderId)
-
- drawImagePlaceholderHint(canvas, result.boundingBox, hasSelectedImage, placeholderId)
- } else {
- drawStandardDetection(canvas, result)
- }
- }
-
- return mutableBitmap
- }
-
- /**
- * Checks if the given X, Y coordinates intersect with any drawn delete icon.
- * @return The placeholderId if a delete icon was tapped, null otherwise.
- */
- fun getTappedDeleteIconId(x: Float, y: Float): String? {
- return deleteIconClickableAreas.entries.firstOrNull { it.value.contains(x, y) }?.key
- }
-
- /**
- * Filters and maps image placeholder detections to their corresponding auto-generated IDs.
- * Placeholders are sorted from top to bottom, left to right to ensure consistent ID assignment.
- *
- * @param detections The full list of detections.
- * @return A map linking each placeholder [DetectionResult] to its generated string ID (e.g., "ph_0").
- */
- private fun mapPlaceholderDetections(detections: List): Map {
- return detections
- .filter { it.label == IMAGE_PLACEHOLDER_LABEL }
- .sortedWith(compareBy({ it.boundingBox.top }, { it.boundingBox.left }))
- .mapIndexed { index, detection ->
- detection to "ph_$index"
- }.toMap()
- }
-
- /**
- * Draws a standard detection bounding box and its corresponding text label.
- * It uses different colors depending on whether the detection is from YOLO or Text Recognition.
- *
- * @param canvas The canvas to draw the detection on.
- * @param result The [DetectionResult] containing the coordinates, label, and detection type.
- */
- private fun drawStandardDetection(canvas: Canvas, result: DetectionResult) {
- val paint = if (result.isYolo) boundingBoxPaint else textRecognitionBoxPaint
- canvas.drawRect(result.boundingBox, paint)
-
- val label = result.label.take(15)
- val text = if (result.text.isNotEmpty()) "$label: ${result.text}" else label
- canvas.drawText(text, result.boundingBox.left, result.boundingBox.top - 5, textPaint)
- }
-
- /**
- * Draws a highlighted region and a central badge for an image placeholder detection.
- *
- * @param canvas The canvas to draw the hint on.
- * @param boundingBox The rectangular bounds of the detected image placeholder.
- * @param hasSelectedImage A flag indicating whether the user has already selected an image for this placeholder.
- */
- private fun drawImagePlaceholderHint(
- canvas: Canvas,
- boundingBox: RectF,
- hasSelectedImage: Boolean,
- placeholderId: String
- ) {
- canvas.drawRect(boundingBox, imagePlaceholderFillPaint)
- canvas.drawRect(boundingBox, imagePlaceholderPaint)
-
- val badgeHeight = (boundingBox.height() * 0.24f).coerceIn(44f, 72f)
- val badgeTop = (boundingBox.centerY() - badgeHeight / 2f).coerceAtLeast(boundingBox.top + 8f)
- val badgeBottom = (badgeTop + badgeHeight).coerceAtMost(boundingBox.bottom - 8f)
-
- val badgeRect = RectF(
- boundingBox.left + 12f,
- badgeTop,
- boundingBox.right - 12f,
- badgeBottom
- )
-
- val bgPaint = if (hasSelectedImage) deleteBadgeBackgroundPaint else imagePlaceholderBadgePaint
- canvas.drawRoundRect(badgeRect, 16f, 16f, bgPaint)
-
- drawPlaceholderIcon(canvas, badgeRect, hasSelectedImage)
-
- if (hasSelectedImage) {
- deleteIconClickableAreas[placeholderId] = badgeRect
- }
- }
-
- private fun drawPlaceholderIcon(canvas: Canvas, badgeRect: RectF, hasSelectedImage: Boolean) {
- val iconDrawable = if (hasSelectedImage) imagePlaceholderDeleteDrawable else imagePlaceholderUploadDrawable
- if (iconDrawable == null) return
-
- val iconSize = minOf(badgeRect.width(), badgeRect.height()) * 0.80f
-
- val left = (badgeRect.centerX() - iconSize / 2f).toInt()
- val top = (badgeRect.centerY() - iconSize / 2f).toInt()
- val right = (badgeRect.centerX() + iconSize / 2f).toInt()
- val bottom = (badgeRect.centerY() + iconSize / 2f).toInt()
-
- iconDrawable.alpha = 255
- iconDrawable.setBounds(left, top, right, bottom)
- iconDrawable.draw(canvas)
- }
-
- fun clearCache() {
- deleteIconClickableAreas.clear()
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/MetadataDetector.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/MetadataDetector.kt
deleted file mode 100644
index 68cee4064a..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/MetadataDetector.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package org.appdevforall.codeonthego.computervision.utils
-
-object MetadataDetector {
- private val metadataSnippets = listOf(
- ""
- )
-
- private val metadataKeywords = listOf(
- "layout_width",
- "layout_height",
- "layout_margin",
- "layout_gravity",
- "textstyle",
- "textcolor",
- "textsize",
- "padding",
- "orientation",
- "baselinealigned",
- "match_parent",
- "wrap_content"
- )
-
- private val xmlAttributeRegex = Regex("""\b(?:android|app|tools):[a-zA-Z_]+\b""")
- fun isCanvasMetadata(text: String): Boolean {
- val lowerText = text.lowercase()
- if (lowerText.isBlank()) return false
- if (metadataSnippets.any { snippet -> lowerText.contains(snippet) }) return true
- if (xmlAttributeRegex.containsMatchIn(lowerText)) return true
- if (metadataKeywords.any { keyword -> lowerText.contains(keyword) }) return true
- return false
- }
-
- fun isMetadataLabel(label: String): Boolean {
- val normalized = label.trim().lowercase()
- return normalized == "margin_metadata" || normalized.contains("metadata")
- }
-
- fun isMetadataDetection(label: String, text: String): Boolean {
- return isMetadataLabel(label) || isCanvasMetadata(text)
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/OcrExtensions.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/OcrExtensions.kt
deleted file mode 100644
index b0030dd5fd..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/OcrExtensions.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.appdevforall.codeonthego.computervision.utils
-
-private val PUNCTUATION_DELIMITERS = Regex("\\s*[,;|/\\n]+\\s*")
-private val WHITESPACE_DELIMITERS = Regex("\\s+")
-private val OCR_NUMERIC_PATTERN = Regex("^[0-9oOlIzZsSbB]+$")
-
-/**
- * Extracts separated entries from a raw OCR string.
- * Tries to split by punctuation first. If no punctuation is found,
- * it falls back to space-separated tokens, provided they all look like numbers or OCR artifacts.
- */
-internal fun String.extractOcrEntries(): List {
- // Try to split by explicit punctuation or newlines
- val punctuatedTokens = this.split(PUNCTUATION_DELIMITERS).filter { it.isNotBlank() }
-
- // If successfully split into multiple items (or none), return them
- if (punctuatedTokens.size != 1) {
- return punctuatedTokens
- }
-
- // Fallback: check if elements were separated only by spaces
- val whitespaceTokens = this.trim().split(WHITESPACE_DELIMITERS).filter { it.isNotBlank() }
-
- // Only accept space separation if all resulting tokens look like numbers (or OCR artifacts)
- val isSpaceSeparatedOcrNumbers = whitespaceTokens.size > 1 &&
- whitespaceTokens.all { it.matches(OCR_NUMERIC_PATTERN) }
-
- return if (isSpaceSeparatedOcrNumbers) whitespaceTokens else punctuatedTokens
-}
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/OcrTextAssembler.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/OcrTextAssembler.kt
deleted file mode 100644
index 21dec611f8..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/OcrTextAssembler.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package org.appdevforall.codeonthego.computervision.utils
-
-import com.google.mlkit.vision.text.Text
-
-object OcrTextAssembler {
- const val DEFAULT_SPACE_GAP_TOLERANCE_PX = 15f
-
- fun extractTextWithTolerance(
- textBlocks: List,
- maxSpaceGap: Float = DEFAULT_SPACE_GAP_TOLERANCE_PX
- ): String {
- return textBlocks.joinToString(" ") { block ->
- block.lines.joinToString(" ") { line ->
- joinElementsWithTolerance(line, maxSpaceGap)
- }
- }.trim()
- }
-
- fun joinElementsWithTolerance(line: Text.Line, maxSpaceGap: Float = DEFAULT_SPACE_GAP_TOLERANCE_PX): String {
- val elements = line.elements.sortedBy { it.boundingBox?.left ?: 0 }
- if (elements.isEmpty()) return ""
-
- val builder = StringBuilder()
- var prevRight = elements.first().boundingBox?.right ?: 0
- builder.append(elements.first().text)
-
- for (i in 1 until elements.size) {
- val current = elements[i]
- val currentBox = current.boundingBox
-
- if (currentBox != null) {
- val gap = currentBox.left - prevRight
- if (gap > maxSpaceGap) {
- builder.append(" ")
- }
- prevRight = currentBox.right
- } else {
- builder.append(" ")
- }
- builder.append(current.text)
- }
- return builder.toString().trim()
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/PlaceholderUtils.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/PlaceholderUtils.kt
deleted file mode 100644
index 1a2eff8e4d..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/PlaceholderUtils.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.appdevforall.codeonthego.computervision.utils
-
-import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
-import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
-
-const val IMAGE_PLACEHOLDER_LABEL = "image_placeholder"
-
-fun List.getSortedPlaceholders(): List {
- return this.filter { it.label == IMAGE_PLACEHOLDER_LABEL }
- .sortedWith(compareBy({ it.boundingBox.top }, { it.boundingBox.left }))
-}
-
-fun List.getSortedScaledPlaceholders(): List {
- return this.filter { it.label == IMAGE_PLACEHOLDER_LABEL }
- .sortedWith(compareBy({ it.y }, { it.x }))
-}
-
-/**
- * Associates ordered image placeholders with their selected drawable references.
- * Useful for mapping user-selected gallery images to the physical canvas bounding boxes.
- */
-fun List.buildPlaceholderOverrides(selectedImagesByPlaceholderId: Map): Map {
- val placeholders = this.getSortedScaledPlaceholders()
-
- return placeholders.mapIndexedNotNull { index, box ->
- val drawableReference = selectedImagesByPlaceholderId["ph_$index"]
- ?: return@mapIndexedNotNull null
- box to drawableReference
- }.toMap()
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/SmartBoundaryDetector.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/SmartBoundaryDetector.kt
deleted file mode 100644
index a72ab07e32..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/SmartBoundaryDetector.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-package org.appdevforall.codeonthego.computervision.utils
-
-import android.graphics.Bitmap
-import org.appdevforall.codeonthego.computervision.utils.BitmapUtils.calculateVerticalProjection
-
-object SmartBoundaryDetector {
-
- private const val DEFAULT_EDGE_IGNORE_PERCENT = 0.05f
- private const val LEFT_ZONE_END_PERCENT = 0.5f
- private const val RIGHT_ZONE_START_PERCENT = 0.5f
- private const val MIN_GAP_WIDTH_PERCENT = 0.02
- private const val PRIMARY_ACTIVITY_THRESHOLD = 0.05f
- private const val FALLBACK_ACTIVITY_THRESHOLD = 0.01f
- private const val LEFT_FALLBACK_BOUND_PERCENT = 0.15f
- private const val RIGHT_FALLBACK_BOUND_PERCENT = 0.85f
-
- fun detectSmartBoundaries(
- bitmap: Bitmap,
- edgeIgnorePercent: Float = DEFAULT_EDGE_IGNORE_PERCENT
- ): Pair {
- val width = bitmap.width
- val projection = calculateVerticalProjection(bitmap)
- val minimumGapWidth = (width * MIN_GAP_WIDTH_PERCENT).toInt()
-
- val ignoredEdgePixels = (width * edgeIgnorePercent).toInt()
- val leftZoneEnd = (width * LEFT_ZONE_END_PERCENT).toInt()
- val rightZoneStart = (width * RIGHT_ZONE_START_PERCENT).toInt()
- val rightZoneEnd = width - ignoredEdgePixels
-
- if (ignoredEdgePixels >= leftZoneEnd || rightZoneStart >= rightZoneEnd) {
- return Pair(
- (width * LEFT_FALLBACK_BOUND_PERCENT).toInt(),
- (width * RIGHT_FALLBACK_BOUND_PERCENT).toInt()
- )
- }
-
- val leftSignal = projection.copyOfRange(ignoredEdgePixels, leftZoneEnd)
- var (leftBound, leftGapLength) = findBestGapMidpoint(leftSignal, offset = ignoredEdgePixels)
- if (leftBound == null || leftGapLength < minimumGapWidth) {
- leftBound = findBestGapMidpoint(leftSignal, offset = ignoredEdgePixels, normalizeSignal = true).first
- }
-
- val rightSignal = projection.copyOfRange(rightZoneStart, rightZoneEnd)
- var (rightBound, rightGapLength) = findBestGapMidpoint(rightSignal, offset = rightZoneStart)
- if (rightBound == null || rightGapLength < minimumGapWidth) {
- rightBound = findBestGapMidpoint(rightSignal, offset = rightZoneStart, normalizeSignal = true).first
- }
-
- val finalLeftBound = leftBound ?: (width * LEFT_FALLBACK_BOUND_PERCENT).toInt()
- val finalRightBound = rightBound ?: (width * RIGHT_FALLBACK_BOUND_PERCENT).toInt()
- return Pair(finalLeftBound, finalRightBound)
- }
-
- private fun findBestGapMidpoint(
- signalSegment: FloatArray,
- offset: Int = 0,
- normalizeSignal: Boolean = false
- ): Pair {
- if (signalSegment.isEmpty()) {
- return Pair(null, 0)
- }
-
- val signal = if (normalizeSignal) {
- val minValue = signalSegment.minOrNull() ?: 0f
- FloatArray(signalSegment.size) { index -> signalSegment[index] - minValue }
- } else {
- signalSegment
- }
-
- val activityThresholdMultiplier = if (normalizeSignal) {
- FALLBACK_ACTIVITY_THRESHOLD
- } else {
- PRIMARY_ACTIVITY_THRESHOLD
- }
- val threshold = (signal.maxOrNull() ?: 0f) * activityThresholdMultiplier
-
- var maxGapLength = 0
- var maxGapMidpoint: Int? = null
- var currentGapStart = -1
- var previousIsActive = true
-
- signal.forEachIndexed { index, value ->
- val isActive = value > threshold
- if (previousIsActive && !isActive) {
- currentGapStart = index
- }
-
- val isGapClosing = currentGapStart != -1 && (index + 1 == signal.size || (!isActive && signal[index + 1] > threshold))
- if (isGapClosing) {
- val gapLength = index - currentGapStart + 1
- if (gapLength > maxGapLength) {
- maxGapLength = gapLength
- maxGapMidpoint = currentGapStart + (gapLength / 2)
- }
- currentGapStart = -1
- }
-
- previousIsActive = isActive
- }
-
- return Pair(maxGapMidpoint?.plus(offset), maxGapLength)
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/TextCleaner.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/TextCleaner.kt
deleted file mode 100644
index 495744a78c..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/TextCleaner.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package org.appdevforall.codeonthego.computervision.utils
-
-object TextCleaner {
-
- private val nonAlphanumericRegex = Regex("[^a-zA-Z0-9 ]")
- private val leadingMarkerRegex = Regex("^[\\[\\]()●○□☑✓-]+\\s*")
- private val leadingStandaloneCircleRegex = Regex("^[O0o]\\s+")
- private val duplicatedLeadingCircleRegex = Regex("^[O0o](?=[oO][a-z])")
-
- fun cleanText(text: String): String {
- return text.replace("\n", " ")
- .replace(nonAlphanumericRegex, "")
- .trim()
- }
-
- fun cleanTextStrippingLeadingO(text: String): String {
- val cleanedText = text.trim()
- .replace(leadingMarkerRegex, "")
- .replace(leadingStandaloneCircleRegex, "")
- .replace(duplicatedLeadingCircleRegex, "")
-
- return cleanedText.ifEmpty { text }
- }
-
- fun cleanTextPreservingLeadingO(text: String): String {
- var cleanedText = text.trim()
- .replace(Regex("^[\\[\\]()●○□☑✓-]+\\s*"), "")
-
- cleanedText = cleanedText.replace(Regex("^[DT]?opti[oa]n", RegexOption.IGNORE_CASE), "Option")
- cleanedText = cleanedText.replace(Regex("^pti[oa]n", RegexOption.IGNORE_CASE), "Option")
- cleanedText = cleanedText.replace(Regex("^optton", RegexOption.IGNORE_CASE), "Option")
-
- return cleanedText.ifEmpty { text }
- }
-}
diff --git a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/XmlFileManager.kt b/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/XmlFileManager.kt
deleted file mode 100644
index 4cd00c5c24..0000000000
--- a/cv-image-to-xml/src/main/java/org/appdevforall/codeonthego/computervision/utils/XmlFileManager.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-package org.appdevforall.codeonthego.computervision.utils
-
-import android.content.ContentValues
-import android.content.Context
-import android.os.Build
-import android.os.Environment
-import android.provider.MediaStore
-import androidx.annotation.RequiresApi
-import java.io.File
-import java.io.FileOutputStream
-import java.io.IOException
-
-/**
- * Utility class responsible for managing file input/output operations.
- * Specifically handles saving XML files to the device's public Downloads directory,
- * adapting to different Android API levels (Scoped Storage vs Legacy).
- *
- * @property context The application or activity context needed to access content resolvers.
- */
-class XmlFileManager(private val context: Context) {
-
- /**
- * Saves an XML string to a file in the device's public Downloads directory.
- * It automatically handles the differences in file storage APIs between
- * Android 10 (API 29+) and older versions.
- *
- * @param xmlString The XML content to be saved.
- * @param fileName The desired name for the output file. Defaults to "layout_result.xml".
- * @return A [Result] containing the file name if successful, or an [IOException] on failure.
- */
- fun saveXmlToDownloads(xmlString: String, fileName: String = "layout_result.xml"): Result {
- return try {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- saveUsingMediaStore(xmlString, fileName)
- } else {
- saveUsingLegacyFileApi(xmlString, fileName)
- }
- Result.success(fileName)
- } catch (e: IOException) {
- Result.failure(e)
- }
- }
-
- /**
- * Saves the file using the MediaStore API.
- * This is the required approach for Android 10 (API 29) and above due to Scoped Storage restrictions.
- *
- * @param xmlString The XML content to write.
- * @param fileName The name of the file.
- * @throws IOException If the MediaStore record cannot be created or written to.
- */
- @RequiresApi(Build.VERSION_CODES.Q)
- private fun saveUsingMediaStore(xmlString: String, fileName: String) {
- val resolver = context.contentResolver
- val contentValues = ContentValues().apply {
- put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
- put(MediaStore.MediaColumns.MIME_TYPE, "text/xml")
- put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
- }
-
- val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
- ?: throw IOException("Failed to create new MediaStore record for $fileName.")
-
- resolver.openOutputStream(uri)?.use { outputStream ->
- outputStream.write(xmlString.toByteArray())
- } ?: throw IOException("Failed to open output stream for MediaStore URI.")
- }
-
- /**
- * Saves the file using the legacy File API.
- * This approach is used for devices running Android 9 (API 28) or lower.
- *
- * @param xmlString The XML content to write.
- * @param fileName The name of the file.
- * @throws IOException If the Downloads directory or the file cannot be created.
- */
- @Suppress("DEPRECATION")
- private fun saveUsingLegacyFileApi(xmlString: String, fileName: String) {
- val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
-
- if (!downloadsDir.exists() && !downloadsDir.mkdirs()) {
- throw IOException("Failed to create Downloads directory.")
- }
-
- val file = File(downloadsDir, fileName)
- FileOutputStream(file).use { outputStream ->
- outputStream.write(xmlString.toByteArray())
- }
- }
-}
diff --git a/cv-image-to-xml/src/main/res/drawable/ic_launcher_background.xml b/cv-image-to-xml/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 07d5da9cbf..0000000000
--- a/cv-image-to-xml/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/cv-image-to-xml/src/main/res/drawable/ic_placeholder_delete.xml b/cv-image-to-xml/src/main/res/drawable/ic_placeholder_delete.xml
deleted file mode 100644
index 20cebf71cb..0000000000
--- a/cv-image-to-xml/src/main/res/drawable/ic_placeholder_delete.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/cv-image-to-xml/src/main/res/drawable/ic_placeholder_upload.xml b/cv-image-to-xml/src/main/res/drawable/ic_placeholder_upload.xml
deleted file mode 100644
index 203884bda9..0000000000
--- a/cv-image-to-xml/src/main/res/drawable/ic_placeholder_upload.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/cv-image-to-xml/src/main/res/layout/activity_computer_vision.xml b/cv-image-to-xml/src/main/res/layout/activity_computer_vision.xml
deleted file mode 100644
index e2a4499218..0000000000
--- a/cv-image-to-xml/src/main/res/layout/activity_computer_vision.xml
+++ /dev/null
@@ -1,87 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/cv-image-to-xml/src/main/res/layout/testing_result.xml b/cv-image-to-xml/src/main/res/layout/testing_result.xml
deleted file mode 100644
index 4805bc3998..0000000000
--- a/cv-image-to-xml/src/main/res/layout/testing_result.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
diff --git a/cv-image-to-xml/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/cv-image-to-xml/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index 6f3b755bf5..0000000000
--- a/cv-image-to-xml/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/cv-image-to-xml/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index 6f3b755bf5..0000000000
--- a/cv-image-to-xml/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/res/mipmap-hdpi/ic_launcher.webp b/cv-image-to-xml/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78ecd..0000000000
Binary files a/cv-image-to-xml/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/cv-image-to-xml/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/cv-image-to-xml/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d1ba..0000000000
Binary files a/cv-image-to-xml/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/cv-image-to-xml/src/main/res/mipmap-mdpi/ic_launcher.webp b/cv-image-to-xml/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d64e5..0000000000
Binary files a/cv-image-to-xml/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/cv-image-to-xml/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/cv-image-to-xml/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611da08..0000000000
Binary files a/cv-image-to-xml/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/cv-image-to-xml/src/main/res/mipmap-xhdpi/ic_launcher.webp b/cv-image-to-xml/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a3070fe..0000000000
Binary files a/cv-image-to-xml/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/cv-image-to-xml/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/cv-image-to-xml/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a6956b3..0000000000
Binary files a/cv-image-to-xml/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/cv-image-to-xml/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/cv-image-to-xml/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77f9f..0000000000
Binary files a/cv-image-to-xml/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/cv-image-to-xml/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/cv-image-to-xml/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f50836..0000000000
Binary files a/cv-image-to-xml/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/cv-image-to-xml/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/cv-image-to-xml/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d6427e6..0000000000
Binary files a/cv-image-to-xml/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/cv-image-to-xml/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/cv-image-to-xml/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae37cb..0000000000
Binary files a/cv-image-to-xml/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/cv-image-to-xml/src/main/res/values-night/themes.xml b/cv-image-to-xml/src/main/res/values-night/themes.xml
deleted file mode 100644
index 72f672ccad..0000000000
--- a/cv-image-to-xml/src/main/res/values-night/themes.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/res/values/colors.xml b/cv-image-to-xml/src/main/res/values/colors.xml
deleted file mode 100644
index c8524cd961..0000000000
--- a/cv-image-to-xml/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- #FF000000
- #FFFFFFFF
-
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/res/values/strings.xml b/cv-image-to-xml/src/main/res/values/strings.xml
deleted file mode 100644
index fd5883c7b6..0000000000
--- a/cv-image-to-xml/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-
- images
-
- Generate XML
- Update Layout
-
- Detect
- Update
- Save
-
- Yes
- No
-
- No image selected
- Image capture cancelled
- Camera permission is required.
- Please select an image first
- Please run detection on an image first.
- This will overwrite the contents of \'%s\'. Are you sure you want to continue?
- Model or labels could not be loaded
- Saved to Downloads/%s
- Error saving file: %s
- Share XML Layout
- Image selected for placeholder.
- Image deleted from the project
-
- layout
- Image placeholder for object detection
- New Picture
- From the Camera
-
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/res/values/themes.xml b/cv-image-to-xml/src/main/res/values/themes.xml
deleted file mode 100644
index 2bc538cb64..0000000000
--- a/cv-image-to-xml/src/main/res/values/themes.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/res/xml/backup_rules.xml b/cv-image-to-xml/src/main/res/xml/backup_rules.xml
deleted file mode 100644
index 4df9255824..0000000000
--- a/cv-image-to-xml/src/main/res/xml/backup_rules.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/cv-image-to-xml/src/main/res/xml/data_extraction_rules.xml b/cv-image-to-xml/src/main/res/xml/data_extraction_rules.xml
deleted file mode 100644
index 9ee9997b0b..0000000000
--- a/cv-image-to-xml/src/main/res/xml/data_extraction_rules.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/cv-image-to-xml/src/test/java/com/example/images/ExampleUnitTest.kt b/cv-image-to-xml/src/test/java/com/example/images/ExampleUnitTest.kt
deleted file mode 100644
index 6f26f3eb77..0000000000
--- a/cv-image-to-xml/src/test/java/com/example/images/ExampleUnitTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.example.images
-
-import org.junit.Test
-
-import org.junit.Assert.*
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
- @Test
- fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
- }
-}
\ No newline at end of file
diff --git a/cv-image-to-xml/src/test/java/org/appdevforall/codeonthego/computervision/domain/FuzzyAttributeParserTest.kt b/cv-image-to-xml/src/test/java/org/appdevforall/codeonthego/computervision/domain/FuzzyAttributeParserTest.kt
deleted file mode 100644
index 0a2f106c94..0000000000
--- a/cv-image-to-xml/src/test/java/org/appdevforall/codeonthego/computervision/domain/FuzzyAttributeParserTest.kt
+++ /dev/null
@@ -1,571 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain
-
-import org.appdevforall.codeonthego.computervision.domain.parser.FuzzyAttributeParser
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotNull
-import org.junit.Assert.assertNull
-import org.junit.Assert.assertTrue
-import org.junit.Test
-
-class FuzzyAttributeParserTest {
-
- @Test
- fun `parse returns empty map for null annotation`() {
- val result = FuzzyAttributeParser.parse(null, "Button")
- assertTrue(result.isEmpty())
- }
-
- @Test
- fun `parse returns empty map for blank annotation`() {
- val result = FuzzyAttributeParser.parse(" ", "TextView")
- assertTrue(result.isEmpty())
- }
-
- @Test
- fun `delimited happy path parses width height and id`() {
- val annotation = "width: 100dp | height: 80dp | id: my_btn"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("100dp", result["android:layout_width"])
- assertEquals("80dp", result["android:layout_height"])
- assertEquals("my_btn", result["android:id"])
- }
-
- @Test
- fun `delimited parses background as backgroundTint for Button`() {
- val annotation = "background: red | width: 100dp"
- val resultButton = FuzzyAttributeParser.parse(annotation, "Button")
- assertEquals("#FF0000", resultButton["app:backgroundTint"])
-
- val resultText = FuzzyAttributeParser.parse(annotation, "TextView")
- assertEquals("#FF0000", resultText["android:background"])
- }
-
- @Test
- fun `delimited parses text and hint attributes`() {
- val annotation = "text: Hello World | hint: Enter name"
- val result = FuzzyAttributeParser.parse(annotation, "EditText")
-
- assertEquals("Hello World", result["android:text"])
- assertEquals("Enter name", result["android:hint"])
- }
-
- @Test
- fun `delimited parses src attribute`() {
- val annotation = "src: my_image.png | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "ImageView")
-
- assertEquals("@drawable/my_image", result["android:src"])
- }
-
- @Test
- fun `OCR garbled drawable name wm to m`() {
- val annotation = "src: iwmages | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "ImageView")
-
- assertEquals("@drawable/images", result["android:src"])
- }
-
- @Test
- fun `OCR garbled drawable name rn to m`() {
- val annotation = "src: irnagebg | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "ImageView")
-
- assertEquals("@drawable/imagebg", result["android:src"])
- }
-
- @Test
- fun `delimited parses textSize with sp suffix`() {
- val annotation = "text_size: 14 | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("14sp", result["android:textSize"])
- }
-
- @Test
- fun `delimited parses textColor with color map lookup`() {
- val annotation = "textcolor: blue | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("#0000FF", result["android:textColor"])
- }
-
- @Test
- fun `OCR garbled keys are fuzzy matched via delimited`() {
- val annotation = "wldth: 100dp | hejght: 80dp"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("100dp", result["android:layout_width"])
- assertEquals("80dp", result["android:layout_height"])
- }
-
- @Test
- fun `OCR garbled dimension value with spaces is cleaned`() {
- val annotation = "width: 100 dp | height: 80 dp"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("100dp", result["android:layout_width"])
- assertEquals("80dp", result["android:layout_height"])
- }
-
- @Test
- fun `OCR garbled color name is fuzzy matched`() {
- val annotation = "background: bIue | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("#0000FF", result["android:background"])
- }
-
- @Test
- fun `OCR garbled match_parent is fuzzy matched`() {
- val annotation = "width: match parcnt | height: 80dp"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("match_parent", result["android:layout_width"])
- }
-
- @Test
- fun `OCR garbled wrap_content is fuzzy matched`() {
- val annotation = "height: wrap_contnt | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("wrap_content", result["android:layout_height"])
- }
-
- @Test
- fun `OCR dimension with trailing zero before dp is normalized`() {
- val annotation = "height: 1500dp | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "EditText")
-
- assertEquals("150dp", result["android:layout_height"])
- }
-
- @Test
- fun `zero padded dimension values are normalized`() {
- val annotation = "height: 0010dp | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "EditText")
-
- assertEquals("10dp", result["android:layout_height"])
- }
-
- @Test
- fun `all zero padded dimension values normalize to zero`() {
- val annotation = "height: 0000dp | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "EditText")
-
- assertEquals("0dp", result["android:layout_height"])
- }
-
- @Test
- fun `empty chunks between pipes are skipped`() {
- val annotation = "width: 100dp | | height: 80dp"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals(2, result.size)
- assertEquals("100dp", result["android:layout_width"])
- assertEquals("80dp", result["android:layout_height"])
- }
-
- @Test
- fun `chunk without colon infers key-value boundary`() {
- val annotation = "width 100dp | id my_btn"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("100dp", result["android:layout_width"])
- assertEquals("my_btn", result["android:id"])
- }
-
- @Test
- fun `garbage key below threshold is dropped`() {
- val annotation = "xyzabc: 100dp | width: 200dp"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("200dp", result["android:layout_width"])
- assertNull(result["xyzabc"])
- }
-
- @Test
- fun `multiple colons preserves value after first colon`() {
- val annotation = "text: Hello: World | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("Hello: World", result["android:text"])
- }
-
- @Test
- fun `id value has special characters cleaned`() {
- val annotation = "id: radius slider | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "ImageView")
-
- assertEquals("radius_slider", result["android:id"])
- }
-
- @Test
- fun `OCR garbled checkbox group id is normalized without changing custom ids`() {
- val checkboxResult = FuzzyAttributeParser.parse("id: cbgraup2 | width: 100dp", "CheckBox")
- val customResult = FuzzyAttributeParser.parse("id: radius_slider | width: 100dp", "ImageView")
-
- assertEquals("cb_group_2", checkboxResult["android:id"])
- assertEquals("radius_slider", customResult["android:id"])
- }
-
- @Test
- fun `hex color values pass through unchanged`() {
- val annotation = "background: #FF5733 | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("#FF5733", result["android:background"])
- }
-
- @Test
- fun `android resource color values pass through unchanged`() {
- val annotation = "background: @android:color/transparent | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("@android:color/transparent", result["android:background"])
- }
-
- @Test
- fun `entries attribute maps to tools namespace`() {
- val annotation = "entries: @array/items | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "Spinner")
-
- assertEquals("@array/items", result["tools:entries"])
- }
-
- @Test
- fun `inputType attribute parses correctly`() {
- val annotation = "input_type: textPassword | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "EditText")
-
- assertEquals("textPassword", result["android:inputType"])
- }
-
- @Test
- fun `gravity attribute parses correctly`() {
- val annotation = "gravity: center | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("center", result["android:gravity"])
- }
-
- @Test
- fun `layout_gravity attribute parses correctly`() {
- val annotation = "layout_gravity: center | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("center", result["android:layout_gravity"])
- }
-
- @Test
- fun `style attribute parses correctly`() {
- val annotation = "style: @style/Widget.MaterialComponents.Button | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("@style/Widget.MaterialComponents.Button", result["style"])
- }
-
- @Test
- fun `negative dimension values are handled`() {
- val annotation = "width: -16dp | height: 80dp"
- val result = FuzzyAttributeParser.parse(annotation, "View")
-
- assertEquals("-16dp", result["android:layout_width"])
- }
-
- @Test
- fun `purple color is fuzzy matched`() {
- val annotation = "textcolor: pruple | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("#800080", result["android:textColor"])
- }
-
- @Test
- fun `layout_weight cleans non-numeric noise`() {
- val annotation = "layout_weight: 1 Layout | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("1", result["android:layout_weight"])
- }
-
- @Test
- fun `multiple attributes with mixed clean and garbled`() {
- val annotation = "width: match_parent | bg: blue | text_size: 20"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("match_parent", result["android:layout_width"])
- assertEquals("#0000FF", result["app:backgroundTint"])
- assertEquals("20sp", result["android:textSize"])
- }
-
- @Test
- fun `non-delimited colon scanning parses multiple attributes`() {
- val annotation = "width: 100dp height: 80dp id: my_btn"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("100dp", result["android:layout_width"])
- assertEquals("80dp", result["android:layout_height"])
- assertEquals("my_btn", result["android:id"])
- }
-
- @Test
- fun `non-delimited with OCR garbled layout_height`() {
- val annotation = "width: 100 dp Layout _height: 80 dp id: radius_slider"
- val result = FuzzyAttributeParser.parse(annotation, "ImageView")
-
- assertEquals("100dp", result["android:layout_width"])
- assertEquals("80dp", result["android:layout_height"])
- assertEquals("radius_slider", result["android:id"])
- }
-
- @Test
- fun `non-delimited with OCR garbled id as ld`() {
- val annotation = "height: 80 dp ld: done_button"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("80dp", result["android:layout_height"])
- assertEquals("done_button", result["android:id"])
- }
-
- @Test
- fun `non-delimited real OCR scenario from user report`() {
- val annotation = "width: 100 dp Layout _height: 80 dp ld: radius_slider"
- val result = FuzzyAttributeParser.parse(annotation, "ImageView")
-
- assertNotNull(result["android:layout_width"])
- assertNotNull(result["android:layout_height"])
- }
-
- @Test
- fun `non-delimited background color for button`() {
- val annotation = "width: 100dp background: red"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("100dp", result["android:layout_width"])
- assertEquals("#FF0000", result["app:backgroundTint"])
- }
-
- @Test
- fun `margin attributes parse correctly`() {
- val annotation = "margin_top: 8dp | margin_bottom: 16dp | padding: 12dp"
- val result = FuzzyAttributeParser.parse(annotation, "View")
-
- assertEquals("8dp", result["android:layout_marginTop"])
- assertEquals("16dp", result["android:layout_marginBottom"])
- assertEquals("12dp", result["android:padding"])
- }
-
- @Test
- fun `padding attributes parse correctly`() {
- val annotation = "padding_start: 16dp | padding_end: 16dp"
- val result = FuzzyAttributeParser.parse(annotation, "LinearLayout")
-
- assertEquals("16dp", result["android:paddingStart"])
- assertEquals("16dp", result["android:paddingEnd"])
- }
-
- @Test
- fun `visibility attribute parses correctly`() {
- val annotation = "visibility: gone | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "View")
-
- assertEquals("gone", result["android:visibility"])
- }
-
- @Test
- fun `orientation attribute parses correctly`() {
- val annotation = "orientation: horizontal | width: match_parent"
- val result = FuzzyAttributeParser.parse(annotation, "LinearLayout")
-
- assertEquals("horizontal", result["android:orientation"])
- }
-
- @Test
- fun `text style attributes parse correctly`() {
- val annotation = "text_style: bold | text_alignment: center | max_lines: 2"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("bold", result["android:textStyle"])
- assertEquals("center", result["android:textAlignment"])
- assertEquals("2", result["android:maxLines"])
- }
-
- @Test
- fun `elevation attribute parses as dp`() {
- val annotation = "elevation: 4 | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "View")
-
- assertEquals("4dp", result["android:elevation"])
- }
-
- @Test
- fun `card attributes parse correctly`() {
- val annotation = "corner_radius: 8dp | card_elevation: 4dp | card_background_color: white"
- val result = FuzzyAttributeParser.parse(annotation, "CardView")
-
- assertEquals("8dp", result["app:cardCornerRadius"])
- assertEquals("4dp", result["app:cardElevation"])
- assertEquals("#FFFFFF", result["app:cardBackgroundColor"])
- }
-
- @Test
- fun `slider attributes parse correctly`() {
- val annotation = "value_from: 0 | value_to: 100 | step_size: 5"
- val result = FuzzyAttributeParser.parse(annotation, "Slider")
-
- assertEquals("0", result["app:valueFrom"])
- assertEquals("100", result["app:valueTo"])
- assertEquals("5", result["app:stepSize"])
- }
-
- @Test
- fun `min and max attributes parse correctly`() {
- val annotation = "min: 0 | max: 100 | progress: 50"
- val result = FuzzyAttributeParser.parse(annotation, "ProgressBar")
-
- assertEquals("0", result["android:min"])
- assertEquals("100", result["android:max"])
- assertEquals("50", result["android:progress"])
- }
-
- @Test
- fun `scale_type attribute parses correctly`() {
- val annotation = "scale_type: fitCenter | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "ImageView")
-
- assertEquals("fitCenter", result["android:scaleType"])
- }
-
- @Test
- fun `stroke attributes parse correctly`() {
- val annotation = "stroke_color: black | stroke_width: 2dp"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("#000000", result["app:strokeColor"])
- assertEquals("2dp", result["app:strokeWidth"])
- }
-
- @Test
- fun `checked attribute parses correctly`() {
- val annotation = "checked: true | width: wrap_content"
- val result = FuzzyAttributeParser.parse(annotation, "CheckBox")
-
- assertEquals("true", result["android:checked"])
- }
-
- @Test
- fun `max_length and single_line parse correctly`() {
- val annotation = "max_length: 50 | single_line: true"
- val result = FuzzyAttributeParser.parse(annotation, "EditText")
-
- assertEquals("50", result["android:maxLength"])
- assertEquals("true", result["android:singleLine"])
- }
-
- @Test
- fun `font_family attribute parses correctly`() {
- val annotation = "font_family: sans-serif-medium | width: 100dp"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("sans-serif-medium", result["android:fontFamily"])
- }
-
- @Test
- fun `non-delimited full garbled scenario`() {
- val annotation = "wldth: 100 dp Layout _height: 80 dp ld: radius_slider background: blue"
- val result = FuzzyAttributeParser.parse(annotation, "ImageView")
-
- assertEquals("100dp", result["android:layout_width"])
- assertEquals("80dp", result["android:layout_height"])
- assertEquals("radius_slider", result["android:id"])
- assertEquals("#0000FF", result["android:background"])
- }
-
- @Test
- fun `colonless trailing attribute after last colon-scanned key`() {
- val annotation = "width: 100dp background red"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("100dp", result["android:layout_width"])
- assertEquals("#FF0000", result["app:backgroundTint"])
- }
-
- @Test
- fun `colonless trailing color for non-button`() {
- val annotation = "text: Next backgrounli red"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("Next", result["android:text"])
- assertEquals("#FF0000", result["android:background"])
- }
-
- @Test
- fun `multiple colonless trailing attributes`() {
- val annotation = "width: 100dp background red gravity center"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("100dp", result["android:layout_width"])
- assertEquals("#FF0000", result["android:background"])
- assertEquals("center", result["android:gravity"])
- }
-
- @Test
- fun `standalone trailing color without key is inferred as background`() {
- val annotation = "text: Next red"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("Next", result["android:text"])
- assertEquals("#FF0000", result["app:backgroundTint"])
- }
-
- @Test
- fun `standalone trailing color for non-button`() {
- val annotation = "text: Submit blue"
- val result = FuzzyAttributeParser.parse(annotation, "TextView")
-
- assertEquals("Submit", result["android:text"])
- assertEquals("#0000FF", result["android:background"])
- }
-
- @Test
- fun `standalone trailing fuzzy color is matched`() {
- val annotation = "width: 200dp height: 200dp text: Next pruple"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("200dp", result["android:layout_width"])
- assertEquals("200dp", result["android:layout_height"])
- assertEquals("Next", result["android:text"])
- assertEquals("#800080", result["app:backgroundTint"])
- }
-
- @Test
- fun `semicolons treated as colons`() {
- val annotation = "width; 100dp height; 80dp id; my_btn"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("100dp", result["android:layout_width"])
- assertEquals("80dp", result["android:layout_height"])
- assertEquals("my_btn", result["android:id"])
- }
-
- @Test
- fun `colonless key-value in middle of annotation`() {
- val annotation = "id: btn_end text Start background: red"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("btn_end", result["android:id"])
- assertEquals("Start", result["android:text"])
- assertEquals("#FF0000", result["app:backgroundTint"])
- }
-
- @Test
- fun `real OCR garbled annotation with semicolons and noise`() {
- val annotation = "Ld; btn_start text; Start backgraund: gray"
- val result = FuzzyAttributeParser.parse(annotation, "Button")
-
- assertEquals("btn_start", result["android:id"])
- assertEquals("Start", result["android:text"])
- assertEquals("#808080", result["app:backgroundTint"])
- }
-}
diff --git a/cv-image-to-xml/src/test/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/OcrSanitizerRulesTest.kt b/cv-image-to-xml/src/test/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/OcrSanitizerRulesTest.kt
deleted file mode 100644
index 7821e96b98..0000000000
--- a/cv-image-to-xml/src/test/java/org/appdevforall/codeonthego/computervision/domain/parser/sanitizer/OcrSanitizerRulesTest.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.parser.sanitizer
-
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-class OcrSanitizerRulesTest {
-
- @Test
- fun `color sanitizer fixes merged background color tokens`() {
- val sanitizer = ColorSanitizer()
-
- assertEquals("background red", sanitizer.sanitize("backgroundired"))
- assertEquals("background red", sanitizer.sanitize("backgroundred"))
- }
-
- @Test
- fun `text attribute sanitizer normalizes OCR variants of text style`() {
- val sanitizer = TextAttributeSanitizer()
-
- assertEquals("text_style", sanitizer.sanitize("text style"))
- assertEquals("text_style", sanitizer.sanitize("text stjle"))
- }
-
- @Test
- fun `dimension sanitizer fixes width and height OCR mistakes`() {
- val sanitizer = DimensionSanitizer()
-
- assertEquals("layout_width: 120dp", sanitizer.sanitize("iayout widh. 120dp"))
- assertEquals("layout_height: 48dp", sanitizer.sanitize("layout heist. 48dp"))
- }
-
- @Test
- fun `dimension sanitizer normalizes match parent OCR variants`() {
- val sanitizer = DimensionSanitizer()
-
- assertEquals("layout_width: match_parent", sanitizer.sanitize("layout_width: match parent"))
- assertEquals("layout_width: match_parent", sanitizer.sanitize("layout_width: match-parrent"))
- }
-
- @Test
- fun `margin and padding sanitizer preserves edge names`() {
- val sanitizer = MarginPaddingSanitizer()
-
- assertEquals("layout_margin_top: 16dp", sanitizer.sanitize("layout_margin top: 16dp"))
- assertEquals("padding_end: 8dp", sanitizer.sanitize("padding end: 8dp"))
- }
-
- @Test
- fun `structure sanitizer rewrites horizontal center layout phrase`() {
- val sanitizer = StructureSanitizer()
-
- assertEquals(
- "layout_gravity: center_horizontal",
- sanitizer.sanitize("horizontal gravity: center layout")
- )
- }
-
- @Test
- fun `default sanitizer applies multiple OCR cleanup rules in sequence`() {
- val sanitizer = OcrSanitizerFactory.createDefaultSanitizer()
- val input = "backgroundired | text stjle: bold | iayout widh. match parrent | padding end: 8dp"
-
- assertEquals(
- "background red | text_style: bold | layout_width: match_parent | padding_end: 8dp",
- sanitizer.sanitize(input)
- )
- }
-}
diff --git a/cv-image-to-xml/src/test/java/org/appdevforall/codeonthego/computervision/domain/xml/LayoutRendererTest.kt b/cv-image-to-xml/src/test/java/org/appdevforall/codeonthego/computervision/domain/xml/LayoutRendererTest.kt
deleted file mode 100644
index 0443c730e2..0000000000
--- a/cv-image-to-xml/src/test/java/org/appdevforall/codeonthego/computervision/domain/xml/LayoutRendererTest.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-package org.appdevforall.codeonthego.computervision.domain.xml
-
-import android.graphics.Rect
-import org.appdevforall.codeonthego.computervision.domain.model.LayoutItem
-import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
-import org.junit.Assert.assertTrue
-import org.junit.Test
-
-class LayoutRendererTest {
-
- @Test
- fun `checkbox group uses canonical ids when OCR annotation id is noisy`() {
- val first = checkboxBox(y = 0, text = "Option A")
- val second = checkboxBox(y = 20, text = "Option B", checked = true)
- val context = XmlContext()
-
- val renderer = LayoutRenderer(
- context = context,
- annotations = mapOf(
- first to "id: cbgraup2 | textColor: gray"
- )
- )
-
- renderer.render(LayoutItem.CheckboxGroup(listOf(first, second), "vertical"))
-
- val xml = context.toString()
-
- assertTrue(xml.contains("""android:id="@+id/cb_group_2_a""""))
- assertTrue(xml.contains("""android:id="@+id/cb_group_2_b""""))
- assertTrue(!xml.contains("cbgraup2"))
- }
-
- @Test
- fun `radio group ignores group style ids on child radios`() {
- val first = radioBox(y = 0, text = "choice A")
- val second = radioBox(y = 20, text = "choice B", checked = true)
- val context = XmlContext()
-
- val renderer = LayoutRenderer(
- context = context,
- annotations = mapOf(
- first to "id: rb_group_1 | textSize: 16sp"
- )
- )
-
- renderer.render(LayoutItem.RadioGroup(listOf(first, second), "vertical"))
-
- val xml = context.toString()
-
- assertTrue(xml.contains("""android:id="@+id/radio_button_unchecked_0""""))
- assertTrue(xml.contains("""android:id="@+id/radio_button_checked_0""""))
- assertTrue(!xml.contains("""android:id="@+id/rb_group_1""""))
- }
-
- @Test
- fun `radio group ignores group style ids even when parser leaks trailing tokens`() {
- val first = radioBox(y = 0, text = "choice A")
- val second = radioBox(y = 20, text = "choice B", checked = true)
- val context = XmlContext()
-
- val renderer = LayoutRenderer(
- context = context,
- annotations = mapOf(
- first to "id: rb_group_1_text_site_16sp | textColor: black"
- )
- )
-
- renderer.render(LayoutItem.RadioGroup(listOf(first, second), "vertical"))
-
- val xml = context.toString()
-
- assertTrue(xml.contains("""android:id="@+id/radio_button_unchecked_0""""))
- assertTrue(!xml.contains("""android:id="@+id/rb_group_1_text_site_16sp""""))
- }
-
- private fun checkboxBox(y: Int, text: String, checked: Boolean = false): ScaledBox {
- val label = if (checked) "checkbox_checked" else "checkbox_unchecked"
- return ScaledBox(
- label = label,
- text = text,
- x = 0,
- y = y,
- w = 20,
- h = 20,
- centerX = 10,
- centerY = y + 10,
- rect = Rect(0, y, 20, y + 20)
- )
- }
-
- private fun radioBox(y: Int, text: String, checked: Boolean = false): ScaledBox {
- val label = if (checked) "radio_button_checked" else "radio_button_unchecked"
- return ScaledBox(
- label = label,
- text = text,
- x = 0,
- y = y,
- w = 20,
- h = 20,
- centerX = 10,
- centerY = y + 10,
- rect = Rect(0, y, 20, y + 20)
- )
- }
-}
diff --git a/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/UIExtension.kt b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/UIExtension.kt
index 7d08d52859..65e6e1792e 100644
--- a/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/UIExtension.kt
+++ b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/UIExtension.kt
@@ -65,8 +65,11 @@ data class MenuItem @JvmOverloads constructor(
* both the sidebar and the toolbar surfaces.
*/
val tooltipTag: String? = null,
- val icon: Int? = null,
-)
+ val icon: Int? = null
+) {
+ var isEnabledProvider: (() -> Boolean)? = null
+ var isVisibleProvider: (() -> Boolean)? = null
+}
data class ContextMenuContext(
val file: java.io.File?,
@@ -139,4 +142,4 @@ data class FabAction(
val isEnabled: Boolean = true,
val isVisible: Boolean = true,
val action: () -> Unit
-)
\ No newline at end of file
+)
diff --git a/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeServices.kt b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeServices.kt
index b614cef6ce..f7c02716c7 100644
--- a/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeServices.kt
+++ b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeServices.kt
@@ -150,6 +150,25 @@ interface IdeUIService {
* @return true if UI operations can be performed, false otherwise
*/
fun isUIAvailable(): Boolean
+
+ /**
+ * Opens a fullscreen host surface for a plugin-owned Fragment.
+ *
+ * The host app owns only the generic container. The plugin owns the Fragment class and all
+ * feature-specific behavior.
+ */
+ fun openPluginScreen(
+ pluginId: String,
+ fragmentClassName: String,
+ title: String? = null
+ ): Boolean = false
+
+ companion object {
+ const val ACTION_OPEN_PLUGIN_SCREEN = "com.itsaky.androidide.plugins.OPEN_PLUGIN_SCREEN"
+ const val EXTRA_PLUGIN_ID = "com.itsaky.androidide.plugins.extra.PLUGIN_ID"
+ const val EXTRA_FRAGMENT_CLASS_NAME = "com.itsaky.androidide.plugins.extra.FRAGMENT_CLASS_NAME"
+ const val EXTRA_TITLE = "com.itsaky.androidide.plugins.extra.TITLE"
+ }
}
/**
diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeUIServiceImpl.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeUIServiceImpl.kt
index a9115753bc..f766225fc2 100644
--- a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeUIServiceImpl.kt
+++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeUIServiceImpl.kt
@@ -3,6 +3,7 @@
package com.itsaky.androidide.plugins.manager.services
import android.app.Activity
+import android.content.Intent
import com.itsaky.androidide.plugins.manager.core.PluginManager
import com.itsaky.androidide.plugins.services.IdeUIService
@@ -25,4 +26,22 @@ class IdeUIServiceImpl(
override fun isUIAvailable(): Boolean {
return getCurrentActivity() != null
}
+
+ override fun openPluginScreen(
+ pluginId: String,
+ fragmentClassName: String,
+ title: String?
+ ): Boolean {
+ val activity = getCurrentActivity() ?: return false
+ val intent = Intent(IdeUIService.ACTION_OPEN_PLUGIN_SCREEN).apply {
+ setPackage(activity.packageName)
+ putExtra(IdeUIService.EXTRA_PLUGIN_ID, pluginId)
+ putExtra(IdeUIService.EXTRA_FRAGMENT_CLASS_NAME, fragmentClassName)
+ putExtra(IdeUIService.EXTRA_TITLE, title)
+ }
+ return runCatching {
+ activity.startActivity(intent)
+ true
+ }.getOrDefault(false)
+ }
}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index bf4dd33435..1b75673afd 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -189,7 +189,6 @@ include(
":plugin-manager",
":llama-api",
":llama-impl",
- ":cv-image-to-xml",
":llama-api",
":llama-impl",
":compose-preview"