test(emu): emulator-only build type with the scan fixture under the normal application ID
A server that registers only the normal scanner callback offers the E2E package no sign-in, so the fixture could not be used against it. The new `emu` build type (initWith debug, no applicationIdSuffix, versionNameSuffix -emu, label "QQQ Mobile EMULATOR TEST") reuses the E2E fixture driver source under io.qrun.mobile.scanner. It does not take the E2E manifest or network security config: cleartext stays off and only system trust is used. Because it carries the normal application ID, the fixture driver is now created only for the disposable .e2e package (unchanged behaviour) or when android.os.Build describes an SDK emulator image: HARDWARE goldfish or ranchu, PRODUCT sdk_*, MODEL sdk_* or "Android SDK built for *", and a FINGERPRINT naming that same product. Otherwise there is no driver. Debug, qa and release source sets are unchanged and contain no ingress. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
d5c3ac2d6a
commit
0419cdea91
5 files changed
+123
-5
No files matched your search
@@ -51,6 +51,15 @@ The byte-array extra `io.qrun.mobile.scanner.e2e.raw_data` wins when both are pr
|
|||||||
|
|
||||||
This validates intent parsing only; the server treats source/symbology as client-reported metadata. The E2E package signs in with its own callback (`io.qrun.mobile.scanner.e2e.oauth://callback/oauth2redirect`, see [AUTHENTICATION.md](AUTHENTICATION.md)), so a server that advertises only the normal scanner callback offers it no browser sign-in.
|
This validates intent parsing only; the server treats source/symbology as client-reported metadata. The E2E package signs in with its own callback (`io.qrun.mobile.scanner.e2e.oauth://callback/oauth2redirect`, see [AUTHENTICATION.md](AUTHENTICATION.md)), so a server that advertises only the normal scanner callback offers it no browser sign-in.
|
||||||
|
|
||||||
|
### Emulator test build under the normal application ID
|
||||||
|
|
||||||
|
For such a server, `:scanner-host:assembleEmu` builds **QQQ Mobile EMULATOR TEST**: the same fixture driver under the normal application ID `io.qrun.mobile.scanner`, so it signs in with the normal callback. Its version name ends in `-emu`. Send it the broadcast above with `-p io.qrun.mobile.scanner`.
|
||||||
|
|
||||||
|
- It is debug-signed, so it can never be installed beside or over the production-signed application. `adb uninstall io.qrun.mobile.scanner` must come first, which removes that installation's session and device enrollment; going back means uninstalling the test build and reinstalling the signed APK.
|
||||||
|
- It does not use the E2E manifest or network security config: cleartext stays off and only the system trust store is used.
|
||||||
|
- The fixture driver is created only when `android.os.Build` describes an SDK emulator image, and every signal must agree: `HARDWARE` is `goldfish` or `ranchu`, `PRODUCT` starts with `sdk_`, `MODEL` starts with `sdk_` or `Android SDK built for `, and `FINGERPRINT` names that same product. Anywhere else there is no driver and the scan control reports hardware scanning as unavailable. The disposable `.e2e` package is exempt and behaves as before.
|
||||||
|
- Never install it on a handheld. It is a local test aid, not a release artifact, and CI does not build it.
|
||||||
|
|
||||||
## Automated checks
|
## Automated checks
|
||||||
|
|
||||||
The GitHub **Scanner Android** workflow runs JVM tests, lint, APK assembly, the shared native UI suite, and the real-server scanner E2E on pushes and pull requests. The emulator job checks out exact core and companion qbit commits, verifies them in a fresh private Maven repository, and starts a fresh isolated demo database. [Fixture provenance](CI-FIXTURE.md) describes the source pins, runtime checks and distinction from published release dependencies. Sanitized instrumentation results are uploaded as workflow artifacts. Hosted execution is verified separately from a local run.
|
The GitHub **Scanner Android** workflow runs JVM tests, lint, APK assembly, the shared native UI suite, and the real-server scanner E2E on pushes and pull requests. The emulator job checks out exact core and companion qbit commits, verifies them in a fresh private Maven repository, and starts a fresh isolated demo database. [Fixture provenance](CI-FIXTURE.md) describes the source pins, runtime checks and distinction from published release dependencies. Sanitized instrumentation results are uploaded as workflow artifacts. Hosted execution is verified separately from a local run.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ aboutLibraries {
|
|||||||
fetchRemoteLicense = false
|
fetchRemoteLicense = false
|
||||||
fetchRemoteFunding = false
|
fetchRemoteFunding = false
|
||||||
includePlatform = false
|
includePlatform = false
|
||||||
filterVariants = arrayOf("debug", "release", "e2e", "qa")
|
filterVariants = arrayOf("debug", "release", "e2e", "qa", "emu")
|
||||||
// The plugin also scans compile classpaths. This SDK is not redistributed;
|
// The plugin also scans compile classpaths. This SDK is not redistributed;
|
||||||
// verifyEmdkSdk separately forbids it in every runtime classpath.
|
// verifyEmdkSdk separately forbids it in every runtime classpath.
|
||||||
exclusionPatterns = listOf(Regex("^com\\.symbol:emdk$").toPattern())
|
exclusionPatterns = listOf(Regex("^com\\.symbol:emdk$").toPattern())
|
||||||
@@ -26,7 +26,7 @@ aboutLibraries {
|
|||||||
// changing a selected dependency can reuse the previous dependency_cache.json.
|
// changing a selected dependency can reuse the previous dependency_cache.json.
|
||||||
tasks.withType<com.mikepenz.aboutlibraries.plugin.AboutLibrariesCollectorTask>().configureEach {
|
tasks.withType<com.mikepenz.aboutlibraries.plugin.AboutLibrariesCollectorTask>().configureEach {
|
||||||
inputs.property("resolvedDependencyCoordinates", providers.provider {
|
inputs.property("resolvedDependencyCoordinates", providers.provider {
|
||||||
listOf("debug", "release", "e2e", "qa").flatMap { variant ->
|
listOf("debug", "release", "e2e", "qa", "emu").flatMap { variant ->
|
||||||
listOf("CompileClasspath", "RuntimeClasspath").flatMap { suffix ->
|
listOf("CompileClasspath", "RuntimeClasspath").flatMap { suffix ->
|
||||||
val configurationName = "$variant$suffix"
|
val configurationName = "$variant$suffix"
|
||||||
configurations.getByName(configurationName).incoming.resolutionResult.allComponents.map { component ->
|
configurations.getByName(configurationName).incoming.resolutionResult.allComponents.map { component ->
|
||||||
@@ -89,11 +89,22 @@ android {
|
|||||||
applicationIdSuffix = ".qa"
|
applicationIdSuffix = ".qa"
|
||||||
matchingFallbacks += "debug"
|
matchingFallbacks += "debug"
|
||||||
}
|
}
|
||||||
|
// Emulator-only test build: the E2E scan fixture under the normal application ID, so a server that
|
||||||
|
// registers only the normal sign-in callback accepts it. Debug-signed, so it can never update or sit
|
||||||
|
// beside the production-signed application, and its fixture driver refuses to exist off an emulator.
|
||||||
|
create("emu") {
|
||||||
|
initWith(getByName("debug"))
|
||||||
|
versionNameSuffix = "-emu"
|
||||||
|
matchingFallbacks += "debug"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
testBuildType = scannerTestBuildType
|
testBuildType = scannerTestBuildType
|
||||||
sourceSets.getByName("debug").java.srcDir("src/production/java")
|
sourceSets.getByName("debug").java.srcDir("src/production/java")
|
||||||
sourceSets.getByName("release").java.srcDir("src/production/java")
|
sourceSets.getByName("release").java.srcDir("src/production/java")
|
||||||
sourceSets.getByName("qa").java.srcDir("src/production/java")
|
sourceSets.getByName("qa").java.srcDir("src/production/java")
|
||||||
|
// Only the fixture driver and its tests are shared; the E2E manifest and network security config are not.
|
||||||
|
sourceSets.getByName("emu").java.srcDir("src/e2e/java")
|
||||||
|
sourceSets.getByName("testEmu").java.srcDir("src/testE2e/java")
|
||||||
if(scannerTestBuildType == "qa") {
|
if(scannerTestBuildType == "qa") {
|
||||||
sourceSets.getByName("androidTestQa").java.srcDir("src/qaAndroidTest/java")
|
sourceSets.getByName("androidTestQa").java.srcDir("src/qaAndroidTest/java")
|
||||||
sourceSets.getByName("androidTestQa").manifest.srcFile("src/qaAndroidTest/AndroidManifest.xml")
|
sourceSets.getByName("androidTestQa").manifest.srcFile("src/qaAndroidTest/AndroidManifest.xml")
|
||||||
@@ -148,7 +159,7 @@ val verifyEmdkSdk = tasks.register("verifyEmdkSdk") {
|
|||||||
}.artifacts.artifacts
|
}.artifacts.artifacts
|
||||||
check(vendorRuntime.isEmpty()) { "Zebra EMDK must remain compile-only; found in ${runtime.name}" }
|
check(vendorRuntime.isEmpty()) { "Zebra EMDK must remain compile-only; found in ${runtime.name}" }
|
||||||
}
|
}
|
||||||
listOf("debug", "release", "e2e", "qa").forEach { variant ->
|
listOf("debug", "release", "e2e", "qa", "emu").forEach { variant ->
|
||||||
val artifacts = configurations.getByName("${variant}CompileClasspath").incoming.artifactView {
|
val artifacts = configurations.getByName("${variant}CompileClasspath").incoming.artifactView {
|
||||||
componentFilter {
|
componentFilter {
|
||||||
it is org.gradle.api.artifacts.component.ModuleComponentIdentifier && it.group == "com.symbol" && it.module == "emdk"
|
it is org.gradle.api.artifacts.component.ModuleComponentIdentifier && it.group == "com.symbol" && it.module == "emdk"
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
|
|
||||||
internal const val HARDWARE_SCAN_SOURCE = "e2e-fixture"
|
internal const val HARDWARE_SCAN_SOURCE = "e2e-fixture"
|
||||||
|
internal const val E2E_PACKAGE = "io.qrun.mobile.scanner.e2e"
|
||||||
internal const val E2E_RAW_DATA_EXTRA = "io.qrun.mobile.scanner.e2e.raw_data"
|
internal const val E2E_RAW_DATA_EXTRA = "io.qrun.mobile.scanner.e2e.raw_data"
|
||||||
internal const val E2E_RAW_DATA_BASE64_EXTRA = "io.qrun.mobile.scanner.e2e.raw_data_base64"
|
internal const val E2E_RAW_DATA_BASE64_EXTRA = "io.qrun.mobile.scanner.e2e.raw_data_base64"
|
||||||
internal const val E2E_AIM_IDENTIFIER_EXTRA = "io.qrun.mobile.scanner.e2e.aim_identifier"
|
internal const val E2E_AIM_IDENTIFIER_EXTRA = "io.qrun.mobile.scanner.e2e.aim_identifier"
|
||||||
@@ -22,9 +24,34 @@ internal fun e2eRawData(raw: ByteArray?, base64: String?): ByteArray? = raw ?: b
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Disposable E2E package only. No debug/release source set contains this ingress.
|
** An Android SDK emulator image, judged conservatively from android.os.Build: every signal must agree.
|
||||||
|
** The virtual board (goldfish/ranchu), an `sdk_` product, an SDK model name, and a fingerprint that names
|
||||||
|
** that same product. A real handheld fails the first test; one odd property never passes alone.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
internal fun createScannerDriver(context: Context): ScannerDriver = object : ScannerDriver {
|
internal fun isAndroidEmulator(hardware: String?, product: String?, model: String?, fingerprint: String?): Boolean =
|
||||||
|
(hardware == "goldfish" || hardware == "ranchu") &&
|
||||||
|
product != null && product.startsWith("sdk_") &&
|
||||||
|
model != null && (model.startsWith("sdk_") || model.startsWith("Android SDK built for ")) &&
|
||||||
|
fingerprint != null && fingerprint.contains("/$product/")
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** The disposable E2E package may take fixture scans anywhere, as before. Any other package that carries
|
||||||
|
** this source (the `emu` build type uses the normal application ID) may do so only on an emulator.
|
||||||
|
*******************************************************************************/
|
||||||
|
internal fun scanFixtureAllowed(packageName: String, hardware: String?, product: String?, model: String?, fingerprint: String?): Boolean =
|
||||||
|
packageName == E2E_PACKAGE || isAndroidEmulator(hardware, product, model, fingerprint)
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** E2E and emulator test builds only. No debug/qa/release source set contains this ingress, and off the
|
||||||
|
** disposable E2E package there is no driver at all unless the device is an Android emulator.
|
||||||
|
*******************************************************************************/
|
||||||
|
internal fun createScannerDriver(context: Context): ScannerDriver? =
|
||||||
|
if (scanFixtureAllowed(context.packageName, Build.HARDWARE, Build.PRODUCT, Build.MODEL, Build.FINGERPRINT)) fixtureScannerDriver(context) else null
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Takes one broadcast per pending read, only while the scanner widget holds it open.
|
||||||
|
*******************************************************************************/
|
||||||
|
private fun fixtureScannerDriver(context: Context): ScannerDriver = object : ScannerDriver {
|
||||||
private var listener: ScannerDriver.Listener? = null
|
private var listener: ScannerDriver.Listener? = null
|
||||||
private var registered = false
|
private var registered = false
|
||||||
private var pending = false
|
private var pending = false
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Emulator-only test build under the normal application ID. Only the label differs from the main manifest:
|
||||||
|
cleartext stays off and there is no network security config, so HTTPS with system trust is the only transport. -->
|
||||||
|
<application android:label="QQQ Mobile EMULATOR TEST" tools:replace="android:label" />
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/* Copyright (c) 2026 QRunIO. SPDX-License-Identifier: Apache-2.0 */
|
||||||
|
package io.qrun.mobile.scanner
|
||||||
|
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** The fixture driver exists off the disposable E2E package only on an Android emulator.
|
||||||
|
*******************************************************************************/
|
||||||
|
class ScanFixtureGuardTest {
|
||||||
|
private val hardware = "ranchu"
|
||||||
|
private val product = "sdk_gphone64_x86_64"
|
||||||
|
private val model = "sdk_gphone64_x86_64"
|
||||||
|
private val fingerprint = "google/sdk_gphone64_x86_64/emu64xa:15/AE3A.240806.043/12960925:userdebug/dev-keys"
|
||||||
|
private val handheld = "Zebra/TC22/TC22:13/13-00-00.00-TG-U00-STD/000:user/release-keys"
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Values as an API 35 Google APIs emulator image reports them, and an older generic image.
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
fun sdkEmulatorImagesAreRecognised() {
|
||||||
|
assertTrue(isAndroidEmulator(hardware, product, model, fingerprint))
|
||||||
|
assertTrue(isAndroidEmulator("goldfish", "sdk_x86", "Android SDK built for x86", "generic/sdk_x86/generic_x86:9/PSR1.180720.075/5124027:userdebug/test-keys"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fake handheld values: nothing about a real device may pass, whatever it is.
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
fun realHardwareIsRefused() {
|
||||||
|
assertFalse(isAndroidEmulator("qcom", "TC22", "TC22", handheld))
|
||||||
|
assertFalse(isAndroidEmulator("qcom", "MC9400", "MC9400", "Zebra/MC9400/MC9400:13/13-00-00.00-TG-U00-STD/000:user/release-keys"))
|
||||||
|
assertFalse(isAndroidEmulator(null, null, null, null))
|
||||||
|
assertFalse(isAndroidEmulator("", "", "", ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Every signal is required; one emulator-looking property on otherwise real values proves nothing.
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
fun everySignalMustAgree() {
|
||||||
|
assertFalse(isAndroidEmulator("qcom", product, model, fingerprint))
|
||||||
|
assertFalse(isAndroidEmulator("ranchu2", product, model, fingerprint))
|
||||||
|
assertFalse(isAndroidEmulator(hardware, "TC22", model, fingerprint))
|
||||||
|
assertFalse(isAndroidEmulator(hardware, "sdk", model, fingerprint))
|
||||||
|
assertFalse(isAndroidEmulator(hardware, product, "TC22", fingerprint))
|
||||||
|
assertFalse(isAndroidEmulator(hardware, product, model, handheld))
|
||||||
|
assertFalse(isAndroidEmulator(hardware, product, model, "google/sdk_gphone64_arm64/emu64a:15/AE3A.240806.043/12960925:userdebug/dev-keys"))
|
||||||
|
assertFalse(isAndroidEmulator(hardware, product, model, null))
|
||||||
|
assertFalse(isAndroidEmulator("ranchu", "TC22", "TC22", handheld))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** The disposable E2E package keeps its behaviour; the normal application ID is emulator-only.
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
fun onlyTheE2ePackageIsExemptFromTheEmulatorRule() {
|
||||||
|
assertTrue(scanFixtureAllowed("io.qrun.mobile.scanner.e2e", "qcom", "TC22", "TC22", handheld))
|
||||||
|
assertFalse(scanFixtureAllowed("io.qrun.mobile.scanner", "qcom", "TC22", "TC22", handheld))
|
||||||
|
assertFalse(scanFixtureAllowed("io.qrun.mobile.scanner.qa", "qcom", "TC22", "TC22", handheld))
|
||||||
|
assertFalse(scanFixtureAllowed("io.qrun.mobile.scanner.e2e.other", "qcom", "TC22", "TC22", handheld))
|
||||||
|
assertTrue(scanFixtureAllowed("io.qrun.mobile.scanner", hardware, product, model, fingerprint))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user