diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index c38f7514ff..e0a7a7f52d 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..9338045d2c --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/ApduResponse.java @@ -0,0 +1,125 @@ +/* + * 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 { + + 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 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 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 byte[] swInsNotSupported() { + return new byte[] { (byte) 0x6D, (byte) 0x00 }; + } + + /// SW = `6E 00` -- CLA not supported. + public static byte[] swClaNotSupported() { + return new byte[] { (byte) 0x6E, (byte) 0x00 }; + } + + /// SW = `67 00` -- wrong length / Lc. + public static byte[] swWrongLength() { + return new byte[] { (byte) 0x67, (byte) 0x00 }; + } + + /// SW = `69 82` -- security condition not satisfied. + public static byte[] swSecurityNotSatisfied() { + return new byte[] { (byte) 0x69, (byte) 0x82 }; + } + + /// 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`. + 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..b0538d741d --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/MifareClassic.java @@ -0,0 +1,128 @@ +/* + * 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 { + + // Well-known MIFARE Classic keys are returned via accessors so callers + // can't mutate a shared instance (SpotBugs MS_PKGPROTECT). + + /// 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 + /// (`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() { + 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..c866b1c313 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/NdefRecord.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.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 [#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; + /// 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; + + // 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 byte[] rtdUri() { + return clone(RTD_URI); + } + + /// RTD for SmartPoster (URI + title). + public static byte[] rtdSmartPoster() { + return clone(RTD_SMART_POSTER); + } + + /// RTD for Android Application Record (external type + /// `android.com:pkg`). + public static byte[] rtdAndroidApp() { + return clone(RTD_ANDROID_APP); + } + + 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) { + // 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); + } + } + + private static byte[] toAscii(String s) { + try { + return s.getBytes("US-ASCII"); + } catch (java.io.UnsupportedEncodingException e) { + // US-ASCII is required by JLS to be present on every JVM. + throw new RuntimeException(e.toString(), e); + } + } + + private static String fromUtf8(byte[] data, int offset, int length) { + try { + return new String(data, offset, length, "UTF-8"); + } catch (java.io.UnsupportedEncodingException e) { + // UTF-8 is required by JLS to be present on every JVM. + throw new RuntimeException(e.toString(), e); + } + } + + 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..ce8d7f738f --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/Nfc.java @@ -0,0 +1,298 @@ +/* + * 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.swSuccess()); +/// } +/// } +/// 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) { + 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() { + @Override + public void onSucess(Tag tag) { + if (tag == null) { + chained.error(new NfcException(NfcError.TAG_LOST, + "tag-read produced no 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); + } + }); + } + + /// 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, + 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() { + @Override + 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() { + @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); + } + }); + } + + /// 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..5d691c6012 --- /dev/null +++ b/CodenameOne/src/com/codename1/nfc/NfcReadOptions.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 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 (byte[] src : aids) { + 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 630039d20c..3053239028 100644 --- a/CodenameOne/src/com/codename1/ui/Display.java +++ b/CodenameOne/src/com/codename1/ui/Display.java @@ -4311,6 +4311,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 8bc2539018..bf146bda53 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..843ede9599 --- /dev/null +++ b/Ports/Android/src/com/codename1/impl/android/AndroidNfc.java @@ -0,0 +1,591 @@ +/* + * 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() { + final Activity activity = AndroidImplementation.getActivity(); + final NfcAdapter a = adapter(); + if (activity == null || a == null) { + return; + } + 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() { + 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); + } + } + AndroidTag wrapped = new AndroidTag(rawTag, types); + AsyncResource r; + Set listenerSnapshot; + synchronized (this) { + r = pendingRead; + pendingRead = null; + pendingOptions = null; + listenerSnapshot = new HashSet(listeners); + if (listeners.isEmpty()) { + 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) { + 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..3d0453d714 --- /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.swFileNotFound(); + } + try { + byte[] resp = d.processCommand(apdu); + return resp != null ? resp : ApduResponse.swUnknownError(); + } catch (Throwable t) { + return ApduResponse.swUnknownError(); + } + } + + @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..184b7e6d19 --- /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.swFileNotFound(); + lastHceResponse = copyOf(resp); + return resp; + } + byte[] resp; + try { + resp = svc.processCommand(command); + if (resp == null) { + resp = ApduResponse.swUnknownError(); + } + } catch (Throwable t) { + resp = ApduResponse.swUnknownError(); + } + 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.swSuccess())); + 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 3d31220c4e..0a9c8d6aa2 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 2c9c651385..f1c6497a6c 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" @@ -11302,6 +11303,478 @@ 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 + +// 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; + } +#endif + return JAVA_FALSE; +} + +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___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; + } +#endif + return JAVA_FALSE; +} + +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. + 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. +} + // ==================================================================== // Crypto bridge _R_int wrappers // diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index e8e071a380..268ed0d0aa 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 d45895e8ea..6d18c8decf 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..ddbd4639c0 --- /dev/null +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNfc.java @@ -0,0 +1,558 @@ +/* + * 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); + } + + // 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 void registerHostCardEmulationService( + HostCardEmulationService service) { + 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 void unregisterHostCardEmulationService() { + synchronized (IOSNfc.class) { + 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.swUnknownError(); + } + IOSNfc nfc = (IOSNfc) Display.getInstance().getNfc(); + if (nfc != null) { + nfc.nativeInstance.hceSendResponse(resp != null + ? resp + : com.codename1.nfc.ApduResponse.swUnknownError()); + } + } + }); + } + + /** 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; + } + AsyncResource r = new AsyncResource(); + IOSNative ni = bindTagOp(handle, r); + if (ni != null) { + ni.nfcReadNdefFromTag(takeId(r), handle); + } + return r; + } + + @Override + public AsyncResource writeNdef(NdefMessage message) { + if (message == null) { + AsyncResource r = new AsyncResource(); + r.error(new NfcException(NfcError.INVALID_NDEF, + "null message")); + return r; + } + 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() { + 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; + } + } + + /** + * 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; + } + + + /** 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..8af2c8ca14 --- /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 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 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) +| `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.swSuccess()); + } + return ApduResponse.swInsNotSupported(); + } +} + +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..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 @@ -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 hceXmlDir = new File(projectDir, "src/main/res/xml"); + hceXmlDir.mkdirs(); + try { + OutputStream apduStream = new FileOutputStream( + new File(hceXmlDir, "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 109203720e..9a019090ad 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 @@ -86,6 +86,8 @@ public class IPhoneBuilder extends Executor { private boolean usesCryptoAPI; private boolean usesCryptoGcm; 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. @@ -667,6 +669,12 @@ public void usesClass(String cls) { usesCryptoAPI = true; } } + if (!usesNfc && cls.indexOf("com/codename1/nfc/") == 0) { + usesNfc = true; + if (cls.equals("com/codename1/nfc/HostCardEmulationService")) { + usesNfcHce = true; + } + } } @Override @@ -1620,6 +1628,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 394e5eea1b..58368cadca 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 @@ -91,7 +91,31 @@ public final class GenerateCN1AccessRegistry { "com.codename1.security.Otp", "com.codename1.security.Base32", "com.codename1.security.Base64Url", - "com.codename1.security.CryptoException" + "com.codename1.security.CryptoException", + // 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[]{