Componentes y ejemplos
Esta sección documenta cada componente con su contrato y un ejemplo de uso tomado de la aplicación de demostración. Los ejemplos envuelven las llamadas AIDL en funciones suspend para simplificar el manejo de los callbacks.
Impresora (IPrinter)
La impresión es asíncrona y se basa en plantillas. No se imprime de forma programática elemento por elemento: se envía una plantilla en formato JSON (la estructura del documento) y un objeto de datos (los valores). El resultado llega por un listener.
Contrato
| Método | Descripción |
|---|---|
| printWithTemplate(request) | Inicia la impresión (oneway). El resultado llega por onPrintResult. |
| generatePreview(request) | Genera una vista previa sin imprimir |
| registerListener(l) / unregisterListener(l) | Registra o quita el listener de resultados. |
| getPaperUsageMm() | Consumo de papel estimado acumulado, en milímetros. |
| resetPaperCounter() | Reinicia el contador de papel (al cambiar el rollo). |
Código de éxito. En la respuesta de impresión, resultCode == -1 indica ÉXITO. Cualquier otro valor es error o estado intermedio.
Estructura de la plantilla
La plantilla es un arreglo de "items". Cada item tiene una lista unit con elementos. Las variables se escriben como <nombre> en el campo data y se rellenan con el objeto de datos. Tipos de unidad soportados:
| Tipo | Uso |
|---|---|
| text | Texto. Acepta align, size, style. |
| row | Fila de columnas (columns), cada una con su weight y alineación. |
| line | Línea separadora horizontal. |
| feed | Avance de papel (líneas en blanco). |
| image | Imagen. El data es base64 (PNG/JPEG) o un nombre de archivo en el anfitrión. |
| qrcode | Código QR. Acepta width y height. |
| barcode | Código de barras. El format define el tipo (por ejemplo PDF_417, CODE_128). |
Ejemplo: Imprimir una boleta con logo, PDF417 y QR
Plantilla (resumida) con un logo, una fila de total y un PDF417:
[
{ "unit": [ { "type": "image", "data": "<logo>",
"align": "center", "width": 160, "height": 160 } ] },
{ "unit": [ { "type": "text", "data": "<comercio>",
"align": "center", "size": "FONT_BIG",
"style": "TEXT_STYLE_BOLD" } ] },
{ "unit": [ { "type": "line", "data": "" } ] },
{ "unit": [ { "type": "row", "columns": [
{ "type": "text", "data": "TOTAL", "align": "left" },
{ "type": "text", "data": "<total>", "align": "right" } ] } ] },
{ "unit": [ { "type": "barcode", "data": "<pdf417_data>",
"format": "PDF_417", "align": "center",
"width": 360, "height": 120 } ] },
{ "unit": [ { "type": "qrcode", "data": "<qr_data>",
"align": "center", "width": 240, "height": 240 } ] }
]
Datos que rellenan las variables:
{
"logo": "<PNG en base64>",
"comercio": "TIENDA DEMO BCI",
"total": "$7.400",
"pdf417_data": "BCI-DEMO|FOLIO:000123456|TOTAL:7400",
"qr_data": "https://bci.cl/boleta/000123456"
}
Invocación con listener, envuelta en una función suspend:
suspend fun imprimir(template: String, data: String, key: String): Boolean =
suspendCancellableCoroutine { cont ->
val p = IPrinter.Stub.asInterface(
bridge.getRawBinder(HalComponent.PRINTER))
val listener = object : IPrinterListener.Stub() {
override fun onStatusChanged(status: Int) {}
override fun onPrintResult(resp: PrintWithTemplateResponse?) {
val ok = (resp?.resultCode == -1) // -1 = éxito
if (cont.isActive) cont.resume(ok)
}
}
p.registerListener(listener)
val req = PrintWithTemplateRequest().apply {
this.template = template; this.data = data
this.msgVersion = ""; this.key = key
}
p.printWithTemplate(req) // oneway: retorna de inmediato
}
Imágenes desde una app integradora. El motor de impresión decodifica el campo data de una imagen como base64 si empieza con los marcadores de PNG o JPEG. Si en cambio se entrega un nombre de archivo, el motor lo busca en el almacenamiento de la aplicación anfitriona, no en la del integrador. Por eso, desde una app de terceros conviene siempre enviar la imagen como base64. (En la demo, el pato.bmp se carga desde assets, se recodifica a PNG y se envía en base64.)
Cámara / Escáner (ICamera)
Permite escanear códigos (QR, PDF417, códigos de barras 1D y 2D) usando la cámara del terminal. En equipos PAX se utiliza el escáner nativo del SDK, que abre su propia pantalla de captura con mira y entrega el resultado por el listener.
Dónde ocurre la captura (modelo A1). El escáner por cámara necesita una ventana en primer plano del proceso anfitrión para dibujar su preview. Por eso, en el modelo A1, la captura se realiza en una pantalla de escaneo de la propia aplicación anfitriona (BCI Pagos): cuando el integrador invoca scanCodes, el anfitrión muestra su pantalla de escaneo, captura el código y devuelve el resultado por IPC. El integrador no necesita aportar ninguna superficie ni Activity para la cámara. Requisito: la app anfitriona debe estar en ejecución; si no hay una pantalla viva del anfitrión, la operación responde error sin afectar otros flujos.
Contrato (métodos principales)
| Método | Descripción |
|---|---|
| open(key, cameraType) | Abre la cámara (0 trasera, 1 frontal). |
| scanCodes(key, n, formats, timeout, allowDuplicates, listener) | Escanea hasta n códigos. formats vacío = todos. |
| takePicture(key, listener) | Captura una foto; la ruta llega por onPictureTaken. |
| setPreviewSurface(key, surface) | Fija una superficie de vista previa (escaneo por software). |
| setFlashOn(key, isOn) | Enciende/apaga la linterna. |
| stop(key) / close(key) | Detiene el escaneo / libera la cámara. |
Resultado del escaneo (ICameraListener)
- onScanSuccess(result): entrega el resultado con la lista de códigos leídos (cada uno con formato y texto). El formato indica el tipo de lectura (por ejemplo EAN13, QRCODE, PDF417).
- onTimeout(partial): venció el tiempo; puede traer resultados parciales.
- onError(code, msg): error durante el escaneo.
Ejemplo: escanear un código con el escáner nativo PAX
suspend fun escanearCamara(key: String, timeoutMs: Long): List<String> =
suspendCancellableCoroutine { cont ->
val cam = ICamera.Stub.asInterface(
bridge.getRawBinder(HalComponent.CAMERA))
val codigos = mutableListOf<String>()
val listener = object : ICameraListener.Stub() {
override fun onScanSuccess(r: CameraScannerResult?) {
r?.barcodes?.forEach { codigos.add("${it.format}: ${it.text}") }
try { cam.unregisterListener(this) } catch (_: Exception) {}
cont.resume(codigos)
}
override fun onTimeout(r: CameraScannerResult?) {
try { cam.unregisterListener(this) } catch (_: Exception) {}
cont.resume(codigos)
}
override fun onError(c: Int, m: String?) {
try { cam.unregisterListener(this) } catch (_: Exception) {}
cont.resume(emptyList())
}
override fun onPictureTaken(path: String?) {}
override fun onStatusUpdate(s: Int) {}
override fun onScanAreaUpdate(l:Int,t:Int,r:Int,b:Int,w:Int,h:Int) {}
}
cam.registerListener(listener)
// En PAX, sin surface: usa el escáner nativo (su propia pantalla).
cam.scanCodes(key, 1, emptyList(), timeoutMs, false, listener)
}
Libere el listener al terminar. El servicio limita a un cliente por componente. Dé de baja el listener (unregisterListener) al recibir el resultado, no solo al cancelar; de lo contrario, un segundo escaneo puede ser rechazado por límite de cliente.
Escáner nativo vs. software. En terminales PAX se usa el escáner nativo del SDK: lee más rápido y con mejor calidad, y muestra su propia ventana de escaneo (en A1, dentro de la pantalla del anfitrión). La decodificación por software con setPreviewSurface queda como alternativa para equipos sin escáner nativo, aportando el integrador su propia superficie de vista previa.
Lector de hardware dedicado (IScanner)
Algunos terminales incluyen un lector de hardware dedicado (infrarrojo/láser, típicamente accionado por un gatillo). Este componente lo expone de forma independiente de la cámara.
| Método | Descripción |
|---|---|
| config(key, scannerConfig) | Configura tiempo de espera y modo (lectura única o ráfaga). |
| scan(key, listener) | Inicia una lectura; el resultado llega por el listener. |
| setFlashOn(key, isOn) | Control de iluminación, si el hardware lo soporta. |
| getStatus() | Estado del lector. |
| registerListener / unregisterListener | Gestión del listener. |
Ejemplo: lectura con el lector dedicado
suspend fun escanearHw(key: String, timeoutMs: Int): List<String> =
suspendCancellableCoroutine { cont ->
val sc = IScanner.Stub.asInterface(
bridge.getRawBinder(HalComponent.SCANNER))
val codigos = mutableListOf<String>()
val listener = object : IScannerListener.Stub() {
override fun onSuccess(result: String?) {
if (!result.isNullOrEmpty()) codigos.add(result)
try { sc.unregisterListener(this) } catch (_: Exception) {}
cont.resume(codigos)
}
override fun onTimeout() {
try { sc.unregisterListener(this) } catch (_: Exception) {}
cont.resume(codigos)
}
override fun onError(c: Int, m: String?) {
try { sc.unregisterListener(this) } catch (_: Exception) {}
cont.resume(emptyList())
}
}
val cfg = ScannerConfig().apply {
continuousTimes = 1; timeout = timeoutMs
}
sc.config(key, cfg)
sc.registerListener(listener)
sc.scan(key, listener)
}
Disponibilidad del lector dedicado. No todos los terminales tienen lector de gatillo. En equipos que solo disponen de cámara, este componente puede no leer; en ese caso, use el componente de cámara para escanear.
Banda magnética (IMagneticStripe)
Lee los tracks de una tarjeta de banda magnética. El modelo correcto de uso es por eventos: se abre el lector, se registra un listener, y al deslizar la tarjeta llega el callback con los datos. El lector queda esperando el deslizamiento; no es una lectura instantánea.
| Método | Descripción |
|---|---|
| open(key, securityLevel) | Abre el lector e inicia la espera del deslizamiento. securityLevel controla el enmascaramiento de los datos sensibles (ver nota al final de la sección); use 1 salvo que un flujo autorizado requiera el dato completo. |
| registerListener(l) | Registra el listener que recibirá onSwiped. |
| read(key) | Lectura puntual del estado actual (no espera; uso avanzado). |
| close(key) | Cierra el lector. |
| executeOperation(key, opCode, params) | Procesa los tracks (por ejemplo, extraer datos). |
Ejemplo: lectura por evento
suspend fun leerBanda(key: String): MagStripeTrackData? =
suspendCancellableCoroutine { cont ->
val m = IMagneticStripe.Stub.asInterface(
bridge.getRawBinder(HalComponent.MAGSTRIPE))
val listener = object : IMagStripeListener.Stub() {
override fun onSwiped(data: MagStripeTrackData?) {
cont.resume(data)
m.close(key)
}
override fun onError(c: Int, msg: String?) { cont.resume(null) }
override fun onOperationResult(op: Int, ok: Boolean, json: String?) {}
}
m.registerListener(listener)
m.open(key, 1) // abre con nivel de seguridad 1 (enmascarado) y queda esperando el deslizamiento
}
No use read() puntual para esperar la tarjeta. El método read() consulta el estado en ese instante y devuelve vacío si no hay tarjeta justo en ese momento: no espera. Para capturar un deslizamiento, use open() + listener onSwiped como en el ejemplo.
Datos sensibles y nivel de seguridad. El lector entrega los tracks de la tarjeta. Para controlar cuánta información sensible se expone, open recibe un parámetro securityLevel que determina cómo viajan los datos hacia la aplicación:
| Nivel | Comportamiento |
|---|---|
| 1 | Datos sensibles enmascarados. Dentro de cada track, las secuencias largas de dígitos se enmascaran conservando solo los primeros 6 y los últimos 4. Es el nivel más restrictivo y el que aplica si se omite el parámetro o se envía un valor fuera de rango. |
| 2 | Nivel intermedio: los datos sensibles embebidos en los tracks viajan enmascarados. |
| 3 | Sin enmascaramiento: los tracks viajan completos. Úselo solo en flujos que lo requieran explícitamente y estén autorizados para ello. |
Independiente del nivel, el tratamiento, almacenamiento y transmisión de estos datos es responsabilidad de la aplicación integradora y está sujeto a las normas de seguridad de la industria de medios de pago. Minimice su exposición, nunca los registre en logs ni los transmita en claro, y solicite el nivel 3 únicamente cuando sea estrictamente necesario.
Información del equipo (IDeviceInfo)
Entrega datos identificatorios del terminal. Es de solo lectura y síncrono.
| Método | Descripción |
|---|---|
| getModel(key) | Modelo del terminal. |
| getSerialNumber(key) | Número de serie. |
| getFirmwareVersion(key) | Versión de firmware / part number. |
| getStatus(key) | Estado del componente. |
val d = IDeviceInfo.Stub.asInterface(
bridge.getRawBinder(HalComponent.DEVICEINFO))
val modelo = d.getModel(key)
val serie = d.getSerialNumber(key)
val firmware = d.getFirmwareVersion(key)
Señal sonora (IBeep)
Reproduce señales sonoras de retroalimentación. Útil para confirmar éxito o error de una operación.
val beep = IBeep.Stub.asInterface(
bridge.getRawBinder(HalComponent.BEEP))
beep.beepSuccess(key) // tono de éxito
beep.beepError(key) // tono de error
NFC (INfc)
Permite detectar y leer etiquetas/tarjetas sin contacto (UID, tecnología, e intercambio de comandos APDU). La detección y lectura están operativas.
Alcance del componente NFC. En la versión actual, las operaciones de ESCRITURA de etiquetas NFC (escribir NDEF, formatear, bloquear, gestionar contraseñas) no están implementadas y devuelven un resultado negativo. La detección y la lectura sí funcionan.
Diagnóstico (IDiagnostic)
Expone métricas de uso de cada componente (número de llamadas, fallos, tiempos promedio). Pensado para herramientas administrativas y de monitoreo.
| Método | Descripción |
|---|---|
| getAllMetrics(key) | Métricas de todos los componentes. |
| getComponentMetrics(key, name) | Métricas de un componente puntual. |
| resetMetrics(key) | Reinicia los contadores. |
| performSelfTest(key) | Autodiagnóstico (en la versión actual es un marcador de posición). |
