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

Size
11 KB
Lines
181
Language
markdown
SHA-256
e36a8457495e…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1# CKey for Android
2
3Kotlin/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
11minSdk 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
17dependencyResolutionManagement { repositories { google(); mavenCentral() } }
18
19// app/build.gradle.kts
20dependencies {
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
27To 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
32val symbol = CKeySymbol.encode("https://openckey.org/tour") // version 0 = smallest that fits, level M, STANDARD
33val sized = CKeySymbol.encode(bytes, version = 0, level = EccLevel.H, layout = Layout.PRINT)
34
35val bitmap: Bitmap = symbol.toBitmap(sizePx = 600) // ARGB_8888, opaque, default style
36val clear = symbol.toBitmap(600, RasterPolicy.HYBRID, transparent = true, style = CKeyStyle.CONCEPT)
37val png: ByteArray = symbol.toPng(1024, dpi = 300.0)
38val svg: String = symbol.toSvg(scale = 8)
39val pdf: ByteArray = symbol.toPdf(widthPt = 144.0) // 2 inches
40val 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)
55CKeyImage(data = "https://openckey.org", modifier = Modifier.size(200.dp), style = CKeyStyle.DEFAULT)
56
57// Classic View
58val view = CKeyView(context).apply { data = "https://openckey.org"; onError = { Log.w("CKey", it) } }
59```
60
61Both 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
67val result: CKeyResult? = CKeyDecoder.decode(gray, width, height, stride)
68val fromImage = CKeyDecoder.decode(image) // android.media.Image (YUV_420_888 / Y8); you close it
69val fromBitmap = CKeyDecoder.decode(bitmap) // photos, screenshots
70
71result?.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
78The decoder follows the iOS reader's pipeline:
79
801. It finds border candidates in both polarities.
812. 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.
823. 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
87val analyzer = CKeyAnalyzer { result -> show(result) } // runs on the main thread
88imageAnalysis.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
97when (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
108Only 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
112A 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
115val 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.
121when (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
129Same-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
133The 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
135To opt out, set the mode to 0 (one line):
136
137```kotlin
138CKey.telemetry = 0 // off: nothing counted, nothing sent, counters purged
139CKey.telemetry = 2 // local only: counters via CKey.telemetryCounters(), never sent
140CKey.telemetry = 1 // default
141CKey.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
174The 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
181CI: `.github/workflows/sdk-android.yml`.