Restore typed SSD client baseline
ober
03e6a0d14d2f932dae8006100ccbc3c46c0f12ab
--- a/jandroid.ss +++ b/jandroid.ss @@ -41,7 +41,7 @@ #f)) (error 'find-app "expected `(def app '(...))` in app spec"))) -(def (find-fragment forms path) +(def (find-fragment-data forms path) (or (for/or ((form forms)) (if (and (pair? form) (eq? (car form) 'def) @@ -52,6 +52,31 @@ #f)) (error 'find-fragment "expected `(def fragment '(...))` in fragment file" path))) +(def (fragment-include? item) + (and (pair? item) + (eq? (car item) 'include-fragment) + (= (length item) 2) + (string? (cadr item)))) + +(def (find-fragment forms path (ancestors '())) + (when (member path ancestors) + (error 'find-fragment "fragment include cycle" (reverse (cons path ancestors)))) + (let ((fragment (find-fragment-data forms path)) + (base-dir (path-directory path)) + (next-ancestors (cons path ancestors))) + (apply append + (map (lambda (item) + (if (fragment-include? item) + (let ((included-path + (resolve-local-source-path + base-dir (cadr item) '(".ss")))) + (find-fragment + (read-all-forms included-path) + included-path + next-ancestors)) + (list item))) + fragment)))) + (def (entries spec) (if (and (pair? spec) (eq? (car spec) 'android-app)) (cdr spec) --- a/templates/ssd-review.ss +++ b/templates/ssd-review.ss @@ -2,7073 +2,18793 @@ (def fragment '( - (kotlin-file-lines "com/sfb/ssdreview/BoxTypes.kt" - ( - "package com.sfb.ssdreview" - "" - "import android.content.Context" - "" - "data class BoxType(val id: String, val name: String) {" - " val display: String get() = \"$id $name\"" - "}" - "" - "object BoxTypes {" - " private var cache: List<BoxType>? = null" - " private val weaponTerms = listOf(" - " \"add\"," - " \"antiproton\"," - " \"atomicmissile\"," - " \"axiontorpedo\"," - " \"bioelectricbolt\"," - " \"bombthrower\"," - " \"bosondrill\"," - " \"cannon\"," - " \"clusterbomb\"," - " \"deathboltrack\"," - " \"disruptor\"," - " \"drone\"," - " \"energyhowitzer\"," - " \"esg\"," - " \"fireball\"," - " \"fusion\"," - " \"gausscannon\"," - " \"hellbore\"," - " \"hellgun\"," - " \"hypercannon\"," - " \"hyperdrone\"," - " \"implosionbolt\"," - " \"implosiontorpedo\"," - " \"ioncannon\"," - " \"ionpulsecannon\"," - " \"kineticcannon\"," - " \"kineticwave\"," - " \"laser\"," - " \"massdriver\"," - " \"megaphaser\"," - " \"missile\"," - " \"mine\"," - " \"morter\"," - " \"mortar\"," - " \"neutronbeam\"," - " \"neutrongun\"," - " \"novacannon\"," - " \"optionmount\"," - " \"particlebeam\"," - " \"particlecannon\"," - " \"phaser\"," - " \"photon\"," - " \"plasma\"," - " \"positronlancet\"," - " \"ppd\"," - " \"prospectingcannon\"," - " \"proton\"," - " \"pulsecannon\"," - " \"pulseemitter\"," - " \"quantumcannon\"," - " \"railgun\"," - " \"rocket\"," - " \"sfg\"," - " \"shortrangecannon\"," - " \"sonicpulser\"," - " \"spaceauger\"," - " \"stingtorpedo\"," - " \"subspacerocket\"," - " \"tachyonbeam\"," - " \"tachyongun\"," - " \"tachyonmissile\"," - " \"torp\"," - " \"torpedo\"," - " \"trh\"," - " \"trl\"," - " \"webbreaker\"," - " \"webcaster\"," - " \"websnare\"" - " )" - " private val nonWeaponTerms = listOf(" - " \"charge\"," - " \"degradation\"," - " \"fighterbox\"," - " \"hangar\"," - " \"hit\"," - " \"internalweaponbay\"," - " \"link\"," - " \"mechlink\"," - " \"rail\"," - " \"round\"," - " \"stabilizer\"," - " \"targetaccentuator\"," - " \"targetacquisitiongear\"," - " \"targetacquisitionguide\"" - " )" - "" - " fun load(context: Context): List<BoxType> {" - " cache?.let { return it }" - " val items = mutableListOf<BoxType>()" - " context.assets.open(\"box_types.csv\").bufferedReader().useLines { lines ->" - " lines.drop(1).forEach { line ->" - " val comma = line.indexOf(',')" - " if (comma > 0) {" - " val id = line.substring(0, comma).trim()" - " val name = line.substring(comma + 1).trim()" - " if (id.isNotBlank() && name.isNotBlank()) {" - " items.add(BoxType(id, name))" - " }" - " }" - " }" - " }" - " cache = items" - " return items" - " }" - "" - " fun resolve(context: Context, raw: String): BoxType? {" - " val text = raw.trim()" - " if (text.isBlank()) return null" - " val normalized = normalize(text)" - " val lowered = text.lowercase()" - " val items = load(context)" - " items.firstOrNull {" - " it.id == text ||" - " it.display.lowercase() == lowered ||" - " it.name.lowercase() == lowered ||" - " normalize(it.name) == normalized" - " }?.let { return it }" - " items.mapNotNull { item ->" - " val displayIndex = lowered.lastIndexOf(item.display.lowercase())" - " val nameIndex = lowered.lastIndexOf(item.name.lowercase())" - " val normalizedIndex = normalized.lastIndexOf(normalize(item.name))" - " val index = maxOf(displayIndex, nameIndex, normalizedIndex)" - " if (index >= 0) index to item else null" - " }.maxWithOrNull(compareBy<Pair<Int, BoxType>> { it.first }.thenBy { it.second.display.length })?.let { return it.second }" - " val matches = items.filter {" - " it.display.lowercase().contains(lowered) ||" - " normalize(it.name).contains(normalized)" - " }" - " return matches.singleOrNull()" - " }" - "" - " fun isWeapon(context: Context, raw: String): Boolean {" - " val text = raw.trim()" - " if (text.isBlank()) return false" - " val resolved = resolve(context, text)" - " val normalized = normalize(resolved?.name ?: text)" - " if (nonWeaponTerms.any { normalized.contains(it) }) return false" - " return weaponTerms.any { normalized.contains(it) }" - " }" - "" - " private fun normalize(value: String): String =" - " value.lowercase().filter { it.isLetterOrDigit() }" - "}" - )) - (kotlin-file-lines "com/sfb/ssdreview/Guessing.kt" - ( - "package com.sfb.ssdreview" - "" - "import android.content.Context" - "" - "object Guessing {" - " fun applyInitialGuesses(context: Context, session: SsdSession) {" - " val race = raceKey(session.sourceName + \" \" + session.sourceUri)" - " session.groups.forEach { group ->" - " if (!shouldReplaceLabel(group)) return@forEach" - " val guess = guessForGroup(group, session.imageWidth, session.imageHeight, race)" - " val boxType = BoxTypes.resolve(context, guess.boxType)" - " group.label = \"${guess.label}?\"" - " group.boxTypeId = boxType?.id ?: group.boxTypeId" - " group.notes = \"Guess: ${guess.label}, ${guess.reason}\"" - " }" - " }" - "" - " private fun shouldReplaceLabel(group: SsdGroup): Boolean {" - " val label = group.label.trim()" - " if (group.status == \"reviewed\" || group.status == \"approved\") return false" - " if (label.isBlank()) return true" - " if (label.endsWith(\"?\")) return true" - " return Regex(\"\"\"^\\d+\\s+boxes$\"\"\").matches(label)" - " }" - "" - " private fun guessForGroup(group: SsdGroup, imageWidth: Int, imageHeight: Int, race: String): Guess {" - " val x1 = group.bbox[0]" - " val y1 = group.bbox[1]" - " val x2 = group.bbox[2]" - " val y2 = group.bbox[3]" - " val width = (x2 - x1).coerceAtLeast(1f)" - " val height = (y2 - y1).coerceAtLeast(1f)" - " val count = group.count" - " val cx = (x1 + x2) / 2f" - " val cy = (y1 + y2) / 2f" - " val nearEdge = y1 < imageHeight * 0.22f ||" - " y2 > imageHeight * 0.78f ||" - " x1 < imageWidth * 0.16f ||" - " x2 > imageWidth * 0.84f" - " val lineLike = width >= height * 1.8f || height >= width * 1.8f" - " val compactBank = width * height < imageWidth * imageHeight * 0.035f" - "" - " if (count >= 3 && nearEdge && (lineLike || compactBank || count >= 5)) {" - " return if (race == \"andromedan\") {" - " Guess(\"pa panel\", \"PA Panel\", \"Andromedan outer-edge bank geometry\")" - " } else {" - " Guess(" - " shieldLabel(cx / imageWidth.coerceAtLeast(1), cy / imageHeight.coerceAtLeast(1))," - " \"Shield\"," - " \"outer-edge bank geometry\"" - " )" - " }" - " }" - " if (count >= 10 && cy < imageHeight * 0.34f && width >= height * 1.4f) {" - " return if (race == \"andromedan\") {" - " Guess(\"pa panel\", \"PA Panel\", \"upper bank geometry\")" - " } else {" - " Guess(\"shield-1\", \"Shield\", \"upper horizontal bank geometry\")" - " }" - " }" - " if (count <= 4 && width * height < imageWidth * imageHeight * 0.006f) {" - " if (nearEdge && count >= 2) {" - " return if (race == \"andromedan\") {" - " Guess(\"pa panel\", \"PA Panel\", \"small Andromedan outer-edge bank geometry\")" - " } else {" - " Guess(" - " shieldLabel(cx / imageWidth.coerceAtLeast(1), cy / imageHeight.coerceAtLeast(1))," - " \"Shield\"," - " \"small outer-edge bank geometry\"" - " )" - " }" - " }" - " return Guess(if (race == \"andromedan\") \"phaser-2\" else \"phaser\", \"Phaser\", \"compact weapon-size group\")" - " }" - " if (count in 1..3 && cy < imageHeight * 0.34f && cx > imageWidth * 0.35f && cx < imageWidth * 0.65f) {" - " return Guess(\"bridge\", \"Bridge\", \"upper center command position\")" - " }" - " if (count in 3..12 && cy > imageHeight * 0.32f && cy < imageHeight * 0.72f) {" - " return Guess(\"hull\", \"Forward Hull\", \"central internal bank\")" - " }" - " return Guess(\"${count} boxes\", \"\", \"unclassified detected group\")" - " }" - "" - " private fun shieldLabel(rx: Float, ry: Float): String = when {" - " ry < 0.26f -> \"shield-1\"" - " rx > 0.70f && ry < 0.58f -> \"shield-2\"" - " rx > 0.58f && ry >= 0.58f -> \"shield-3\"" - " ry > 0.72f -> \"shield-4\"" - " rx < 0.42f && ry >= 0.58f -> \"shield-5\"" - " rx < 0.30f -> \"shield-6\"" - " else -> \"shield\"" - " }" - "" - " private fun raceKey(text: String): String {" - " val lower = text.lowercase()" - " return when {" - " lower.contains(\"andromedan\") || lower.contains(\"module_c3\") || lower.contains(\"module c3\") -> \"andromedan\"" - " lower.contains(\"federation\") -> \"federation\"" - " lower.contains(\"klingon\") -> \"klingon\"" - " lower.contains(\"romulan\") -> \"romulan\"" - " lower.contains(\"gorn\") -> \"gorn\"" - " lower.contains(\"kzinti\") -> \"kzinti\"" - " lower.contains(\"lyran\") -> \"lyran\"" - " lower.contains(\"tholian\") -> \"tholian\"" - " lower.contains(\"hydran\") -> \"hydran\"" - " else -> \"\"" - " }" - " }" - "" - " private data class Guess(val label: String, val boxType: String, val reason: String)" - "}" - "" - )) - (kotlin-file-lines "com/sfb/ssdreview/MainActivity.kt" - ( - "package com.sfb.ssdreview" - "" - "import android.Manifest" - "import android.app.Activity" - "import android.app.AlertDialog" - "import android.content.ContentUris" - "import android.content.Intent" - "import android.content.res.Configuration" - "import android.content.pm.PackageManager" - "import android.graphics.Bitmap" - "import android.graphics.Canvas" - "import android.graphics.Color" - "import android.graphics.Matrix" - "import android.graphics.BitmapFactory" - "import android.graphics.Paint" - "import android.graphics.Typeface" - "import android.graphics.drawable.GradientDrawable" - "import android.graphics.pdf.PdfRenderer" - "import android.net.Uri" - "import android.os.Build" - "import android.os.Bundle" - "import android.os.Environment" - "import android.os.ParcelFileDescriptor" - "import android.os.SystemClock" - "import android.provider.DocumentsContract" - "import android.provider.MediaStore" - "import android.provider.OpenableColumns" - "import android.provider.Settings" - "import android.text.Editable" - "import android.text.InputType" - "import android.text.TextWatcher" - "import android.util.Base64" - "import android.view.Gravity" - "import android.view.HapticFeedbackConstants" - "import android.view.View" - "import android.widget.ArrayAdapter" - "import android.widget.AutoCompleteTextView" - "import android.widget.Button" - "import android.widget.EditText" - "import android.widget.LinearLayout" - "import android.widget.ScrollView" - "import android.widget.TextView" - "import android.widget.Toast" - "import androidx.documentfile.provider.DocumentFile" - "import androidx.core.content.FileProvider" - "import org.json.JSONArray" - "import org.json.JSONObject" - "import java.io.File" - "import java.io.ByteArrayOutputStream" - "import java.io.PrintWriter" - "import java.io.StringWriter" - "import java.net.HttpURLConnection" - "import java.net.Inet4Address" - "import java.net.NetworkInterface" - "import java.net.URL" - "import java.util.UUID" - "import java.util.concurrent.atomic.AtomicReference" - "import kotlin.concurrent.thread" - "import kotlin.system.exitProcess" - "import kotlin.math.ceil" - "import kotlin.math.max" - "import kotlin.math.min" - "" - "class MainActivity : Activity() {" - " private lateinit var truthStore: TruthStore" - " private lateinit var reviewView: SsdReviewView" - " private lateinit var status: TextView" - " private lateinit var activityStatus: TextView" - "" - " private var pdfUri: Uri? = null" - " private var pdfName: String = \"\"" - " private var pdfSha1: String = \"\"" - " private var serverPdfId: String = \"\"" - " private var pageIndex: Int = 0" - " private var pageCount: Int = 0" - " private var currentBitmap: Bitmap? = null" - " private var currentSession: SsdSession? = null" - " private var selectedGroupId: String? = null" - " private var appendMode: Boolean = false" - " private var ssdAreaMode: Boolean = false" - " private var selfTestMode: Boolean = false" - " private var selfTestOcrMode: Boolean = false" - " private var selfTestServerLabelsMode: Boolean = false" - " private var reopenSsdBrowserOnResume: Boolean = false" - " @Volatile private var aiPollingJobId: String = \"\"" - " @Volatile private var loadGeneration: Int = 0" - " @Volatile private var activeActivityId: Int = 0" - " @Volatile private var pagePipelineActive: Boolean = false" - " private val clientLogQueue = ArrayDeque<JSONObject>()" - " private val clientLogQueueLock = Any()" - " @Volatile private var clientLogWorkerRunning: Boolean = false" - " private val boxTypeDisplays: List<String> by lazy { BoxTypes.load(this).map { it.display } }" - " private val firingArcDisplays = listOf(" - " \"FA\"," - " \"FH\"," - " \"FX\"," - " \"RX\"," - " \"LS\"," - " \"RS\"," - " \"L\"," - " \"R\"," - " \"LF\"," - " \"RF\"," - " \"LR\"," - " \"RR\"," - " \"A\"," - " \"LA\"," - " \"RA\"," - " \"360\"" - " )" - "" - " override fun onCreate(savedInstanceState: Bundle?) {" - " super.onCreate(savedInstanceState)" - " installClientCrashLogger()" - " logClientEvent(\"ssd-app-start\")" - " selfTestMode = intent.getBooleanExtra(EXTRA_SELF_TEST, false)" - " selfTestOcrMode = intent.getBooleanExtra(EXTRA_SELF_TEST_OCR, false)" - " selfTestServerLabelsMode = intent.getBooleanExtra(EXTRA_SELF_TEST_SERVER_LABELS, false)" - " truthStore = TruthStore(this)" - " refreshServerEndpointAsync(\"start\", allowLoopback = selfTestMode)" - " setContentView(buildUi())" - " setStatus(\"Open an SSD PDF. ${truthStore.storageSummary()}\")" - " if (selfTestMode) {" - " if (selfTestServerLabelsMode) loadSelfTestServerLabelsFixture() else if (selfTestOcrMode) loadSelfTestOcrFixture() else loadSelfTestFixture()" - " } else {" - " restoreLastPdf()" - " }" - " val seedActivity = beginActivity(\"Loading bundled truth seed\")" - " truthStore.seedBundledTruthAsync { imported ->" - " if (imported > 0) {" - " runOnUiThread {" - " finishActivity(seedActivity, \"Merged truth seed: $imported files\")" - " if (selfTestMode) {" - " logSelfTestState(\"seed-merged\")" - " } else {" - " setStatus(\"Truth seed merged $imported files\")" - " if (pdfUri != null && currentBitmap == null && currentSession == null && !pagePipelineActive) renderCurrentPage()" - " }" - " }" - " } else {" - " finishActivity(seedActivity, \"Bundled truth seed already current\")" - " }" - " }" - " }" - "" - " override fun onResume() {" - " super.onResume()" - " refreshServerEndpointAsync(\"resume\", allowLoopback = selfTestMode)" - " resumePendingAiReport()" - " resumePendingApkInstall()" - " if (reopenSsdBrowserOnResume && hasDirectDownloadAccess()) {" - " reopenSsdBrowserOnResume = false" - " openPdf()" - " }" - " }" - "" - " private fun refreshServerEndpointAsync(reason: String, allowLoopback: Boolean = false) {" - " thread {" - " try {" - " truthStore.ensureReachableServerEndpoint(allowLoopback = allowLoopback)" - " truthStore.flushPendingRemotePosts()" - " logClientEvent(\"server-endpoint-refreshed\", JSONObject().put(\"reason\", reason))" - " } catch (error: Exception) {" - " logClientEvent(\"server-endpoint-refresh-failed\", JSONObject().put(\"reason\", reason).put(\"error\", throwableLog(error)))" - " }" - " }" - " }" - "" - " private fun findServerAsync() {" - " val activity = beginActivity(\"Finding sfb-server\")" - " thread {" - " try {" - " activityStep(activity, \"Scanning local network\")" - " val info = truthStore.discoverServerEndpoint()" - " truthStore.flushPendingRemotePosts()" - " runOnUiThread {" - " finishActivity(activity, \"Found server\")" - " setStatus(\"Server selected: ${truthStore.remoteEndpointLabel()}\")" - " logClientEvent(\"server-discovery-succeeded\", JSONObject().put(\"info\", info))" - " }" - " } catch (error: Exception) {" - " runOnUiThread {" - " finishActivity(activity, \"Find server failed: ${error.message}\", success = false)" - " setStatus(\"Find server failed: ${error.message}\")" - " logClientEvent(\"server-discovery-failed\", JSONObject().put(\"error\", throwableLog(error)))" - " }" - " }" - " }" - " }" - "" - " private fun showServerConfigDialog() {" - " val input = EditText(this).apply {" - " inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI" - " setSingleLine(true)" - " setText(truthStore.remoteEndpointLabel())" - " selectAll()" - " }" - " AlertDialog.Builder(this)" - " .setTitle(\"sfb-server URL\")" - " .setView(input)" - " .setPositiveButton(\"Save\") { _, _ ->" - " val value = input.text.toString().trim()" - " val activity = beginActivity(\"Checking server URL\")" - " thread {" - " try {" - " truthStore.setRemoteEndpoint(value)" - " truthStore.refreshServerEndpoint()" - " truthStore.flushPendingRemotePosts()" - " runOnUiThread {" - " finishActivity(activity, \"Server URL saved\")" - " setStatus(\"Server URL saved: ${truthStore.remoteEndpointLabel()}\")" - " }" - " } catch (error: Exception) {" - " runOnUiThread {" - " finishActivity(activity, \"Server URL failed: ${error.message}\", success = false)" - " setStatus(\"Server URL failed: ${error.message}\")" - " }" - " }" - " }" - " }" - " .setNegativeButton(\"Cancel\", null)" - " .show()" - " }" - "" - " override fun onPause() {" - " super.onPause()" - " persistCurrentPdfState(includeViewport = true)" - " }" - "" - " override fun onConfigurationChanged(newConfig: Configuration) {" - " super.onConfigurationChanged(newConfig)" - " val viewport = reviewView.captureViewport()" - " reviewView.post {" - " reviewView.restoreViewport(viewport)" - " reviewView.invalidate()" - " }" - " logClientEvent(\"ssd-config-changed\", JSONObject().put(\"pipeline_active\", pagePipelineActive).put(\"generation\", loadGeneration))" - " }" - "" - " private fun loadSelfTestFixture() {" - " pdfName = \"SFB SSD Self Test\"" - " pdfSha1 = \"ssdselftestfixture\"" - " pageIndex = 0" - " pageCount = 1" - " pdfUri = Uri.parse(\"selftest://ssd-review\")" - " val bitmap = Bitmap.createBitmap(900, 1200, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.WHITE) }" - " val cells = mutableListOf(" - " selfTestCell(\"c0001\", 170f, 120f), selfTestCell(\"c0002\", 205f, 120f)," - " selfTestCell(\"c0003\", 240f, 120f), selfTestCell(\"c0004\", 275f, 120f)," - " selfTestCell(\"c0005\", 430f, 390f), selfTestCell(\"c0006\", 430f, 425f)," - " selfTestCell(\"c0007\", 610f, 410f), selfTestCell(\"c0008\", 645f, 410f)," - " selfTestCell(\"c0009\", 330f, 720f), selfTestCell(\"c0010\", 365f, 720f)" - " )" - " val session = SsdSession(" - " sessionId = \"ssd-selftest-session\"," - " sourceKey = SELF_TEST_SOURCE_KEY," - " sourceName = pdfName," - " sourceUri = pdfUri.toString()," - " page = 1," - " pageCount = 1," - " dpi = DPI," - " imageWidth = bitmap.width," - " imageHeight = bitmap.height," - " cells = cells," - " groups = mutableListOf(" - " selfTestGroup(\"g0001\", \"shield-1?\", \"26\", \"c0001\", \"c0002\", \"c0003\", \"c0004\")," - " selfTestGroup(\"g0002\", \"excess damage?\", \"13\", \"c0005\", \"c0006\")," - " selfTestGroup(\"g0003\", \"phaser-1\", \"33\", \"c0007\", \"c0008\").apply { status = \"reviewed\" }," - " selfTestGroup(\"g0004\", \"battery?\", \"18\", \"c0009\", \"c0010\")," - " selfTestGroup(\"g0005\", \"shield-1?\", \"26\", \"c0001\", \"c0002\", \"c0003\", \"c0004\")" - " )" - " )" - " session.groups.forEach { session.recomputeGroup(it) }" - " truthStore.applyTruth(session)" - " currentBitmap = bitmap" - " currentSession = session" - " selectedGroupId = session.groups.firstOrNull()?.id" - " reviewView.bitmap = bitmap" - " reviewView.session = session" - " reviewView.selectedGroupId = selectedGroupId" - " reviewView.ssdArea = session.ssdArea" - " setStatus(\"SELFTEST_READY groups=${session.groups.size} labels=${selfTestLabelCount(session)} selected=${selectedGroupId ?: \"\"}\")" - " logSelfTestState(\"loaded\")" - " }" - "" - " private fun loadSelfTestOcrFixture() {" - " pdfName = \"SFB SSD OCR Self Test\"" - " pdfSha1 = \"ssdocrselftestfixture\"" - " pageIndex = 0" - " pageCount = 1" - " pdfUri = Uri.parse(\"selftest://ssd-review-ocr\")" - " val bitmap = Bitmap.createBitmap(900, 1200, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.WHITE) }" - " drawSelfTestOcrPage(bitmap)" - " val session = SsdSession(" - " sessionId = \"ssd-selftest-ocr-session\"," - " sourceKey = sourceKey(pdfSha1, 1, DPI)," - " sourceName = pdfName," - " sourceUri = pdfUri.toString()," - " page = 1," - " pageCount = 1," - " dpi = DPI," - " imageWidth = bitmap.width," - " imageHeight = bitmap.height," - " cells = mutableListOf()," - " groups = mutableListOf()" - " )" - " installSelfTestProposalFixture(session)" - " currentBitmap = bitmap" - " currentSession = session" - " selectedGroupId = null" - " reviewView.bitmap = bitmap" - " reviewView.session = session" - " reviewView.selectedGroupId = null" - " reviewView.ssdArea = null" - " setStatus(\"SELFTEST_OCR_READY\")" - " logSelfTestState(\"ocr-loaded\")" - " }" - "" - " private fun loadSelfTestServerLabelsFixture() {" - " pdfName = \"omega-1-ssd-front.pdf\"" - " pdfSha1 = \"ssdserverlabelsfixture\"" - " pageIndex = 1" - " pageCount = 2" - " pdfUri = Uri.parse(\"selftest://server-labels\")" - " val renderGeneration = ++loadGeneration" - " val bitmap = Bitmap.createBitmap(900, 1200, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.WHITE) }" - " val session = SsdSession(" - " sessionId = \"ssd-selftest-server-labels-session\"," - " sourceKey = sourceKey(pdfSha1, pageIndex + 1, DPI)," - " sourceName = pdfName," - " sourceUri = pdfUri.toString()," - " page = pageIndex + 1," - " pageCount = pageCount," - " dpi = DPI," - " imageWidth = bitmap.width," - " imageHeight = bitmap.height," - " cells = mutableListOf()," - " groups = mutableListOf()" - " )" - " currentBitmap = bitmap" - " currentSession = session" - " selectedGroupId = null" - " reviewView.bitmap = bitmap" - " reviewView.session = session" - " reviewView.selectedGroupId = null" - " reviewView.ssdArea = null" - " setStatus(\"SELFTEST_SERVER_LABELS_READY\")" - " logSelfTestState(\"server-labels-loaded\")" - " thread {" - " try {" - " truthStore.ensureReachableServerEndpoint(allowLoopback = true)" - " val serverPdf = truthStore.findServerPdfByName(pdfName)" - " ?: throw IllegalStateException(\"Self-test server PDF was not found\")" - " val pdfId = serverPdf.optString(\"pdf_id\", serverPdf.optString(\"id\"))" - " if (pdfId.isBlank()) throw IllegalStateException(\"Self-test server PDF has no id\")" - " runOnUiThread {" - " if (renderGeneration != loadGeneration) return@runOnUiThread" - " serverPdfId = pdfId" - " pdfName = serverPdf.optString(\"name\", pdfName)" - " pdfSha1 = pdfId" - " pageCount = serverPdf.optInt(\"page_count\", 1).coerceAtLeast(1)" - " pageIndex = 1.coerceAtMost(pageCount - 1)" - " pdfUri = Uri.parse(\"sfbserver:$pdfId\")" - " renderCurrentPage()" - " }" - " } catch (error: Exception) {" - " runOnUiThread { setStatus(\"SELFTEST_SERVER_PAGE_FAILED ${error.message}\") }" - " }" - " }" - " }" - "" - " private fun installSelfTestProposalFixture(session: SsdSession) {" - " val cells = JSONArray()" - " .put(JSONObject().put(\"id\", \"pc0001\").put(\"x\", 240).put(\"y\", 238).put(\"w\", 30).put(\"h\", 30).put(\"detector\", \"self-test-proposal\"))" - " .put(JSONObject().put(\"id\", \"pc0002\").put(\"x\", 276).put(\"y\", 238).put(\"w\", 30).put(\"h\", 30).put(\"detector\", \"self-test-proposal\"))" - " .put(JSONObject().put(\"id\", \"pc0003\").put(\"x\", 240).put(\"y\", 274).put(\"w\", 30).put(\"h\", 30).put(\"detector\", \"self-test-proposal\"))" - " .put(JSONObject().put(\"id\", \"pc0004\").put(\"x\", 276).put(\"y\", 274).put(\"w\", 30).put(\"h\", 30).put(\"detector\", \"self-test-proposal\"))" - " val group = JSONObject()" - " .put(\"id\", \"vg0001\")" - " .put(\"label\", \"lab?\")" - " .put(\"box_type_id\", \"\")" - " .put(\"status\", \"candidate\")" - " .put(\"count\", 4)" - " .put(\"bbox\", JSONArray(listOf(240, 238, 306, 304)))" - " .put(\"cell_ids\", JSONArray(listOf(\"pc0001\", \"pc0002\", \"pc0003\", \"pc0004\")))" - " .put(\"proposal\", JSONObject()" - " .put(\"label\", \"lab\")" - " .put(\"box_type_id\", \"\")" - " .put(\"confidence\", 0.98)" - " .put(\"reason\", \"self-test bundled proposal\"))" - " .put(\"vision_review\", JSONObject()" - " .put(\"accepted\", true)" - " .put(\"label\", \"lab\")" - " .put(\"box_type_id\", \"\")" - " .put(\"confidence\", 0.99)" - " .put(\"reason\", \"self-test accepted visual proposal\"))" - " val rawNoiseGroup = JSONObject()" - " .put(\"id\", \"rawbad0001\")" - " .put(\"label\", \"sensor?\")" - " .put(\"box_type_id\", \"22\")" - " .put(\"status\", \"candidate\")" - " .put(\"count\", 1)" - " .put(\"bbox\", JSONArray(listOf(390, 238, 430, 304)))" - " .put(\"cell_ids\", JSONArray())" - " .put(\"proposal\", JSONObject()" - " .put(\"label\", \"sensor\")" - " .put(\"box_type_id\", \"22\")" - " .put(\"confidence\", 0.99)" - " .put(\"reason\", \"self-test unreviewed raw proposal must be hidden\"))" - " val proposal = JSONObject()" - " .put(\"schema_version\", 1)" - " .put(\"proposal_schema\", \"ssd-proposal-v1\")" - " .put(\"source_key\", session.sourceKey)" - " .put(\"source\", JSONObject()" - " .put(\"kind\", \"pdf\")" - " .put(\"path\", session.sourceUri)" - " .put(\"name\", session.sourceName)" - " .put(\"page\", session.page)" - " .put(\"page_count\", session.pageCount)" - " .put(\"dpi\", session.dpi))" - " .put(\"image\", JSONObject().put(\"width\", session.imageWidth).put(\"height\", session.imageHeight))" - " .put(\"cells\", cells)" - " .put(\"groups\", JSONArray().put(group).put(rawNoiseGroup))" - " truthStore.saveProposalSnapshotForSelfTest(proposal)" - " }" - "" - " private fun drawSelfTestOcrPage(bitmap: Bitmap) {" - " val canvas = Canvas(bitmap)" - " val fill = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " style = Paint.Style.FILL" - " color = Color.rgb(238, 206, 96)" - " }" - " val border = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " style = Paint.Style.STROKE" - " strokeWidth = 3.5f" - " color = Color.rgb(20, 20, 20)" - " }" - " val text = Paint(Paint.ANTI_ALIAS_FLAG).apply {" - " color = Color.BLACK" - " textSize = 52f" - " typeface = Typeface.DEFAULT_BOLD" - " }" - " fun cell(x: Float, y: Float) {" - " canvas.drawRect(x, y, x + 30f, y + 30f, fill)" - " canvas.drawRect(x, y, x + 30f, y + 30f, border)" - " }" - " fun label(value: String, x: Float, y: Float) {" - " canvas.drawText(value, x, y, text)" - " }" - " label(\"LAB\", 235f, 220f)" - " cell(240f, 238f); cell(276f, 238f); cell(240f, 274f); cell(276f, 274f)" - " label(\"TRAN\", 545f, 410f)" - " cell(560f, 430f); cell(560f, 466f)" - " label(\"PH-1\", 250f, 700f)" - " cell(258f, 720f); cell(294f, 720f)" - " }" - "" - " private fun selfTestCell(id: String, x: Float, y: Float): SsdCell =" - " SsdCell(id = id, x = x, y = y, w = 28f, h = 28f, detector = \"self-test\")" - "" - " private fun selfTestGroup(id: String, label: String, boxTypeId: String, vararg cellIds: String): SsdGroup =" - " SsdGroup(id = id, label = label, boxTypeId = boxTypeId, status = \"candidate\", cellIds = cellIds.toMutableList(), notes = \"Self-test fixture\")" - "" - " private fun selfTestLabelCount(session: SsdSession): Int =" - " session.groups.count { it.label.isNotBlank() }" - "" - " private fun truthRawGroupCount(truth: JSONObject?): Int =" - " truth?.optJSONArray(\"groups\")?.length() ?: 0" - "" - " private fun truthSuppressedGroupCount(truth: JSONObject?): Int =" - " truth?.optJSONArray(\"suppressed_groups\")?.length() ?: 0" - "" - " private fun truthUsefulGroupCount(truth: JSONObject?): Int {" - " val groups = truth?.optJSONArray(\"groups\") ?: return 0" - " var count = 0" - " for (i in 0 until groups.length()) {" - " val group = groups.optJSONObject(i) ?: continue" - " val label = group.optString(\"label\").trim()" - " val status = group.optString(\"status\").trim().lowercase()" - " if (label.isBlank() || label.endsWith(\"?\")) continue" - " if (status == \"reviewed\" || status == \"approved\") count += 1" - " }" - " return count" - " }" - "" - " private fun logSelfTestState(stage: String) {" - " if (!selfTestMode) return" - " val session = currentSession ?: return" - " val labels = session.groups.joinToString(\"|\") { \"${it.id}:${it.label}:${it.status}:${it.boxTypeId}\" }" - " val ocrText = session.ocrWords.joinToString(\" \") { it.text }.take(300)" - " logClientEvent(\"ssd-selftest-state\", JSONObject()" - " .put(\"stage\", stage)" - " .put(\"groups\", session.groups.size)" - " .put(\"label_count\", selfTestLabelCount(session))" - " .put(\"question_label_count\", session.groups.count { it.label.trim().endsWith(\"?\") })" - " .put(\"reviewed_count\", session.groups.count { it.status == \"reviewed\" || it.status == \"approved\" })" - " .put(\"ocr_word_count\", session.ocrWords.size)" - " .put(\"ocr_group_count\", session.groups.count { it.notes.contains(\"OCR:\") })" - " .put(\"proposal_group_count\", session.groups.count { it.notes.startsWith(\"Proposal\") || it.notes.startsWith(\"AI proposal\") })" - " .put(\"ocr_text\", ocrText)" - " .put(\"selected\", selectedGroupId ?: \"\")" - " .put(\"select_mode\", reviewView.selectMode)" - " .put(\"append_mode\", appendMode)" - " .put(\"ssd_area_mode\", ssdAreaMode)" - " .put(\"viewport_scale\", reviewView.captureViewport()?.scale ?: 0f)" - " .put(\"viewport_offset_x\", reviewView.captureViewport()?.offsetX ?: 0f)" - " .put(\"viewport_offset_y\", reviewView.captureViewport()?.offsetY ?: 0f)" - " .put(\"viewport_rotation\", reviewView.captureViewport()?.rotationDegrees ?: 0)" - " .put(\"manual_grid_horizontal\", manualCellGridDimensions(floatArrayOf(0f, 0f, 160f, 40f), 4).let { \"${it.first}x${it.second}\" })" - " .put(\"manual_grid_vertical\", manualCellGridDimensions(floatArrayOf(0f, 0f, 40f, 160f), 4).let { \"${it.first}x${it.second}\" })" - " .put(\"manual_grid_square\", manualCellGridDimensions(floatArrayOf(0f, 0f, 80f, 80f), 4).let { \"${it.first}x${it.second}\" })" - " .put(\"bitmap_nonwhite_samples\", selfTestBitmapNonwhiteSamples())" - " .put(\"labels\", labels))" - " }" - "" - " private fun selfTestBitmapNonwhiteSamples(): Int {" - " val bitmap = currentBitmap ?: return 0" - " val stepX = max(1, bitmap.width / 40)" - " val stepY = max(1, bitmap.height / 40)" - " var count = 0" - " var y = 0" - " while (y < bitmap.height) {" - " var x = 0" - " while (x < bitmap.width) {" - " val pixel = bitmap.getPixel(x, y)" - " if (Color.red(pixel) < 245 || Color.green(pixel) < 245 || Color.blue(pixel) < 245) count += 1" - " x += stepX" - " }" - " y += stepY" - " }" - " return count" - " }" - "" - " private fun buildUi(): View {" - " val root = LinearLayout(this).apply {" - " orientation = LinearLayout.VERTICAL" - " setPadding(12, 12, 12, 12)" - " }" - " status = TextView(this).apply {" - " textSize = 14f" - " setPadding(8, 8, 8, 10)" - " }" - " root.addView(status)" - " activityStatus = TextView(this).apply {" - " textSize = 13f" - " setTextColor(Color.rgb(15, 23, 42))" - " setPadding(10, 7, 10, 7)" - " visibility = View.GONE" - " setBackgroundColor(Color.rgb(226, 232, 240))" - " }" - " root.addView(activityStatus, LinearLayout.LayoutParams(" - " LinearLayout.LayoutParams.MATCH_PARENT," - " LinearLayout.LayoutParams.WRAP_CONTENT" - " ))" - " reviewView = SsdReviewView(this).apply {" - " onStatus = { setStatus(it) }" - " onAreaSelected = { rect, ids -> handleAreaSelected(rect, ids) }" - " onGroupTapped = { selectGroup(it) }" - " }" - " root.addView(reviewView, LinearLayout.LayoutParams(" - " LinearLayout.LayoutParams.MATCH_PARENT," - " 0," - " 1f" - " ))" - " val controls = LinearLayout(this).apply {" - " orientation = LinearLayout.VERTICAL" - " addView(actionRow(" - " button(\"Open SSDs\", ButtonKind.PRIMARY) { openPdf() }," - " button(\"Server SSDs\", ButtonKind.ACCENT) { openServerPdfs() }," - " button(\"Detect\", ButtonKind.ACCENT) { detectCurrent(forceDetect = true) }," - " button(\"Set SSD Area\", ButtonKind.MODE) { startSsdAreaMode() }," - " ))" - " addView(actionRow(" - " button(\"Open Any\") { openAnyPdf() }," - " button(\"Rotate\") { rotatePage() }," - " button(\"Fit\") { reviewView.fitImage() }," - " ))" - " addView(actionRow(" - " button(\"Prev Page\") { changePage(-1) }," - " button(\"Page #\") { showPageDialog() }," - " button(\"Next Page\") { changePage(1) }," - " ))" - " addView(actionRow(" - " button(\"Pan\", ButtonKind.NEUTRAL) { setSelectMode(false) }," - " button(\"Select Area\", ButtonKind.PRIMARY) { setSelectMode(true) }," - " button(\"Add\", ButtonKind.ACCENT) { startAppendMode() }," - " ))" - " addView(actionRow(" - " button(\"Prev\", ButtonKind.NAV) { selectRelativeGroup(-1) }," - " button(\"Next\", ButtonKind.PRIMARY) { selectRelativeGroup(1) }," - " button(\"Edit\") { editSelectedGroup() }," - " ))" - " addView(actionRow(" - " button(\"Approve\", ButtonKind.ACCENT) { approveSelectedGroup() }," - " button(\"Delete\", ButtonKind.DANGER) { deleteSelectedGroup() }," - " button(\"Save\") { saveTruth() }," - " ))" - " addView(actionRow(" - " button(\"Send Display to AI\", ButtonKind.PRIMARY) { showAiIssueDialog() }," - " ))" - " addView(actionRow(" - " button(\"Pull Truth\") { pullTruthDump() }," - " button(\"Push Truth\") { pushTruthDump() }," - " button(\"Find Server\") { findServerAsync() }," - " button(\"Server URL\") { showServerConfigDialog() }," - " button(\"Sync Folder\") { syncTruthFolder() }," - " ))" - " }" - " root.addView(ScrollView(this).apply {" - " isFillViewport = false" - " addView(controls)" - " }, LinearLayout.LayoutParams(" - " LinearLayout.LayoutParams.MATCH_PARENT," - " LinearLayout.LayoutParams.WRAP_CONTENT" - " ))" - " return root" - " }" - "" - " private enum class ButtonKind(val backgroundColor: Int, val foregroundColor: Int, val textSp: Float, val heightDp: Int) {" - " PRIMARY(Color.rgb(37, 99, 235), Color.WHITE, 18f, 56)," - " ACCENT(Color.rgb(5, 150, 105), Color.WHITE, 18f, 56)," - " MODE(Color.rgb(245, 158, 11), Color.rgb(15, 23, 42), 17f, 54)," - " NAV(Color.rgb(51, 65, 85), Color.WHITE, 17f, 52)," - " NEUTRAL(Color.rgb(226, 232, 240), Color.rgb(15, 23, 42), 15f, 48)," - " DANGER(Color.rgb(185, 28, 28), Color.WHITE, 16f, 50)" - " }" - "" - " private fun dp(value: Int): Int = (value * resources.displayMetrics.density + 0.5f).toInt()" - "" - " private fun button(label: String, kind: ButtonKind = ButtonKind.NEUTRAL, action: () -> Unit): Button =" - " Button(this).apply {" - " text = label" - " textSize = kind.textSp" - " minHeight = dp(kind.heightDp)" - " setTextColor(kind.foregroundColor)" - " setAllCaps(false)" - " setPadding(dp(10), dp(6), dp(10), dp(6))" - " background = GradientDrawable().apply {" - " cornerRadius = dp(9).toFloat()" - " setColor(kind.backgroundColor)" - " }" - " setOnClickListener { action() }" - " }" - "" - " private fun actionRow(vararg buttons: Button): LinearLayout =" - " LinearLayout(this).apply {" - " orientation = LinearLayout.HORIZONTAL" - " gravity = Gravity.CENTER_VERTICAL" - " buttons.forEach { item ->" - " val params = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)" - " params.setMargins(dp(3), dp(3), dp(3), dp(3))" - " addView(item, params)" - " }" - " }" - "" - " private fun boxTypeInput(showDropdownOnFocus: Boolean = true): AutoCompleteTextView =" - " AutoCompleteTextView(this).apply {" - " inputType = InputType.TYPE_CLASS_TEXT" - " threshold = 1" - " setAdapter(ArrayAdapter(this@MainActivity, android.R.layout.simple_dropdown_item_1line, boxTypeDisplays))" - " setOnClickListener { showDropDown() }" - " setOnFocusChangeListener { _, hasFocus -> if (showDropdownOnFocus && hasFocus) post { showDropDown() } }" - " }" - "" - " private fun firingArcInput(): AutoCompleteTextView =" - " AutoCompleteTextView(this).apply {" - " hint = \"Firing arc, e.g. FA/FH/LS/RS\"" - " inputType = InputType.TYPE_CLASS_TEXT" - " threshold = 0" - " setAdapter(ArrayAdapter(this@MainActivity, android.R.layout.simple_dropdown_item_1line, firingArcDisplays))" - " setOnClickListener { showDropDown() }" - " setOnFocusChangeListener { _, hasFocus -> if (hasFocus) post { showDropDown() } }" - " }" - "" - " private fun wireBoxTypeDialogBehavior(" - " label: EditText," - " boxType: AutoCompleteTextView," - " firingArc: AutoCompleteTextView,"