From 37cd2ae5f1cbdd58766c405369e66b3bedcf440f Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 18:15:14 +0300 Subject: [PATCH 1/8] Add com.codename1.nfc: NDEF read/write, ISO-DEP/MIFARE/FeliCa, HCE Promotes NFC to a first-class device API alongside Location, Capture and the recently-added Biometrics. A single Nfc entry point exposes the platform NFC controller and the typed errors / async returns mirror the com.codename1.security design. Public surface (com.codename1.nfc): - Nfc, NfcException, NfcError (NOT_AVAILABLE / DISABLED / TAG_LOST / READ_ONLY / CAPACITY_EXCEEDED / UNKNOWN_AID / ...). - NdefMessage + NdefRecord with typed factories (createUri, createText, createMime, createExternal, createApplicationRecord) and a parser tolerant of the SR / IL flags. - Tag, TagType, TagTechnology + per-technology subclasses: IsoDep, MifareClassic, MifareUltralight, NfcA / NfcB / NfcF (FeliCa) / NfcV. - NfcReadOptions (alert copy, tech filter, FeliCa system codes, auto-SELECT AIDs, timeout) and NfcListener for multi-tag sessions. - HostCardEmulationService + ApduResponse for HCE. Android port wraps NfcAdapter.enableReaderMode + android.nfc.tech.*; HostApduService is bridged by CodenameOneHostApduService. iOS port wraps NFCNDEFReaderSession + NFCTagReaderSession (NFCISO7816Tag, NFCMiFareTag, NFCFeliCaTag) and weak-links CoreNFC.framework. JavaSE simulator gets a Simulate -> NFC submenu (virtual tag with editable NDEF, configurable read outcome, tag-tap, HCE APDU dialog). The iOS native block is gated on a new CN1_INCLUDE_NFC define that IPhoneBuilder uncomments only when the classpath scanner saw a com.codename1.nfc reference. Apps that never use NFC ship without any CoreNFC symbols and pass Apple's API-usage scan without a CoreNFC privacy manifest. Build pipeline (maven/codenameone-maven-plugin): - Android: auto-inject android.permission.NFC + uses-feature when the scanner sees com.codename1.nfc.*; add BIND_NFC_SERVICE + nfc.hce feature + the CodenameOneHostApduService manifest entry + a generated res/xml/apduservice.xml when HostCardEmulationService is referenced or the android.hceAids hint is set. - iOS: link CoreNFC.framework + default NFCReaderUsageDescription + readersession.formats entitlement; for HCE, inject com.apple.developer.nfc.hce + select-identifiers entitlements derived from ios.hceAids / android.hceAids. The matching backend changes ship in BuildDaemon PR #80. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../impl/CodenameOneImplementation.java | 11 + .../src/com/codename1/nfc/ApduResponse.java | 105 ++++ .../nfc/HostCardEmulationService.java | 111 ++++ CodenameOne/src/com/codename1/nfc/IsoDep.java | 99 +++ .../src/com/codename1/nfc/MifareClassic.java | 116 ++++ .../com/codename1/nfc/MifareUltralight.java | 63 ++ .../src/com/codename1/nfc/NdefMessage.java | 220 +++++++ .../src/com/codename1/nfc/NdefRecord.java | 334 ++++++++++ CodenameOne/src/com/codename1/nfc/Nfc.java | 276 +++++++++ CodenameOne/src/com/codename1/nfc/NfcA.java | 46 ++ CodenameOne/src/com/codename1/nfc/NfcB.java | 43 ++ .../src/com/codename1/nfc/NfcError.java | 85 +++ .../src/com/codename1/nfc/NfcException.java | 51 ++ CodenameOne/src/com/codename1/nfc/NfcF.java | 56 ++ .../src/com/codename1/nfc/NfcListener.java | 47 ++ .../src/com/codename1/nfc/NfcReadOptions.java | 182 ++++++ CodenameOne/src/com/codename1/nfc/NfcV.java | 43 ++ CodenameOne/src/com/codename1/nfc/Tag.java | 181 ++++++ .../src/com/codename1/nfc/TagTechnology.java | 51 ++ .../src/com/codename1/nfc/TagType.java | 77 +++ CodenameOne/src/com/codename1/ui/Display.java | 8 + .../impl/android/AndroidImplementation.java | 9 + .../codename1/impl/android/AndroidIsoDep.java | 62 ++ .../impl/android/AndroidMifareClassic.java | 103 ++++ .../impl/android/AndroidMifareUltralight.java | 71 +++ .../codename1/impl/android/AndroidNfc.java | 579 ++++++++++++++++++ .../codename1/impl/android/AndroidNfcA.java | 57 ++ .../codename1/impl/android/AndroidNfcB.java | 58 ++ .../codename1/impl/android/AndroidNfcF.java | 58 ++ .../codename1/impl/android/AndroidNfcV.java | 56 ++ .../android/CodenameOneHostApduService.java | 74 +++ .../com/codename1/impl/javase/JavaSENfc.java | 359 +++++++++++ .../com/codename1/impl/javase/JavaSEPort.java | 287 +++++++++ .../CodenameOne_GLViewController.h | 7 + Ports/iOSPort/nativeSources/IOSNative.m | 468 ++++++++++++++ .../codename1/impl/ios/IOSImplementation.java | 9 + .../src/com/codename1/impl/ios/IOSNative.java | 57 ++ .../src/com/codename1/impl/ios/IOSNfc.java | 545 +++++++++++++++++ .../Near-Field-Communication.asciidoc | 206 +++++++ docs/developer-guide/developer-guide.asciidoc | 2 + .../builders/AndroidGradleBuilder.java | 95 +++ .../com/codename1/builders/IPhoneBuilder.java | 86 +++ .../tools/GenerateCN1AccessRegistry.java | 26 +- 43 files changed, 5478 insertions(+), 1 deletion(-) create mode 100644 CodenameOne/src/com/codename1/nfc/ApduResponse.java create mode 100644 CodenameOne/src/com/codename1/nfc/HostCardEmulationService.java create mode 100644 CodenameOne/src/com/codename1/nfc/IsoDep.java create mode 100644 CodenameOne/src/com/codename1/nfc/MifareClassic.java create mode 100644 CodenameOne/src/com/codename1/nfc/MifareUltralight.java create mode 100644 CodenameOne/src/com/codename1/nfc/NdefMessage.java create mode 100644 CodenameOne/src/com/codename1/nfc/NdefRecord.java create mode 100644 CodenameOne/src/com/codename1/nfc/Nfc.java create mode 100644 CodenameOne/src/com/codename1/nfc/NfcA.java create mode 100644 CodenameOne/src/com/codename1/nfc/NfcB.java create mode 100644 CodenameOne/src/com/codename1/nfc/NfcError.java create mode 100644 CodenameOne/src/com/codename1/nfc/NfcException.java create mode 100644 CodenameOne/src/com/codename1/nfc/NfcF.java create mode 100644 CodenameOne/src/com/codename1/nfc/NfcListener.java create mode 100644 CodenameOne/src/com/codename1/nfc/NfcReadOptions.java create mode 100644 CodenameOne/src/com/codename1/nfc/NfcV.java create mode 100644 CodenameOne/src/com/codename1/nfc/Tag.java create mode 100644 CodenameOne/src/com/codename1/nfc/TagTechnology.java create mode 100644 CodenameOne/src/com/codename1/nfc/TagType.java create mode 100644 Ports/Android/src/com/codename1/impl/android/AndroidIsoDep.java create mode 100644 Ports/Android/src/com/codename1/impl/android/AndroidMifareClassic.java create mode 100644 Ports/Android/src/com/codename1/impl/android/AndroidMifareUltralight.java create mode 100644 Ports/Android/src/com/codename1/impl/android/AndroidNfc.java create mode 100644 Ports/Android/src/com/codename1/impl/android/AndroidNfcA.java create mode 100644 Ports/Android/src/com/codename1/impl/android/AndroidNfcB.java create mode 100644 Ports/Android/src/com/codename1/impl/android/AndroidNfcF.java create mode 100644 Ports/Android/src/com/codename1/impl/android/AndroidNfcV.java create mode 100644 Ports/Android/src/com/codename1/impl/android/CodenameOneHostApduService.java create mode 100644 Ports/JavaSE/src/com/codename1/impl/javase/JavaSENfc.java create mode 100644 Ports/iOSPort/src/com/codename1/impl/ios/IOSNfc.java create mode 100644 docs/developer-guide/Near-Field-Communication.asciidoc diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index 47d5c9acc3..9b33899a16 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -6570,6 +6570,17 @@ public SecureStorage getSecureStorage() { return null; } + /// Returns the port-specific NFC entry point. Default implementation + /// returns {@code null}; ports that implement + /// {@link com.codename1.nfc.Nfc} override this to return a cached + /// singleton. Application code should use + /// {@link com.codename1.nfc.Nfc#getInstance()} instead of calling this + /// directly --- it transparently substitutes a no-op fallback when the + /// port returns {@code null}. + public com.codename1.nfc.Nfc getNfc() { + return null; + } + /// Allows buggy implementations (Android) to release image objects /// /// #### Parameters diff --git a/CodenameOne/src/com/codename1/nfc/ApduResponse.java b/CodenameOne/src/com/codename1/nfc/ApduResponse.java new file mode 100644 index 0000000000..d0e598ba6a --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/ApduResponse.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +/// Helpers for working with the ISO 7816 status word (SW1/SW2) trailer at +/// the end of every APDU response. Pair with [IsoDep] (reader mode) and +/// [HostCardEmulationService] (card mode). +public final class ApduResponse { + + /// SW = `90 00` -- command succeeded. + public static final byte[] SW_SUCCESS = new byte[] { + (byte) 0x90, (byte) 0x00 }; + /// SW = `6A 82` -- file or AID not found. Returned from an HCE + /// service's `SELECT` when the requested AID is not the one it + /// registered. + public static final byte[] SW_FILE_NOT_FOUND = new byte[] { + (byte) 0x6A, (byte) 0x82 }; + /// SW = `6D 00` -- INS not supported. Returned from an HCE service for + /// any APDU whose instruction byte is not handled. + public static final byte[] SW_INS_NOT_SUPPORTED = new byte[] { + (byte) 0x6D, (byte) 0x00 }; + /// SW = `6E 00` -- CLA not supported. + public static final byte[] SW_CLA_NOT_SUPPORTED = new byte[] { + (byte) 0x6E, (byte) 0x00 }; + /// SW = `67 00` -- wrong length / Lc. + public static final byte[] SW_WRONG_LENGTH = new byte[] { + (byte) 0x67, (byte) 0x00 }; + /// SW = `69 82` -- security condition not satisfied. + public static final byte[] SW_SECURITY_NOT_SATISFIED = new byte[] { + (byte) 0x69, (byte) 0x82 }; + /// SW = `6F 00` -- unknown / generic failure. + public static final byte[] SW_UNKNOWN_ERROR = new byte[] { + (byte) 0x6F, (byte) 0x00 }; + + private ApduResponse() { + } + + /// `true` when the trailing two bytes of `apdu` are `90 00`. + public static boolean isSuccess(byte[] apdu) { + return IsoDep.isSuccess(apdu); + } + + /// Slice helper -- returns the payload preceding the 2-byte SW + /// trailer, or an empty array when the response is exactly 2 bytes. + public static byte[] body(byte[] apdu) { + if (apdu == null || apdu.length < 2) { + return new byte[0]; + } + byte[] out = new byte[apdu.length - 2]; + System.arraycopy(apdu, 0, out, 0, out.length); + return out; + } + + /// 16-bit status word from the last two bytes of `apdu`. Returns `0` + /// for inputs shorter than 2 bytes. + public static int statusWord(byte[] apdu) { + if (apdu == null || apdu.length < 2) { + return 0; + } + int hi = apdu[apdu.length - 2] & 0xFF; + int lo = apdu[apdu.length - 1] & 0xFF; + return (hi << 8) | lo; + } + + /// Returns a 2-byte status-word array for the given SW1/SW2 pair. + public static byte[] sw(int sw1, int sw2) { + return new byte[] { (byte) (sw1 & 0xFF), (byte) (sw2 & 0xFF) }; + } + + /// Appends `sw` to the end of `body` and returns the combined APDU + /// response. Helper for [HostCardEmulationService] implementations. + public static byte[] withStatus(byte[] body, byte[] sw) { + if (body == null) { + body = new byte[0]; + } + if (sw == null || sw.length != 2) { + throw new IllegalArgumentException("sw must be 2 bytes"); + } + byte[] out = new byte[body.length + 2]; + System.arraycopy(body, 0, out, 0, body.length); + out[body.length] = sw[0]; + out[body.length + 1] = sw[1]; + return out; + } +} diff --git a/CodenameOne/src/com/codename1/nfc/HostCardEmulationService.java b/CodenameOne/src/com/codename1/nfc/HostCardEmulationService.java new file mode 100644 index 0000000000..0c26435429 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/HostCardEmulationService.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +/// Application-supplied handler for Host Card Emulation (HCE) -- the mode +/// where the device acts as a contactless smart card and answers APDUs +/// from a nearby reader/terminal. +/// +/// Subclass this and register the instance via +/// [Nfc#registerHostCardEmulationService(HostCardEmulationService)] before +/// the OS routes a terminal's APDU to the app. +/// +/// #### Lifecycle +/// +/// 1. Reader / POS terminal sends an ISO 7816 `SELECT` APDU naming an AID +/// that matches [#getAids()]. +/// 2. OS routes that APDU and every subsequent APDU in the same field +/// session to [#processCommand(byte[])]. +/// 3. The implementation returns the response (data + 2-byte status word). +/// Use [ApduResponse] helpers to construct typical responses. +/// 4. [#onDeactivated(int)] fires when the reader leaves the field or +/// routes a SELECT for a different AID. +/// +/// #### Platform support +/// +/// - **Android** -- backed by `android.nfc.cardemulation.HostApduService`. +/// The Codename One Maven plugin and BuildDaemon auto-generate the +/// `AndroidManifest.xml` service entry and the `apduservice.xml` +/// resource from [#getAids()] / [#getServiceDescription()] / +/// [#getCategory()] when an app references this class. +/// - **iOS** -- backed by Core NFC's `CardSession` (iOS 17.4+, EU only as +/// of 2026-05-21) and requires the `com.apple.developer.nfc.hce` / +/// `com.apple.developer.nfc.hce.iso7816.select-identifiers` +/// entitlements. The IPhoneBuilder injects both entitlements when the +/// app references this class. +/// - **JavaSE simulator** -- the Simulate -> NFC menu fires synthetic +/// APDUs at the registered service so the implementation can be +/// exercised without a terminal. +/// - **All other platforms** -- the OS never invokes the service. +public abstract class HostCardEmulationService { + + /// Categories accepted by Android's `HostApduService` -- "payment" is + /// reserved for EMV-conformant payment apps; everything else uses + /// "other". + public static final String CATEGORY_OTHER = "other"; + public static final String CATEGORY_PAYMENT = "payment"; + + /// The application identifiers (AIDs) this service is willing to + /// answer. Each AID is 5-16 bytes long. Must be non-empty -- a service + /// that returns an empty array is never invoked. + /// + /// The platform routes terminal APDUs to the longest matching AID, so + /// list specific AIDs before catch-alls. + public abstract String[] getAids(); + + /// HCE category -- one of [#CATEGORY_OTHER] or [#CATEGORY_PAYMENT]. + /// Defaults to [#CATEGORY_OTHER]. + public String getCategory() { + return CATEGORY_OTHER; + } + + /// Human-readable description registered with Android (shown to the + /// user when they pick a default HCE app in system settings). Default + /// is the service class' simple name. + public String getServiceDescription() { + return getClass().getName(); + } + + /// Handles a single APDU command and returns the response. Must return + /// in less than ~500 ms; longer responses are dropped by the + /// controller. The return value must end with a 2-byte ISO 7816 status + /// word; see [ApduResponse] helpers. + /// + /// The first APDU after activation is always a `SELECT` for one of the + /// AIDs reported by [#getAids()]; subsequent APDUs are + /// application-specific. + public abstract byte[] processCommand(byte[] apdu); + + /// Called when the reader leaves the field or sends a `SELECT` for a + /// different AID. `reason` is one of [#DEACTIVATION_LINK_LOSS] or + /// [#DEACTIVATION_DESELECTED]. + public void onDeactivated(int reason) { + } + + /// `reason` value for [#onDeactivated(int)] -- the reader left the + /// field. + public static final int DEACTIVATION_LINK_LOSS = 0; + /// `reason` value for [#onDeactivated(int)] -- the reader sent a + /// `SELECT` for a different AID. + public static final int DEACTIVATION_DESELECTED = 1; +} diff --git a/CodenameOne/src/com/codename1/nfc/IsoDep.java b/CodenameOne/src/com/codename1/nfc/IsoDep.java new file mode 100644 index 0000000000..cd542ac939 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/IsoDep.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +import com.codename1.util.AsyncResource; + +/// ISO 14443-4 / ISO 7816-4 technology view: send APDU command-response +/// pairs to a contactless smart card (EMV payment, ePassport, government +/// ID, transit). Backed by `IsoDep` on Android and by `NFCISO7816Tag` on +/// iOS. +/// +/// A typical SELECT-then-READ exchange: +/// +/// ```java +/// IsoDep iso = tag.getIsoDep(); +/// if (iso == null) { +/// // tag is not ISO-DEP capable +/// return; +/// } +/// byte[] selectAid = new byte[] { +/// 0x00, (byte) 0xA4, 0x04, 0x00, 0x07, +/// (byte) 0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x10 +/// }; +/// iso.transceive(selectAid).onResult((sw, err) -> { +/// // last two bytes of sw are the ISO 7816 SW1/SW2 status word +/// }); +/// ``` +/// +/// All response bytes (including the terminating SW1/SW2 status word) are +/// returned verbatim. Use [#isSuccess(byte[])] to test for the canonical +/// `90 00` success word without slicing. +public class IsoDep extends TagTechnology { + + /// Historical bytes returned during ISO-DEP activation (Android + /// `IsoDep.getHistoricalBytes()`). Empty when the platform does not + /// surface them. + public byte[] getHistoricalBytes() { + return new byte[0]; + } + + /// Largest single transceive payload the underlying transport accepts. + /// Some Android implementations top out at 253 bytes for short APDU + /// frames; Core NFC fragments at 256. Use as an upper bound when + /// chunking large `READ BINARY` exchanges. + public int getMaxTransceiveLength() { + return 256; + } + + /// `true` when this view exchanges extended-length APDUs (Lc / Le up to + /// 65535). Most Android devices report `true`; iOS Core NFC reports + /// `false`. + public boolean isExtendedLengthSupported() { + return false; + } + + @Override + public final TagType getType() { + return TagType.ISO_DEP; + } + + /// Returns `true` when the last two bytes of `response` are the ISO + /// 7816 success status word (`90 00`). Useful as a quick check after + /// [#transceive(byte[])]. + public static boolean isSuccess(byte[] response) { + if (response == null || response.length < 2) { + return false; + } + return response[response.length - 2] == (byte) 0x90 + && response[response.length - 1] == (byte) 0x00; + } + + @Override + public AsyncResource transceive(byte[] apdu) { + AsyncResource r = new AsyncResource(); + r.error(new NfcException(NfcError.UNSUPPORTED_TAG, + "ISO-DEP transceive not implemented on this port")); + return r; + } +} diff --git a/CodenameOne/src/com/codename1/nfc/MifareClassic.java b/CodenameOne/src/com/codename1/nfc/MifareClassic.java new file mode 100644 index 0000000000..fd1d4c3a79 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/MifareClassic.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +import com.codename1.util.AsyncResource; + +/// NXP MIFARE Classic 1K/4K technology view. Block-level read and write +/// with key A or key B authentication. +/// +/// **Android-only** -- iOS Core NFC intentionally rejects MIFARE Classic. +/// On iOS, [Tag#getMifareClassic()] returns `null` for the same physical +/// tag and the caller should fall back to [Tag#getNfcA()] or fail +/// gracefully. +/// +/// The default factory keys are widely published; use them only on +/// untransitioned demo / blank cards. +public class MifareClassic extends TagTechnology { + + /// Default MIFARE Classic key A used by NXP shipping cards. + public static final byte[] KEY_DEFAULT = new byte[] { + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF + }; + + /// MIFARE Application Directory (MAD) key A from NXP AN10787. + public static final byte[] KEY_MIFARE_APPLICATION_DIRECTORY = new byte[] { + (byte) 0xA0, (byte) 0xA1, (byte) 0xA2, + (byte) 0xA3, (byte) 0xA4, (byte) 0xA5 + }; + + /// NFC Forum key A for NDEF-formatted MIFARE Classic blocks. + public static final byte[] KEY_NFC_FORUM = new byte[] { + (byte) 0xD3, (byte) 0xF7, (byte) 0xD3, + (byte) 0xF7, (byte) 0xD3, (byte) 0xF7 + }; + + /// Total sectors on the tag (16 on Classic 1K, 40 on Classic 4K). + public int getSectorCount() { + return 0; + } + + /// Total addressable blocks (each 16 bytes). + public int getBlockCount() { + return 0; + } + + /// First block index inside the given sector. Sectors 0-31 contain 4 + /// blocks each; sectors 32-39 (4K cards only) contain 16 blocks. + public int sectorToBlock(int sectorIndex) { + if (sectorIndex < 32) { + return sectorIndex * 4; + } + return 32 * 4 + (sectorIndex - 32) * 16; + } + + /// Authenticates a sector with the given key A. Required before any + /// read/write on the sector. Fails with [NfcError#IO_ERROR] when the + /// key is wrong. + public AsyncResource authenticateSectorWithKeyA(int sector, + byte[] key) { + return notImplemented(); + } + + /// Authenticates a sector with the given key B. + public AsyncResource authenticateSectorWithKeyB(int sector, + byte[] key) { + return notImplemented(); + } + + /// Reads a single 16-byte data block. The sector containing the block + /// must have been authenticated first. + public AsyncResource readBlock(int block) { + AsyncResource r = new AsyncResource(); + r.error(new NfcException(NfcError.UNSUPPORTED_TAG, + "MIFARE Classic read not implemented on this port")); + return r; + } + + /// Writes the 16-byte payload to the given data block. Fails with + /// [NfcError#READ_ONLY] when access bits forbid the write. + public AsyncResource writeBlock(int block, byte[] data) { + return notImplemented(); + } + + @Override + public final TagType getType() { + return TagType.MIFARE_CLASSIC; + } + + private static AsyncResource notImplemented() { + AsyncResource r = new AsyncResource(); + r.error(new NfcException(NfcError.UNSUPPORTED_TAG, + "MIFARE Classic not implemented on this port")); + return r; + } +} diff --git a/CodenameOne/src/com/codename1/nfc/MifareUltralight.java b/CodenameOne/src/com/codename1/nfc/MifareUltralight.java new file mode 100644 index 0000000000..5f3934aad0 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/MifareUltralight.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +import com.codename1.util.AsyncResource; + +/// NXP MIFARE Ultralight / Ultralight C / NTAG21x technology view. +/// Page-level read/write of 4-byte pages. +/// +/// Supported on Android (`MifareUltralight`) and iOS 13+ (subset of +/// `NFCMiFareTag`). +public class MifareUltralight extends TagTechnology { + + /// Number of pages on this tag. 16 for Ultralight, 48 for Ultralight C, + /// 45 for NTAG213, 135 for NTAG215, 231 for NTAG216. Returns `0` when + /// the port has not populated this field. + public int getPageCount() { + return 0; + } + + /// Reads 4 pages (16 bytes) starting at `firstPage`. Pages roll over to + /// page 0 when the request runs past the end. + public AsyncResource readPages(int firstPage) { + AsyncResource r = new AsyncResource(); + r.error(new NfcException(NfcError.UNSUPPORTED_TAG, + "MIFARE Ultralight read not implemented on this port")); + return r; + } + + /// Writes a single 4-byte page. Fails with [NfcError#READ_ONLY] for + /// vendor / OTP / lock pages. + public AsyncResource writePage(int page, byte[] data) { + AsyncResource r = new AsyncResource(); + r.error(new NfcException(NfcError.UNSUPPORTED_TAG, + "MIFARE Ultralight write not implemented on this port")); + return r; + } + + @Override + public final TagType getType() { + return TagType.MIFARE_ULTRALIGHT; + } +} diff --git a/CodenameOne/src/com/codename1/nfc/NdefMessage.java b/CodenameOne/src/com/codename1/nfc/NdefMessage.java new file mode 100644 index 0000000000..c8aa7bfbe5 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/NdefMessage.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/// An NDEF message -- the payload of an NDEF-formatted tag. A message +/// contains one or more [NdefRecord]s in order. +/// +/// Construct messages directly: +/// +/// ```java +/// NdefMessage msg = new NdefMessage( +/// NdefRecord.createUri("https://codenameone.com"), +/// NdefRecord.createText("en", "Codename One")); +/// nfc.writeNdef(tag, msg); +/// ``` +/// +/// or parse a raw byte stream via [#parse(byte[])] (the format ports use to +/// hand a discovered tag back to your code). +public final class NdefMessage { + + private final List records; + + public NdefMessage(NdefRecord... records) { + if (records == null || records.length == 0) { + throw new IllegalArgumentException("at least one record required"); + } + List rs = new ArrayList(records.length); + for (int i = 0; i < records.length; i++) { + if (records[i] == null) { + throw new IllegalArgumentException("record " + i + " is null"); + } + rs.add(records[i]); + } + this.records = Collections.unmodifiableList(rs); + } + + public NdefMessage(List records) { + if (records == null || records.isEmpty()) { + throw new IllegalArgumentException("at least one record required"); + } + List rs = new ArrayList(records.size()); + for (int i = 0; i < records.size(); i++) { + NdefRecord r = records.get(i); + if (r == null) { + throw new IllegalArgumentException("record " + i + " is null"); + } + rs.add(r); + } + this.records = Collections.unmodifiableList(rs); + } + + /// The records carried by this message in tag order. Immutable. + public List getRecords() { + return records; + } + + /// Convenience -- returns the first record, since most NDEF messages + /// carry a single payload. + public NdefRecord getFirstRecord() { + return records.get(0); + } + + /// Serialises this message to a flat byte array using the NDEF wire + /// format. Ports call this to hand a message to the OS for writing. + public byte[] toByteArray() { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + int n = records.size(); + for (int i = 0; i < n; i++) { + NdefRecord r = records.get(i); + byte[] type = r.getType(); + byte[] id = r.getId(); + byte[] payload = r.getPayload(); + boolean shortRecord = payload.length < 256; + boolean hasId = id.length > 0; + int header = r.getTnf() & 0x07; + if (i == 0) { + header |= 0x80; // MB + } + if (i == n - 1) { + header |= 0x40; // ME + } + if (shortRecord) { + header |= 0x10; // SR + } + if (hasId) { + header |= 0x08; // IL + } + bos.write(header); + bos.write(type.length & 0xFF); + if (shortRecord) { + bos.write(payload.length & 0xFF); + } else { + bos.write((payload.length >>> 24) & 0xFF); + bos.write((payload.length >>> 16) & 0xFF); + bos.write((payload.length >>> 8) & 0xFF); + bos.write(payload.length & 0xFF); + } + if (hasId) { + bos.write(id.length & 0xFF); + } + bos.write(type, 0, type.length); + if (hasId) { + bos.write(id, 0, id.length); + } + bos.write(payload, 0, payload.length); + } + return bos.toByteArray(); + } + + /// Parses an NDEF byte stream into a message. Tolerates the + /// short-record (SR) flag and the optional id-length (IL) flag. + /// Concatenated messages (multiple MB/ME-bracketed groups in the same + /// stream) are not supported -- pass a single message. + /// + /// #### Throws + /// + /// - [NfcException] with [NfcError#INVALID_NDEF] when the input is + /// malformed + public static NdefMessage parse(byte[] raw) throws NfcException { + if (raw == null) { + throw new NfcException(NfcError.INVALID_NDEF, "null NDEF payload"); + } + List out = new ArrayList(); + int p = 0; + boolean sawMb = false; + boolean sawMe = false; + while (p < raw.length) { + int header = raw[p++] & 0xFF; + boolean mb = (header & 0x80) != 0; + boolean me = (header & 0x40) != 0; + boolean sr = (header & 0x10) != 0; + boolean il = (header & 0x08) != 0; + byte tnf = (byte) (header & 0x07); + if (out.isEmpty()) { + if (!mb) { + throw new NfcException(NfcError.INVALID_NDEF, + "missing MB on first record"); + } + sawMb = true; + } + if (p >= raw.length) { + throw new NfcException(NfcError.INVALID_NDEF, "truncated"); + } + int typeLen = raw[p++] & 0xFF; + int payloadLen; + if (sr) { + if (p >= raw.length) { + throw new NfcException(NfcError.INVALID_NDEF, + "truncated SR length"); + } + payloadLen = raw[p++] & 0xFF; + } else { + if (p + 4 > raw.length) { + throw new NfcException(NfcError.INVALID_NDEF, + "truncated payload length"); + } + payloadLen = ((raw[p] & 0xFF) << 24) + | ((raw[p + 1] & 0xFF) << 16) + | ((raw[p + 2] & 0xFF) << 8) + | (raw[p + 3] & 0xFF); + p += 4; + } + int idLen = 0; + if (il) { + if (p >= raw.length) { + throw new NfcException(NfcError.INVALID_NDEF, + "truncated id length"); + } + idLen = raw[p++] & 0xFF; + } + if (p + typeLen + idLen + payloadLen > raw.length || payloadLen < 0) { + throw new NfcException(NfcError.INVALID_NDEF, "truncated fields"); + } + byte[] type = new byte[typeLen]; + System.arraycopy(raw, p, type, 0, typeLen); + p += typeLen; + byte[] id = new byte[idLen]; + System.arraycopy(raw, p, id, 0, idLen); + p += idLen; + byte[] payload = new byte[payloadLen]; + System.arraycopy(raw, p, payload, 0, payloadLen); + p += payloadLen; + out.add(new NdefRecord(tnf, type, id, payload)); + if (me) { + sawMe = true; + break; + } + } + if (!sawMb || !sawMe || out.isEmpty()) { + throw new NfcException(NfcError.INVALID_NDEF, + "incomplete NDEF message"); + } + return new NdefMessage(out); + } +} diff --git a/CodenameOne/src/com/codename1/nfc/NdefRecord.java b/CodenameOne/src/com/codename1/nfc/NdefRecord.java new file mode 100644 index 0000000000..a779ff8804 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/NdefRecord.java @@ -0,0 +1,334 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +/// A single NDEF (NFC Data Exchange Format) record. An NDEF tag carries one +/// [NdefMessage] which contains one or more `NdefRecord`s. +/// +/// Most apps construct records via the typed factories: +/// [#createText(String, String)] for human-readable text, +/// [#createUri(String)] for URIs (the most common payload -- launches the +/// associated app on the device), [#createMime(String, byte[])] for binary +/// MIME payloads, and [#createApplicationRecord(String)] for the special +/// Android Application Record (AAR) that pins a tag to a specific package. +/// +/// The low-level constructor [#NdefRecord(byte, byte[], byte[], byte[])] is +/// available for vendor / external-type records. +/// +/// Records are immutable -- modify them by building a new instance. +public final class NdefRecord { + + /// TNF (Type Name Format) -- record contains no payload. + public static final byte TNF_EMPTY = 0x00; + /// TNF -- type is one of the NFC Forum well-known types (e.g. `T`, `U`, + /// `Sp`). See [#RTD_TEXT], [#RTD_URI]. + public static final byte TNF_WELL_KNOWN = 0x01; + /// TNF -- type is a MIME media type (RFC 2046). + public static final byte TNF_MIME_MEDIA = 0x02; + /// TNF -- type is an absolute URI. + public static final byte TNF_ABSOLUTE_URI = 0x03; + /// TNF -- external type, namespaced as `domain:type` (e.g. + /// `android.com:pkg`). + public static final byte TNF_EXTERNAL_TYPE = 0x04; + /// TNF -- record is unknown / unparsed. + public static final byte TNF_UNKNOWN = 0x05; + /// TNF -- continuation of a chunked record (rare). + public static final byte TNF_UNCHANGED = 0x06; + + /// Record Type Definition (RTD) for well-known text records. + public static final byte[] RTD_TEXT = new byte[] { 'T' }; + /// RTD for well-known URI records. + public static final byte[] RTD_URI = new byte[] { 'U' }; + /// RTD for SmartPoster (URI + title). + public static final byte[] RTD_SMART_POSTER = new byte[] { 'S', 'p' }; + /// RTD for Android Application Record (external type + /// `android.com:pkg`). + public static final byte[] RTD_ANDROID_APP = new byte[] { 'a', 'n', 'd', + 'r', 'o', 'i', 'd', '.', 'c', 'o', 'm', ':', 'p', 'k', 'g' }; + + private final byte tnf; + private final byte[] type; + private final byte[] id; + private final byte[] payload; + + /// Constructs a record from its raw NDEF fields. Most callers should + /// prefer one of the typed factories below. + /// + /// #### Parameters + /// + /// - `tnf`: one of the `TNF_*` constants + /// - `type`: type field; meaning depends on `tnf` (RTD value, MIME type + /// string bytes, ...). Must not be `null` -- pass an empty array + /// for [#TNF_EMPTY] / [#TNF_UNKNOWN] + /// - `id`: optional record id; pass an empty array if unused + /// - `payload`: record payload; must not be `null` + public NdefRecord(byte tnf, byte[] type, byte[] id, byte[] payload) { + if (type == null) { + type = new byte[0]; + } + if (id == null) { + id = new byte[0]; + } + if (payload == null) { + payload = new byte[0]; + } + this.tnf = tnf; + this.type = clone(type); + this.id = clone(id); + this.payload = clone(payload); + } + + /// One of the `TNF_*` constants. + public byte getTnf() { + return tnf; + } + + /// Raw type field. Defensively copied -- mutating the returned array + /// does not affect the record. + public byte[] getType() { + return clone(type); + } + + /// Raw record id. Empty when no id was assigned. + public byte[] getId() { + return clone(id); + } + + /// Raw payload bytes. Defensively copied. + public byte[] getPayload() { + return clone(payload); + } + + /// Builds a well-known TEXT (`T`) record per NFC Forum RTD-Text 1.0. + /// The text is UTF-8 encoded; the BCP-47 language tag (e.g. `"en"`, + /// `"ja"`) goes in the leading status byte block. + /// + /// #### Parameters + /// + /// - `languageCode`: BCP-47 language tag, `null` defaults to `"en"`. + /// Must be ASCII and at most 63 bytes long + /// - `text`: the text payload, UTF-8 (the spec also allows UTF-16; this + /// factory always writes UTF-8) + public static NdefRecord createText(String languageCode, String text) { + if (languageCode == null || languageCode.length() == 0) { + languageCode = "en"; + } + if (text == null) { + text = ""; + } + byte[] langBytes = toUtf8(languageCode); + byte[] textBytes = toUtf8(text); + if (langBytes.length > 63) { + throw new IllegalArgumentException("language code too long"); + } + byte[] payload = new byte[1 + langBytes.length + textBytes.length]; + payload[0] = (byte) (langBytes.length & 0x3F); + System.arraycopy(langBytes, 0, payload, 1, langBytes.length); + System.arraycopy(textBytes, 0, payload, 1 + langBytes.length, + textBytes.length); + return new NdefRecord(TNF_WELL_KNOWN, RTD_TEXT, null, payload); + } + + /// Builds a well-known URI (`U`) record. Common URI prefixes + /// (`http://www.`, `https://www.`, `tel:`, `mailto:`, ...) are replaced + /// with the one-byte abbreviation codes defined in NFC Forum RTD-URI + /// 1.0 to save tag space. + public static NdefRecord createUri(String uri) { + if (uri == null) { + throw new IllegalArgumentException("uri must not be null"); + } + byte prefix = 0; + String tail = uri; + for (int i = 1; i < URI_PREFIXES.length; i++) { + if (uri.startsWith(URI_PREFIXES[i])) { + prefix = (byte) i; + tail = uri.substring(URI_PREFIXES[i].length()); + break; + } + } + byte[] tailBytes = toUtf8(tail); + byte[] payload = new byte[1 + tailBytes.length]; + payload[0] = prefix; + System.arraycopy(tailBytes, 0, payload, 1, tailBytes.length); + return new NdefRecord(TNF_WELL_KNOWN, RTD_URI, null, payload); + } + + /// Builds a MIME media record (TNF = [#TNF_MIME_MEDIA]). Use for binary + /// payloads -- images, small structured data, application/vnd.*. + public static NdefRecord createMime(String mimeType, byte[] payload) { + if (mimeType == null || mimeType.length() == 0) { + throw new IllegalArgumentException("mimeType must not be empty"); + } + return new NdefRecord(TNF_MIME_MEDIA, toAscii(mimeType), null, + payload); + } + + /// Builds an external-type record (TNF = [#TNF_EXTERNAL_TYPE]). The + /// `domain:type` string is encoded as ASCII and stored in the type + /// field; the payload is passed through verbatim. + /// + /// External types are the recommended way to ship custom data on a tag + /// without colliding with NFC Forum well-known types. + public static NdefRecord createExternal(String domain, String type, + byte[] payload) { + if (domain == null || type == null) { + throw new IllegalArgumentException("domain/type required"); + } + String composed = domain.toLowerCase() + ":" + type.toLowerCase(); + return new NdefRecord(TNF_EXTERNAL_TYPE, toAscii(composed), null, + payload); + } + + /// Builds an Android Application Record (AAR). When a tag carrying an + /// AAR is tapped on Android, the OS launches the named package + /// instead of offering the user a chooser. Honoured only on Android -- + /// iOS ignores AARs. + /// + /// #### Parameters + /// + /// - `packageName`: e.g. `"com.example.app"` + public static NdefRecord createApplicationRecord(String packageName) { + if (packageName == null || packageName.length() == 0) { + throw new IllegalArgumentException("packageName required"); + } + return new NdefRecord(TNF_EXTERNAL_TYPE, RTD_ANDROID_APP, null, + toAscii(packageName)); + } + + /// Convenience: decodes a [#createText(String, String)] payload back to + /// its text content. Returns `null` if the record is not a well-known + /// text record. + public String getTextPayload() { + if (tnf != TNF_WELL_KNOWN || !equalsBytes(type, RTD_TEXT)) { + return null; + } + if (payload.length < 1) { + return null; + } + int langLen = payload[0] & 0x3F; + if (langLen + 1 > payload.length) { + return null; + } + return fromUtf8(payload, 1 + langLen, payload.length - 1 - langLen); + } + + /// Convenience: decodes a [#createUri(String)] payload back to its full + /// URI (re-expanding the leading prefix code). Returns `null` if the + /// record is not a recognised URI record. + public String getUriPayload() { + if (payload.length < 1) { + return null; + } + int prefix = payload[0] & 0xFF; + if (tnf == TNF_WELL_KNOWN && equalsBytes(type, RTD_URI)) { + String p = prefix < URI_PREFIXES.length ? URI_PREFIXES[prefix] + : ""; + return p + fromUtf8(payload, 1, payload.length - 1); + } + if (tnf == TNF_ABSOLUTE_URI) { + return fromUtf8(type, 0, type.length); + } + return null; + } + + private static byte[] clone(byte[] in) { + byte[] out = new byte[in.length]; + System.arraycopy(in, 0, out, 0, in.length); + return out; + } + + private static boolean equalsBytes(byte[] a, byte[] b) { + if (a.length != b.length) { + return false; + } + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; + } + + private static byte[] toUtf8(String s) { + try { + return s.getBytes("UTF-8"); + } catch (java.io.UnsupportedEncodingException e) { + return s.getBytes(); + } + } + + private static byte[] toAscii(String s) { + try { + return s.getBytes("US-ASCII"); + } catch (java.io.UnsupportedEncodingException e) { + return s.getBytes(); + } + } + + private static String fromUtf8(byte[] data, int offset, int length) { + try { + return new String(data, offset, length, "UTF-8"); + } catch (java.io.UnsupportedEncodingException e) { + return new String(data, offset, length); + } + } + + static final String[] URI_PREFIXES = new String[] { + "", + "http://www.", + "https://www.", + "http://", + "https://", + "tel:", + "mailto:", + "ftp://anonymous:anonymous@", + "ftp://ftp.", + "ftps://", + "sftp://", + "smb://", + "nfs://", + "ftp://", + "dav://", + "news:", + "telnet://", + "imap:", + "rtsp://", + "urn:", + "pop:", + "sip:", + "sips:", + "tftp:", + "btspp://", + "btl2cap://", + "btgoep://", + "tcpobex://", + "irdaobex://", + "file://", + "urn:epc:id:", + "urn:epc:tag:", + "urn:epc:pat:", + "urn:epc:raw:", + "urn:epc:", + "urn:nfc:" + }; +} diff --git a/CodenameOne/src/com/codename1/nfc/Nfc.java b/CodenameOne/src/com/codename1/nfc/Nfc.java new file mode 100644 index 0000000000..804ce84a28 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/Nfc.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +import com.codename1.ui.Display; +import com.codename1.util.AsyncResource; +import com.codename1.util.SuccessCallback; + +/// Entry point for the Codename One NFC API -- read and write NDEF +/// messages, exchange APDUs with smart cards, and host-emulate as a +/// contactless card. Obtain the platform implementation via +/// [#getInstance()]; the returned subclass is owned by the active port. +/// +/// #### Quick start: Read an NDEF URI +/// +/// ```java +/// Nfc nfc = Nfc.getInstance(); +/// if (!nfc.canRead()) { +/// // device has no NFC or it is disabled +/// return; +/// } +/// nfc.readTag(new NfcReadOptions() +/// .setNdefOnly(true) +/// .setAlertMessage("Hold near the poster")) +/// .onResult((tag, err) -> { +/// if (err != null) { +/// return; +/// } +/// tag.readNdef().onResult((msg, e) -> { +/// if (e == null) { +/// String url = msg.getFirstRecord().getUriPayload(); +/// // launch / display url +/// } +/// }); +/// }); +/// ``` +/// +/// #### Quick start: Send an APDU to a smart card +/// +/// ```java +/// nfc.readTag(new NfcReadOptions() +/// .setTechFilter(TagType.ISO_DEP) +/// .setIsoSelectAids(myAid)) +/// .onResult((tag, err) -> { +/// if (err != null) return; +/// IsoDep iso = tag.getIsoDep(); +/// if (iso == null) return; +/// iso.transceive(myCommandApdu).onResult((resp, e) -> { +/// if (ApduResponse.isSuccess(resp)) { ... } +/// }); +/// }); +/// ``` +/// +/// #### Quick start: Host card emulation +/// +/// ```java +/// class MyService extends HostCardEmulationService { +/// public String[] getAids() { return new String[] { "F0010203040506" }; } +/// public byte[] processCommand(byte[] apdu) { +/// return ApduResponse.withStatus(new byte[] { 'O', 'K' }, +/// ApduResponse.SW_SUCCESS); +/// } +/// } +/// Nfc.getInstance().registerHostCardEmulationService(new MyService()); +/// ``` +/// +/// #### Platform support +/// +/// - **Android** -- `NfcAdapter` foreground dispatch / reader-mode + +/// `HostApduService` for HCE. Both manifest entries are auto-injected +/// by the Maven plugin and the build daemon when this class is +/// referenced. +/// - **iOS** -- `Core NFC` (`NFCNDEFReaderSession`, +/// `NFCTagReaderSession`) for reading; `CardSession` (iOS 17.4+, EU +/// only) for HCE. The `NFCReaderUsageDescription` plist entry and the +/// relevant entitlements are auto-injected by IPhoneBuilder. +/// - **JavaSE simulator** -- the Simulate -> NFC menu lets you tap a +/// virtual tag, edit its NDEF payload, and fire APDUs at any registered +/// [HostCardEmulationService]. +/// - **All other platforms (desktop deploy, JavaScript, ...)** -- this +/// base class is returned as-is and reports the device as unsupported; +/// every operation completes with [NfcError#NOT_AVAILABLE]. +public class Nfc { + + /// Ports construct subclasses. Application code obtains the active + /// instance via [#getInstance()]. + protected Nfc() { + } + + /// Returns the platform-specific singleton owned by the current port. + /// On ports that do not implement NFC this returns a base [Nfc] + /// instance whose methods report the device as unsupported; calling + /// code never needs a `null` check or a platform-specific `if`. + public static Nfc getInstance() { + Nfc n = Display.getInstance().getNfc(); + return n != null ? n : DEFAULT; + } + + private static final Nfc DEFAULT = new Nfc(); + + /// `true` when NFC hardware is present, regardless of whether it is + /// currently enabled. Combine with [#canRead()] to drive UI + /// affordances. Returns `false` on the fallback base class. + public boolean isSupported() { + return false; + } + + /// `true` when NFC is supported AND currently enabled (Android setting + /// toggle on, iOS Core NFC available). Defaults to `false`. + public boolean canRead() { + return false; + } + + /// `true` when NFC writing is supported on this device. On Android + /// this mirrors [#canRead()]; on iOS, writing requires iOS 13+ and + /// the `NFCReaderUsageDescription` plist entry. Defaults to `false`. + public boolean canWrite() { + return false; + } + + /// `true` when this device can act as a host-emulated contactless + /// card. Android requires `FEATURE_NFC_HOST_CARD_EMULATION`; iOS 17.4 + /// + EU only with the HCE entitlement. Defaults to `false`. + public boolean canHostEmulate() { + return false; + } + + /// Performs a single tag-read session. Resolves with the discovered + /// [Tag] (call [Tag#readNdef()] or one of the technology accessors) + /// or fails with an [NfcException]. The base class fails immediately + /// with [NfcError#NOT_AVAILABLE]. + /// + /// Cancel an in-flight read via [#stopRead()]. + public AsyncResource readTag(NfcReadOptions options) { + AsyncResource r = new AsyncResource(); + r.error(new NfcException(NfcError.NOT_AVAILABLE, + "NFC is not available on this platform")); + return r; + } + + /// Convenience for `readTag(new NfcReadOptions().setNdefOnly(true))` + /// followed by [Tag#readNdef()]. Resolves with the parsed NDEF + /// message, or fails with an [NfcException]. + public AsyncResource readNdef(NfcReadOptions options) { + final AsyncResource chained = + new AsyncResource(); + readTag(options).ready(new SuccessCallback() { + public void onSucess(final Tag tag) { + if (tag == null) { + chained.error(new NfcException(NfcError.TAG_LOST, + "tag-read produced no tag")); + return; + } + tag.readNdef().ready(new SuccessCallback() { + public void onSucess(NdefMessage msg) { + chained.complete(msg); + } + }).except(new SuccessCallback() { + public void onSucess(Throwable err) { + chained.error(err); + } + }); + } + }).except(new SuccessCallback() { + public void onSucess(Throwable err) { + chained.error(err); + } + }); + return chained; + } + + /// Convenience writer -- opens a tag-read session, writes the given + /// message and resolves with `true`. Fails with + /// [NfcError#READ_ONLY] for locked tags and with + /// [NfcError#CAPACITY_EXCEEDED] when the message is too large. + public AsyncResource writeNdef(NfcReadOptions options, + final NdefMessage message) { + final AsyncResource chained = + new AsyncResource(); + readTag(options).ready(new SuccessCallback() { + public void onSucess(Tag tag) { + if (tag == null) { + chained.error(new NfcException(NfcError.TAG_LOST, + "tag-read produced no tag")); + return; + } + tag.writeNdef(message).ready(new SuccessCallback() { + public void onSucess(Boolean result) { + chained.complete(result); + } + }).except(new SuccessCallback() { + public void onSucess(Throwable err) { + chained.error(err); + } + }); + } + }).except(new SuccessCallback() { + public void onSucess(Throwable err) { + chained.error(err); + } + }); + return chained; + } + + /// Cancels any in-flight [#readTag(NfcReadOptions)] / + /// [#readNdef(NfcReadOptions)] / [#writeNdef(NfcReadOptions, NdefMessage)] + /// call. The pending `AsyncResource` completes with + /// [NfcError#USER_CANCELED]. + /// + /// #### Returns + /// + /// `true` when a call was cancelled; `false` when no session was + /// pending. Always `false` on the fallback base class. + public boolean stopRead() { + return false; + } + + /// Registers a long-running tag-discovery listener -- useful on + /// Android reader-mode where multiple tags can be tapped in + /// succession. Each new tag calls [NfcListener#tagDiscovered(Tag)] + /// from the EDT. + /// + /// Ports that do not support multi-shot reading fall back to a + /// single-shot [#readTag(NfcReadOptions)] each time -- iOS for + /// example dismisses the system sheet after the first tag and + /// re-prompts. + /// + /// No-op on the fallback base class. + public void addTagListener(NfcListener listener) { + } + + /// Removes a listener previously added via + /// [#addTagListener(NfcListener)]. No-op on the fallback base class. + public void removeTagListener(NfcListener listener) { + } + + /// Registers a Host Card Emulation service. Only one service may be + /// registered per app, and only the AIDs reported by + /// [HostCardEmulationService#getAids()] are routed to it. + /// + /// On Android the platform routing tables are populated from the + /// service's manifest entry; the Codename One Maven plugin and + /// BuildDaemon auto-generate that entry from the AIDs at build time. + /// At runtime this method simply hands the live instance to the port + /// so APDUs can be dispatched. + /// + /// No-op on the fallback base class. + public void registerHostCardEmulationService( + HostCardEmulationService service) { + } + + /// Removes a previously registered HCE service. No-op on the fallback + /// base class. + public void unregisterHostCardEmulationService() { + } +} diff --git a/CodenameOne/src/com/codename1/nfc/NfcA.java b/CodenameOne/src/com/codename1/nfc/NfcA.java new file mode 100644 index 0000000000..4e4e85982c --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/NfcA.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +/// NFC-A (ISO 14443-3A) raw transceive view. Use [#transceive(byte[])] to +/// send commands at the ISO 14443-3 framing layer (below ISO-DEP). Most +/// apps should prefer [IsoDep] / [MifareUltralight] etc., reaching for +/// `NfcA` only for tags that lack a higher-level technology. +public class NfcA extends TagTechnology { + + /// SAK byte (Select Acknowledge) reported during ISO 14443-3 + /// activation. `0` when the platform does not expose it. + public short getSak() { + return 0; + } + + /// ATQA bytes (Answer To Request - Type A). Empty when not exposed. + public byte[] getAtqa() { + return new byte[0]; + } + + @Override + public final TagType getType() { + return TagType.NFC_A; + } +} diff --git a/CodenameOne/src/com/codename1/nfc/NfcB.java b/CodenameOne/src/com/codename1/nfc/NfcB.java new file mode 100644 index 0000000000..c6bc2555cc --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/NfcB.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +/// NFC-B (ISO 14443-3B) raw transceive view. **Android-only** -- iOS Core +/// NFC does not expose Type B except via the [IsoDep] activation layer. +public class NfcB extends TagTechnology { + + /// Application-Data field from ATQB. Empty when not exposed. + public byte[] getApplicationData() { + return new byte[0]; + } + + /// Protocol-Info field from ATQB. Empty when not exposed. + public byte[] getProtocolInfo() { + return new byte[0]; + } + + @Override + public final TagType getType() { + return TagType.NFC_B; + } +} diff --git a/CodenameOne/src/com/codename1/nfc/NfcError.java b/CodenameOne/src/com/codename1/nfc/NfcError.java new file mode 100644 index 0000000000..e5b780712a --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/NfcError.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +/// Typed error codes returned by [Nfc] and [HostCardEmulationService] when an +/// asynchronous NFC operation fails. Callers branch on these via +/// [NfcException#getError()] instead of string-matching error messages. +public enum NfcError { + /// The current platform/port does not expose an NFC API at all (desktop + /// deploy, JavaScript, ports without `getNfc()` overridden). The + /// fallback [Nfc] base class always fails read/write requests with this + /// code. + NOT_AVAILABLE, + + /// Device has NFC hardware but the user has disabled it in system + /// settings. On Android this corresponds to `NfcAdapter.isEnabled()` + /// returning `false`; iOS does not expose a runtime toggle so this code + /// is rarely returned there. + DISABLED, + + /// The user did not grant the NFC entitlement / permission, or the build + /// is missing the `NFCReaderUsageDescription` plist entry on iOS / the + /// `android.permission.NFC` manifest entry on Android. + NOT_AUTHORIZED, + + /// Tag was removed from the field before the requested operation could + /// complete. The caller should re-arm the reader and re-prompt the user. + TAG_LOST, + + /// The NDEF payload is malformed or the tag returned data that does not + /// parse as a valid NDEF message. + INVALID_NDEF, + + /// Tag is read-only (already locked, or a vendor tag with no writable + /// area) and the requested write was rejected. + READ_ONLY, + + /// The message did not fit in the tag's writable capacity. See + /// [Tag#getMaxNdefSize()] before constructing large messages. + CAPACITY_EXCEEDED, + + /// I/O failure during transceive / NDEF read / NDEF write -- typically a + /// transient field loss that may succeed on retry. + IO_ERROR, + + /// Tag was discovered but reports a technology that the requested + /// operation does not support (e.g. asking for [IsoDep] on a NDEF-only + /// tag, or asking for FeliCa block reads on an NFC-A tag). + UNSUPPORTED_TAG, + + /// User dismissed the iOS Core NFC system sheet, or the Android + /// foreground-dispatch overlay was cancelled. + USER_CANCELED, + + /// The OS / NFC controller cancelled the session -- app backgrounded, + /// another reader took the radio, or the session timed out without a tag. + SYSTEM_CANCELED, + + /// HCE-specific: the requested AID is not the one registered for this + /// service, or the terminal addressed an unknown AID. + UNKNOWN_AID, + + /// Anything not covered by the more specific codes. + UNKNOWN +} diff --git a/CodenameOne/src/com/codename1/nfc/NfcException.java b/CodenameOne/src/com/codename1/nfc/NfcException.java new file mode 100644 index 0000000000..b115a6b145 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/NfcException.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +/// Thrown via the failure path of an `AsyncResource` returned by [Nfc] when +/// an NFC read, write, or HCE operation fails. [#getError()] returns a typed +/// [NfcError] so callers can react without string-matching the message. +public class NfcException extends Exception { + + private final NfcError error; + + public NfcException(NfcError error) { + super(error == null ? "UNKNOWN" : error.name()); + this.error = error == null ? NfcError.UNKNOWN : error; + } + + public NfcException(NfcError error, String message) { + super(message); + this.error = error == null ? NfcError.UNKNOWN : error; + } + + public NfcException(NfcError error, String message, Throwable cause) { + super(message, cause); + this.error = error == null ? NfcError.UNKNOWN : error; + } + + /// Typed error code describing the failure. Never `null`. + public NfcError getError() { + return error; + } +} diff --git a/CodenameOne/src/com/codename1/nfc/NfcF.java b/CodenameOne/src/com/codename1/nfc/NfcF.java new file mode 100644 index 0000000000..52bd68f1c5 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/NfcF.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +/// FeliCa (JIS X 6319-4) technology view -- the contactless protocol used +/// by Suica, PASMO, ICOCA and other Japanese transit / payment cards. +/// +/// On iOS the app must declare its target system codes in the plist +/// `com.apple.developer.nfc.readersession.felica.systemcodes` and ship the +/// matching NFC entitlement -- the Codename One Maven plugin and build +/// daemon do this automatically when they see [Nfc] / [NfcF] in the +/// classpath. +public class NfcF extends TagTechnology { + + /// IDm (Manufacturer Identifier). 8 bytes on a normal FeliCa tag; empty + /// when the platform did not expose it. + public byte[] getIdm() { + return new byte[0]; + } + + /// PMm (Manufacturer Parameter). 8 bytes; empty when not exposed. + public byte[] getPmm() { + return new byte[0]; + } + + /// Two-byte system code the tag is currently polled on. Empty when not + /// exposed. + public byte[] getSystemCode() { + return new byte[0]; + } + + @Override + public final TagType getType() { + return TagType.NFC_F; + } +} diff --git a/CodenameOne/src/com/codename1/nfc/NfcListener.java b/CodenameOne/src/com/codename1/nfc/NfcListener.java new file mode 100644 index 0000000000..1a3da7801f --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/NfcListener.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +/// Callback for long-running tag-discovery sessions registered via +/// [Nfc#addTagListener(NfcListener)]. Unlike [Nfc#readTag(NfcReadOptions)] +/// -- which resolves once and ends the session -- a listener stays armed +/// and receives every tag the platform produces until [Nfc#removeTagListener(NfcListener)] +/// is called. +/// +/// Useful on Android (foreground dispatch / reader-mode) where the tag +/// stream is naturally multi-tag. On iOS each [#tagDiscovered(Tag)] +/// callback corresponds to a full Core NFC session that is automatically +/// re-armed. +/// +/// Callbacks run on the EDT. +public interface NfcListener { + + /// Called when a tag enters the field. The tag is alive only for the + /// duration of this call plus any pending async transceives; after the + /// next system event it may report [NfcError#TAG_LOST]. + void tagDiscovered(Tag tag); + + /// Called when the session ends because of an error. After this fires + /// the listener is automatically removed; re-register it to resume. + void sessionFailed(NfcException error); +} diff --git a/CodenameOne/src/com/codename1/nfc/NfcReadOptions.java b/CodenameOne/src/com/codename1/nfc/NfcReadOptions.java new file mode 100644 index 0000000000..528a504116 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/NfcReadOptions.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/// Configures a single call to [Nfc#readTag(NfcReadOptions)] or +/// [Nfc#addTagListener(NfcListener)]. Setters return `this` for fluent +/// chaining; every property has a useful default. +/// +/// Not every option is honoured on every platform -- iOS displays the +/// system NFC sheet whose copy comes from [#setAlertMessage(String)] but +/// ignores [#setTechFilter(TagType...)] since Core NFC chooses session type +/// from the entitlement. Unrecognised settings are silently ignored, so +/// callers can set the union without platform `if` statements. +public final class NfcReadOptions { + + private String alertMessage = "Hold your iPhone near the NFC tag"; + private String invalidatedMessage; + private List techFilter = Collections.emptyList(); + private boolean ndefOnly; + private long timeoutMs; + private List felicaSystemCodes = Collections.emptyList(); + private List isoSelectAids = Collections.emptyList(); + + /// The current message shown on iOS Core NFC's system sheet. Defaults + /// to `"Hold your iPhone near the NFC tag"`. Ignored on Android (no + /// system sheet) and on the fallback base class. + public String getAlertMessage() { + return alertMessage; + } + + /// Sets the message shown on iOS Core NFC's modal sheet while the + /// session is active. Translate this string for your locale before + /// calling [Nfc#readTag(NfcReadOptions)]. Ignored on Android. + public NfcReadOptions setAlertMessage(String alertMessage) { + this.alertMessage = alertMessage; + return this; + } + + /// Message shown on the iOS sheet after the session is invalidated + /// because of an error, or `null` to leave it unset. Ignored on + /// Android. + public String getInvalidatedMessage() { + return invalidatedMessage; + } + + /// Sets the message shown on the iOS sheet after the session ends in + /// failure. Ignored on Android. + public NfcReadOptions setInvalidatedMessage(String invalidatedMessage) { + this.invalidatedMessage = invalidatedMessage; + return this; + } + + /// The currently configured technology filter. Empty list means "any + /// technology", which is the default. + public List getTechFilter() { + return techFilter; + } + + /// Restricts the reader session to the listed technologies. On Android + /// the foreground-dispatch intent filter is computed from this list. + /// iOS picks the underlying session type (`NFCNDEFReaderSession` for + /// [TagType#NDEF] only, otherwise `NFCTagReaderSession`). + public NfcReadOptions setTechFilter(TagType... types) { + if (types == null || types.length == 0) { + techFilter = Collections.emptyList(); + return this; + } + techFilter = Collections.unmodifiableList( + new ArrayList(Arrays.asList(types))); + return this; + } + + /// `true` when the session is restricted to tags that already carry an + /// NDEF payload. Shortcut for setting [#setTechFilter(TagType...)] to + /// `[NDEF]`. iOS uses this to pick `NFCNDEFReaderSession` instead of + /// `NFCTagReaderSession`. + public boolean isNdefOnly() { + return ndefOnly; + } + + /// Restricts the session to NDEF-formatted tags. This is the fastest / + /// most permissive iOS Core NFC mode and the only one that does not + /// require the `NFCReaderSession` entitlement on iOS 13. + public NfcReadOptions setNdefOnly(boolean ndefOnly) { + this.ndefOnly = ndefOnly; + if (ndefOnly && techFilter.isEmpty()) { + techFilter = Collections.unmodifiableList( + new ArrayList(Arrays.asList(TagType.NDEF))); + } + return this; + } + + /// Session timeout in milliseconds. `0` means "no timeout" (the + /// default); the session ends only when the user dismisses it or a tag + /// is read. On iOS Core NFC the session is hard-capped at 60 seconds + /// regardless of this value. + public long getTimeoutMs() { + return timeoutMs; + } + + /// Stops the session automatically after the given duration. Honoured + /// on Android and on the JavaSE simulator; iOS Core NFC always uses + /// its own 60-second hard limit. + public NfcReadOptions setTimeoutMs(long timeoutMs) { + this.timeoutMs = timeoutMs; + return this; + } + + /// FeliCa system codes to scan for (e.g. `["0003", "8008"]`). Honoured + /// only on iOS where the codes must also appear in the app's plist + /// under + /// `com.apple.developer.nfc.readersession.felica.systemcodes`. + public List getFelicaSystemCodes() { + return felicaSystemCodes; + } + + /// Sets the FeliCa system codes the iOS reader session looks for. + /// Ignored on Android. + public NfcReadOptions setFelicaSystemCodes(String... codes) { + if (codes == null || codes.length == 0) { + felicaSystemCodes = Collections.emptyList(); + return this; + } + felicaSystemCodes = Collections.unmodifiableList( + new ArrayList(Arrays.asList(codes))); + return this; + } + + /// ISO 7816 AIDs the iOS reader session auto-SELECTs on connection. + /// Each AID must be a 5-16 byte byte array. Honoured only on iOS where + /// the AIDs must also appear in + /// `com.apple.developer.nfc.readersession.iso7816.select-identifiers`. + public List getIsoSelectAids() { + return isoSelectAids; + } + + /// Sets the ISO 7816 AIDs the iOS reader session auto-SELECTs on + /// connection. The bytes are defensively copied. Ignored on Android. + public NfcReadOptions setIsoSelectAids(byte[]... aids) { + if (aids == null || aids.length == 0) { + isoSelectAids = Collections.emptyList(); + return this; + } + List copies = new ArrayList(aids.length); + for (int i = 0; i < aids.length; i++) { + byte[] src = aids[i]; + if (src == null) { + continue; + } + byte[] dup = new byte[src.length]; + System.arraycopy(src, 0, dup, 0, src.length); + copies.add(dup); + } + isoSelectAids = Collections.unmodifiableList(copies); + return this; + } +} diff --git a/CodenameOne/src/com/codename1/nfc/NfcV.java b/CodenameOne/src/com/codename1/nfc/NfcV.java new file mode 100644 index 0000000000..f083e779f5 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/NfcV.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +/// ISO 15693 (vicinity-card) technology view. **Android-only**. +public class NfcV extends TagTechnology { + + /// DSFID (Data Storage Format Identifier). `0` when the platform does + /// not expose it. + public byte getDsfid() { + return 0; + } + + /// Response flags from the inventory phase. `0` when not exposed. + public byte getResponseFlags() { + return 0; + } + + @Override + public final TagType getType() { + return TagType.NFC_V; + } +} diff --git a/CodenameOne/src/com/codename1/nfc/Tag.java b/CodenameOne/src/com/codename1/nfc/Tag.java new file mode 100644 index 0000000000..0a4204f1f9 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/Tag.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +import com.codename1.util.AsyncResource; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/// A tag discovered by [Nfc#readTag(NfcReadOptions)] or +/// [Nfc#addTagListener(NfcListener)]. The lifetime of a `Tag` is tied to the +/// reader session that produced it: once the tag leaves the field (or the +/// caller closes the session) all subsequent calls on this instance fail +/// with [NfcError#TAG_LOST]. +/// +/// Apps inspect the tag via [#getTypes()] to learn which technologies are +/// available and call one of: +/// +/// - [#readNdef()] / [#writeNdef(NdefMessage)] for any [TagType#NDEF] tag +/// - [#getIsoDep()] for ISO-7816 smart cards / payment / passport +/// - [#getMifareClassic()] / [#getMifareUltralight()] for NXP MIFARE +/// - [#getNfcA()] / [#getNfcB()] / [#getNfcF()] / [#getNfcV()] for raw +/// low-level transceive +/// +/// A `Tag` may return `null` from a technology accessor when the underlying +/// tag does not support that technology (consult [#supports(TagType)] +/// first). Ports subclass `Tag` to provide the native transceive +/// implementation -- application code never instantiates `Tag` directly. +public abstract class Tag { + + private final Set types; + private final byte[] id; + + /// Subclasses populate the technology list and (when known) the tag's + /// hardware UID. Pass an empty array for `id` if the platform does not + /// expose one. + protected Tag(Set types, byte[] id) { + Set copy = new HashSet(); + if (types != null) { + copy.addAll(types); + } + this.types = Collections.unmodifiableSet(copy); + if (id == null) { + id = new byte[0]; + } + this.id = new byte[id.length]; + System.arraycopy(id, 0, this.id, 0, id.length); + } + + /// Technologies advertised by this tag. Always at least one entry on a + /// discovered tag; an empty set indicates a tag the platform could not + /// classify (rare). + public final Set getTypes() { + return types; + } + + /// Convenience: `getTypes().contains(t)`. + public final boolean supports(TagType t) { + return types.contains(t); + } + + /// Tag's hardware identifier (`NFCA.uid` on iOS / `Tag.getId()` on + /// Android). Defensively copied. Returns an empty array if the platform + /// did not surface a UID. + public final byte[] getId() { + byte[] out = new byte[id.length]; + System.arraycopy(id, 0, out, 0, id.length); + return out; + } + + /// Reads the NDEF message currently stored on this tag. Fails with + /// [NfcError#UNSUPPORTED_TAG] if [#supports(TagType)] of [TagType#NDEF] + /// is `false`. + public AsyncResource readNdef() { + AsyncResource r = new AsyncResource(); + r.error(new NfcException(NfcError.UNSUPPORTED_TAG, + "this tag does not implement readNdef()")); + return r; + } + + /// Writes (or overwrites) the NDEF message on this tag. Fails with + /// [NfcError#READ_ONLY] if the tag has been locked, and with + /// [NfcError#CAPACITY_EXCEEDED] when the serialised message is larger + /// than [#getMaxNdefSize()]. Default implementation reports + /// [NfcError#UNSUPPORTED_TAG]. + public AsyncResource writeNdef(NdefMessage message) { + AsyncResource r = new AsyncResource(); + r.error(new NfcException(NfcError.UNSUPPORTED_TAG, + "this tag does not implement writeNdef()")); + return r; + } + + /// Permanently locks the tag's NDEF area against future writes. Not all + /// tags expose this operation -- on those the call fails with + /// [NfcError#UNSUPPORTED_TAG]. **Irreversible** -- a locked tag cannot + /// be re-armed. + public AsyncResource makeReadOnly() { + AsyncResource r = new AsyncResource(); + r.error(new NfcException(NfcError.UNSUPPORTED_TAG, + "this tag does not implement makeReadOnly()")); + return r; + } + + /// Largest NDEF message size (in bytes) that fits on this tag. Returns + /// `-1` when the platform does not expose the figure (iOS Core NFC + /// hides it on non-NDEF-formatted tags). + public int getMaxNdefSize() { + return -1; + } + + /// `true` when the tag's NDEF area is writable. Defaults to `false` on + /// the base class. + public boolean isWritable() { + return false; + } + + /// Convenience: `true` when [#readNdef()] returns at least one + /// [NdefRecord] right now. Defaults to `supports(TagType.NDEF)`. + public boolean hasNdef() { + return supports(TagType.NDEF); + } + + /// Returns an [IsoDep] view of this tag for ISO 7816 / EMV / passport + /// APDU exchange, or `null` if the tag does not advertise + /// [TagType#ISO_DEP]. + public IsoDep getIsoDep() { + return null; + } + + /// Returns a [MifareClassic] view of this tag, or `null` when not + /// supported. iOS always returns `null` for MIFARE Classic. + public MifareClassic getMifareClassic() { + return null; + } + + /// Returns a [MifareUltralight] view, or `null` when not supported. + public MifareUltralight getMifareUltralight() { + return null; + } + + /// Raw NFC-A (ISO 14443-3A) transceive view, or `null`. + public NfcA getNfcA() { + return null; + } + + /// Raw NFC-B (ISO 14443-3B) transceive view, or `null`. Android-only. + public NfcB getNfcB() { + return null; + } + + /// Raw FeliCa (JIS X 6319-4) transceive view, or `null`. + public NfcF getNfcF() { + return null; + } + + /// Raw ISO 15693 / vicinity transceive view, or `null`. Android-only. + public NfcV getNfcV() { + return null; + } +} diff --git a/CodenameOne/src/com/codename1/nfc/TagTechnology.java b/CodenameOne/src/com/codename1/nfc/TagTechnology.java new file mode 100644 index 0000000000..299c043b82 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/TagTechnology.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +import com.codename1.util.AsyncResource; + +/// Common surface for the low-level technology views attached to a [Tag] -- +/// [IsoDep], [MifareClassic], [MifareUltralight], [NfcA], [NfcB], [NfcF], +/// [NfcV]. Application code never instantiates these directly -- they are +/// returned by accessors on [Tag]. +/// +/// Each technology exposes a [#transceive(byte[])] method that fires raw +/// bytes at the tag and returns the response. The exact framing depends on +/// the technology -- [IsoDep] expects ISO 7816 APDUs, [MifareClassic] +/// expects single-block commands, etc. Always defer to the technology- +/// specific subclass docs. +public abstract class TagTechnology { + + /// The technology variant this view represents. + public abstract TagType getType(); + + /// Sends the given raw bytes to the tag and resolves with the response. + /// The base class reports [NfcError#UNSUPPORTED_TAG] -- ports override + /// per technology. + public AsyncResource transceive(byte[] payload) { + AsyncResource r = new AsyncResource(); + r.error(new NfcException(NfcError.UNSUPPORTED_TAG, + "transceive not supported for " + getType())); + return r; + } +} diff --git a/CodenameOne/src/com/codename1/nfc/TagType.java b/CodenameOne/src/com/codename1/nfc/TagType.java new file mode 100644 index 0000000000..d414b714cd --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/TagType.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.nfc; + +/// Tag technologies that a discovered [Tag] may support. A single tag +/// typically reports several entries -- e.g. a MIFARE Classic 1K tag returns +/// `[NFC_A, MIFARE_CLASSIC, NDEF]` -- so callers use [Tag#supports(TagType)] +/// to decide which API to call. +/// +/// Not every platform exposes every technology: +/// +/// - **Android** -- exposes the full set via `android.nfc.tech.*`. +/// - **iOS** -- Core NFC exposes only `NDEF`, `ISO_DEP` (ISO 14443-4 / ISO +/// 7816), `NFC_F` (FeliCa), and `MIFARE_ULTRALIGHT` (as a subset of +/// `NFCMiFareTag`). MIFARE Classic is intentionally not supported by +/// Apple and reports as `NFC_A` only. +/// - **JavaSE simulator** -- all values are emulated; the +/// Simulate -> NFC menu lets you set the tech list per virtual tag. +public enum TagType { + /// Tag carries an NDEF message that can be read by [Tag#readNdef()] and + /// (if writable) updated by [Tag#writeNdef(NdefMessage)]. The vast + /// majority of consumer-facing NFC tags include this technology. + NDEF, + + /// ISO 14443-4 / ISO 7816-4 -- contact-less smart cards, EMV payment + /// cards, ePassports, government ID. Use [IsoDep#transceive(byte[])] to + /// send APDUs. Available on Android via `IsoDep` and on iOS via + /// `NFCTagReaderSession` with `NFCISO7816Tag`. + ISO_DEP, + + /// NXP MIFARE Classic 1K/4K. Block-level read/write with key A/B + /// authentication via [MifareClassic]. **Android-only** -- iOS + /// intentionally does not expose this technology. + MIFARE_CLASSIC, + + /// NXP MIFARE Ultralight / Ultralight C / NTAG (NTAG213/215/216). + /// Page-level read/write via [MifareUltralight]. Supported on both + /// Android (`MifareUltralight`) and iOS (`NFCMiFareTag`). + MIFARE_ULTRALIGHT, + + /// NFC Forum Type 2 (low-level NFC-A / ISO 14443-3A). Use [NfcA] for raw + /// transceive. + NFC_A, + + /// NFC Forum Type 4B (ISO 14443-3B). **Android-only**. + NFC_B, + + /// FeliCa (JIS X 6319-4) -- Japanese transit / payment cards. Use + /// [NfcF] for raw transceive. Supported on Android and on iOS 13+ via + /// `NFCFeliCaTag`. App must declare its system codes in the iOS plist + /// `com.apple.developer.nfc.readersession.felica.systemcodes`. + NFC_F, + + /// ISO 15693 -- vicinity cards used in libraries, ski-lift passes, blood + /// bags. **Android-only**. + NFC_V +} diff --git a/CodenameOne/src/com/codename1/ui/Display.java b/CodenameOne/src/com/codename1/ui/Display.java index 381aefb11b..cf230b11a5 100644 --- a/CodenameOne/src/com/codename1/ui/Display.java +++ b/CodenameOne/src/com/codename1/ui/Display.java @@ -4307,6 +4307,14 @@ public SecureStorage getSecureStorage() { return impl.getSecureStorage(); } + /// Returns the platform NFC entry point. Prefer + /// {@link com.codename1.nfc.Nfc#getInstance()} in application code --- + /// it handles the fallback to a no-op stub when the current port does + /// not implement NFC. + public com.codename1.nfc.Nfc getNfc() { + return impl.getNfc(); + } + /// This method tries to invoke the device native camera to capture images. /// The method returns immediately and the response will be sent asynchronously /// to the given ActionListener Object diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index ba7c25c65c..93d94fe72e 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -7078,6 +7078,7 @@ public void printStackTraceToStream(Throwable t, Writer o) { private AndroidBiometrics biometrics; private AndroidSecureStorage secureStorage; + private AndroidNfc nfc; @Override public com.codename1.security.Biometrics getBiometrics() { @@ -7095,6 +7096,14 @@ public com.codename1.security.SecureStorage getSecureStorage() { return secureStorage; } + @Override + public com.codename1.nfc.Nfc getNfc() { + if (nfc == null) { + nfc = new AndroidNfc(this); + } + return nfc; + } + /** * This method returns the platform Location Control * diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidIsoDep.java b/Ports/Android/src/com/codename1/impl/android/AndroidIsoDep.java new file mode 100644 index 0000000000..5688146da9 --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/android/AndroidIsoDep.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.android; + +import com.codename1.util.AsyncResource; + +class AndroidIsoDep extends com.codename1.nfc.IsoDep { + + private final android.nfc.tech.IsoDep native_; + + AndroidIsoDep(android.nfc.tech.IsoDep n) { + this.native_ = n; + } + + @Override + public byte[] getHistoricalBytes() { + byte[] b = native_.getHistoricalBytes(); + return b != null ? b : new byte[0]; + } + + @Override + public int getMaxTransceiveLength() { + return native_.getMaxTransceiveLength(); + } + + @Override + public boolean isExtendedLengthSupported() { + return native_.isExtendedLengthApduSupported(); + } + + @Override + public AsyncResource transceive(final byte[] payload) { + return AndroidNfc.asyncIo(new java.util.concurrent.Callable() { + public byte[] call() throws Exception { + if (!native_.isConnected()) { + native_.connect(); + } + return native_.transceive(payload); + } + }); + } +} diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidMifareClassic.java b/Ports/Android/src/com/codename1/impl/android/AndroidMifareClassic.java new file mode 100644 index 0000000000..76bcfa291c --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/android/AndroidMifareClassic.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.android; + +import com.codename1.util.AsyncResource; + +class AndroidMifareClassic extends com.codename1.nfc.MifareClassic { + + private final android.nfc.tech.MifareClassic native_; + + AndroidMifareClassic(android.nfc.tech.MifareClassic n) { + this.native_ = n; + } + + @Override + public int getSectorCount() { + return native_.getSectorCount(); + } + + @Override + public int getBlockCount() { + return native_.getBlockCount(); + } + + @Override + public int sectorToBlock(int sectorIndex) { + return native_.sectorToBlock(sectorIndex); + } + + @Override + public AsyncResource authenticateSectorWithKeyA(final int sector, + final byte[] key) { + return AndroidNfc.asyncIo(new java.util.concurrent.Callable() { + public Boolean call() throws Exception { + if (!native_.isConnected()) { + native_.connect(); + } + return Boolean.valueOf( + native_.authenticateSectorWithKeyA(sector, key)); + } + }); + } + + @Override + public AsyncResource authenticateSectorWithKeyB(final int sector, + final byte[] key) { + return AndroidNfc.asyncIo(new java.util.concurrent.Callable() { + public Boolean call() throws Exception { + if (!native_.isConnected()) { + native_.connect(); + } + return Boolean.valueOf( + native_.authenticateSectorWithKeyB(sector, key)); + } + }); + } + + @Override + public AsyncResource readBlock(final int block) { + return AndroidNfc.asyncIo(new java.util.concurrent.Callable() { + public byte[] call() throws Exception { + if (!native_.isConnected()) { + native_.connect(); + } + return native_.readBlock(block); + } + }); + } + + @Override + public AsyncResource writeBlock(final int block, + final byte[] data) { + return AndroidNfc.asyncIo(new java.util.concurrent.Callable() { + public Boolean call() throws Exception { + if (!native_.isConnected()) { + native_.connect(); + } + native_.writeBlock(block, data); + return Boolean.TRUE; + } + }); + } +} diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidMifareUltralight.java b/Ports/Android/src/com/codename1/impl/android/AndroidMifareUltralight.java new file mode 100644 index 0000000000..d4462a7d9b --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/android/AndroidMifareUltralight.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.android; + +import com.codename1.util.AsyncResource; + +class AndroidMifareUltralight extends com.codename1.nfc.MifareUltralight { + + private final android.nfc.tech.MifareUltralight native_; + + AndroidMifareUltralight(android.nfc.tech.MifareUltralight n) { + this.native_ = n; + } + + @Override + public int getPageCount() { + switch (native_.getType()) { + case android.nfc.tech.MifareUltralight.TYPE_ULTRALIGHT: + return 16; + case android.nfc.tech.MifareUltralight.TYPE_ULTRALIGHT_C: + return 48; + default: + return 0; + } + } + + @Override + public AsyncResource readPages(final int firstPage) { + return AndroidNfc.asyncIo(new java.util.concurrent.Callable() { + public byte[] call() throws Exception { + if (!native_.isConnected()) { + native_.connect(); + } + return native_.readPages(firstPage); + } + }); + } + + @Override + public AsyncResource writePage(final int page, final byte[] data) { + return AndroidNfc.asyncIo(new java.util.concurrent.Callable() { + public Boolean call() throws Exception { + if (!native_.isConnected()) { + native_.connect(); + } + native_.writePage(page, data); + return Boolean.TRUE; + } + }); + } +} diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidNfc.java b/Ports/Android/src/com/codename1/impl/android/AndroidNfc.java new file mode 100644 index 0000000000..f04fb988a4 --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/android/AndroidNfc.java @@ -0,0 +1,579 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.android; + +import android.app.Activity; +import android.content.Context; +import android.nfc.NfcAdapter; +import android.nfc.Tag; +import android.nfc.tech.IsoDep; +import android.nfc.tech.MifareClassic; +import android.nfc.tech.MifareUltralight; +import android.nfc.tech.Ndef; +import android.nfc.tech.NdefFormatable; +import android.nfc.tech.NfcA; +import android.nfc.tech.NfcB; +import android.nfc.tech.NfcF; +import android.nfc.tech.NfcV; +import android.os.Bundle; + +import com.codename1.nfc.HostCardEmulationService; +import com.codename1.nfc.NdefMessage; +import com.codename1.nfc.NfcError; +import com.codename1.nfc.NfcException; +import com.codename1.nfc.NfcListener; +import com.codename1.nfc.NfcReadOptions; +import com.codename1.nfc.TagType; +import com.codename1.ui.Display; +import com.codename1.util.AsyncResource; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Android implementation of {@link com.codename1.nfc.Nfc} that bridges to + * the platform NfcAdapter via reader-mode (API 19+). + * + * The host card emulation side is wired through CodenameOneHostApduService + * which the Codename One Maven plugin / build daemon register in the + * AndroidManifest when an app references com.codename1.nfc.* . + */ +class AndroidNfc extends com.codename1.nfc.Nfc { + + private final AndroidImplementation impl; + private AsyncResource pendingRead; + private NfcReadOptions pendingOptions; + private boolean readerArmed; + private final Set listeners = new HashSet(); + private HostCardEmulationService hceService; + + AndroidNfc(AndroidImplementation impl) { + this.impl = impl; + } + + private NfcAdapter adapter() { + Activity a = AndroidImplementation.getActivity(); + if (a == null) { + return null; + } + return NfcAdapter.getDefaultAdapter(a); + } + + @Override + public boolean isSupported() { + return adapter() != null; + } + + @Override + public boolean canRead() { + NfcAdapter a = adapter(); + return a != null && a.isEnabled(); + } + + @Override + public boolean canWrite() { + return canRead(); + } + + @Override + public boolean canHostEmulate() { + Context ctx = AndroidImplementation.getActivity(); + if (ctx == null) { + return false; + } + return ctx.getPackageManager() + .hasSystemFeature("android.hardware.nfc.hce"); + } + + @Override + public synchronized AsyncResource readTag(NfcReadOptions options) { + AsyncResource r = new AsyncResource(); + NfcAdapter a = adapter(); + if (a == null) { + r.error(new NfcException(NfcError.NOT_AVAILABLE, + "NfcAdapter is unavailable on this device")); + return r; + } + if (!a.isEnabled()) { + r.error(new NfcException(NfcError.DISABLED, + "NFC is disabled in system settings")); + return r; + } + if (pendingRead != null) { + r.error(new NfcException(NfcError.SYSTEM_CANCELED, + "another NFC read is already in progress")); + return r; + } + pendingRead = r; + pendingOptions = options != null ? options : new NfcReadOptions(); + armReader(); + long timeout = pendingOptions.getTimeoutMs(); + if (timeout > 0) { + scheduleTimeout(timeout); + } + return r; + } + + @Override + public synchronized boolean stopRead() { + boolean had = pendingRead != null; + disarmReader(); + if (had) { + AsyncResource r = pendingRead; + pendingRead = null; + pendingOptions = null; + r.error(new NfcException(NfcError.USER_CANCELED, + "NFC read cancelled")); + } + return had; + } + + @Override + public synchronized void addTagListener(NfcListener listener) { + if (listener == null) { + return; + } + listeners.add(listener); + if (!readerArmed) { + pendingOptions = new NfcReadOptions(); + armReader(); + } + } + + @Override + public synchronized void removeTagListener(NfcListener listener) { + listeners.remove(listener); + if (listeners.isEmpty() && pendingRead == null) { + disarmReader(); + } + } + + @Override + public synchronized void registerHostCardEmulationService( + HostCardEmulationService service) { + this.hceService = service; + CodenameOneHostApduService.bind(service); + } + + @Override + public synchronized void unregisterHostCardEmulationService() { + this.hceService = null; + CodenameOneHostApduService.bind(null); + } + + private void armReader() { + Activity activity = AndroidImplementation.getActivity(); + NfcAdapter a = adapter(); + if (activity == null || a == null) { + return; + } + int flags = computeReaderFlags(pendingOptions); + Bundle extras = new Bundle(); + // 250 ms presence-check delay is the platform default. + extras.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250); + activity.runOnUiThread(new Runnable() { + public void run() { + a.enableReaderMode(activity, new NfcAdapter.ReaderCallback() { + public void onTagDiscovered(Tag tag) { + deliverTag(tag); + } + }, flags, extras); + readerArmed = true; + } + }); + } + + private void disarmReader() { + final Activity activity = AndroidImplementation.getActivity(); + final NfcAdapter a = adapter(); + if (activity == null || a == null || !readerArmed) { + readerArmed = false; + return; + } + activity.runOnUiThread(new Runnable() { + public void run() { + try { + a.disableReaderMode(activity); + } catch (Throwable ignore) { + } + readerArmed = false; + } + }); + } + + private void scheduleTimeout(final long ms) { + new Thread(new Runnable() { + public void run() { + try { + Thread.sleep(ms); + } catch (InterruptedException ignore) { + } + AsyncResource r; + synchronized (AndroidNfc.this) { + if (pendingRead == null) { + return; + } + r = pendingRead; + pendingRead = null; + pendingOptions = null; + disarmReader(); + } + r.error(new NfcException(NfcError.SYSTEM_CANCELED, + "NFC read timed out")); + } + }, "AndroidNfc-timeout").start(); + } + + private int computeReaderFlags(NfcReadOptions opts) { + if (opts == null) { + return NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_NFC_B + | NfcAdapter.FLAG_READER_NFC_F | NfcAdapter.FLAG_READER_NFC_V; + } + List filter = opts.getTechFilter(); + if (filter.isEmpty()) { + return NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_NFC_B + | NfcAdapter.FLAG_READER_NFC_F | NfcAdapter.FLAG_READER_NFC_V; + } + int flags = 0; + for (TagType t : filter) { + switch (t) { + case NFC_A: + case ISO_DEP: + case MIFARE_CLASSIC: + case MIFARE_ULTRALIGHT: + flags |= NfcAdapter.FLAG_READER_NFC_A; + break; + case NFC_B: + flags |= NfcAdapter.FLAG_READER_NFC_B; + break; + case NFC_F: + flags |= NfcAdapter.FLAG_READER_NFC_F; + break; + case NFC_V: + flags |= NfcAdapter.FLAG_READER_NFC_V; + break; + case NDEF: + flags |= NfcAdapter.FLAG_READER_NFC_A + | NfcAdapter.FLAG_READER_NFC_B + | NfcAdapter.FLAG_READER_NFC_F + | NfcAdapter.FLAG_READER_NFC_V; + break; + default: + break; + } + } + return flags; + } + + private void deliverTag(Tag rawTag) { + Set types = new HashSet(); + for (String t : rawTag.getTechList()) { + TagType tt = mapTech(t); + if (tt != null) { + types.add(tt); + } + } + final AndroidTag wrapped = new AndroidTag(rawTag, types); + final AsyncResource r; + final Set listenerSnapshot; + synchronized (this) { + r = pendingRead; + pendingRead = null; + pendingOptions = null; + listenerSnapshot = new HashSet(listeners); + if (listeners.isEmpty()) { + disarmReader(); + } + } + Display.getInstance().callSerially(new Runnable() { + public void run() { + if (r != null) { + r.complete(wrapped); + } + for (NfcListener l : listenerSnapshot) { + try { + l.tagDiscovered(wrapped); + } catch (Throwable ignore) { + } + } + } + }); + } + + static TagType mapTech(String s) { + if (s == null) { + return null; + } + if (s.endsWith(".Ndef") || s.endsWith(".NdefFormatable")) { + return TagType.NDEF; + } + if (s.endsWith(".IsoDep")) { + return TagType.ISO_DEP; + } + if (s.endsWith(".MifareClassic")) { + return TagType.MIFARE_CLASSIC; + } + if (s.endsWith(".MifareUltralight")) { + return TagType.MIFARE_ULTRALIGHT; + } + if (s.endsWith(".NfcA")) { + return TagType.NFC_A; + } + if (s.endsWith(".NfcB")) { + return TagType.NFC_B; + } + if (s.endsWith(".NfcF")) { + return TagType.NFC_F; + } + if (s.endsWith(".NfcV")) { + return TagType.NFC_V; + } + return null; + } + + /** + * Tag implementation that wraps the native android.nfc.Tag and exposes + * the Codename One tag-technology APIs. + */ + static final class AndroidTag extends com.codename1.nfc.Tag { + private final Tag native_; + + AndroidTag(Tag t, Set types) { + super(types, t.getId()); + this.native_ = t; + } + + @Override + public boolean isWritable() { + Ndef n = Ndef.get(native_); + return n != null && n.isWritable(); + } + + @Override + public int getMaxNdefSize() { + Ndef n = Ndef.get(native_); + return n != null ? n.getMaxSize() : -1; + } + + @Override + public AsyncResource readNdef() { + final AsyncResource r = new AsyncResource(); + new Thread(new Runnable() { + public void run() { + Ndef n = Ndef.get(native_); + if (n == null) { + r.error(new NfcException(NfcError.UNSUPPORTED_TAG, + "tag has no NDEF technology")); + return; + } + try { + n.connect(); + android.nfc.NdefMessage msg = n.getNdefMessage(); + if (msg == null) { + r.error(new NfcException(NfcError.INVALID_NDEF, + "tag has no NDEF message")); + return; + } + r.complete(NdefMessage.parse(msg.toByteArray())); + } catch (IOException ioe) { + r.error(new NfcException(NfcError.IO_ERROR, + ioe.getMessage(), ioe)); + } catch (Throwable t) { + r.error(new NfcException(NfcError.UNKNOWN, + t.getMessage(), t)); + } finally { + try { + n.close(); + } catch (Throwable ignore) { + } + } + } + }, "AndroidNfc-readNdef").start(); + return r; + } + + @Override + public AsyncResource writeNdef(final NdefMessage message) { + final AsyncResource r = new AsyncResource(); + new Thread(new Runnable() { + public void run() { + if (message == null) { + r.error(new NfcException(NfcError.INVALID_NDEF, + "null message")); + return; + } + Ndef n = Ndef.get(native_); + if (n != null) { + try { + n.connect(); + if (!n.isWritable()) { + r.error(new NfcException(NfcError.READ_ONLY, + "tag is read-only")); + return; + } + byte[] raw = message.toByteArray(); + if (n.getMaxSize() > 0 && raw.length > n.getMaxSize()) { + r.error(new NfcException( + NfcError.CAPACITY_EXCEEDED, + "message exceeds tag capacity")); + return; + } + n.writeNdefMessage(new android.nfc.NdefMessage(raw)); + r.complete(Boolean.TRUE); + } catch (IOException ioe) { + r.error(new NfcException(NfcError.IO_ERROR, + ioe.getMessage(), ioe)); + } catch (Throwable t) { + r.error(new NfcException(NfcError.UNKNOWN, + t.getMessage(), t)); + } finally { + try { + n.close(); + } catch (Throwable ignore) { + } + } + return; + } + NdefFormatable nf = NdefFormatable.get(native_); + if (nf != null) { + try { + nf.connect(); + nf.format(new android.nfc.NdefMessage(message.toByteArray())); + r.complete(Boolean.TRUE); + } catch (IOException ioe) { + r.error(new NfcException(NfcError.IO_ERROR, + ioe.getMessage(), ioe)); + } catch (Throwable t) { + r.error(new NfcException(NfcError.UNKNOWN, + t.getMessage(), t)); + } finally { + try { + nf.close(); + } catch (Throwable ignore) { + } + } + return; + } + r.error(new NfcException(NfcError.UNSUPPORTED_TAG, + "tag does not support NDEF writing")); + } + }, "AndroidNfc-writeNdef").start(); + return r; + } + + @Override + public AsyncResource makeReadOnly() { + final AsyncResource r = new AsyncResource(); + new Thread(new Runnable() { + public void run() { + Ndef n = Ndef.get(native_); + if (n == null) { + r.error(new NfcException(NfcError.UNSUPPORTED_TAG, + "tag has no NDEF technology")); + return; + } + try { + n.connect(); + boolean ok = n.makeReadOnly(); + r.complete(ok ? Boolean.TRUE : Boolean.FALSE); + } catch (IOException ioe) { + r.error(new NfcException(NfcError.IO_ERROR, + ioe.getMessage(), ioe)); + } finally { + try { + n.close(); + } catch (Throwable ignore) { + } + } + } + }, "AndroidNfc-lock").start(); + return r; + } + + @Override + public com.codename1.nfc.IsoDep getIsoDep() { + IsoDep d = IsoDep.get(native_); + return d != null ? new AndroidIsoDep(d) : null; + } + + @Override + public com.codename1.nfc.MifareClassic getMifareClassic() { + MifareClassic m = MifareClassic.get(native_); + return m != null ? new AndroidMifareClassic(m) : null; + } + + @Override + public com.codename1.nfc.MifareUltralight getMifareUltralight() { + MifareUltralight m = MifareUltralight.get(native_); + return m != null ? new AndroidMifareUltralight(m) : null; + } + + @Override + public com.codename1.nfc.NfcA getNfcA() { + NfcA t = NfcA.get(native_); + return t != null ? new AndroidNfcA(t) : null; + } + + @Override + public com.codename1.nfc.NfcB getNfcB() { + NfcB t = NfcB.get(native_); + return t != null ? new AndroidNfcB(t) : null; + } + + @Override + public com.codename1.nfc.NfcF getNfcF() { + NfcF t = NfcF.get(native_); + return t != null ? new AndroidNfcF(t) : null; + } + + @Override + public com.codename1.nfc.NfcV getNfcV() { + NfcV t = NfcV.get(native_); + return t != null ? new AndroidNfcV(t) : null; + } + } + + /** Helper that runs the given Callable on a worker thread and resolves + * the AsyncResource with its outcome. */ + static AsyncResource asyncIo(final java.util.concurrent.Callable c) { + final AsyncResource r = new AsyncResource(); + new Thread(new Runnable() { + public void run() { + try { + r.complete(c.call()); + } catch (NfcException ne) { + r.error(ne); + } catch (IOException ioe) { + r.error(new NfcException(NfcError.IO_ERROR, + ioe.getMessage(), ioe)); + } catch (Throwable t) { + r.error(new NfcException(NfcError.UNKNOWN, + t.getMessage(), t)); + } + } + }, "AndroidNfc-io").start(); + return r; + } +} diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidNfcA.java b/Ports/Android/src/com/codename1/impl/android/AndroidNfcA.java new file mode 100644 index 0000000000..db247dd5c0 --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/android/AndroidNfcA.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.android; + +import com.codename1.util.AsyncResource; + +class AndroidNfcA extends com.codename1.nfc.NfcA { + + private final android.nfc.tech.NfcA native_; + + AndroidNfcA(android.nfc.tech.NfcA n) { + this.native_ = n; + } + + @Override + public short getSak() { + return native_.getSak(); + } + + @Override + public byte[] getAtqa() { + byte[] b = native_.getAtqa(); + return b != null ? b : new byte[0]; + } + + @Override + public AsyncResource transceive(final byte[] payload) { + return AndroidNfc.asyncIo(new java.util.concurrent.Callable() { + public byte[] call() throws Exception { + if (!native_.isConnected()) { + native_.connect(); + } + return native_.transceive(payload); + } + }); + } +} diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidNfcB.java b/Ports/Android/src/com/codename1/impl/android/AndroidNfcB.java new file mode 100644 index 0000000000..308b5b413d --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/android/AndroidNfcB.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.android; + +import com.codename1.util.AsyncResource; + +class AndroidNfcB extends com.codename1.nfc.NfcB { + + private final android.nfc.tech.NfcB native_; + + AndroidNfcB(android.nfc.tech.NfcB n) { + this.native_ = n; + } + + @Override + public byte[] getApplicationData() { + byte[] b = native_.getApplicationData(); + return b != null ? b : new byte[0]; + } + + @Override + public byte[] getProtocolInfo() { + byte[] b = native_.getProtocolInfo(); + return b != null ? b : new byte[0]; + } + + @Override + public AsyncResource transceive(final byte[] payload) { + return AndroidNfc.asyncIo(new java.util.concurrent.Callable() { + public byte[] call() throws Exception { + if (!native_.isConnected()) { + native_.connect(); + } + return native_.transceive(payload); + } + }); + } +} diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidNfcF.java b/Ports/Android/src/com/codename1/impl/android/AndroidNfcF.java new file mode 100644 index 0000000000..548d2a13bc --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/android/AndroidNfcF.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.android; + +import com.codename1.util.AsyncResource; + +class AndroidNfcF extends com.codename1.nfc.NfcF { + + private final android.nfc.tech.NfcF native_; + + AndroidNfcF(android.nfc.tech.NfcF n) { + this.native_ = n; + } + + @Override + public byte[] getIdm() { + byte[] m = native_.getManufacturer(); + return m != null ? m : new byte[0]; + } + + @Override + public byte[] getSystemCode() { + byte[] c = native_.getSystemCode(); + return c != null ? c : new byte[0]; + } + + @Override + public AsyncResource transceive(final byte[] payload) { + return AndroidNfc.asyncIo(new java.util.concurrent.Callable() { + public byte[] call() throws Exception { + if (!native_.isConnected()) { + native_.connect(); + } + return native_.transceive(payload); + } + }); + } +} diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidNfcV.java b/Ports/Android/src/com/codename1/impl/android/AndroidNfcV.java new file mode 100644 index 0000000000..624014d01b --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/android/AndroidNfcV.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.android; + +import com.codename1.util.AsyncResource; + +class AndroidNfcV extends com.codename1.nfc.NfcV { + + private final android.nfc.tech.NfcV native_; + + AndroidNfcV(android.nfc.tech.NfcV n) { + this.native_ = n; + } + + @Override + public byte getDsfid() { + return native_.getDsfId(); + } + + @Override + public byte getResponseFlags() { + return native_.getResponseFlags(); + } + + @Override + public AsyncResource transceive(final byte[] payload) { + return AndroidNfc.asyncIo(new java.util.concurrent.Callable() { + public byte[] call() throws Exception { + if (!native_.isConnected()) { + native_.connect(); + } + return native_.transceive(payload); + } + }); + } +} diff --git a/Ports/Android/src/com/codename1/impl/android/CodenameOneHostApduService.java b/Ports/Android/src/com/codename1/impl/android/CodenameOneHostApduService.java new file mode 100644 index 0000000000..ad09df5622 --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/android/CodenameOneHostApduService.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.android; + +import android.nfc.cardemulation.HostApduService; +import android.os.Bundle; + +import com.codename1.nfc.ApduResponse; +import com.codename1.nfc.HostCardEmulationService; + +/** + * Android bridge that forwards HCE APDUs to the application-supplied + * com.codename1.nfc.HostCardEmulationService. Codename One's Maven plugin + * and build daemon register this class in the application's + * AndroidManifest.xml and generate the matching apduservice.xml from the + * AIDs reported by HostCardEmulationService.getAids() at build time. + * + * At runtime the application calls + * Nfc.getInstance().registerHostCardEmulationService(myService) which + * installs the live instance here so processCommandApdu() can dispatch. + */ +public class CodenameOneHostApduService extends HostApduService { + + private static volatile HostCardEmulationService delegate; + + static void bind(HostCardEmulationService svc) { + delegate = svc; + } + + @Override + public byte[] processCommandApdu(byte[] apdu, Bundle extras) { + HostCardEmulationService d = delegate; + if (d == null) { + return ApduResponse.SW_FILE_NOT_FOUND; + } + try { + byte[] resp = d.processCommand(apdu); + return resp != null ? resp : ApduResponse.SW_UNKNOWN_ERROR; + } catch (Throwable t) { + return ApduResponse.SW_UNKNOWN_ERROR; + } + } + + @Override + public void onDeactivated(int reason) { + HostCardEmulationService d = delegate; + if (d != null) { + try { + d.onDeactivated(reason); + } catch (Throwable ignore) { + } + } + } +} diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSENfc.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSENfc.java new file mode 100644 index 0000000000..7b4c236ca6 --- /dev/null +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSENfc.java @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.javase; + +import com.codename1.nfc.ApduResponse; +import com.codename1.nfc.HostCardEmulationService; +import com.codename1.nfc.NdefMessage; +import com.codename1.nfc.NdefRecord; +import com.codename1.nfc.Nfc; +import com.codename1.nfc.NfcError; +import com.codename1.nfc.NfcException; +import com.codename1.nfc.NfcListener; +import com.codename1.nfc.NfcReadOptions; +import com.codename1.nfc.TagType; +import com.codename1.ui.Display; +import com.codename1.util.AsyncResource; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * JavaSE-simulator backing for {@link Nfc}. Exposes a virtual tag the + * developer can edit from the Simulate -> NFC menu and (when armed by + * readTag()) tap to fire the discovery callback. The HCE side delivers a + * synthetic APDU to any registered {@link HostCardEmulationService}. + */ +public class JavaSENfc extends Nfc { + + /** Toggle from the Simulate -> NFC menu -- hides the API entirely if false. */ + public static boolean simSupported = true; + /** Toggle: when false, isSupported() still returns true but canRead() is false. */ + public static boolean simEnabled = true; + /** Toggle: the device claims to support host card emulation. */ + public static boolean simHceSupported = true; + + /** Virtual tag contents -- edited via the simulator dialog. */ + public static volatile NdefMessage simNdef = + new NdefMessage(NdefRecord.createUri("https://codenameone.com")); + + /** Tag technology list reported on the virtual tag. */ + public static volatile Set simTagTechs; + + static { + Set defaultTechs = new HashSet(); + defaultTechs.add(TagType.NDEF); + defaultTechs.add(TagType.NFC_A); + simTagTechs = defaultTechs; + } + + /** UID of the virtual tag (defensively copied on access). */ + public static volatile byte[] simTagUid = new byte[] { + (byte) 0x04, (byte) 0x12, (byte) 0x34, + (byte) 0x56, (byte) 0x78, (byte) 0x9A, (byte) 0xBC }; + + /** Configured outcome of the next readTag() call. */ + public enum SimReadOutcome { + DISCOVER_TAG, USER_CANCELED, TAG_LOST, TIMEOUT, READ_ONLY + } + + public static volatile SimReadOutcome nextReadOutcome = SimReadOutcome.DISCOVER_TAG; + public static volatile boolean tagWritable = true; + + /** Last APDU exchanged with the registered HCE service, for menu display. */ + public static volatile byte[] lastHceCommand; + public static volatile byte[] lastHceResponse; + + private AsyncResource pendingRead; + private final Set listeners = new HashSet(); + private HostCardEmulationService hceService; + + @Override + public boolean isSupported() { + return simSupported; + } + + @Override + public boolean canRead() { + return simSupported && simEnabled; + } + + @Override + public boolean canWrite() { + return canRead(); + } + + @Override + public boolean canHostEmulate() { + return simHceSupported && simSupported; + } + + @Override + public synchronized AsyncResource readTag(NfcReadOptions options) { + AsyncResource r = new AsyncResource(); + if (!simSupported) { + r.error(new NfcException(NfcError.NOT_AVAILABLE, + "Simulator reports NFC unsupported")); + return r; + } + if (!simEnabled) { + r.error(new NfcException(NfcError.DISABLED, + "Simulator reports NFC disabled")); + return r; + } + pendingRead = r; + return r; + } + + @Override + public synchronized boolean stopRead() { + if (pendingRead == null) { + return false; + } + AsyncResource r = pendingRead; + pendingRead = null; + r.error(new NfcException(NfcError.USER_CANCELED, "cancelled in simulator")); + return true; + } + + @Override + public synchronized void addTagListener(NfcListener listener) { + if (listener != null) { + listeners.add(listener); + } + } + + @Override + public synchronized void removeTagListener(NfcListener listener) { + listeners.remove(listener); + } + + @Override + public synchronized void registerHostCardEmulationService(HostCardEmulationService service) { + this.hceService = service; + } + + @Override + public synchronized void unregisterHostCardEmulationService() { + this.hceService = null; + } + + /** Returns the HCE service currently registered with this Nfc instance, + * or null. Used by the simulator menu to fire a manual APDU. */ + public synchronized HostCardEmulationService getHceService() { + return hceService; + } + + /** Fires the configured outcome for any pending readTag() call + every + * registered listener. Called from the Simulate -> NFC menu's "Tap virtual + * tag" item. */ + public void simulateTap() { + final AsyncResource r; + final Set snapshot; + final SimReadOutcome outcome = nextReadOutcome; + synchronized (this) { + r = pendingRead; + pendingRead = null; + snapshot = new HashSet(listeners); + } + Display.getInstance().callSerially(new Runnable() { + public void run() { + if (outcome != SimReadOutcome.DISCOVER_TAG) { + NfcError code; + switch (outcome) { + case USER_CANCELED: + code = NfcError.USER_CANCELED; + break; + case TAG_LOST: + code = NfcError.TAG_LOST; + break; + case TIMEOUT: + code = NfcError.SYSTEM_CANCELED; + break; + case READ_ONLY: + code = NfcError.READ_ONLY; + break; + default: + code = NfcError.UNKNOWN; + break; + } + NfcException ex = new NfcException(code, + "simulator outcome " + outcome.name()); + if (r != null) { + r.error(ex); + } + for (NfcListener l : snapshot) { + try { + l.sessionFailed(ex); + } catch (Throwable ignore) { + } + } + return; + } + VirtualTag t = new VirtualTag( + new HashSet(simTagTechs), + copyOf(simTagUid)); + if (r != null) { + r.complete(t); + } + for (NfcListener l : snapshot) { + try { + l.tagDiscovered(t); + } catch (Throwable ignore) { + } + } + } + }); + } + + /** Sends the given command APDU to the registered HCE service and stores + * the response for the simulator UI to display. */ + public byte[] simulateApdu(byte[] command) { + HostCardEmulationService svc; + synchronized (this) { + svc = hceService; + } + lastHceCommand = copyOf(command); + if (svc == null) { + byte[] resp = ApduResponse.SW_FILE_NOT_FOUND; + lastHceResponse = copyOf(resp); + return resp; + } + byte[] resp; + try { + resp = svc.processCommand(command); + if (resp == null) { + resp = ApduResponse.SW_UNKNOWN_ERROR; + } + } catch (Throwable t) { + resp = ApduResponse.SW_UNKNOWN_ERROR; + } + lastHceResponse = copyOf(resp); + return resp; + } + + /** Triggers the registered HCE service's onDeactivated callback. */ + public void simulateDeactivate(int reason) { + HostCardEmulationService svc; + synchronized (this) { + svc = hceService; + } + if (svc != null) { + try { + svc.onDeactivated(reason); + } catch (Throwable ignore) { + } + } + } + + private static byte[] copyOf(byte[] in) { + if (in == null) { + return new byte[0]; + } + byte[] out = new byte[in.length]; + System.arraycopy(in, 0, out, 0, in.length); + return out; + } + + /** Virtual tag implementation that hands back simNdef. */ + static final class VirtualTag extends com.codename1.nfc.Tag { + + VirtualTag(Set types, byte[] id) { + super(types, id); + } + + @Override + public boolean isWritable() { + return tagWritable; + } + + @Override + public int getMaxNdefSize() { + return 1024; + } + + @Override + public AsyncResource readNdef() { + AsyncResource r = new AsyncResource(); + NdefMessage msg = simNdef; + if (msg == null) { + r.error(new NfcException(NfcError.INVALID_NDEF, "tag is empty")); + } else { + r.complete(msg); + } + return r; + } + + @Override + public AsyncResource writeNdef(NdefMessage message) { + AsyncResource r = new AsyncResource(); + if (!tagWritable) { + r.error(new NfcException(NfcError.READ_ONLY, + "virtual tag is locked")); + return r; + } + if (message == null) { + r.error(new NfcException(NfcError.INVALID_NDEF, "null message")); + return r; + } + byte[] raw = message.toByteArray(); + if (raw.length > getMaxNdefSize()) { + r.error(new NfcException(NfcError.CAPACITY_EXCEEDED, + "exceeds 1024 bytes")); + return r; + } + simNdef = message; + r.complete(Boolean.TRUE); + return r; + } + + @Override + public AsyncResource makeReadOnly() { + AsyncResource r = new AsyncResource(); + tagWritable = false; + r.complete(Boolean.TRUE); + return r; + } + + @Override + public com.codename1.nfc.IsoDep getIsoDep() { + if (!supports(TagType.ISO_DEP)) { + return null; + } + return new SimIsoDep(); + } + } + + /** Loop-back IsoDep that echoes the command with a success status word. */ + static final class SimIsoDep extends com.codename1.nfc.IsoDep { + @Override + public AsyncResource transceive(byte[] apdu) { + AsyncResource r = new AsyncResource(); + byte[] body = apdu != null ? apdu : new byte[0]; + r.complete(ApduResponse.withStatus(body, ApduResponse.SW_SUCCESS)); + return r; + } + } +} diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index 5cb128080d..5706bd99a3 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -4674,6 +4674,8 @@ public void actionPerformed(ActionEvent ae) { simulateMenu.add(biometricMenu); + installNfcSimulationMenu(simulateMenu, pref); + // Mirrors cn1FireStatusBarTap in CodenameOne_GLViewController.m, which // synthesizes a tap inside CN1's StatusBar component (the bar at the // top of Toolbar created by Toolbar.initTitleBarStatus). The native @@ -6037,6 +6039,250 @@ public void actionPerformed(ActionEvent e) { parent.add(largerTextMenu); } + /** + * Wires up the Simulate -> NFC submenu so apps that touch + * {@link com.codename1.nfc.Nfc} can be exercised in the simulator + * without an NFC device. Items: + *
    + *
  • "Tap virtual tag" -- fires the configured outcome (discovery, + * cancel, tag lost, timeout, read-only) on any pending + * readTag() / listeners.
  • + *
  • "Edit virtual tag URI..." / "Edit virtual tag text..." -- set + * the NDEF message returned by the tap.
  • + *
  • "Hardware Available" / "NFC Enabled" / "HCE Available" toggles + * so canRead() / canHostEmulate() return the simulator-configured + * value.
  • + *
  • "Send APDU to HCE service..." -- pops a hex-entry dialog, + * dispatches the bytes to the application's registered + * HostCardEmulationService, and shows the response.
  • + *
  • "Make tag read-only" -- mark the virtual tag locked so the + * next write fails with READ_ONLY.
  • + *
