Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
README.md
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | # CKey for Android |
| 2 | |
| 3 | Kotlin/NDK SDK for [CKey](../../README.md) optical codes. It is a thin wrapper over the C++20 core's C ABI (`include/ckey/ckey.h`, `include/ckey/telemetry.h`). Encoding, decoding and rendering all happen in the core. The SDK converts arrays and turns pixels into `Bitmap`s. |
| 4 | |
| 5 | | Artifact | Contents | Dependencies | |
| 6 | |---|---|---| |
| 7 | | `org.openckey:ckey` | Headless core: `CKeySymbol`, `CKeyDecoder`, `CKeyScanner`, `CKeyPayload`, `CKey` (telemetry), the classic `CKeyView`, and `libckey_jni.so` for arm64-v8a, armeabi-v7a, x86_64 and x86 | Kotlin stdlib only | |
| 8 | | `org.openckey:ckey-compose` | `@Composable CKeyImage` | `ckey`, Compose UI/Foundation | |
| 9 | | `org.openckey:ckey-camerax` | `CKeyAnalyzer` (`ImageAnalysis.Analyzer`) | `ckey`, CameraX core | |
| 10 | |
| 11 | minSdk 24, compileSdk 34. Version = the repository's `VERSION` file (for example `0.1.0-build.85`). |
| 12 | |
| 13 | ## Install |
| 14 | |
| 15 | ```kotlin |
| 16 | // settings.gradle.kts: add the repository CKey is published to |
| 17 | dependencyResolutionManagement { repositories { google(); mavenCentral() } } |
| 18 | |
| 19 | // app/build.gradle.kts |
| 20 | dependencies { |
| 21 | implementation("org.openckey:ckey:0.1.0-build.85") |
| 22 | implementation("org.openckey:ckey-compose:0.1.0-build.85") // optional: CKeyImage |
| 23 | implementation("org.openckey:ckey-camerax:0.1.0-build.85") // optional: CKeyAnalyzer |
| 24 | } |
| 25 | ``` |
| 26 | |
| 27 | To build from source, run `gradle -p sdk/android publishAllPublicationsToLocalRepository` (Gradle 8.10.2+, JDK 17+, Android SDK with NDK 27.2 and CMake 3.22.1). It writes a Maven repository to `sdk/android/build/maven`. The native build compiles the core sources in `src/` in place; nothing is copied. |
| 28 | |
| 29 | ## Render |
| 30 | |
| 31 | ```kotlin |
| 32 | val symbol = CKeySymbol.encode("https://openckey.org/tour") // version 0 = smallest that fits, level M, STANDARD |
| 33 | val sized = CKeySymbol.encode(bytes, version = 0, level = EccLevel.H, layout = Layout.PRINT) |
| 34 | |
| 35 | val bitmap: Bitmap = symbol.toBitmap(sizePx = 600) // ARGB_8888, opaque, default style |
| 36 | val clear = symbol.toBitmap(600, RasterPolicy.HYBRID, transparent = true, style = CKeyStyle.CONCEPT) |
| 37 | val png: ByteArray = symbol.toPng(1024, dpi = 300.0) |
| 38 | val svg: String = symbol.toSvg(scale = 8) |
| 39 | val pdf: ByteArray = symbol.toPdf(widthPt = 144.0) // 2 inches |
| 40 | val rgba = symbol.toRgba(512) // straight-alpha RGBA8, no Bitmap |
| 41 | ``` |
| 42 | |
| 43 | * **Pixels.** The core renders straight (non-premultiplied) RGBA8. `toBitmap` premultiplies it in the bridge, because `Bitmap.copyPixelsFromBuffer` reads ARGB_8888 memory as premultiplied RGBA. With `transparent = true`, unpainted pixels have alpha 0. |
| 44 | * **Styles.** Start from `CKeyStyle.DEFAULT` (black on white) or `CKeyStyle.CONCEPT` and use `copy(...)`. Colours are `0xRRGGBB`. `policy` takes one of three values: |
| 45 | * `StylePolicy.STRICT` (the default) refuses a style that fails the safety rules with a `CKeyException` that carries the core's reason. |
| 46 | * `ADAPTIVE` lets the core repair the style. |
| 47 | * `OVERRIDE` draws the style as given. |
| 48 | * **Checks.** `symbol.checkStyle(style)` explains a refusal. `symbol.certify(style, transparent, backdrop, model)` runs the core's modelled-acquisition certification. A transparent symbol is never certified without a backdrop. |
| 49 | * **Logo.** The raster backend draws `CKeyStyle.logo = CKeyLogo.fromBitmap(bmp)`. SVG export uses `logoHref`. |
| 50 | |
| 51 | ### Views |
| 52 | |
| 53 | ```kotlin |
| 54 | // Compose (ckey-compose) |
| 55 | CKeyImage(data = "https://openckey.org", modifier = Modifier.size(200.dp), style = CKeyStyle.DEFAULT) |
| 56 | |
| 57 | // Classic View |
| 58 | val view = CKeyView(context).apply { data = "https://openckey.org"; onError = { Log.w("CKey", it) } } |
| 59 | ``` |
| 60 | |
| 61 | Both are square and render at their exact pixel size off the main thread. |
| 62 | |
| 63 | ## Scan |
| 64 | |
| 65 | ```kotlin |
| 66 | // One frame of 8-bit gray (any stride), or a direct ByteBuffer, or a camera Image's Y plane |
| 67 | val result: CKeyResult? = CKeyDecoder.decode(gray, width, height, stride) |
| 68 | val fromImage = CKeyDecoder.decode(image) // android.media.Image (YUV_420_888 / Y8); you close it |
| 69 | val fromBitmap = CKeyDecoder.decode(bitmap) // photos, screenshots |
| 70 | |
| 71 | result?.let { |
| 72 | it.bytes; it.text /* null if not UTF-8 text per the conventions */; it.hex |
| 73 | it.profile /* P0, P0_1, P1, P2, P3 */; it.version; it.layout; it.quad |
| 74 | it.action /* CKeyPayloadAction? — never performed automatically */ |
| 75 | } |
| 76 | ``` |
| 77 | |
| 78 | The decoder follows the iOS reader's pipeline: |
| 79 | |
| 80 | 1. It finds border candidates in both polarities. |
| 81 | 2. For each candidate it refines the border to Profile 0's field and decodes Profile 0, which identifies the version and layout from the keys, using a 64 KB output buffer. |
| 82 | 3. If that fails, it refines and decodes Profiles 1, 2 and 3, each against its own field. |
| 83 | |
| 84 | ### CameraX (ckey-camerax) |
| 85 | |
| 86 | ```kotlin |
| 87 | val analyzer = CKeyAnalyzer { result -> show(result) } // runs on the main thread |
| 88 | imageAnalysis.setAnalyzer(cameraExecutor, analyzer) // STRATEGY_KEEP_ONLY_LATEST, YUV_420_888 |
| 89 | // later: analyzer.stop() when unbinding; analyzer.scanAgain() after showing a result |
| 90 | ``` |
| 91 | |
| 92 | `CKeyAnalyzer` analyzes at most 20 frames per second by default. It is a thin layer over `CKeyScanner`, which you can drive from any camera stack: call `start()`, then `process(...)` for each frame, then `stop()`. |
| 93 | |
| 94 | ### Payload actions |
| 95 | |
| 96 | ```kotlin |
| 97 | when (val action = CKeyPayload.classify(text)) { |
| 98 | is CKeyPayloadAction.Web -> button(action.label) { open(Intent.ACTION_VIEW, action.uri) } // "Open example.com" |
| 99 | is CKeyPayloadAction.Email -> button(action.label) { open(Intent.ACTION_SENDTO, action.uri) } |
| 100 | is CKeyPayloadAction.Call -> button(action.label) { open(Intent.ACTION_DIAL, action.uri) } // never ACTION_CALL |
| 101 | is CKeyPayloadAction.Message -> button(action.label) { open(Intent.ACTION_SENDTO, action.uri) } |
| 102 | is CKeyPayloadAction.Location, is CKeyPayloadAction.Address -> button(action.label) { open(Intent.ACTION_VIEW, action.uri) } // geo: URI |
| 103 | is CKeyPayloadAction.Contact -> button(action.label) { shareVcard(action.vcard) } |
| 104 | null -> Unit // plain text: no action |
| 105 | } |
| 106 | ``` |
| 107 | |
| 108 | Only the allowlisted forms in `spec/CKey-Payload-Conventions.md` produce an action. They are `http(s)` (with a host), `mailto` (with an `@`), `tel` and `sms` (at least 3 digits), `geo` (with a valid range), `address` (non-empty) and complete vCards. The URI schemes `javascript:`, `data:`, `file:` and `intent:`, and every other scheme, stay plain text. Always keep the payload visible next to the action. |
| 109 | |
| 110 | ## Safe Actions |
| 111 | |
| 112 | A Safe Action (`ckey-action:1:…`) is a signed code from a known site for this app. `CKeySafeActions` verifies it with the core and decides whether it runs; the app only pins keys and registers handlers. |
| 113 | |
| 114 | ```kotlin |
| 115 | val actions = CKeySafeActions("com.viadere") |
| 116 | .trust("citeory.com", "2026-11", "<64 hex digits>", requestInfo = "https://citeory.com/api/ckey/requests/{request_id}") |
| 117 | .register("open.library", listOf("citeory.com")) { action -> openItem(action.paramsText) } |
| 118 | .register("signin.request", listOf("citeory.com"), CKeySafeActionScope.CROSS_DEVICE) { action -> approveSignIn(action) } |
| 119 | |
| 120 | // Off the main thread: cross-device actions fetch the issuer's request information. |
| 121 | when (val outcome = actions.handle(text)) { |
| 122 | CKeySafeActionOutcome.NotASafeAction -> showPayload(text) // the ordinary payload rules |
| 123 | is CKeySafeActionOutcome.Performed -> Unit // same-device: already ran |
| 124 | is CKeySafeActionOutcome.NeedsConfirmation -> runOnUiThread { CKeySafeActionPrompt.show(activity, outcome.request) } |
| 125 | is CKeySafeActionOutcome.Rejected -> showUnverified(outcome.reason) |
| 126 | } |
| 127 | ``` |
| 128 | |
| 129 | Same-device actions run at once. Cross-device actions run only from `CKeySafeActionPrompt`, which shows the verified issuer and who is asking; there is no other way to approve one. Anything that fails a check runs nothing; show that the code claims to be a Safe Action but could not be verified, with the reason. See [spec/CKey-Safe-Actions.md](../../spec/CKey-Safe-Actions.md). |
| 130 | |
| 131 | ## Telemetry |
| 132 | |
| 133 | The default mode is 1. Only aggregate, anonymous scan counters are sent: session counts, frame counts, decodes per profile, timing histograms, the SDK version, the platform `android` and the UTC day. Payloads, URLs, user or device IDs and location are never collected. See [docs/adoption-and-telemetry.md](../../docs/adoption-and-telemetry.md). |
| 134 | |
| 135 | To opt out, set the mode to 0 (one line): |
| 136 | |
| 137 | ```kotlin |
| 138 | CKey.telemetry = 0 // off: nothing counted, nothing sent, counters purged |
| 139 | CKey.telemetry = 2 // local only: counters via CKey.telemetryCounters(), never sent |
| 140 | CKey.telemetry = 1 // default |
| 141 | CKey.telemetry = 3 // IllegalArgumentException (never silently 1) |
| 142 | ``` |
| 143 | |
| 144 | * **Recording.** `CKeyScanner` and `CKeyAnalyzer` record sessions. `CKeyDecoder` on its own records nothing. |
| 145 | * **Sending.** Sends run on one low-priority daemon thread, never on the scan thread, using `HttpURLConnection` to `CKEY_TELEMETRY_ENDPOINT`. There is at most one batch per hour. |
| 146 | * **When sends happen.** |
| 147 | * When the core's `should_send` says a batch is due, checked after scan sessions and every 10 minutes. |
| 148 | * When the app goes to the background, if `CKey.install(context)` was called. `CKeyView` and `CKeyImage` call it for you. This uses `Application.ActivityLifecycleCallbacks`, with no androidx.lifecycle dependency. |
| 149 | * **Permissions.** The AAR declares no permissions. Batches are sent only if your app already holds `INTERNET`. |
| 150 | * **Persistence across launches.** This is off by default. Use `CKey.exportTelemetryState()` and `CKey.importTelemetryState(json)`. |
| 151 | * **Disclosure.** Disclose telemetry according to your own store and legal requirements, for example the Play Data safety form ("App activity / app interactions", not linked to the user). |
| 152 | |
| 153 | ## Scan-only and render-only apps |
| 154 | |
| 155 | * **Kotlin and Java.** Rendering, scanning and telemetry are separate classes, so R8 removes what an app does not reference. |
| 156 | * **Native code.** The published AAR contains every module (about 0.5 MB per ABI). Builds from source can compile parts out: |
| 157 | |
| 158 | ``` |
| 159 | gradle -p sdk/android :ckey:assembleRelease -Pckey.native.render=false # scanner only |
| 160 | gradle -p sdk/android :ckey:assembleRelease -Pckey.native.scan=false # renderer only |
| 161 | gradle -p sdk/android :ckey:assembleRelease -Pckey.native.telemetry=false |
| 162 | ``` |
| 163 | |
| 164 | A call into a module that was compiled out throws `UnsupportedOperationException`. The flags `CKey.hasScanner`, `CKey.hasRenderer` and `CKey.telemetryAvailable` report what is present. Without the telemetry module, `CKey.telemetry` reads 0 and nothing is ever counted or sent. The compose and camerax modules are optional. |
| 165 | |
| 166 | ## Tests |
| 167 | |
| 168 | | What | Command | |
| 169 | |---|---| |
| 170 | | Unit tests (pure JVM: payload rules, telemetry validation, sender loop) | `gradle -p sdk/android :ckey:testDebugUnitTest` | |
| 171 | | Instrumented round trips on a device or emulator | `gradle -p sdk/android :ckey:connectedDebugAndroidTest` | |
| 172 | | Host JNI harness, no Android SDK needed | `sdk/android/jni-host-test/run.sh` | |
| 173 | |
| 174 | The host JNI harness does the following: |
| 175 | |
| 176 | * Checks the Kotlin `external` signatures against `javac -h` output. |
| 177 | * Builds `libckey_jni.so` for the desktop. |
| 178 | * Runs a Java harness: encode, render RGBA, convert to gray, decode; styles; exports; telemetry. |
| 179 | * If a Kotlin compiler is available (for example the one bundled with Gradle), compiles the Kotlin sources against small `android.*` stubs and runs the Kotlin API checks and the unit tests. |
| 180 | |
| 181 | CI: `.github/workflows/sdk-android.yml`. |