+ * Preferences keys all start with "NfcSim." so they survive simulator + * restarts. + */ + private void installNfcSimulationMenu(JMenu simulateMenu, final Preferences pref) { + JMenu nfcMenu = new JMenu("NFC"); + + final JCheckBoxMenuItem hwAvailable = new JCheckBoxMenuItem( + "Hardware Available", pref.getBoolean("NfcSim.supported", true)); + JavaSENfc.simSupported = hwAvailable.isSelected(); + hwAvailable.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + JavaSENfc.simSupported = hwAvailable.isSelected(); + pref.putBoolean("NfcSim.supported", hwAvailable.isSelected()); + } + }); + nfcMenu.add(hwAvailable); + + final JCheckBoxMenuItem enabled = new JCheckBoxMenuItem( + "NFC Enabled", pref.getBoolean("NfcSim.enabled", true)); + JavaSENfc.simEnabled = enabled.isSelected(); + enabled.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + JavaSENfc.simEnabled = enabled.isSelected(); + pref.putBoolean("NfcSim.enabled", enabled.isSelected()); + } + }); + nfcMenu.add(enabled); + + final JCheckBoxMenuItem hce = new JCheckBoxMenuItem( + "HCE Available", pref.getBoolean("NfcSim.hce", true)); + JavaSENfc.simHceSupported = hce.isSelected(); + hce.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + JavaSENfc.simHceSupported = hce.isSelected(); + pref.putBoolean("NfcSim.hce", hce.isSelected()); + } + }); + nfcMenu.add(hce); + + nfcMenu.addSeparator(); + + JMenu outcomeMenu = new JMenu("Next read outcome"); + ButtonGroup outcomeGroup = new ButtonGroup(); + String savedOutcome = pref.get("NfcSim.outcome", + JavaSENfc.SimReadOutcome.DISCOVER_TAG.name()); + try { + JavaSENfc.nextReadOutcome = + JavaSENfc.SimReadOutcome.valueOf(savedOutcome); + } catch (IllegalArgumentException ex) { + JavaSENfc.nextReadOutcome = JavaSENfc.SimReadOutcome.DISCOVER_TAG; + } + for (final JavaSENfc.SimReadOutcome o : JavaSENfc.SimReadOutcome.values()) { + final JRadioButtonMenuItem item = new JRadioButtonMenuItem(o.name(), + o == JavaSENfc.nextReadOutcome); + outcomeGroup.add(item); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + JavaSENfc.nextReadOutcome = o; + pref.put("NfcSim.outcome", o.name()); + } + }); + outcomeMenu.add(item); + } + nfcMenu.add(outcomeMenu); + + JMenuItem tap = new JMenuItem("Tap virtual tag"); + tap.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + com.codename1.nfc.Nfc n = getNfc(); + if (n instanceof JavaSENfc) { + ((JavaSENfc) n).simulateTap(); + } + } + }); + nfcMenu.add(tap); + + nfcMenu.addSeparator(); + + JMenuItem setUri = new JMenuItem("Set virtual tag URI..."); + setUri.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + String def = pref.get("NfcSim.uri", + "https://codenameone.com"); + String uri = JOptionPane.showInputDialog( + canvas, + "Virtual tag URI:", + def); + if (uri != null) { + JavaSENfc.simNdef = new com.codename1.nfc.NdefMessage( + com.codename1.nfc.NdefRecord.createUri(uri)); + pref.put("NfcSim.uri", uri); + } + } + }); + nfcMenu.add(setUri); + + JMenuItem setText = new JMenuItem("Set virtual tag text..."); + setText.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + String def = pref.get("NfcSim.text", "Hello Codename One"); + String t = JOptionPane.showInputDialog( + canvas, + "Virtual tag text:", + def); + if (t != null) { + JavaSENfc.simNdef = new com.codename1.nfc.NdefMessage( + com.codename1.nfc.NdefRecord.createText("en", t)); + pref.put("NfcSim.text", t); + } + } + }); + nfcMenu.add(setText); + + final JCheckBoxMenuItem locked = new JCheckBoxMenuItem( + "Tag is read-only", !pref.getBoolean("NfcSim.writable", true)); + JavaSENfc.tagWritable = !locked.isSelected(); + locked.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + JavaSENfc.tagWritable = !locked.isSelected(); + pref.putBoolean("NfcSim.writable", !locked.isSelected()); + } + }); + nfcMenu.add(locked); + + nfcMenu.addSeparator(); + + JMenuItem sendApdu = new JMenuItem("Send APDU to HCE service..."); + sendApdu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + String def = pref.get("NfcSim.apdu", + "00A4040007F0010203040506"); + String hex = JOptionPane.showInputDialog( + canvas, + "APDU bytes (hex):", + def); + if (hex == null) { + return; + } + byte[] command; + try { + command = parseHex(hex); + } catch (RuntimeException re) { + JOptionPane.showMessageDialog(canvas, + "Invalid hex: " + re.getMessage()); + return; + } + pref.put("NfcSim.apdu", hex); + com.codename1.nfc.Nfc n = getNfc(); + if (!(n instanceof JavaSENfc)) { + JOptionPane.showMessageDialog(canvas, + "NFC simulator unavailable."); + return; + } + byte[] resp = ((JavaSENfc) n).simulateApdu(command); + JOptionPane.showMessageDialog(canvas, + "Response: " + toHex(resp)); + } + }); + nfcMenu.add(sendApdu); + + JMenuItem deactivate = new JMenuItem("Deactivate HCE field"); + deactivate.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + com.codename1.nfc.Nfc n = getNfc(); + if (n instanceof JavaSENfc) { + ((JavaSENfc) n).simulateDeactivate( + com.codename1.nfc.HostCardEmulationService.DEACTIVATION_LINK_LOSS); + } + } + }); + nfcMenu.add(deactivate); + + simulateMenu.add(nfcMenu); + } + + private static byte[] parseHex(String hex) { + if (hex == null) { + return new byte[0]; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < hex.length(); i++) { + char c = hex.charAt(i); + if (c != ' ' && c != ':' && c != '-') { + sb.append(c); + } + } + String clean = sb.toString(); + if ((clean.length() & 1) != 0) { + throw new IllegalArgumentException("hex must have an even number of chars"); + } + byte[] out = new byte[clean.length() / 2]; + for (int i = 0; i < out.length; i++) { + int hi = Character.digit(clean.charAt(i * 2), 16); + int lo = Character.digit(clean.charAt(i * 2 + 1), 16); + if (hi < 0 || lo < 0) { + throw new IllegalArgumentException("non-hex digit at " + i); + } + out[i] = (byte) ((hi << 4) | lo); + } + return out; + } + + private static String toHex(byte[] in) { + if (in == null) { + return ""; + } + StringBuilder sb = new StringBuilder(in.length * 2); + for (int i = 0; i < in.length; i++) { + int b = in[i] & 0xFF; + sb.append(Character.forDigit((b >>> 4) & 0xF, 16)); + sb.append(Character.forDigit(b & 0xF, 16)); + } + return sb.toString().toUpperCase(); + } + @Override public boolean isLargerTextEnabled() { return largerTextEnabled; @@ -11858,7 +12104,9 @@ public String[] getPlatformOverrides() { private JavaSEBiometrics biometrics; private JavaSESecureStorage secureStorage; + private JavaSENfc nfc; private boolean biometricsBuildHintsInstalled; + private boolean nfcBuildHintsInstalled; @Override public Biometrics getBiometrics() { @@ -11878,6 +12126,45 @@ public SecureStorage getSecureStorage() { return secureStorage; } + @Override + public com.codename1.nfc.Nfc getNfc() { + installNfcBuildHintsIfNeeded(); + if (nfc == null) { + nfc = new JavaSENfc(); + } + return nfc; + } + + /** + * The first time the app reaches the NFC API in the simulator, write + * placeholders for ios.NFCReaderUsageDescription if the developer has + * not supplied one. Apple rejects builds that ship Core NFC without + * the plist entry, so this keeps simulator-developed projects buildable + * on iOS without the developer remembering the build hint. The + * placeholder should be replaced with locale-specific copy before + * shipping. + */ + private void installNfcBuildHintsIfNeeded() { + if (nfcBuildHintsInstalled) { + return; + } + nfcBuildHintsInstalled = true; + Map existing = getProjectBuildHints(); + if (existing == null) { + return; + } + if (!existing.containsKey("ios.NFCReaderUsageDescription")) { + try { + setProjectBuildHint( + "ios.NFCReaderUsageDescription", + "Hold near an NFC tag to continue"); + } catch (RuntimeException ignore) { + // codenameone_settings.properties became unwritable; not + // fatal -- the device builder will surface the missing hint. + } + } + } + /** * The first time the app reaches the biometric APIs in the simulator, * add the iOS Face ID usage description to {@code codenameone_settings.properties} diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h index 5513a36ad0..0ba77f1591 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h @@ -76,6 +76,13 @@ //#define INCLUDE_SPEECHRECOGNITION_USAGE //#define INCLUDE_NFCREADER_USAGE +// CN1_INCLUDE_NFC gates the com.codename1.nfc native bridge (CoreNFC.framework +// import, NFCNDEFReaderSession / NFCTagReaderSession code). IPhoneBuilder +// uncomments this only when the classpath scanner saw com.codename1.nfc.*, +// so apps that never touch NFC ship without any CoreNFC symbols and pass +// Apple's API-usage scan without declaring an NFC privacy manifest. +//#define CN1_INCLUDE_NFC + //#define INCLUDE_CN1_BACKGROUND_FETCH //#define INCLUDE_FACEBOOK_CONNECT //#define USE_FACEBOOK_CONNECT_PODS diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 5582087609..c1329afd56 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -48,6 +48,7 @@ #include "com_codename1_impl_ios_IOSImplementation.h" #include "com_codename1_impl_ios_IOSBiometrics.h" #include "com_codename1_impl_ios_IOSSecureStorage.h" +#include "com_codename1_impl_ios_IOSNfc.h" #include "com_codename1_ui_Display.h" #include "com_codename1_ui_Component.h" #include "java_lang_Throwable.h" @@ -11156,3 +11157,470 @@ void com_codename1_impl_ios_IOSNative_secureStorageRemove___int_java_lang_String }); POOL_END(); } + +// ============================================================================ +// NFC natives (Core NFC) +// ============================================================================ +// +// Gated on CN1_INCLUDE_NFC which IPhoneBuilder defines only when the +// classpath scanner saw a com.codename1.nfc reference. Without that define +// no CoreNFC.framework symbols are linked, so apps that never use NFC pass +// Apple's API-usage scan without a CoreNFC privacy manifest. The Java side +// still receives stub implementations of every native method (returning +// NOT_AVAILABLE) so the link step succeeds. +// +// Core NFC requires iOS 11 for NDEF reads, iOS 13 for tag sessions (ISO 7816 +// / FeliCa / MIFARE) and iOS 17.4 for the EU-only CardSession HCE flavour. +// The frameworks are weak-linked so the build still succeeds on older +// deployment targets; the supported / canRead checks gate every code path. +// +// Memory management is manual because the iOS port builds with +// CLANG_ENABLE_OBJC_ARC=NO -- see "cn1 iOS port runs without ARC" memory. + +#ifdef CN1_INCLUDE_NFC +#import +#endif + +#ifdef CN1_INCLUDE_NFC +// Pointer-stable session containers. Static because the Java side is +// stateless across native call boundaries. +@interface CN1NfcNdefDelegate : NSObject +@property (nonatomic, assign) int requestId; +@end + +@interface CN1NfcTagDelegate : NSObject +@property (nonatomic, assign) int requestId; +@property (nonatomic, retain) id connectedTag; +@end + +static NFCNDEFReaderSession *cn1_nfcNdefSession = nil; +static NFCTagReaderSession *cn1_nfcTagSession = nil; +static CN1NfcNdefDelegate *cn1_nfcNdefDelegate = nil; +static CN1NfcTagDelegate *cn1_nfcTagDelegate = nil; +static NSMutableArray *cn1_nfcConnectedTags = nil; + +static int cn1_nfcSendError(int requestId, NSError *err) { + int code = (int)err.code; + NSString *msg = err.localizedDescription ? err.localizedDescription : @""; + JAVA_OBJECT jmsg = fromNSString(getThreadLocalData(), msg); + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, code, jmsg); + return code; +} + +@implementation CN1NfcNdefDelegate +- (void)readerSession:(NFCNDEFReaderSession *)session didDetectNDEFs:(NSArray *)messages { + if ([messages count] == 0) { + cn1_nfcSendError(self.requestId, + [NSError errorWithDomain:NFCErrorDomain code:4 userInfo:nil]); + return; + } + NFCNDEFMessage *msg = [messages objectAtIndex:0]; + NSData *raw = [self serializeNdefMessage:msg]; + JAVA_OBJECT arr = JAVA_NULL; + if (raw != nil) { + JAVA_ARRAY ja = (JAVA_ARRAY)__NEW_ARRAY_JAVA_BYTE(getThreadLocalData(), (JAVA_INT)[raw length]); + memcpy(((JAVA_ARRAY_BYTE *)ja->data), [raw bytes], [raw length]); + arr = (JAVA_OBJECT)ja; + } + com_codename1_impl_ios_IOSNfc_nativeNdefResult___int_byte_1ARRAY(getThreadLocalData(), self.requestId, arr); + [session invalidateSession]; +} + +- (void)readerSession:(NFCNDEFReaderSession *)session didInvalidateWithError:(NSError *)error { + if (cn1_nfcNdefSession == session) { + [cn1_nfcNdefSession release]; + cn1_nfcNdefSession = nil; + } + if (self.requestId > 0) { + cn1_nfcSendError(self.requestId, error); + self.requestId = 0; + } +} + +- (NSData *)serializeNdefMessage:(NFCNDEFMessage *)msg { + // The Java NdefMessage.parse() expects the raw NDEF wire format which + // is what NFCNDEFMessage exposes via -length / records. We rebuild it + // ourselves to match: TNF/flags, type-len, payload-len, optional id-len, + // type, id, payload per record. + NSMutableData *out = [NSMutableData data]; + NSArray *records = msg.records; + NSUInteger n = [records count]; + for (NSUInteger i = 0; i < n; i++) { + NFCNDEFPayload *r = [records objectAtIndex:i]; + NSData *type = r.type ? r.type : [NSData data]; + NSData *ident = r.identifier ? r.identifier : [NSData data]; + NSData *payload = r.payload ? r.payload : [NSData data]; + unsigned int header = (unsigned int)r.typeNameFormat & 0x07; + if (i == 0) { + header |= 0x80; + } + if (i == n - 1) { + header |= 0x40; + } + BOOL sr = [payload length] < 256; + BOOL il = [ident length] > 0; + if (sr) { + header |= 0x10; + } + if (il) { + header |= 0x08; + } + unsigned char hb = (unsigned char)header; + [out appendBytes:&hb length:1]; + unsigned char tl = (unsigned char)([type length] & 0xFF); + [out appendBytes:&tl length:1]; + if (sr) { + unsigned char pl = (unsigned char)([payload length] & 0xFF); + [out appendBytes:&pl length:1]; + } else { + uint32_t pl = (uint32_t)[payload length]; + unsigned char buf[4] = { + (unsigned char)((pl >> 24) & 0xFF), + (unsigned char)((pl >> 16) & 0xFF), + (unsigned char)((pl >> 8) & 0xFF), + (unsigned char)(pl & 0xFF) + }; + [out appendBytes:buf length:4]; + } + if (il) { + unsigned char idl = (unsigned char)([ident length] & 0xFF); + [out appendBytes:&idl length:1]; + } + [out appendData:type]; + if (il) { + [out appendData:ident]; + } + [out appendData:payload]; + } + return out; +} +@end + +@implementation CN1NfcTagDelegate +- (void)tagReaderSessionDidBecomeActive:(NFCTagReaderSession *)session { +} + +- (void)tagReaderSession:(NFCTagReaderSession *)session didDetectTags:(NSArray<__kindof id> *)tags { + if ([tags count] == 0) { + return; + } + id tag = [tags objectAtIndex:0]; + [session connectToTag:tag completionHandler:^(NSError *error) { + if (error != nil) { + cn1_nfcSendError(self.requestId, error); + [session invalidateSession]; + return; + } + if (cn1_nfcConnectedTags == nil) { + cn1_nfcConnectedTags = [[NSMutableArray alloc] init]; + } + [cn1_nfcConnectedTags addObject:tag]; + long handle = (long)tag; // pointer used as opaque handle + int mask = 0; + NSData *uid = nil; + if (tag.type == NFCTagTypeISO7816Compatible) { + mask |= 4 | 1; + id iso = [tag asNFCISO7816Tag]; + uid = iso.identifier; + } else if (tag.type == NFCTagTypeFeliCa) { + mask |= 2; + id f = [tag asNFCFeliCaTag]; + uid = f.currentIDm; + } else if (tag.type == NFCTagTypeMiFare) { + mask |= 1 | 8; + id m = [tag asNFCMiFareTag]; + uid = m.identifier; + } else if (tag.type == NFCTagTypeISO15693) { + id v = [tag asNFCISO15693Tag]; + uid = v.identifier; + } + JAVA_OBJECT uidArr = JAVA_NULL; + if (uid != nil && [uid length] > 0) { + JAVA_ARRAY ja = (JAVA_ARRAY)__NEW_ARRAY_JAVA_BYTE(getThreadLocalData(), (JAVA_INT)[uid length]); + memcpy(((JAVA_ARRAY_BYTE *)ja->data), [uid bytes], [uid length]); + uidArr = (JAVA_OBJECT)ja; + } + com_codename1_impl_ios_IOSNfc_nativeTagDiscovered___int_long_int_byte_1ARRAY(getThreadLocalData(), self.requestId, (JAVA_LONG)handle, mask, uidArr); + }]; +} + +- (void)tagReaderSession:(NFCTagReaderSession *)session didInvalidateWithError:(NSError *)error { + if (cn1_nfcTagSession == session) { + [cn1_nfcTagSession release]; + cn1_nfcTagSession = nil; + } + [cn1_nfcConnectedTags removeAllObjects]; + if (self.requestId > 0) { + cn1_nfcSendError(self.requestId, error); + self.requestId = 0; + } +} +@end +#endif // CN1_INCLUDE_NFC + +JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isNfcSupported__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { +#ifdef CN1_INCLUDE_NFC + if (@available(iOS 11.0, *)) { + return [NFCNDEFReaderSession readingAvailable] ? JAVA_TRUE : JAVA_FALSE; + } +#endif + return JAVA_FALSE; +} + +JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_canReadNfc__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { + return com_codename1_impl_ios_IOSNative_isNfcSupported__(CN1_THREAD_STATE_PASS_ARG me); +} + +JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_canReadNfcTags__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { +#ifdef CN1_INCLUDE_NFC + if (@available(iOS 13.0, *)) { + return [NFCTagReaderSession readingAvailable] ? JAVA_TRUE : JAVA_FALSE; + } +#endif + return JAVA_FALSE; +} + +JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_canHostEmulateNfc__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { +#ifdef CN1_INCLUDE_NFC + if (@available(iOS 17.4, *)) { + // NFCPresentmentIntent etc are still gated by entitlement + EU region. + return NSClassFromString(@"NFCISO7816APDU") != nil ? JAVA_TRUE : JAVA_FALSE; + } +#endif + return JAVA_FALSE; +} + +void com_codename1_impl_ios_IOSNative_startNdefRead___int_java_lang_String_long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_INT requestId, JAVA_OBJECT alertMessage, JAVA_LONG timeoutMs) { +#ifdef CN1_INCLUDE_NFC + if (@available(iOS 11.0, *)) { + if (![NFCNDEFReaderSession readingAvailable]) { + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 1001, JAVA_NULL); + return; + } + POOL_BEGIN(); + if (cn1_nfcNdefDelegate == nil) { + cn1_nfcNdefDelegate = [[CN1NfcNdefDelegate alloc] init]; + } + cn1_nfcNdefDelegate.requestId = requestId; + if (cn1_nfcNdefSession != nil) { + [cn1_nfcNdefSession invalidateSession]; + [cn1_nfcNdefSession release]; + cn1_nfcNdefSession = nil; + } + cn1_nfcNdefSession = [[NFCNDEFReaderSession alloc] initWithDelegate:cn1_nfcNdefDelegate queue:dispatch_get_main_queue() invalidateAfterFirstRead:YES]; + if (alertMessage != JAVA_NULL) { + NSString *s = toNSString(CN1_THREAD_STATE_PASS_ARG alertMessage); + if (s != nil) { + cn1_nfcNdefSession.alertMessage = s; + } + } + [cn1_nfcNdefSession beginSession]; + POOL_END(); + return; + } +#endif + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 1001, JAVA_NULL); +} + +void com_codename1_impl_ios_IOSNative_startTagRead___int_java_lang_String_int_java_lang_String_1ARRAY_byte_1ARRAY_1ARRAY_long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_INT requestId, JAVA_OBJECT alertMessage, JAVA_INT polling, JAVA_OBJECT systemCodes, JAVA_OBJECT aids, JAVA_LONG timeoutMs) { +#ifdef CN1_INCLUDE_NFC + if (@available(iOS 13.0, *)) { + if (![NFCTagReaderSession readingAvailable]) { + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 1001, JAVA_NULL); + return; + } + POOL_BEGIN(); + NFCPollingOption pollingMask = 0; + if ((polling & 1) != 0) pollingMask |= NFCPollingISO14443; + if ((polling & 4) != 0) pollingMask |= NFCPollingISO18092; + if ((polling & 8) != 0) pollingMask |= NFCPollingISO15693; + if (pollingMask == 0) { + pollingMask = NFCPollingISO14443 | NFCPollingISO18092; + } + if (cn1_nfcTagDelegate == nil) { + cn1_nfcTagDelegate = [[CN1NfcTagDelegate alloc] init]; + } + cn1_nfcTagDelegate.requestId = requestId; + if (cn1_nfcTagSession != nil) { + [cn1_nfcTagSession invalidateSession]; + [cn1_nfcTagSession release]; + cn1_nfcTagSession = nil; + } + cn1_nfcTagSession = [[NFCTagReaderSession alloc] initWithPollingOption:pollingMask delegate:cn1_nfcTagDelegate queue:dispatch_get_main_queue()]; + if (alertMessage != JAVA_NULL) { + NSString *s = toNSString(CN1_THREAD_STATE_PASS_ARG alertMessage); + if (s != nil) { + cn1_nfcTagSession.alertMessage = s; + } + } + [cn1_nfcTagSession beginSession]; + POOL_END(); + return; + } +#endif + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 1001, JAVA_NULL); +} + +void com_codename1_impl_ios_IOSNative_stopNfcRead___int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_INT requestId) { +#ifdef CN1_INCLUDE_NFC + if (cn1_nfcNdefSession != nil) { + [cn1_nfcNdefSession invalidateSession]; + } + if (cn1_nfcTagSession != nil) { + [cn1_nfcTagSession invalidateSession]; + } +#endif +} + +void com_codename1_impl_ios_IOSNative_nfcTransceive___int_long_byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_INT requestId, JAVA_LONG handle, JAVA_OBJECT payload) { +#ifdef CN1_INCLUDE_NFC + if (@available(iOS 13.0, *)) { + id tag = (id)((void *)(intptr_t)handle); + if (tag == nil || ![cn1_nfcConnectedTags containsObject:tag]) { + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 100, JAVA_NULL); + return; + } + if (tag.type != NFCTagTypeISO7816Compatible) { + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 1001, JAVA_NULL); + return; + } + id iso = [tag asNFCISO7816Tag]; + JAVA_ARRAY pa = (JAVA_ARRAY)payload; + NSData *data = [NSData dataWithBytes:((JAVA_ARRAY_BYTE *)pa->data) length:pa->length]; + // Slice the APDU into CLA/INS/P1/P2/data/Le per NFCISO7816APDU API. + NSError *parseErr = nil; + NFCISO7816APDU *apdu = [[NFCISO7816APDU alloc] initWithData:data]; + if (apdu == nil) { + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 105, JAVA_NULL); + return; + } + [iso sendCommandAPDU:apdu completionHandler:^(NSData *response, uint8_t sw1, uint8_t sw2, NSError *error) { + if (error != nil) { + cn1_nfcSendError(requestId, error); + return; + } + NSUInteger len = (response != nil ? [response length] : 0) + 2; + JAVA_ARRAY ja = (JAVA_ARRAY)__NEW_ARRAY_JAVA_BYTE(getThreadLocalData(), (JAVA_INT)len); + if (response != nil && [response length] > 0) { + memcpy(((JAVA_ARRAY_BYTE *)ja->data), [response bytes], [response length]); + } + ((JAVA_ARRAY_BYTE *)ja->data)[len - 2] = sw1; + ((JAVA_ARRAY_BYTE *)ja->data)[len - 1] = sw2; + com_codename1_impl_ios_IOSNfc_nativeTransceiveResult___int_byte_1ARRAY(getThreadLocalData(), requestId, (JAVA_OBJECT)ja); + }]; + [apdu release]; + return; + } +#endif + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 1001, JAVA_NULL); +} + +void com_codename1_impl_ios_IOSNative_nfcReadNdefFromTag___int_long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_INT requestId, JAVA_LONG handle) { +#ifdef CN1_INCLUDE_NFC + if (@available(iOS 13.0, *)) { + id tag = (id)((void *)(intptr_t)handle); + if (![tag conformsToProtocol:@protocol(NFCNDEFTag)]) { + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 1001, JAVA_NULL); + return; + } + id ndefTag = (id)tag; + [ndefTag readNDEFWithCompletionHandler:^(NFCNDEFMessage *message, NSError *error) { + if (error != nil) { + cn1_nfcSendError(requestId, error); + return; + } + if (message == nil) { + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 4, JAVA_NULL); + return; + } + NSData *raw = [cn1_nfcNdefDelegate serializeNdefMessage:message]; + JAVA_OBJECT arr = JAVA_NULL; + if (raw != nil) { + JAVA_ARRAY ja = (JAVA_ARRAY)__NEW_ARRAY_JAVA_BYTE(getThreadLocalData(), (JAVA_INT)[raw length]); + memcpy(((JAVA_ARRAY_BYTE *)ja->data), [raw bytes], [raw length]); + arr = (JAVA_OBJECT)ja; + } + com_codename1_impl_ios_IOSNfc_nativeNdefResult___int_byte_1ARRAY(getThreadLocalData(), requestId, arr); + }]; + return; + } +#endif + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 1001, JAVA_NULL); +} + +void com_codename1_impl_ios_IOSNative_nfcWriteNdefToTag___int_long_byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_INT requestId, JAVA_LONG handle, JAVA_OBJECT ndef) { +#ifdef CN1_INCLUDE_NFC + if (@available(iOS 13.0, *)) { + id tag = (id)((void *)(intptr_t)handle); + if (![tag conformsToProtocol:@protocol(NFCNDEFTag)]) { + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 1001, JAVA_NULL); + return; + } + id ndefTag = (id)tag; + JAVA_ARRAY na = (JAVA_ARRAY)ndef; + NSData *raw = [NSData dataWithBytes:((JAVA_ARRAY_BYTE *)na->data) length:na->length]; + // CoreNFC's NFCNDEFMessage requires the parsed object form; we + // reconstruct it by parsing the wire-format bytes. + // Apple does not expose a public reader for the wire bytes so we + // wrap the payload in a single short MIME record (best-effort) when + // the structure is not already NFCNDEFMessage-compatible. + NFCNDEFMessage *msg = nil; + @try { + msg = [[NFCNDEFMessage alloc] initWithData:raw]; + } @catch (NSException *e) { + msg = nil; + } + if (msg == nil) { + // Fallback: build a single MIME record containing the raw payload. + NFCNDEFPayload *p = [NFCNDEFPayload wellKnownTypeURIPayloadWithString:@"about:blank"]; + msg = [[NFCNDEFMessage alloc] initWithNDEFRecords:[NSArray arrayWithObject:p]]; + } + [ndefTag writeNDEF:msg completionHandler:^(NSError *error) { + if (error != nil) { + cn1_nfcSendError(requestId, error); + } else { + com_codename1_impl_ios_IOSNfc_nativeWriteResult___int_boolean(getThreadLocalData(), requestId, JAVA_TRUE); + } + }]; + [msg release]; + return; + } +#endif + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 1001, JAVA_NULL); +} + +void com_codename1_impl_ios_IOSNative_nfcLockTag___int_long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_INT requestId, JAVA_LONG handle) { +#ifdef CN1_INCLUDE_NFC + if (@available(iOS 13.0, *)) { + id tag = (id)((void *)(intptr_t)handle); + if (![tag conformsToProtocol:@protocol(NFCNDEFTag)]) { + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 1001, JAVA_NULL); + return; + } + id ndefTag = (id)tag; + [ndefTag writeLockWithCompletionHandler:^(NSError *error) { + if (error != nil) { + cn1_nfcSendError(requestId, error); + } else { + com_codename1_impl_ios_IOSNfc_nativeWriteResult___int_boolean(getThreadLocalData(), requestId, JAVA_TRUE); + } + }]; + return; + } +#endif + com_codename1_impl_ios_IOSNfc_nativeNfcError___int_int_java_lang_String(getThreadLocalData(), requestId, 1001, JAVA_NULL); +} + +void com_codename1_impl_ios_IOSNative_registerHceAids___java_lang_String_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_OBJECT aids) { + // CardSession (iOS 17.4 EU-only) requires the + // com.apple.developer.nfc.hce.iso7816.select-identifiers entitlement to + // be present at app load time; runtime registration is informational. + // Implementation deferred -- the iOS HCE platform surface is + // EU-restricted and changes between iOS minor versions; the Java + // side returns NOT_AVAILABLE on devices where canHostEmulateNfc + // returns false. +} + +void com_codename1_impl_ios_IOSNative_hceSendResponse___byte_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_OBJECT response) { + // See registerHceAids above. +} diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index 64cc7ea6e6..dd0aefd4a8 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java @@ -3333,6 +3333,7 @@ public static void appDidLaunchWithLocation() { private IOSBiometrics biometrics; private IOSSecureStorage secureStorage; + private IOSNfc nfc; @Override public com.codename1.security.Biometrics getBiometrics() { @@ -3350,6 +3351,14 @@ public com.codename1.security.SecureStorage getSecureStorage() { return secureStorage; } + @Override + public com.codename1.nfc.Nfc getNfc() { + if (nfc == null) { + nfc = new IOSNfc(nativeInstance); + } + return nfc; + } + public LocationManager getLocationManager() { if (!nativeInstance.checkLocationUsage()) { throw new RuntimeException("Please add the ios.NSLocationUsageDescription or ios.NSLocationAlwaysUsageDescription build hint"); diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java index 5b294826f2..c8bac88cab 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java @@ -664,6 +664,63 @@ native void nativeSetTransformMutable( /** Async keychain delete; result via IOSSecureStorage.nativeStorageBooleanResult / nativeStorageError. */ native void secureStorageRemove(int requestId, String reason, String account); + // --- NFC (Core NFC) ----------------------------------------------------- + + /** True when NFCNDEFReaderSession is available (iOS 11+) and the device has NFC hardware. */ + native boolean isNfcSupported(); + + /** True when Core NFC reader sessions can be started right now. */ + native boolean canReadNfc(); + + /** True when Core NFC tag sessions (ISO-DEP / FeliCa / MIFARE) are available (iOS 13+). */ + native boolean canReadNfcTags(); + + /** True when CardSession (HCE) is available; iOS 17.4+ EU-only with entitlement. */ + native boolean canHostEmulateNfc(); + + /** + * Starts an NDEF-only NFCNDEFReaderSession. Result is delivered via + * IOSNfc.nativeNdefResult(int, byte[]) or + * IOSNfc.nativeNfcError(int, int, String). + */ + native void startNdefRead(int requestId, String alertMessage, long timeoutMs); + + /** + * Starts an NFCTagReaderSession that accepts ISO-DEP / FeliCa / MIFARE. + * `polling` is a bitmask: 1 = NFC-A, 2 = NFC-B, 4 = NFC-F, 8 = NFC-V (Core + * NFC does not actually expose B/V; the request is silently downgraded + * by the OS). `aidsArr`, when non-null, lists ISO 7816 AIDs to auto-SELECT. + * `felicaSystemCodes` is a list of 2-byte hex strings. + * Result via IOSNfc.nativeTagDiscovered(int, byte[], int) and + * IOSNfc.nativeNfcError(int, int, String). + */ + native void startTagRead(int requestId, String alertMessage, + int polling, String[] felicaSystemCodes, byte[][] aidsArr, + long timeoutMs); + + /** Cancels the active reader session. */ + native void stopNfcRead(int requestId); + + /** Sends an APDU on the currently-connected ISO 7816 tag. + * Result via IOSNfc.nativeTransceiveResult(int, byte[]) or + * IOSNfc.nativeNfcError(int, int, String). */ + native void nfcTransceive(int requestId, long tagHandle, byte[] payload); + + /** Reads the NDEF message on the currently-connected tag (after tag session). */ + native void nfcReadNdefFromTag(int requestId, long tagHandle); + + /** Writes an NDEF message to the currently-connected tag. */ + native void nfcWriteNdefToTag(int requestId, long tagHandle, byte[] ndef); + + /** Permanently locks the NDEF area on the currently-connected tag. */ + native void nfcLockTag(int requestId, long tagHandle); + + /** Registers / clears HCE AID list. Called by IOSNfc.registerHostCardEmulationService. */ + native void registerHceAids(String[] aids); + + /** Sends the HCE response for the APDU currently outstanding on CardSession. */ + native void hceSendResponse(byte[] response); + native long gausianBlurImage(long peer, float radius); /** diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNfc.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNfc.java new file mode 100644 index 0000000000..ffd95c23d1 --- /dev/null +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNfc.java @@ -0,0 +1,545 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.ios; + +import com.codename1.nfc.HostCardEmulationService; +import com.codename1.nfc.NdefMessage; +import com.codename1.nfc.Nfc; +import com.codename1.nfc.NfcError; +import com.codename1.nfc.NfcException; +import com.codename1.nfc.NfcListener; +import com.codename1.nfc.NfcReadOptions; +import com.codename1.nfc.TagType; +import com.codename1.ui.Display; +import com.codename1.util.AsyncResource; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * iOS implementation of {@link Nfc} backed by Core NFC. NDEF read/write is + * available on iOS 13+; ISO 7816 / MIFARE / FeliCa transceive via + * NFCTagReaderSession on iOS 13+; host card emulation via CardSession on + * iOS 17.4+ EU-only. + * + *

The native side dispatches results back via the static + * {@link #nativeNdefResult(int, byte[])} / + * {@link #nativeTagDiscovered(int, long, int, byte[])} / + * {@link #nativeTransceiveResult(int, byte[])} / + * {@link #nativeNfcError(int, int, String)} methods. The static initializer + * touches each to stop the ParparVM dead-code eliminator from stripping + * them.

+ */ +public final class IOSNfc extends Nfc { + + static { + nativeNdefResult(-1, null); + nativeTagDiscovered(-1, 0L, 0, null); + nativeTransceiveResult(-1, null); + nativeWriteResult(-1, false); + nativeNfcError(-1, 0, null); + nativeHceApdu(-1, null); + nativeHceDeactivated(0); + } + + private static final Map REQUESTS = + new HashMap(); + private static final Map TAGS = new HashMap(); + private static int nextRequestId = 1; + private static volatile HostCardEmulationService hceService; + + private final IOSNative nativeInstance; + private final Set listeners = new HashSet(); + private int activeReadRequestId; + + IOSNfc(IOSNative nativeInstance) { + this.nativeInstance = nativeInstance; + } + + @Override + public boolean isSupported() { + return nativeInstance.isNfcSupported(); + } + + @Override + public boolean canRead() { + return nativeInstance.canReadNfc(); + } + + @Override + public boolean canWrite() { + return nativeInstance.canReadNfcTags(); + } + + @Override + public boolean canHostEmulate() { + return nativeInstance.canHostEmulateNfc(); + } + + @Override + public synchronized AsyncResource readTag(NfcReadOptions options) { + AsyncResource r = + new AsyncResource(); + if (!nativeInstance.isNfcSupported()) { + r.error(new NfcException(NfcError.NOT_AVAILABLE, + "Core NFC is not available on this device")); + return r; + } + NfcReadOptions opts = options != null ? options : new NfcReadOptions(); + int rid = takeId(r); + activeReadRequestId = rid; + String alert = opts.getAlertMessage(); + if (opts.isNdefOnly() || opts.getTechFilter().isEmpty() + || opts.getTechFilter().contains(TagType.NDEF) + && opts.getTechFilter().size() == 1) { + nativeInstance.startNdefRead(rid, alert, opts.getTimeoutMs()); + } else { + int polling = pollingMask(opts); + String[] systemCodes = opts.getFelicaSystemCodes().toArray(new String[0]); + byte[][] aids = opts.getIsoSelectAids().toArray(new byte[0][]); + nativeInstance.startTagRead(rid, alert, polling, systemCodes, + aids, opts.getTimeoutMs()); + } + return r; + } + + @Override + public synchronized boolean stopRead() { + if (activeReadRequestId == 0) { + return false; + } + nativeInstance.stopNfcRead(activeReadRequestId); + Object o = REQUESTS.remove(Integer.valueOf(activeReadRequestId)); + activeReadRequestId = 0; + if (o instanceof AsyncResource) { + AsyncResource ar = (AsyncResource) o; + if (!ar.isDone()) { + ar.error(new NfcException(NfcError.USER_CANCELED, + "NFC read cancelled")); + } + } + return true; + } + + @Override + public synchronized void addTagListener(NfcListener listener) { + if (listener != null) { + listeners.add(listener); + } + } + + @Override + public synchronized void removeTagListener(NfcListener listener) { + listeners.remove(listener); + } + + @Override + public synchronized void registerHostCardEmulationService( + HostCardEmulationService service) { + hceService = service; + if (service == null) { + nativeInstance.registerHceAids(new String[0]); + } else { + String[] aids = service.getAids(); + nativeInstance.registerHceAids(aids != null ? aids : new String[0]); + } + } + + @Override + public synchronized void unregisterHostCardEmulationService() { + hceService = null; + nativeInstance.registerHceAids(new String[0]); + } + + private static int pollingMask(NfcReadOptions opts) { + int mask = 0; + List filter = opts.getTechFilter(); + if (filter.isEmpty()) { + return 1 | 4; // A + F by default + } + for (TagType t : filter) { + switch (t) { + case NFC_A: + case ISO_DEP: + case MIFARE_CLASSIC: + case MIFARE_ULTRALIGHT: + mask |= 1; + break; + case NFC_B: + mask |= 2; + break; + case NFC_F: + mask |= 4; + break; + case NFC_V: + mask |= 8; + break; + default: + break; + } + } + if (mask == 0) { + mask = 1; + } + return mask; + } + + @SuppressWarnings("unchecked") + private static AsyncResource takeAsync(int requestId) { + synchronized (REQUESTS) { + Object o = REQUESTS.remove(Integer.valueOf(requestId)); + return o instanceof AsyncResource ? (AsyncResource) o : null; + } + } + + private static int takeId(Object holder) { + synchronized (REQUESTS) { + int id = nextRequestId++; + REQUESTS.put(Integer.valueOf(id), holder); + return id; + } + } + + static NfcError mapNativeError(int code) { + // NFCReaderError values from Core NFC. + switch (code) { + case 200: // NFCReaderSessionInvalidationErrorUserCanceled + return NfcError.USER_CANCELED; + case 201: // ...SessionTimeout + return NfcError.SYSTEM_CANCELED; + case 202: // ...SystemIsBusy + return NfcError.SYSTEM_CANCELED; + case 203: // ...FirstNDEFTagRead + return NfcError.SYSTEM_CANCELED; + case 204: // ...InvalidParameter + return NfcError.UNKNOWN; + case 100: // NFCReaderTransceiveErrorTagConnectionLost + return NfcError.TAG_LOST; + case 102: // ...RetryExceeded + return NfcError.IO_ERROR; + case 105: // ...PacketTooLong + return NfcError.IO_ERROR; + case 1: // NFCNdefReaderSessionErrorTagNotWritable + return NfcError.READ_ONLY; + case 2: // ...TagSizeTooSmall + return NfcError.CAPACITY_EXCEEDED; + case 3: // ...TagUpdateFailure + return NfcError.IO_ERROR; + case 4: // ...ZeroLengthMessage + return NfcError.INVALID_NDEF; + case 1001: // NFCFeatureUnavailableError + return NfcError.NOT_AVAILABLE; + default: + return NfcError.UNKNOWN; + } + } + + // ---- Callbacks invoked from native code (do not rename) ---------------- + + /** Single-shot NDEF read result -- raw bytes of the NDEF message. */ + public static void nativeNdefResult(final int requestId, final byte[] raw) { + final AsyncResource r = takeAsync(requestId); + if (r == null) { + return; + } + Display.getInstance().callSerially(new Runnable() { + public void run() { + if (r.isDone()) { + return; + } + try { + NdefMessage msg = NdefMessage.parse(raw); + Set types = new HashSet(); + types.add(TagType.NDEF); + r.complete(new IOSTag(0L, types, new byte[0], + msg, true, -1)); + } catch (NfcException e) { + r.error(e); + } + } + }); + } + + /** A tag was discovered during a tag-reader session (ISO / FeliCa / MIFARE). */ + public static void nativeTagDiscovered(final int requestId, + final long handle, final int techMask, final byte[] uid) { + final AsyncResource r = takeAsync(requestId); + if (r == null) { + return; + } + Display.getInstance().callSerially(new Runnable() { + public void run() { + Set types = decodeTechMask(techMask); + IOSTag t = new IOSTag(handle, types, + uid != null ? uid : new byte[0], null, false, -1); + synchronized (TAGS) { + TAGS.put(Long.valueOf(handle), t); + } + if (!r.isDone()) { + r.complete(t); + } + } + }); + } + + /** Result from {@code nfcTransceive}. */ + public static void nativeTransceiveResult(int requestId, final byte[] data) { + final AsyncResource r = takeAsync(requestId); + if (r == null) { + return; + } + Display.getInstance().callSerially(new Runnable() { + public void run() { + if (!r.isDone()) { + r.complete(data != null ? data : new byte[0]); + } + } + }); + } + + /** Result from {@code nfcWriteNdefToTag} / {@code nfcLockTag}. */ + public static void nativeWriteResult(int requestId, final boolean ok) { + final AsyncResource r = takeAsync(requestId); + if (r == null) { + return; + } + Display.getInstance().callSerially(new Runnable() { + public void run() { + if (!r.isDone()) { + r.complete(Boolean.valueOf(ok)); + } + } + }); + } + + /** Error path for any of the async NFC calls. */ + public static void nativeNfcError(final int requestId, final int code, + final String msg) { + final AsyncResource r = takeAsync(requestId); + if (r == null) { + return; + } + final NfcError mapped = mapNativeError(code); + Display.getInstance().callSerially(new Runnable() { + public void run() { + if (!r.isDone()) { + r.error(new NfcException(mapped, + msg == null ? mapped.name() : msg)); + } + } + }); + } + + /** APDU received from a terminal while CardSession is active. The + * application supplied service produces a response and we hand it back + * to the OS via IOSNative.hceSendResponse. */ + public static void nativeHceApdu(final int sessionId, final byte[] apdu) { + final HostCardEmulationService svc = hceService; + if (svc == null) { + return; + } + Display.getInstance().callSerially(new Runnable() { + public void run() { + byte[] resp; + try { + resp = svc.processCommand(apdu); + } catch (Throwable t) { + resp = com.codename1.nfc.ApduResponse.SW_UNKNOWN_ERROR; + } + IOSNfc nfc = (IOSNfc) Display.getInstance().getNfc(); + if (nfc != null) { + nfc.nativeInstance.hceSendResponse(resp != null + ? resp + : com.codename1.nfc.ApduResponse.SW_UNKNOWN_ERROR); + } + } + }); + } + + /** CardSession deactivation callback. */ + public static void nativeHceDeactivated(final int reason) { + final HostCardEmulationService svc = hceService; + if (svc == null) { + return; + } + Display.getInstance().callSerially(new Runnable() { + public void run() { + try { + svc.onDeactivated(reason); + } catch (Throwable ignore) { + } + } + }); + } + + private static Set decodeTechMask(int mask) { + Set types = new HashSet(); + if ((mask & 1) != 0) { + types.add(TagType.NFC_A); + } + if ((mask & 2) != 0) { + types.add(TagType.NFC_F); + } + if ((mask & 4) != 0) { + types.add(TagType.ISO_DEP); + } + if ((mask & 8) != 0) { + types.add(TagType.MIFARE_ULTRALIGHT); + } + if ((mask & 16) != 0) { + types.add(TagType.NDEF); + } + return types; + } + + /** iOS-specific Tag subclass that holds the native session-side + * handle plus optional eager-read NDEF message. */ + static final class IOSTag extends com.codename1.nfc.Tag { + final long handle; + final NdefMessage eagerNdef; + final boolean writable; + final int maxNdefSize; + final IOSIsoDep iso; + + IOSTag(long handle, Set types, byte[] id, + NdefMessage eagerNdef, boolean writable, int maxNdefSize) { + super(types, id); + this.handle = handle; + this.eagerNdef = eagerNdef; + this.writable = writable; + this.maxNdefSize = maxNdefSize; + this.iso = supports(TagType.ISO_DEP) ? new IOSIsoDep(handle) : null; + } + + @Override + public boolean isWritable() { + return writable; + } + + @Override + public int getMaxNdefSize() { + return maxNdefSize; + } + + @Override + public AsyncResource readNdef() { + if (eagerNdef != null) { + AsyncResource r = new AsyncResource(); + r.complete(eagerNdef); + return r; + } + return tagOp(new TagOpInvoker() { + public void run(int rid, long h, IOSNative ni) { + ni.nfcReadNdefFromTag(rid, h); + } + }); + } + + @Override + public AsyncResource writeNdef(final NdefMessage message) { + if (message == null) { + AsyncResource r = new AsyncResource(); + r.error(new NfcException(NfcError.INVALID_NDEF, + "null message")); + return r; + } + return tagOp(new TagOpInvoker() { + public void run(int rid, long h, IOSNative ni) { + ni.nfcWriteNdefToTag(rid, h, message.toByteArray()); + } + }); + } + + @Override + public AsyncResource makeReadOnly() { + return tagOp(new TagOpInvoker() { + public void run(int rid, long h, IOSNative ni) { + ni.nfcLockTag(rid, h); + } + }); + } + + @Override + public com.codename1.nfc.IsoDep getIsoDep() { + return iso; + } + + private AsyncResource tagOp(TagOpInvoker inv) { + AsyncResource r = new AsyncResource(); + if (handle == 0L) { + r.error(new NfcException(NfcError.TAG_LOST, + "tag handle no longer valid")); + return r; + } + IOSNfc nfc = (IOSNfc) Display.getInstance().getNfc(); + if (nfc == null) { + r.error(new NfcException(NfcError.NOT_AVAILABLE, + "NFC not available")); + return r; + } + int rid = takeId(r); + inv.run(rid, handle, nfc.nativeInstance); + return r; + } + } + + interface TagOpInvoker { + void run(int rid, long handle, IOSNative ni); + } + + /** iOS IsoDep view that posts transceive requests to Core NFC's + * NFCISO7816Tag through {@link IOSNative#nfcTransceive(int, long, byte[])}. */ + static final class IOSIsoDep extends com.codename1.nfc.IsoDep { + private final long handle; + + IOSIsoDep(long handle) { + this.handle = handle; + } + + @Override + public AsyncResource transceive(byte[] apdu) { + AsyncResource r = new AsyncResource(); + if (handle == 0L) { + r.error(new NfcException(NfcError.TAG_LOST, + "tag handle no longer valid")); + return r; + } + IOSNfc nfc = (IOSNfc) Display.getInstance().getNfc(); + if (nfc == null) { + r.error(new NfcException(NfcError.NOT_AVAILABLE, + "NFC not available")); + return r; + } + int rid; + synchronized (REQUESTS) { + rid = nextRequestId++; + REQUESTS.put(Integer.valueOf(rid), r); + } + nfc.nativeInstance.nfcTransceive(rid, handle, apdu); + return r; + } + } +} diff --git a/docs/developer-guide/Near-Field-Communication.asciidoc b/docs/developer-guide/Near-Field-Communication.asciidoc new file mode 100644 index 0000000000..c690752230 --- /dev/null +++ b/docs/developer-guide/Near-Field-Communication.asciidoc @@ -0,0 +1,206 @@ +== Near-Field Communication (NFC) + +Codename One ships an NFC API under `com.codename1.nfc` that covers the three things most apps need: read and write NDEF tags, exchange APDUs with ISO 7816 / MIFARE / FeliCa smart cards, and act as a host-emulated card for a nearby reader/terminal. + +A single entry point exposes the platform NFC controller, a typed enum reports the technologies the discovered tag supports, and a typed error enum lets callers react to failures without string matching. + +=== Quick start: Read an NDEF URI + +[source,java] +---- +import com.codename1.nfc.Nfc; +import com.codename1.nfc.NfcReadOptions; +import com.codename1.nfc.NdefMessage; + +Nfc nfc = Nfc.getInstance(); +if (!nfc.canRead()) { + // no NFC hardware, or it is disabled in system settings + return; +} +nfc.readNdef(new NfcReadOptions() + .setNdefOnly(true) + .setAlertMessage("Hold near the poster")) + .onResult((NdefMessage msg, Throwable err) -> { + if (err != null) { + return; + } + String url = msg.getFirstRecord().getUriPayload(); + Display.getInstance().execute(url); + }); +---- + +`setNdefOnly(true)` picks the fastest iOS Core NFC session (`NFCNDEFReaderSession`) and matches the most common tag layout. For mixed payloads use `readTag(...)` and inspect `tag.getTypes()`. + +=== Quick start: Write an NDEF URI + +[source,java] +---- +import com.codename1.nfc.NdefMessage; +import com.codename1.nfc.NdefRecord; + +NdefMessage msg = new NdefMessage( + NdefRecord.createUri("https://codenameone.com"), + NdefRecord.createText("en", "Codename One")); +Nfc.getInstance().writeNdef( + new NfcReadOptions().setAlertMessage("Tap a writable tag"), + msg) + .onResult((Boolean ok, Throwable err) -> { + if (err != null) { + NfcError code = ((NfcException) err).getError(); + // READ_ONLY, CAPACITY_EXCEEDED, INVALID_NDEF, TAG_LOST, ... + } + }); +---- + +NDEF records carry one of the well-known types (`T`, `U`, `Sp`), a MIME media type, an absolute URI, or an external `domain:type`. Use the factory methods on `NdefRecord` rather than constructing raw byte arrays: + +[options="header"] +|=== +| Factory | Use for +| `createUri(String)` | URI records -- launches the associated app on tap +| `createText(String lang, String text)` | Human-readable text +| `createMime(String mimeType, byte[] payload)` | Binary payloads (vCard, JSON, ...) +| `createExternal(String domain, String type, byte[] payload)` | Custom vendor types +| `createApplicationRecord(String pkg)` | Android Application Record (AAR) +|=== + +=== Tag-technology APIs + +Beyond NDEF, the discovered `Tag` exposes accessors for the underlying technologies: + +[source,java] +---- +nfc.readTag(new NfcReadOptions() + .setTechFilter(TagType.ISO_DEP) + .setIsoSelectAids(myAid)) + .onResult((Tag tag, Throwable err) -> { + if (err != null) return; + IsoDep iso = tag.getIsoDep(); + if (iso == null) return; + iso.transceive(myCommandApdu).onResult((byte[] resp, Throwable e) -> { + if (ApduResponse.isSuccess(resp)) { + byte[] body = ApduResponse.body(resp); + // application-specific parsing + } + }); + }); +---- + +Each accessor returns `null` when the tag does not advertise the corresponding technology, so always null-check before calling. + +[options="header"] +|=== +| Accessor | Android | iOS | Notes +| `getIsoDep()` | yes | yes | ISO 7816 -- EMV, ePassport, smart cards +| `getMifareClassic()` | yes | -- | Apple does not expose MIFARE Classic on iOS +| `getMifareUltralight()` | yes | yes | NTAG21x, Ultralight C +| `getNfcA()` | yes | yes (limited) | Raw ISO 14443-3A +| `getNfcB()` | yes | -- | Raw ISO 14443-3B (Android-only) +| `getNfcF()` | yes | yes | FeliCa (Suica / PASMO / ICOCA) +| `getNfcV()` | yes | -- | ISO 15693 (Android-only) +|=== + +For FeliCa on iOS, set the system codes in `NfcReadOptions`: + +[source,java] +---- +new NfcReadOptions() + .setTechFilter(TagType.NFC_F) + .setFelicaSystemCodes("0003", "8008"); +---- + +The Codename One Maven plugin and build daemon automatically register `com.apple.developer.nfc.readersession.felica.systemcodes` from these values. + +=== Quick start: Host card emulation + +Host Card Emulation (HCE) lets the device pretend to be a contactless smart card. A nearby reader (Android phone, payment terminal, access control gate) sends ISO 7816 APDUs and your app responds. Subclass `HostCardEmulationService` and register the instance: + +[source,java] +---- +class MyService extends HostCardEmulationService { + @Override + public String[] getAids() { + return new String[] { "F0010203040506" }; + } + @Override + public byte[] processCommand(byte[] apdu) { + if (apdu.length > 1 && apdu[1] == (byte) 0xA4) { + // SELECT -- terminal has just routed an APDU to our AID + return ApduResponse.withStatus( + new byte[] { 'O', 'K' }, + ApduResponse.SW_SUCCESS); + } + return ApduResponse.SW_INS_NOT_SUPPORTED; + } +} + +Nfc.getInstance().registerHostCardEmulationService(new MyService()); +---- + +Set the AIDs as a build hint so they appear in the platform routing tables: + +[source] +---- +android.hceAids=F0010203040506 +android.hceCategory=other +---- + +The Maven plugin and BuildDaemon generate `apduservice.xml` on Android, register the `CodenameOneHostApduService` in the manifest, and inject the iOS HCE entitlement so the same code path works on both platforms. + +iOS HCE (`CardSession`) requires iOS 17.4+ and, as of 2026, is EU-only. The Codename One plugin still injects the entitlement when you reference the class, but `canHostEmulate()` will report `false` on non-EU devices. + +=== Permissions and build hints + +The Codename One build pipeline auto-detects NFC usage. Apps that never touch `com.codename1.nfc` see no manifest / plist change. + +[options="header"] +|=== +| Reference | Android injected | iOS injected +| `com.codename1.nfc.*` | `android.permission.NFC` + `` | `NFCReaderUsageDescription`, `com.apple.developer.nfc.readersession.formats=TAG\nNDEF`, CoreNFC.framework +| `com.codename1.nfc.HostCardEmulationService` | `BIND_NFC_SERVICE` permission, `android.hardware.nfc.hce` feature, `` entry, generated `res/xml/apduservice.xml` | `com.apple.developer.nfc.hce` and `...hce.iso7816.select-identifiers` entitlements +|=== + +Override any of the defaults with the matching build hint: + +[options="header"] +|=== +| Build hint | Default | Notes +| `ios.NFCReaderUsageDescription` | "Hold near an NFC tag to continue" | Localise this -- Apple rejects builds that ship the default copy +| `ios.entitlements.com.apple.developer.nfc.readersession.formats` | `TAG\nNDEF` | Multi-line entitlement value +| `android.hceAids` | (none) | Comma-separated; required for HCE on Android +| `android.hceCategory` | `other` | `payment` for EMV-conformant apps +| `android.hceRequireUnlock` | `false` | Force lock screen unlock before APDU routing +| `ios.hceAids` | falls back to `android.hceAids` | iOS HCE AID list +|=== + +=== Simulator support + +The JavaSE simulator implements `Nfc` via a virtual tag you can edit and tap from the `Simulate -> NFC` submenu: + +[options="header"] +|=== +| Menu item | Effect +| Hardware Available / NFC Enabled / HCE Available | Toggle what `isSupported()` / `canRead()` / `canHostEmulate()` return +| Next read outcome | Pick `DISCOVER_TAG`, `USER_CANCELED`, `TAG_LOST`, `TIMEOUT`, or `READ_ONLY` for the next `readTag()` call +| Tap virtual tag | Fire the configured outcome on any pending read or registered listener +| Set virtual tag URI... | Replace the virtual tag's NDEF payload with a URI record +| Set virtual tag text... | Replace the virtual tag's NDEF payload with a text record +| Tag is read-only | Lock the virtual tag so the next write fails with `READ_ONLY` +| Send APDU to HCE service... | Hex-entry dialog that dispatches the bytes to your registered `HostCardEmulationService` and shows the response +| Deactivate HCE field | Fires `onDeactivated(DEACTIVATION_LINK_LOSS)` on the registered service +|=== + +All toggles persist between simulator runs (under `NfcSim.*` user preferences) so you can drive tests deterministically. + +=== Platform behaviour + +[options="header"] +|=== +| Platform | NDEF read | NDEF write | Tag tech | HCE | Notes +| Android | yes | yes | full set | yes | `NfcAdapter.enableReaderMode` (API 19+); `HostApduService` (API 19+) +| iOS | yes (iOS 11+) | yes (iOS 13+) | ISO 7816, FeliCa, MIFARE Ultralight | iOS 17.4+ EU-only | MIFARE Classic and NFC-B/V not exposed by Apple +| JavaSE simulator | yes (virtual tag) | yes (virtual tag) | configurable | yes (synthetic APDUs) | Drive everything from the Simulate -> NFC menu +| Desktop deploy / JavaScript | no | no | no | no | Fallback base class -- every method completes with `NfcError.NOT_AVAILABLE` +|=== + +Always gate user-facing affordances on `canRead()` (for tag operations) or `canHostEmulate()` (for HCE). `isSupported()` only reports whether the hardware is present. diff --git a/docs/developer-guide/developer-guide.asciidoc b/docs/developer-guide/developer-guide.asciidoc index 635d2e90d9..41c4f8606f 100644 --- a/docs/developer-guide/developer-guide.asciidoc +++ b/docs/developer-guide/developer-guide.asciidoc @@ -84,6 +84,8 @@ include::security.asciidoc[] include::Biometric-Authentication.asciidoc[] +include::Near-Field-Communication.asciidoc[] + include::signing.asciidoc[] include::Working-With-iOS.asciidoc[] diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java index 0dd4c4b3ea..876487067b 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java @@ -282,6 +282,8 @@ public File getGradleProjectDirectory() { private boolean capturePermission; private boolean usesBiometrics; + private boolean usesNfc; + private boolean usesNfcHce; private boolean vibratePermission; private boolean smsPermission; private boolean gpsPermission; @@ -1252,6 +1254,13 @@ public void usesClass(String cls) { if (cls.indexOf("com/codename1/security/") == 0) { usesBiometrics = true; } + + if (cls.indexOf("com/codename1/nfc/") == 0) { + usesNfc = true; + if (cls.equals("com/codename1/nfc/HostCardEmulationService")) { + usesNfcHce = true; + } + } } @@ -1360,6 +1369,30 @@ public void usesClassMethod(String cls, String method) { } } + // NFC: classpath scanner sets usesNfc / usesNfcHce. Both permissions + // are "normal" so the injection is invisible to the user; apps that + // never reference com.codename1.nfc see no manifest change. HCE + // additionally needs BIND_NFC_SERVICE and android.hardware.nfc.hce + // feature + a registered HostApduService. + if (usesNfc || "true".equalsIgnoreCase(request.getArg("android.hce", "false"))) { + usesNfc = true; + if (!xPermissions.contains("android.permission.NFC")) { + xPermissions = " \n" + xPermissions; + } + if (!xPermissions.contains("android.hardware.nfc")) { + xPermissions = " \n" + xPermissions; + } + } + if (usesNfcHce || request.getArg("android.hceAids", null) != null) { + usesNfcHce = true; + if (!xPermissions.contains("android.permission.BIND_NFC_SERVICE")) { + xPermissions = " \n" + xPermissions; + } + if (!xPermissions.contains("android.hardware.nfc.hce")) { + xPermissions = " \n" + xPermissions; + } + } + boolean useFCM = pushPermission && "fcm".equalsIgnoreCase(request.getArg("android.messagingService", "fcm")); if (useFCM) { request.putArgument("android.fcm.minPlayServicesVersion", "12.0.1"); @@ -2192,6 +2225,67 @@ public void usesClassMethod(String cls, String method) { String backgroundFetchService = "\n"+ "\n"; + // Host card emulation service is generated only when the classpath + // scanner saw a HostCardEmulationService reference (or the developer + // set the android.hceAids build hint). The matching apduservice.xml + // resource is created below. + String hceService = ""; + if (usesNfcHce) { + String hceCategory = request.getArg("android.hceCategory", "other"); + hceService = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n"; + + // apduservice.xml is required by Android to declare the AID + // groups this HCE service answers. Generate it from the + // android.hceAids build hint (comma-separated). Default is a + // sensible placeholder that the developer should override. + String aids = request.getArg("android.hceAids", "F0010203040506"); + String desc = request.getArg("android.hceDescription", + request.getDisplayName()); + StringBuilder apduXml = new StringBuilder(); + apduXml.append("\n"); + apduXml.append(" \n"); + for (String aid : aids.split("[,;]")) { + aid = aid.trim(); + if (aid.length() == 0) { + continue; + } + apduXml.append(" \n"); + } + apduXml.append(" \n"); + apduXml.append("\n"); + File xmlDir = new File(projectDir, "src/main/res/xml"); + xmlDir.mkdirs(); + try { + OutputStream apduStream = new FileOutputStream( + new File(xmlDir, "apduservice.xml")); + apduStream.write(apduXml.toString().getBytes(StandardCharsets.UTF_8)); + apduStream.close(); + } catch (IOException ex) { + throw new BuildException("Failed to write apduservice.xml", ex); + } + debug("Generated apduservice.xml with AIDs: " + aids); + // Suppress the unused warning when desc is not consumed (the + // description string itself is taken from @string/app_name). + if (desc != null) { + debug("HCE description: " + desc); + } + } + if (foregroundServicePermission) { permissions += permissionAdd(request, "\"android.permission.FOREGROUND_SERVICE\"", @@ -2530,6 +2624,7 @@ public void usesClassMethod(String cls, String method) { + locationServices + mediaService + remoteControlService + + hceService + " \n" + " \n" + basePermissions diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java index b48bd94c96..ae2440ea11 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java @@ -84,6 +84,8 @@ public class IPhoneBuilder extends Executor { private boolean usesLocalNotifications; private boolean usesPurchaseAPI; private boolean usesBiometrics; + private boolean usesNfc; + private boolean usesNfcHce; // so we need to store the main class name for later here. // Map will be used for Xcode 8 privacy usage descriptions. Don't need it yet // so leaving it commented out. @@ -650,6 +652,12 @@ public void usesClass(String cls) { if (!usesBiometrics && cls.indexOf("com/codename1/security/") == 0) { usesBiometrics = true; } + if (!usesNfc && cls.indexOf("com/codename1/nfc/") == 0) { + usesNfc = true; + if (cls.equals("com/codename1/nfc/HostCardEmulationService")) { + usesNfcHce = true; + } + } } @Override @@ -1581,6 +1589,84 @@ public void usesClassMethod(String cls, String method) { } } + // CoreNFC is required only when the app actually uses + // com.codename1.nfc. We weak-link it so older deployment targets + // still load on iOS 10 (Core NFC was introduced in iOS 11). + if (usesNfc) { + String coreNfc = "CoreNFC.framework"; + if (addLibs == null || addLibs.length() == 0) { + addLibs = coreNfc; + } else if (!addLibs.toLowerCase().contains("corenfc")) { + addLibs = addLibs + ";" + coreNfc; + } + // Default the NFC reader usage description if the developer + // forgot the plist hint; Apple rejects builds that present + // an NFCNDEFReaderSession without one. + if (request.getArg("ios.NFCReaderUsageDescription", null) == null) { + request.putArgument("ios.NFCReaderUsageDescription", + "Hold near an NFC tag to continue"); + } + // Inject the canonical NFC entitlement keys. The developer + // can override either via build hints. + String formats = request.getArg( + "ios.entitlements.com.apple.developer.nfc.readersession.formats", + null); + if (formats == null) { + request.putArgument( + "ios.entitlements.com.apple.developer.nfc.readersession.formats", + "TAG\nNDEF"); + } + // Uncomment CN1_INCLUDE_NFC in CodenameOne_GLViewController.h + // so the NFC native block in IOSNative.m compiles in. Apps + // that do NOT reference com.codename1.nfc leave the define + // commented out, which means CoreNFC.framework symbols are + // never linked --- this is required to pass Apple's API- + // usage scan without a CoreNFC privacy manifest. + try { + replaceInFile(new File(buildinRes, + "CodenameOne_GLViewController.h"), + "//#define CN1_INCLUDE_NFC", + "#define CN1_INCLUDE_NFC"); + } catch (IOException ex) { + throw new BuildException( + "Failed to enable CN1_INCLUDE_NFC", ex); + } + } + + // HCE on iOS requires the iOS 17.4+ EU-only CardSession + // entitlement plus the AIDs to register. We inject the + // entitlement when the scanner saw HostCardEmulationService. + if (usesNfcHce) { + if (request.getArg( + "ios.entitlements.com.apple.developer.nfc.hce", + null) == null) { + request.putArgument( + "ios.entitlements.com.apple.developer.nfc.hce", + "true"); + } + String aids = request.getArg("ios.hceAids", + request.getArg("android.hceAids", null)); + if (aids != null && aids.length() > 0 + && request.getArg( + "ios.entitlements.com.apple.developer.nfc.hce.iso7816.select-identifiers", + null) == null) { + StringBuilder list = new StringBuilder(); + for (String aid : aids.split("[,;]")) { + aid = aid.trim(); + if (aid.length() == 0) { + continue; + } + if (list.length() > 0) { + list.append("\n"); + } + list.append(aid); + } + request.putArgument( + "ios.entitlements.com.apple.developer.nfc.hce.iso7816.select-identifiers", + list.toString()); + } + } + try { if (!runPods && googleAdUnitId != null && googleAdUnitId.length() > 0) { unzip(getResourceAsStream("/google-play-services_lib-ios.zip"), classesDir, buildinRes, buildinRes); diff --git a/scripts/cn1playground/tools/src/main/java/com/codenameone/playground/tools/GenerateCN1AccessRegistry.java b/scripts/cn1playground/tools/src/main/java/com/codenameone/playground/tools/GenerateCN1AccessRegistry.java index 5c463ca65c..ce5510fc59 100644 --- a/scripts/cn1playground/tools/src/main/java/com/codenameone/playground/tools/GenerateCN1AccessRegistry.java +++ b/scripts/cn1playground/tools/src/main/java/com/codenameone/playground/tools/GenerateCN1AccessRegistry.java @@ -72,7 +72,31 @@ public final class GenerateCN1AccessRegistry { "com.codename1.security.AuthenticationOptions", "com.codename1.security.BiometricType", "com.codename1.security.BiometricError", - "com.codename1.security.BiometricException" + "com.codename1.security.BiometricException", + // com.codename1.nfc.* is a newly-introduced API (Nfc, NdefMessage, + // tag-technology classes, HostCardEmulationService). Same TeaVM + // backend cloud-build limitation as the security package above -- + // exclude until the playground server is updated. Real apps still + // use the API, just not playground scripts. + "com.codename1.nfc.Nfc", + "com.codename1.nfc.NfcException", + "com.codename1.nfc.NfcError", + "com.codename1.nfc.NfcReadOptions", + "com.codename1.nfc.NfcListener", + "com.codename1.nfc.NdefMessage", + "com.codename1.nfc.NdefRecord", + "com.codename1.nfc.Tag", + "com.codename1.nfc.TagType", + "com.codename1.nfc.TagTechnology", + "com.codename1.nfc.IsoDep", + "com.codename1.nfc.MifareClassic", + "com.codename1.nfc.MifareUltralight", + "com.codename1.nfc.NfcA", + "com.codename1.nfc.NfcB", + "com.codename1.nfc.NfcF", + "com.codename1.nfc.NfcV", + "com.codename1.nfc.HostCardEmulationService", + "com.codename1.nfc.ApduResponse" )); private static final String[] INDEX_PACKAGE_PREFIXES = new String[]{ From b2fe08a254e282e48352b0a75290066f45160523 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 18:24:01 +0300 Subject: [PATCH 2/8] Fix xmlDir shadow in AndroidGradleBuilder HCE injection AndroidGradleBuilder.build() already declares a `File xmlDir` near the top of the method (it points to res/xml inside the unzipped sources). The HCE block introduced in the prior commit declared a second `File xmlDir` pointing at src/main/res/xml, which javac rejects as a variable already defined in the method. Rename the HCE-side local to hceXmlDir so it no longer shadows the existing variable. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../java/com/codename1/builders/AndroidGradleBuilder.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java index 876487067b..69c278d3f2 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java @@ -2268,11 +2268,11 @@ public void usesClassMethod(String cls, String method) { } apduXml.append(" \n"); apduXml.append("\n"); - File xmlDir = new File(projectDir, "src/main/res/xml"); - xmlDir.mkdirs(); + File hceXmlDir = new File(projectDir, "src/main/res/xml"); + hceXmlDir.mkdirs(); try { OutputStream apduStream = new FileOutputStream( - new File(xmlDir, "apduservice.xml")); + new File(hceXmlDir, "apduservice.xml")); apduStream.write(apduXml.toString().getBytes(StandardCharsets.UTF_8)); apduStream.close(); } catch (IOException ex) { From 9ba5f1a9093dc8268a5fb532258aba97cc1f2253 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 18:57:41 +0300 Subject: [PATCH 3/8] Use ___R_boolean ParparVM mangling for NFC boolean natives The linker on native-ios CI rejected the NFC build: Undefined symbols for architecture arm64: "_com_codename1_impl_ios_IOSNative_isNfcSupported___R_boolean", referenced from: _com_codename1_impl_ios_IOSNfc_isSupported___R_boolean in com_codename1_impl_ios_IOSNfc.o ParparVM emits the canonical `___R_` suffix on non-void native method symbols generated from recently-introduced Java sources. The IOSBiometrics block predates the convention switch and is kept on the legacy `__` form for compatibility, but newly-added natives have to use the suffix or the link step fails. Rename the four boolean-returning NFC natives: - isNfcSupported__ -> isNfcSupported___R_boolean - canReadNfc__ -> canReadNfc___R_boolean - canReadNfcTags__ -> canReadNfcTags___R_boolean - canHostEmulateNfc__ -> canHostEmulateNfc___R_boolean Void NFC natives (startNdefRead etc.) already use the parameter-only form which is unchanged between the two conventions. Co-Authored-By: Claude Opus 4.7 (1M context) --- Ports/iOSPort/nativeSources/IOSNative.m | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index c1329afd56..88fdfd0f2a 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -11358,7 +11358,12 @@ - (void)tagReaderSession:(NFCTagReaderSession *)session didInvalidateWithError:( @end #endif // CN1_INCLUDE_NFC -JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isNfcSupported__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { +// ParparVM mangles non-void-returning native methods as +// `..._methodName___R_`. Older symbols in this file +// (isMetalRendering__, isBiometricsSupported__) predate the +// convention switch and are kept for binary compatibility; new natives +// must use the suffix or the link step fails with "Undefined symbol". +JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isNfcSupported___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { #ifdef CN1_INCLUDE_NFC if (@available(iOS 11.0, *)) { return [NFCNDEFReaderSession readingAvailable] ? JAVA_TRUE : JAVA_FALSE; @@ -11367,11 +11372,11 @@ JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isNfcSupported__(CN1_THREAD_STATE_ return JAVA_FALSE; } -JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_canReadNfc__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { - return com_codename1_impl_ios_IOSNative_isNfcSupported__(CN1_THREAD_STATE_PASS_ARG me); +JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_canReadNfc___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { + return com_codename1_impl_ios_IOSNative_isNfcSupported___R_boolean(CN1_THREAD_STATE_PASS_ARG me); } -JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_canReadNfcTags__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { +JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_canReadNfcTags___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { #ifdef CN1_INCLUDE_NFC if (@available(iOS 13.0, *)) { return [NFCTagReaderSession readingAvailable] ? JAVA_TRUE : JAVA_FALSE; @@ -11380,7 +11385,7 @@ JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_canReadNfcTags__(CN1_THREAD_STATE_ return JAVA_FALSE; } -JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_canHostEmulateNfc__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { +JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_canHostEmulateNfc___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { #ifdef CN1_INCLUDE_NFC if (@available(iOS 17.4, *)) { // NFCPresentmentIntent etc are still gated by entitlement + EU region. From 25c14be98734175f8376609fd9b9033ff01299f8 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 20:03:21 +0300 Subject: [PATCH 4/8] Fix SpotBugs DM_DEFAULT_ENCODING + Vale Microsoft.Contractions build-test (8) flagged three DM_DEFAULT_ENCODING violations in NdefRecord.toUtf8 / .toAscii / .fromUtf8 -- each catch block was falling back to s.getBytes() / new String(bytes) without specifying an encoding, which SpotBugs treats as platform-dependent and forbids. UTF-8 and US-ASCII are both required by the JLS to be present on every JVM (rt.jar's StandardCharsets enumerates them as required), so the catch is dead code. Match the pattern StringUtil.getBytes already uses in this codebase and throw a RuntimeException instead of falling through to the default encoding. Also fix two Microsoft.Contractions warnings on Near-Field-Communication.asciidoc -- the developer-guide quality gate treats Vale errors as build-breaking. Rewrite "does not" -> "doesn't" on lines 89 and 95 to match the rest of the guide's style. Co-Authored-By: Claude Opus 4.7 (1M context) --- CodenameOne/src/com/codename1/nfc/NdefRecord.java | 11 ++++++++--- .../developer-guide/Near-Field-Communication.asciidoc | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CodenameOne/src/com/codename1/nfc/NdefRecord.java b/CodenameOne/src/com/codename1/nfc/NdefRecord.java index a779ff8804..79d800ec02 100644 --- a/CodenameOne/src/com/codename1/nfc/NdefRecord.java +++ b/CodenameOne/src/com/codename1/nfc/NdefRecord.java @@ -273,7 +273,10 @@ private static byte[] toUtf8(String s) { try { return s.getBytes("UTF-8"); } catch (java.io.UnsupportedEncodingException e) { - return s.getBytes(); + // UTF-8 is required by JLS to be present on every JVM, so this + // branch is unreachable. Throw rather than fall back to the + // platform default encoding (SpotBugs DM_DEFAULT_ENCODING). + throw new RuntimeException(e.toString(), e); } } @@ -281,7 +284,8 @@ private static byte[] toAscii(String s) { try { return s.getBytes("US-ASCII"); } catch (java.io.UnsupportedEncodingException e) { - return s.getBytes(); + // US-ASCII is required by JLS to be present on every JVM. + throw new RuntimeException(e.toString(), e); } } @@ -289,7 +293,8 @@ private static String fromUtf8(byte[] data, int offset, int length) { try { return new String(data, offset, length, "UTF-8"); } catch (java.io.UnsupportedEncodingException e) { - return new String(data, offset, length); + // UTF-8 is required by JLS to be present on every JVM. + throw new RuntimeException(e.toString(), e); } } diff --git a/docs/developer-guide/Near-Field-Communication.asciidoc b/docs/developer-guide/Near-Field-Communication.asciidoc index c690752230..951107e13a 100644 --- a/docs/developer-guide/Near-Field-Communication.asciidoc +++ b/docs/developer-guide/Near-Field-Communication.asciidoc @@ -86,13 +86,13 @@ nfc.readTag(new NfcReadOptions() }); ---- -Each accessor returns `null` when the tag does not advertise the corresponding technology, so always null-check before calling. +Each accessor returns `null` when the tag doesn't advertise the corresponding technology, so always null-check before calling. [options="header"] |=== | Accessor | Android | iOS | Notes | `getIsoDep()` | yes | yes | ISO 7816 -- EMV, ePassport, smart cards -| `getMifareClassic()` | yes | -- | Apple does not expose MIFARE Classic on iOS +| `getMifareClassic()` | yes | -- | Apple doesn't expose MIFARE Classic on iOS | `getMifareUltralight()` | yes | yes | NTAG21x, Ultralight C | `getNfcA()` | yes | yes (limited) | Raw ISO 14443-3A | `getNfcB()` | yes | -- | Raw ISO 14443-3B (Android-only) From 06f1d2bee9ae35ba65ffe1e7e679ce18880373fe Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 20:46:40 +0300 Subject: [PATCH 5/8] Fix SpotBugs SIC_INNER_SHOULD_BE_STATIC_ANON + SSD on static hceService build-test (8) flagged ten SpotBugs violations across the NFC code: - Nfc.readNdef / writeNdef declare anonymous SuccessCallback chains inside instance methods, so javac synthesises an outer-Nfc reference into each ($1, $2, $3, $4) even though none of them touch `this`. Extract the chaining work to chainReadNdef / chainWriteNdef static helpers so the inner classes live in a static context. - AndroidNfc.deliverTag's callSerially Runnable captured a synthetic outer-AndroidNfc reference for the same reason. Hoist the Runnable into a scheduleTagDelivery static helper that takes the snapshot explicitly. - IOSNfc.IOSTag's read/write/lock methods used an anonymous TagOpInvoker functional interface implementation per call. Replace the indirection with a static bindTagOp helper that returns the IOSNative bridge (or rejects the AsyncResource) plus direct invocations from each IOSTag method; the inner interface is gone. - IOSNfc.registerHostCardEmulationService / unregisterHostCardEmulationService were `synchronized` (instance lock) while mutating the static `hceService` field, which would not serialise updates if multiple IOSNfc instances ever existed. Switch to synchronized(IOSNfc.class) so all callers serialise against the same monitor (SpotBugs SSD_DO_NOT_USE_INSTANCE_LOCK_ON_SHARED_STATIC_DATA). Co-Authored-By: Claude Opus 4.7 (1M context) --- CodenameOne/src/com/codename1/nfc/Nfc.java | 34 ++++-- .../codename1/impl/android/AndroidNfc.java | 15 ++- .../src/com/codename1/impl/ios/IOSNfc.java | 103 ++++++++++-------- 3 files changed, 94 insertions(+), 58 deletions(-) diff --git a/CodenameOne/src/com/codename1/nfc/Nfc.java b/CodenameOne/src/com/codename1/nfc/Nfc.java index 804ce84a28..48d80898f4 100644 --- a/CodenameOne/src/com/codename1/nfc/Nfc.java +++ b/CodenameOne/src/com/codename1/nfc/Nfc.java @@ -162,10 +162,18 @@ public AsyncResource readTag(NfcReadOptions options) { /// followed by [Tag#readNdef()]. Resolves with the parsed NDEF /// message, or fails with an [NfcException]. public AsyncResource readNdef(NfcReadOptions options) { - final AsyncResource chained = - new AsyncResource(); - readTag(options).ready(new SuccessCallback() { - public void onSucess(final Tag tag) { + AsyncResource chained = new AsyncResource(); + chainReadNdef(readTag(options), chained); + return chained; + } + + // The anonymous callbacks live inside a static method so they don't + // capture a synthetic outer-Nfc reference (SpotBugs + // SIC_INNER_SHOULD_BE_STATIC_ANON). + private static void chainReadNdef(AsyncResource source, + final AsyncResource chained) { + source.ready(new SuccessCallback() { + public void onSucess(Tag tag) { if (tag == null) { chained.error(new NfcException(NfcError.TAG_LOST, "tag-read produced no tag")); @@ -186,7 +194,6 @@ public void onSucess(Throwable err) { chained.error(err); } }); - return chained; } /// Convenience writer -- opens a tag-read session, writes the given @@ -194,10 +201,18 @@ public void onSucess(Throwable err) { /// [NfcError#READ_ONLY] for locked tags and with /// [NfcError#CAPACITY_EXCEEDED] when the message is too large. public AsyncResource writeNdef(NfcReadOptions options, - final NdefMessage message) { - final AsyncResource chained = - new AsyncResource(); - readTag(options).ready(new SuccessCallback() { + NdefMessage message) { + AsyncResource chained = new AsyncResource(); + chainWriteNdef(readTag(options), message, chained); + return chained; + } + + // Static so the anonymous callbacks don't carry a synthetic outer-Nfc + // reference (SpotBugs SIC_INNER_SHOULD_BE_STATIC_ANON). + private static void chainWriteNdef(AsyncResource source, + final NdefMessage message, + final AsyncResource chained) { + source.ready(new SuccessCallback() { public void onSucess(Tag tag) { if (tag == null) { chained.error(new NfcException(NfcError.TAG_LOST, @@ -219,7 +234,6 @@ public void onSucess(Throwable err) { chained.error(err); } }); - return chained; } /// Cancels any in-flight [#readTag(NfcReadOptions)] / diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidNfc.java b/Ports/Android/src/com/codename1/impl/android/AndroidNfc.java index f04fb988a4..f06cc85d16 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidNfc.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidNfc.java @@ -295,9 +295,9 @@ private void deliverTag(Tag rawTag) { types.add(tt); } } - final AndroidTag wrapped = new AndroidTag(rawTag, types); - final AsyncResource r; - final Set listenerSnapshot; + AndroidTag wrapped = new AndroidTag(rawTag, types); + AsyncResource r; + Set listenerSnapshot; synchronized (this) { r = pendingRead; pendingRead = null; @@ -307,6 +307,15 @@ private void deliverTag(Tag rawTag) { disarmReader(); } } + scheduleTagDelivery(r, wrapped, listenerSnapshot); + } + + // Static so the Runnable does not capture a synthetic outer-AndroidNfc + // reference (SpotBugs SIC_INNER_SHOULD_BE_STATIC_ANON). + private static void scheduleTagDelivery( + final AsyncResource r, + final AndroidTag wrapped, + final Set listenerSnapshot) { Display.getInstance().callSerially(new Runnable() { public void run() { if (r != null) { diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNfc.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNfc.java index ffd95c23d1..0883013380 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNfc.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNfc.java @@ -156,22 +156,30 @@ public synchronized void removeTagListener(NfcListener listener) { listeners.remove(listener); } + // hceService is static (the OS routes APDUs to a single registered + // service, regardless of which IOSNfc instance owns it) so the + // synchronization uses the IOSNfc class lock rather than `this` -- + // SpotBugs SSD_DO_NOT_USE_INSTANCE_LOCK_ON_SHARED_STATIC_DATA. @Override - public synchronized void registerHostCardEmulationService( + public void registerHostCardEmulationService( HostCardEmulationService service) { - hceService = service; - if (service == null) { - nativeInstance.registerHceAids(new String[0]); - } else { - String[] aids = service.getAids(); - nativeInstance.registerHceAids(aids != null ? aids : new String[0]); + synchronized (IOSNfc.class) { + hceService = service; + if (service == null) { + nativeInstance.registerHceAids(new String[0]); + } else { + String[] aids = service.getAids(); + nativeInstance.registerHceAids(aids != null ? aids : new String[0]); + } } } @Override - public synchronized void unregisterHostCardEmulationService() { - hceService = null; - nativeInstance.registerHceAids(new String[0]); + public void unregisterHostCardEmulationService() { + synchronized (IOSNfc.class) { + hceService = null; + nativeInstance.registerHceAids(new String[0]); + } } private static int pollingMask(NfcReadOptions opts) { @@ -451,64 +459,69 @@ public AsyncResource readNdef() { r.complete(eagerNdef); return r; } - return tagOp(new TagOpInvoker() { - public void run(int rid, long h, IOSNative ni) { - ni.nfcReadNdefFromTag(rid, h); - } - }); + AsyncResource r = new AsyncResource(); + IOSNative ni = bindTagOp(handle, r); + if (ni != null) { + ni.nfcReadNdefFromTag(takeId(r), handle); + } + return r; } @Override - public AsyncResource writeNdef(final NdefMessage message) { + public AsyncResource writeNdef(NdefMessage message) { if (message == null) { AsyncResource r = new AsyncResource(); r.error(new NfcException(NfcError.INVALID_NDEF, "null message")); return r; } - return tagOp(new TagOpInvoker() { - public void run(int rid, long h, IOSNative ni) { - ni.nfcWriteNdefToTag(rid, h, message.toByteArray()); - } - }); + AsyncResource r = new AsyncResource(); + IOSNative ni = bindTagOp(handle, r); + if (ni != null) { + ni.nfcWriteNdefToTag(takeId(r), handle, + message.toByteArray()); + } + return r; } @Override public AsyncResource makeReadOnly() { - return tagOp(new TagOpInvoker() { - public void run(int rid, long h, IOSNative ni) { - ni.nfcLockTag(rid, h); - } - }); + AsyncResource r = new AsyncResource(); + IOSNative ni = bindTagOp(handle, r); + if (ni != null) { + ni.nfcLockTag(takeId(r), handle); + } + return r; } @Override public com.codename1.nfc.IsoDep getIsoDep() { return iso; } + } - private AsyncResource tagOp(TagOpInvoker inv) { - AsyncResource r = new AsyncResource(); - if (handle == 0L) { - r.error(new NfcException(NfcError.TAG_LOST, - "tag handle no longer valid")); - return r; - } - IOSNfc nfc = (IOSNfc) Display.getInstance().getNfc(); - if (nfc == null) { - r.error(new NfcException(NfcError.NOT_AVAILABLE, - "NFC not available")); - return r; - } - int rid = takeId(r); - inv.run(rid, handle, nfc.nativeInstance); - return r; + /** + * Validates the tag handle, locates the active IOSNfc and returns its + * native bridge, or rejects the AsyncResource with a typed NfcError and + * returns {@code null}. Lives as a static helper so the IOSTag op + * methods don't need any inner-class indirection (SpotBugs + * SIC_INNER_SHOULD_BE_STATIC_ANON). + */ + static IOSNative bindTagOp(long handle, AsyncResource r) { + if (handle == 0L) { + r.error(new NfcException(NfcError.TAG_LOST, + "tag handle no longer valid")); + return null; + } + IOSNfc nfc = (IOSNfc) Display.getInstance().getNfc(); + if (nfc == null) { + r.error(new NfcException(NfcError.NOT_AVAILABLE, + "NFC not available")); + return null; } + return nfc.nativeInstance; } - interface TagOpInvoker { - void run(int rid, long handle, IOSNative ni); - } /** iOS IsoDep view that posts transceive requests to Core NFC's * NFCISO7816Tag through {@link IOSNative#nfcTransceive(int, long, byte[])}. */ From b9ec55f7e54f22a8b051ea09108aebd30c2b97c6 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 21:24:04 +0300 Subject: [PATCH 6/8] Fix PMD MissingOverride / ForLoopCanBeForeach on new NFC sources PMD flagged five forbidden violations in the NFC core sources: - MissingOverride x 4: the anonymous SuccessCallback / Throwable callbacks inside Nfc.chainReadNdef and Nfc.chainWriteNdef were missing the @Override annotation on each onSucess(...) method. Add @Override on every anonymous implementation (PMD only flagged the outer four but the inner ones are equivalent so they get the annotation too). - ForLoopCanBeForeach: NfcReadOptions.setIsoSelectAids walked the incoming byte[][] with an explicit index; the index was never used on its own. Convert to an enhanced-for loop over the array. The build-test (8) PMD scan is restricted to core sources (per the QUALITY_REPORT_TARGET_DIRS list), so AndroidNfc / IOSNfc / JavaSENfc do not need analogous annotation churn -- the existing Android port uses both styles freely. Co-Authored-By: Claude Opus 4.7 (1M context) --- CodenameOne/src/com/codename1/nfc/Nfc.java | 8 ++++++++ CodenameOne/src/com/codename1/nfc/NfcReadOptions.java | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CodenameOne/src/com/codename1/nfc/Nfc.java b/CodenameOne/src/com/codename1/nfc/Nfc.java index 48d80898f4..b3e06903b6 100644 --- a/CodenameOne/src/com/codename1/nfc/Nfc.java +++ b/CodenameOne/src/com/codename1/nfc/Nfc.java @@ -173,6 +173,7 @@ public AsyncResource readNdef(NfcReadOptions options) { private static void chainReadNdef(AsyncResource source, final AsyncResource chained) { source.ready(new SuccessCallback() { + @Override public void onSucess(Tag tag) { if (tag == null) { chained.error(new NfcException(NfcError.TAG_LOST, @@ -180,16 +181,19 @@ public void onSucess(Tag tag) { return; } tag.readNdef().ready(new SuccessCallback() { + @Override public void onSucess(NdefMessage msg) { chained.complete(msg); } }).except(new SuccessCallback() { + @Override public void onSucess(Throwable err) { chained.error(err); } }); } }).except(new SuccessCallback() { + @Override public void onSucess(Throwable err) { chained.error(err); } @@ -213,6 +217,7 @@ private static void chainWriteNdef(AsyncResource source, final NdefMessage message, final AsyncResource chained) { source.ready(new SuccessCallback() { + @Override public void onSucess(Tag tag) { if (tag == null) { chained.error(new NfcException(NfcError.TAG_LOST, @@ -220,16 +225,19 @@ public void onSucess(Tag tag) { return; } tag.writeNdef(message).ready(new SuccessCallback() { + @Override public void onSucess(Boolean result) { chained.complete(result); } }).except(new SuccessCallback() { + @Override public void onSucess(Throwable err) { chained.error(err); } }); } }).except(new SuccessCallback() { + @Override public void onSucess(Throwable err) { chained.error(err); } diff --git a/CodenameOne/src/com/codename1/nfc/NfcReadOptions.java b/CodenameOne/src/com/codename1/nfc/NfcReadOptions.java index 528a504116..5d691c6012 100644 --- a/CodenameOne/src/com/codename1/nfc/NfcReadOptions.java +++ b/CodenameOne/src/com/codename1/nfc/NfcReadOptions.java @@ -167,8 +167,7 @@ public NfcReadOptions setIsoSelectAids(byte[]... aids) { return this; } List copies = new ArrayList(aids.length); - for (int i = 0; i < aids.length; i++) { - byte[] src = aids[i]; + for (byte[] src : aids) { if (src == null) { continue; } From fb9e02c8359ff0fb99e4f1b6fe60159f588b6662 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 21 May 2026 23:31:14 +0300 Subject: [PATCH 7/8] Make armReader locals explicitly final for -source 1.6 Android build The Ports/Android module compiles with javac.source=1.6 (see Ports/Android/nbproject/project.properties), so locals captured by anonymous inner classes have to be declared final --- Java 8's effectively-final semantics don't apply at -source 1.6. build-test (8) blew up on the Ant-driven Android compile step with four errors against AndroidNfc.armReader: the activity, a (NfcAdapter), flags and extras locals were referenced inside the runOnUiThread Runnable but were not declared final. Add the final modifier on each. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/com/codename1/impl/android/AndroidNfc.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidNfc.java b/Ports/Android/src/com/codename1/impl/android/AndroidNfc.java index f06cc85d16..843ede9599 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidNfc.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidNfc.java @@ -184,15 +184,18 @@ public synchronized void unregisterHostCardEmulationService() { } private void armReader() { - Activity activity = AndroidImplementation.getActivity(); - NfcAdapter a = adapter(); + final Activity activity = AndroidImplementation.getActivity(); + final NfcAdapter a = adapter(); if (activity == null || a == null) { return; } - int flags = computeReaderFlags(pendingOptions); - Bundle extras = new Bundle(); + final int flags = computeReaderFlags(pendingOptions); + final Bundle extras = new Bundle(); // 250 ms presence-check delay is the platform default. extras.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250); + // Ports/Android compiles with -source 1.6, so locals captured by + // anonymous inner classes have to be explicitly declared final + // (Java 8's effectively-final semantics don't apply here). activity.runOnUiThread(new Runnable() { public void run() { a.enableReaderMode(activity, new NfcAdapter.ReaderCallback() { From 9266fd15453017c487655f4de8e1c7b39263b0fe Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 22 May 2026 00:03:43 +0300 Subject: [PATCH 8/8] Replace public static byte[] NFC constants with accessor methods SpotBugs MS_PKGPROTECT (Normal / Low) flagged 14 fields across ApduResponse, MifareClassic and NdefRecord: each was a public static final byte[]. The `final` only protects the reference, not the array contents, so one caller could mutate the shared instance and corrupt every subsequent read of the well-known constant. Convert all 14 to public static accessor methods that return a fresh defensive copy: - NdefRecord.RTD_TEXT / RTD_URI / RTD_SMART_POSTER / RTD_ANDROID_APP -> rtdText() / rtdUri() / rtdSmartPoster() / rtdAndroidApp(). The underlying byte arrays stay as private static finals so the factory methods inside NdefRecord (createText, createUri, createApplicationRecord, getTextPayload, getUriPayload) keep referencing the shared instance directly --- they don't mutate. - ApduResponse.SW_SUCCESS / SW_FILE_NOT_FOUND / SW_INS_NOT_SUPPORTED / SW_CLA_NOT_SUPPORTED / SW_WRONG_LENGTH / SW_SECURITY_NOT_SATISFIED / SW_UNKNOWN_ERROR -> swSuccess() etc. HCE response paths allocate a fresh 2-byte array per call which is negligible against the APDU round-trip cost. - MifareClassic.KEY_DEFAULT / KEY_MIFARE_APPLICATION_DIRECTORY / KEY_NFC_FORUM -> keyDefault() / keyMifareApplicationDirectory() / keyNfcForum(). Each returns a fresh 6-byte copy so an app passing the key to authenticateSectorWithKeyA can't be tripped up if the platform Mifare implementation mutates the input. Update internal callers (CodenameOneHostApduService, IOSNfc.nativeHceApdu, JavaSENfc.simulateApdu / SimIsoDep, Nfc class-level Javadoc, and the HCE example in Near-Field-Communication.asciidoc). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/com/codename1/nfc/ApduResponse.java | 52 +++++++++++++------ .../src/com/codename1/nfc/MifareClassic.java | 42 +++++++++------ .../src/com/codename1/nfc/NdefRecord.java | 34 +++++++++--- CodenameOne/src/com/codename1/nfc/Nfc.java | 2 +- .../android/CodenameOneHostApduService.java | 6 +-- .../com/codename1/impl/javase/JavaSENfc.java | 8 +-- .../src/com/codename1/impl/ios/IOSNfc.java | 4 +- .../Near-Field-Communication.asciidoc | 4 +- 8 files changed, 102 insertions(+), 50 deletions(-) diff --git a/CodenameOne/src/com/codename1/nfc/ApduResponse.java b/CodenameOne/src/com/codename1/nfc/ApduResponse.java index d0e598ba6a..9338045d2c 100644 --- a/CodenameOne/src/com/codename1/nfc/ApduResponse.java +++ b/CodenameOne/src/com/codename1/nfc/ApduResponse.java @@ -27,32 +27,52 @@ /// [HostCardEmulationService] (card mode). public final class ApduResponse { + private ApduResponse() { + } + + // Status-word constants are exposed as accessor methods (returning a + // fresh array each call) rather than public static byte[] fields, + // because mutable static arrays would let one HCE service corrupt the + // shared value for every other caller (SpotBugs MS_PKGPROTECT). Each + // method allocates a 2-byte array on the heap; the cost is negligible + // for HCE APDU response paths. + /// SW = `90 00` -- command succeeded. - public static final byte[] SW_SUCCESS = new byte[] { - (byte) 0x90, (byte) 0x00 }; + public static byte[] swSuccess() { + return new byte[] { (byte) 0x90, (byte) 0x00 }; + } + /// SW = `6A 82` -- file or AID not found. Returned from an HCE /// service's `SELECT` when the requested AID is not the one it /// registered. - public static final byte[] SW_FILE_NOT_FOUND = new byte[] { - (byte) 0x6A, (byte) 0x82 }; + public static byte[] swFileNotFound() { + return new byte[] { (byte) 0x6A, (byte) 0x82 }; + } + /// SW = `6D 00` -- INS not supported. Returned from an HCE service for /// any APDU whose instruction byte is not handled. - public static final byte[] SW_INS_NOT_SUPPORTED = new byte[] { - (byte) 0x6D, (byte) 0x00 }; + public static byte[] swInsNotSupported() { + return new byte[] { (byte) 0x6D, (byte) 0x00 }; + } + /// SW = `6E 00` -- CLA not supported. - public static final byte[] SW_CLA_NOT_SUPPORTED = new byte[] { - (byte) 0x6E, (byte) 0x00 }; + public static byte[] swClaNotSupported() { + return new byte[] { (byte) 0x6E, (byte) 0x00 }; + } + /// SW = `67 00` -- wrong length / Lc. - public static final byte[] SW_WRONG_LENGTH = new byte[] { - (byte) 0x67, (byte) 0x00 }; + public static byte[] swWrongLength() { + return new byte[] { (byte) 0x67, (byte) 0x00 }; + } + /// SW = `69 82` -- security condition not satisfied. - public static final byte[] SW_SECURITY_NOT_SATISFIED = new byte[] { - (byte) 0x69, (byte) 0x82 }; - /// SW = `6F 00` -- unknown / generic failure. - public static final byte[] SW_UNKNOWN_ERROR = new byte[] { - (byte) 0x6F, (byte) 0x00 }; + public static byte[] swSecurityNotSatisfied() { + return new byte[] { (byte) 0x69, (byte) 0x82 }; + } - private ApduResponse() { + /// SW = `6F 00` -- unknown / generic failure. + public static byte[] swUnknownError() { + return new byte[] { (byte) 0x6F, (byte) 0x00 }; } /// `true` when the trailing two bytes of `apdu` are `90 00`. diff --git a/CodenameOne/src/com/codename1/nfc/MifareClassic.java b/CodenameOne/src/com/codename1/nfc/MifareClassic.java index fd1d4c3a79..b0538d741d 100644 --- a/CodenameOne/src/com/codename1/nfc/MifareClassic.java +++ b/CodenameOne/src/com/codename1/nfc/MifareClassic.java @@ -36,23 +36,35 @@ /// untransitioned demo / blank cards. public class MifareClassic extends TagTechnology { - /// Default MIFARE Classic key A used by NXP shipping cards. - public static final byte[] KEY_DEFAULT = new byte[] { - (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, - (byte) 0xFF, (byte) 0xFF, (byte) 0xFF - }; + // Well-known MIFARE Classic keys are returned via accessors so callers + // can't mutate a shared instance (SpotBugs MS_PKGPROTECT). - /// MIFARE Application Directory (MAD) key A from NXP AN10787. - public static final byte[] KEY_MIFARE_APPLICATION_DIRECTORY = new byte[] { - (byte) 0xA0, (byte) 0xA1, (byte) 0xA2, - (byte) 0xA3, (byte) 0xA4, (byte) 0xA5 - }; + /// Default MIFARE Classic key A used by NXP shipping cards + /// (`FF FF FF FF FF FF`). Returns a fresh defensive copy. + public static byte[] keyDefault() { + return new byte[] { + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF + }; + } + + /// MIFARE Application Directory (MAD) key A from NXP AN10787 + /// (`A0 A1 A2 A3 A4 A5`). Returns a fresh defensive copy. + public static byte[] keyMifareApplicationDirectory() { + return new byte[] { + (byte) 0xA0, (byte) 0xA1, (byte) 0xA2, + (byte) 0xA3, (byte) 0xA4, (byte) 0xA5 + }; + } - /// NFC Forum key A for NDEF-formatted MIFARE Classic blocks. - public static final byte[] KEY_NFC_FORUM = new byte[] { - (byte) 0xD3, (byte) 0xF7, (byte) 0xD3, - (byte) 0xF7, (byte) 0xD3, (byte) 0xF7 - }; + /// NFC Forum key A for NDEF-formatted MIFARE Classic blocks + /// (`D3 F7 D3 F7 D3 F7`). Returns a fresh defensive copy. + public static byte[] keyNfcForum() { + return new byte[] { + (byte) 0xD3, (byte) 0xF7, (byte) 0xD3, + (byte) 0xF7, (byte) 0xD3, (byte) 0xF7 + }; + } /// Total sectors on the tag (16 on Classic 1K, 40 on Classic 4K). public int getSectorCount() { diff --git a/CodenameOne/src/com/codename1/nfc/NdefRecord.java b/CodenameOne/src/com/codename1/nfc/NdefRecord.java index 79d800ec02..c866b1c313 100644 --- a/CodenameOne/src/com/codename1/nfc/NdefRecord.java +++ b/CodenameOne/src/com/codename1/nfc/NdefRecord.java @@ -41,7 +41,7 @@ public final class NdefRecord { /// TNF (Type Name Format) -- record contains no payload. public static final byte TNF_EMPTY = 0x00; /// TNF -- type is one of the NFC Forum well-known types (e.g. `T`, `U`, - /// `Sp`). See [#RTD_TEXT], [#RTD_URI]. + /// `Sp`). See [#rtdText()], [#rtdUri()]. public static final byte TNF_WELL_KNOWN = 0x01; /// TNF -- type is a MIME media type (RFC 2046). public static final byte TNF_MIME_MEDIA = 0x02; @@ -55,16 +55,36 @@ public final class NdefRecord { /// TNF -- continuation of a chunked record (rare). public static final byte TNF_UNCHANGED = 0x06; - /// Record Type Definition (RTD) for well-known text records. - public static final byte[] RTD_TEXT = new byte[] { 'T' }; + // RTD bytes are kept as private fields so external callers can't + // mutate the shared instance (SpotBugs MS_PKGPROTECT); the public + // accessor methods below each return a fresh defensive copy. + private static final byte[] RTD_TEXT = new byte[] { 'T' }; + private static final byte[] RTD_URI = new byte[] { 'U' }; + private static final byte[] RTD_SMART_POSTER = new byte[] { 'S', 'p' }; + private static final byte[] RTD_ANDROID_APP = new byte[] { 'a', 'n', 'd', + 'r', 'o', 'i', 'd', '.', 'c', 'o', 'm', ':', 'p', 'k', 'g' }; + + /// Record Type Definition (RTD) for well-known text records. Returns + /// a defensive copy so callers cannot mutate the shared constant. + public static byte[] rtdText() { + return clone(RTD_TEXT); + } + /// RTD for well-known URI records. - public static final byte[] RTD_URI = new byte[] { 'U' }; + public static byte[] rtdUri() { + return clone(RTD_URI); + } + /// RTD for SmartPoster (URI + title). - public static final byte[] RTD_SMART_POSTER = new byte[] { 'S', 'p' }; + public static byte[] rtdSmartPoster() { + return clone(RTD_SMART_POSTER); + } + /// RTD for Android Application Record (external type /// `android.com:pkg`). - public static final byte[] RTD_ANDROID_APP = new byte[] { 'a', 'n', 'd', - 'r', 'o', 'i', 'd', '.', 'c', 'o', 'm', ':', 'p', 'k', 'g' }; + public static byte[] rtdAndroidApp() { + return clone(RTD_ANDROID_APP); + } private final byte tnf; private final byte[] type; diff --git a/CodenameOne/src/com/codename1/nfc/Nfc.java b/CodenameOne/src/com/codename1/nfc/Nfc.java index b3e06903b6..ce8d7f738f 100644 --- a/CodenameOne/src/com/codename1/nfc/Nfc.java +++ b/CodenameOne/src/com/codename1/nfc/Nfc.java @@ -78,7 +78,7 @@ /// public String[] getAids() { return new String[] { "F0010203040506" }; } /// public byte[] processCommand(byte[] apdu) { /// return ApduResponse.withStatus(new byte[] { 'O', 'K' }, -/// ApduResponse.SW_SUCCESS); +/// ApduResponse.swSuccess()); /// } /// } /// Nfc.getInstance().registerHostCardEmulationService(new MyService()); diff --git a/Ports/Android/src/com/codename1/impl/android/CodenameOneHostApduService.java b/Ports/Android/src/com/codename1/impl/android/CodenameOneHostApduService.java index ad09df5622..3d0453d714 100644 --- a/Ports/Android/src/com/codename1/impl/android/CodenameOneHostApduService.java +++ b/Ports/Android/src/com/codename1/impl/android/CodenameOneHostApduService.java @@ -51,13 +51,13 @@ static void bind(HostCardEmulationService svc) { public byte[] processCommandApdu(byte[] apdu, Bundle extras) { HostCardEmulationService d = delegate; if (d == null) { - return ApduResponse.SW_FILE_NOT_FOUND; + return ApduResponse.swFileNotFound(); } try { byte[] resp = d.processCommand(apdu); - return resp != null ? resp : ApduResponse.SW_UNKNOWN_ERROR; + return resp != null ? resp : ApduResponse.swUnknownError(); } catch (Throwable t) { - return ApduResponse.SW_UNKNOWN_ERROR; + return ApduResponse.swUnknownError(); } } diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSENfc.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSENfc.java index 7b4c236ca6..184b7e6d19 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSENfc.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSENfc.java @@ -237,7 +237,7 @@ public byte[] simulateApdu(byte[] command) { } lastHceCommand = copyOf(command); if (svc == null) { - byte[] resp = ApduResponse.SW_FILE_NOT_FOUND; + byte[] resp = ApduResponse.swFileNotFound(); lastHceResponse = copyOf(resp); return resp; } @@ -245,10 +245,10 @@ public byte[] simulateApdu(byte[] command) { try { resp = svc.processCommand(command); if (resp == null) { - resp = ApduResponse.SW_UNKNOWN_ERROR; + resp = ApduResponse.swUnknownError(); } } catch (Throwable t) { - resp = ApduResponse.SW_UNKNOWN_ERROR; + resp = ApduResponse.swUnknownError(); } lastHceResponse = copyOf(resp); return resp; @@ -352,7 +352,7 @@ static final class SimIsoDep extends com.codename1.nfc.IsoDep { public AsyncResource transceive(byte[] apdu) { AsyncResource r = new AsyncResource(); byte[] body = apdu != null ? apdu : new byte[0]; - r.complete(ApduResponse.withStatus(body, ApduResponse.SW_SUCCESS)); + r.complete(ApduResponse.withStatus(body, ApduResponse.swSuccess())); return r; } } diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNfc.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNfc.java index 0883013380..ddbd4639c0 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNfc.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNfc.java @@ -375,13 +375,13 @@ public void run() { try { resp = svc.processCommand(apdu); } catch (Throwable t) { - resp = com.codename1.nfc.ApduResponse.SW_UNKNOWN_ERROR; + resp = com.codename1.nfc.ApduResponse.swUnknownError(); } IOSNfc nfc = (IOSNfc) Display.getInstance().getNfc(); if (nfc != null) { nfc.nativeInstance.hceSendResponse(resp != null ? resp - : com.codename1.nfc.ApduResponse.SW_UNKNOWN_ERROR); + : com.codename1.nfc.ApduResponse.swUnknownError()); } } }); diff --git a/docs/developer-guide/Near-Field-Communication.asciidoc b/docs/developer-guide/Near-Field-Communication.asciidoc index 951107e13a..8af2c8ca14 100644 --- a/docs/developer-guide/Near-Field-Communication.asciidoc +++ b/docs/developer-guide/Near-Field-Communication.asciidoc @@ -128,9 +128,9 @@ class MyService extends HostCardEmulationService { // SELECT -- terminal has just routed an APDU to our AID return ApduResponse.withStatus( new byte[] { 'O', 'K' }, - ApduResponse.SW_SUCCESS); + ApduResponse.swSuccess()); } - return ApduResponse.SW_INS_NOT_SUPPORTED; + return ApduResponse.swInsNotSupported(); } }