From 467a55fa6f4dffa8a8c0af84bbcff6e94b763007 Mon Sep 17 00:00:00 2001 From: Roshan Pius Date: Mon, 13 Nov 2023 14:29:09 -0800 Subject: nfc(api): Move nfc classes to framework-nfc Splits out the NFC API classes that are going to be part of NFC mainline module. Note: These files will be eventually moved out to packages/modules/Nfc at some point. Bug: 303286040 Test: Device boots up after flashing Test: atest CtsNfcTestCases Merged-In: I41c1146401236963b9fd83f214fed0b6cecf325e Change-Id: I41c1146401236963b9fd83f214fed0b6cecf325e --- nfc/java/android/nfc/ApduList.aidl | 19 + nfc/java/android/nfc/ApduList.java | 68 + nfc/java/android/nfc/AvailableNfcAntenna.aidl | 19 + nfc/java/android/nfc/AvailableNfcAntenna.java | 121 + nfc/java/android/nfc/Constants.java | 29 + nfc/java/android/nfc/ErrorCodes.java | 114 + nfc/java/android/nfc/FormatException.java | 31 + nfc/java/android/nfc/IAppCallback.aidl | 27 + nfc/java/android/nfc/INfcAdapter.aidl | 90 + nfc/java/android/nfc/INfcAdapterExtras.aidl | 40 + nfc/java/android/nfc/INfcCardEmulation.aidl | 49 + .../nfc/INfcControllerAlwaysOnListener.aidl | 29 + nfc/java/android/nfc/INfcDta.aidl | 34 + nfc/java/android/nfc/INfcFCardEmulation.aidl | 36 + nfc/java/android/nfc/INfcTag.aidl | 50 + nfc/java/android/nfc/INfcUnlockHandler.aidl | 12 + nfc/java/android/nfc/ITagRemovedCallback.aidl | 8 + nfc/java/android/nfc/NdefMessage.aidl | 19 + nfc/java/android/nfc/NdefMessage.java | 272 ++ nfc/java/android/nfc/NdefRecord.aidl | 19 + nfc/java/android/nfc/NdefRecord.java | 1080 ++++++++ nfc/java/android/nfc/NfcActivityManager.java | 410 +++ nfc/java/android/nfc/NfcAdapter.java | 2749 ++++++++++++++++++++ nfc/java/android/nfc/NfcAntennaInfo.aidl | 19 + nfc/java/android/nfc/NfcAntennaInfo.java | 117 + .../android/nfc/NfcControllerAlwaysOnListener.java | 136 + nfc/java/android/nfc/NfcEvent.java | 56 + nfc/java/android/nfc/NfcFrameworkInitializer.java | 71 + nfc/java/android/nfc/NfcManager.java | 73 + nfc/java/android/nfc/NfcServiceManager.java | 126 + nfc/java/android/nfc/Tag.aidl | 19 + nfc/java/android/nfc/Tag.java | 508 ++++ nfc/java/android/nfc/TagLostException.java | 29 + nfc/java/android/nfc/TechListParcel.aidl | 19 + nfc/java/android/nfc/TechListParcel.java | 66 + nfc/java/android/nfc/TransceiveResult.aidl | 19 + nfc/java/android/nfc/TransceiveResult.java | 93 + nfc/java/android/nfc/cardemulation/AidGroup.aidl | 19 + nfc/java/android/nfc/cardemulation/AidGroup.java | 298 +++ .../android/nfc/cardemulation/ApduServiceInfo.aidl | 19 + .../android/nfc/cardemulation/ApduServiceInfo.java | 912 +++++++ .../android/nfc/cardemulation/CardEmulation.java | 1087 ++++++++ .../android/nfc/cardemulation/HostApduService.java | 519 ++++ .../android/nfc/cardemulation/HostNfcFService.java | 279 ++ .../nfc/cardemulation/NfcFCardEmulation.java | 473 ++++ .../android/nfc/cardemulation/NfcFServiceInfo.aidl | 19 + .../android/nfc/cardemulation/NfcFServiceInfo.java | 497 ++++ nfc/java/android/nfc/cardemulation/OWNERS | 2 + .../nfc/cardemulation/OffHostApduService.java | 170 ++ nfc/java/android/nfc/cardemulation/Utils.java | 37 + nfc/java/android/nfc/dta/NfcDta.java | 167 ++ nfc/java/android/nfc/dta/OWNERS | 2 + nfc/java/android/nfc/flags.aconfig | 64 + nfc/java/android/nfc/package.html | 33 + nfc/java/android/nfc/tech/BasicTagTechnology.java | 161 ++ nfc/java/android/nfc/tech/IsoDep.java | 209 ++ nfc/java/android/nfc/tech/MifareClassic.java | 655 +++++ nfc/java/android/nfc/tech/MifareUltralight.java | 280 ++ nfc/java/android/nfc/tech/Ndef.java | 408 +++ nfc/java/android/nfc/tech/NdefFormatable.java | 182 ++ nfc/java/android/nfc/tech/NfcA.java | 173 ++ nfc/java/android/nfc/tech/NfcB.java | 125 + nfc/java/android/nfc/tech/NfcBarcode.java | 130 + nfc/java/android/nfc/tech/NfcF.java | 177 ++ nfc/java/android/nfc/tech/NfcV.java | 126 + nfc/java/android/nfc/tech/OWNERS | 2 + nfc/java/android/nfc/tech/TagTechnology.java | 224 ++ nfc/java/android/nfc/tech/package.html | 13 + 68 files changed, 14138 insertions(+) create mode 100644 nfc/java/android/nfc/ApduList.aidl create mode 100644 nfc/java/android/nfc/ApduList.java create mode 100644 nfc/java/android/nfc/AvailableNfcAntenna.aidl create mode 100644 nfc/java/android/nfc/AvailableNfcAntenna.java create mode 100644 nfc/java/android/nfc/Constants.java create mode 100644 nfc/java/android/nfc/ErrorCodes.java create mode 100644 nfc/java/android/nfc/FormatException.java create mode 100644 nfc/java/android/nfc/IAppCallback.aidl create mode 100644 nfc/java/android/nfc/INfcAdapter.aidl create mode 100644 nfc/java/android/nfc/INfcAdapterExtras.aidl create mode 100644 nfc/java/android/nfc/INfcCardEmulation.aidl create mode 100644 nfc/java/android/nfc/INfcControllerAlwaysOnListener.aidl create mode 100644 nfc/java/android/nfc/INfcDta.aidl create mode 100644 nfc/java/android/nfc/INfcFCardEmulation.aidl create mode 100644 nfc/java/android/nfc/INfcTag.aidl create mode 100644 nfc/java/android/nfc/INfcUnlockHandler.aidl create mode 100644 nfc/java/android/nfc/ITagRemovedCallback.aidl create mode 100644 nfc/java/android/nfc/NdefMessage.aidl create mode 100644 nfc/java/android/nfc/NdefMessage.java create mode 100644 nfc/java/android/nfc/NdefRecord.aidl create mode 100644 nfc/java/android/nfc/NdefRecord.java create mode 100644 nfc/java/android/nfc/NfcActivityManager.java create mode 100644 nfc/java/android/nfc/NfcAdapter.java create mode 100644 nfc/java/android/nfc/NfcAntennaInfo.aidl create mode 100644 nfc/java/android/nfc/NfcAntennaInfo.java create mode 100644 nfc/java/android/nfc/NfcControllerAlwaysOnListener.java create mode 100644 nfc/java/android/nfc/NfcEvent.java create mode 100644 nfc/java/android/nfc/NfcFrameworkInitializer.java create mode 100644 nfc/java/android/nfc/NfcManager.java create mode 100644 nfc/java/android/nfc/NfcServiceManager.java create mode 100644 nfc/java/android/nfc/Tag.aidl create mode 100644 nfc/java/android/nfc/Tag.java create mode 100644 nfc/java/android/nfc/TagLostException.java create mode 100644 nfc/java/android/nfc/TechListParcel.aidl create mode 100644 nfc/java/android/nfc/TechListParcel.java create mode 100644 nfc/java/android/nfc/TransceiveResult.aidl create mode 100644 nfc/java/android/nfc/TransceiveResult.java create mode 100644 nfc/java/android/nfc/cardemulation/AidGroup.aidl create mode 100644 nfc/java/android/nfc/cardemulation/AidGroup.java create mode 100644 nfc/java/android/nfc/cardemulation/ApduServiceInfo.aidl create mode 100644 nfc/java/android/nfc/cardemulation/ApduServiceInfo.java create mode 100644 nfc/java/android/nfc/cardemulation/CardEmulation.java create mode 100644 nfc/java/android/nfc/cardemulation/HostApduService.java create mode 100644 nfc/java/android/nfc/cardemulation/HostNfcFService.java create mode 100644 nfc/java/android/nfc/cardemulation/NfcFCardEmulation.java create mode 100644 nfc/java/android/nfc/cardemulation/NfcFServiceInfo.aidl create mode 100644 nfc/java/android/nfc/cardemulation/NfcFServiceInfo.java create mode 100644 nfc/java/android/nfc/cardemulation/OWNERS create mode 100644 nfc/java/android/nfc/cardemulation/OffHostApduService.java create mode 100644 nfc/java/android/nfc/cardemulation/Utils.java create mode 100644 nfc/java/android/nfc/dta/NfcDta.java create mode 100644 nfc/java/android/nfc/dta/OWNERS create mode 100644 nfc/java/android/nfc/flags.aconfig create mode 100644 nfc/java/android/nfc/package.html create mode 100644 nfc/java/android/nfc/tech/BasicTagTechnology.java create mode 100644 nfc/java/android/nfc/tech/IsoDep.java create mode 100644 nfc/java/android/nfc/tech/MifareClassic.java create mode 100644 nfc/java/android/nfc/tech/MifareUltralight.java create mode 100644 nfc/java/android/nfc/tech/Ndef.java create mode 100644 nfc/java/android/nfc/tech/NdefFormatable.java create mode 100644 nfc/java/android/nfc/tech/NfcA.java create mode 100644 nfc/java/android/nfc/tech/NfcB.java create mode 100644 nfc/java/android/nfc/tech/NfcBarcode.java create mode 100644 nfc/java/android/nfc/tech/NfcF.java create mode 100644 nfc/java/android/nfc/tech/NfcV.java create mode 100644 nfc/java/android/nfc/tech/OWNERS create mode 100644 nfc/java/android/nfc/tech/TagTechnology.java create mode 100644 nfc/java/android/nfc/tech/package.html (limited to 'nfc/java/android') diff --git a/nfc/java/android/nfc/ApduList.aidl b/nfc/java/android/nfc/ApduList.aidl new file mode 100644 index 000000000000..f6236b2bfb3b --- /dev/null +++ b/nfc/java/android/nfc/ApduList.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +parcelable ApduList; \ No newline at end of file diff --git a/nfc/java/android/nfc/ApduList.java b/nfc/java/android/nfc/ApduList.java new file mode 100644 index 000000000000..027141d99c30 --- /dev/null +++ b/nfc/java/android/nfc/ApduList.java @@ -0,0 +1,68 @@ +package android.nfc; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.List; + +/** + * @hide + */ +public class ApduList implements Parcelable { + + private ArrayList commands = new ArrayList(); + + public ApduList() { + } + + public void add(byte[] command) { + commands.add(command); + } + + public List get() { + return commands; + } + + public static final @android.annotation.NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public ApduList createFromParcel(Parcel in) { + return new ApduList(in); + } + + @Override + public ApduList[] newArray(int size) { + return new ApduList[size]; + } + }; + + private ApduList(Parcel in) { + int count = in.readInt(); + + for (int i = 0 ; i < count ; i++) { + + int length = in.readInt(); + byte[] cmd = new byte[length]; + in.readByteArray(cmd); + commands.add(cmd); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(commands.size()); + + for (byte[] cmd : commands) { + dest.writeInt(cmd.length); + dest.writeByteArray(cmd); + } + } +} + + diff --git a/nfc/java/android/nfc/AvailableNfcAntenna.aidl b/nfc/java/android/nfc/AvailableNfcAntenna.aidl new file mode 100644 index 000000000000..9d06e2d7d5eb --- /dev/null +++ b/nfc/java/android/nfc/AvailableNfcAntenna.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +parcelable AvailableNfcAntenna; diff --git a/nfc/java/android/nfc/AvailableNfcAntenna.java b/nfc/java/android/nfc/AvailableNfcAntenna.java new file mode 100644 index 000000000000..6e6512a04971 --- /dev/null +++ b/nfc/java/android/nfc/AvailableNfcAntenna.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents a single available Nfc antenna + * on an Android device. + */ +public final class AvailableNfcAntenna implements Parcelable { + /** + * Location of the antenna on the Y axis in millimeters. + * 0 is the bottom-left when the user is facing the screen + * and the device orientation is Portrait. + */ + private final int mLocationX; + /** + * Location of the antenna on the Y axis in millimeters. + * 0 is the bottom-left when the user is facing the screen + * and the device orientation is Portrait. + */ + private final int mLocationY; + + public AvailableNfcAntenna(int locationX, int locationY) { + this.mLocationX = locationX; + this.mLocationY = locationY; + } + + /** + * Location of the antenna on the X axis in millimeters. + * 0 is the bottom-left when the user is facing the screen + * and the device orientation is Portrait. + */ + public int getLocationX() { + return mLocationX; + } + + /** + * Location of the antenna on the Y axis in millimeters. + * 0 is the bottom-left when the user is facing the screen + * and the device orientation is Portrait. + */ + public int getLocationY() { + return mLocationY; + } + + private AvailableNfcAntenna(Parcel in) { + this.mLocationX = in.readInt(); + this.mLocationY = in.readInt(); + } + + public static final @android.annotation.NonNull Parcelable.Creator + CREATOR = new Parcelable.Creator() { + @Override + public AvailableNfcAntenna createFromParcel(Parcel in) { + return new AvailableNfcAntenna(in); + } + + @Override + public AvailableNfcAntenna[] newArray(int size) { + return new AvailableNfcAntenna[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mLocationX); + dest.writeInt(mLocationY); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mLocationX; + result = prime * result + mLocationY; + return result; + } + + /** + * Returns true if the specified AvailableNfcAntenna contains + * identical specifications. + */ + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + AvailableNfcAntenna other = (AvailableNfcAntenna) obj; + if (this.mLocationX != other.mLocationX) return false; + return this.mLocationY == other.mLocationY; + } + + @Override + public String toString() { + return "AvailableNfcAntenna " + "x: " + mLocationX + " y: " + mLocationY; + } +} diff --git a/nfc/java/android/nfc/Constants.java b/nfc/java/android/nfc/Constants.java new file mode 100644 index 000000000000..f76833063605 --- /dev/null +++ b/nfc/java/android/nfc/Constants.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +/** + * @hide + * TODO(b/303286040): Holds @hide API constants. Formalize these APIs. + */ +public final class Constants { + private Constants() { } + + public static final String SETTINGS_SECURE_NFC_PAYMENT_FOREGROUND = "nfc_payment_foreground"; + public static final String SETTINGS_SECURE_NFC_PAYMENT_DEFAULT_COMPONENT = "nfc_payment_default_component"; + public static final String FEATURE_NFC_ANY = "android.hardware.nfc.any"; +} diff --git a/nfc/java/android/nfc/ErrorCodes.java b/nfc/java/android/nfc/ErrorCodes.java new file mode 100644 index 000000000000..d2c81cd27d90 --- /dev/null +++ b/nfc/java/android/nfc/ErrorCodes.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.compat.annotation.UnsupportedAppUsage; + +/** + * This class defines all the error codes that can be returned by the service + * and producing an exception on the application level. These are needed since + * binders does not support exceptions. + * + * @hide + */ +public class ErrorCodes { + + @UnsupportedAppUsage + public static boolean isError(int code) { + if (code < 0) { + return true; + } else { + return false; + } + } + + public static String asString(int code) { + switch (code) { + case SUCCESS: return "SUCCESS"; + case ERROR_IO: return "IO"; + case ERROR_CANCELLED: return "CANCELLED"; + case ERROR_TIMEOUT: return "TIMEOUT"; + case ERROR_BUSY: return "BUSY"; + case ERROR_CONNECT: return "CONNECT/DISCONNECT"; +// case ERROR_DISCONNECT: return "DISCONNECT"; + case ERROR_READ: return "READ"; + case ERROR_WRITE: return "WRITE"; + case ERROR_INVALID_PARAM: return "INVALID_PARAM"; + case ERROR_INSUFFICIENT_RESOURCES: return "INSUFFICIENT_RESOURCES"; + case ERROR_SOCKET_CREATION: return "SOCKET_CREATION"; + case ERROR_SOCKET_NOT_CONNECTED: return "SOCKET_NOT_CONNECTED"; + case ERROR_BUFFER_TO_SMALL: return "BUFFER_TO_SMALL"; + case ERROR_SAP_USED: return "SAP_USED"; + case ERROR_SERVICE_NAME_USED: return "SERVICE_NAME_USED"; + case ERROR_SOCKET_OPTIONS: return "SOCKET_OPTIONS"; + case ERROR_NFC_ON: return "NFC_ON"; + case ERROR_NOT_INITIALIZED: return "NOT_INITIALIZED"; + case ERROR_SE_ALREADY_SELECTED: return "SE_ALREADY_SELECTED"; + case ERROR_SE_CONNECTED: return "SE_CONNECTED"; + case ERROR_NO_SE_CONNECTED: return "NO_SE_CONNECTED"; + case ERROR_NOT_SUPPORTED: return "NOT_SUPPORTED"; + default: return "UNKNOWN ERROR"; + } + } + + public static final int SUCCESS = 0; + + public static final int ERROR_IO = -1; + + public static final int ERROR_CANCELLED = -2; + + public static final int ERROR_TIMEOUT = -3; + + public static final int ERROR_BUSY = -4; + + public static final int ERROR_CONNECT = -5; + + public static final int ERROR_DISCONNECT = -5; + + public static final int ERROR_READ = -6; + + public static final int ERROR_WRITE = -7; + + public static final int ERROR_INVALID_PARAM = -8; + + public static final int ERROR_INSUFFICIENT_RESOURCES = -9; + + public static final int ERROR_SOCKET_CREATION = -10; + + public static final int ERROR_SOCKET_NOT_CONNECTED = -11; + + public static final int ERROR_BUFFER_TO_SMALL = -12; + + public static final int ERROR_SAP_USED = -13; + + public static final int ERROR_SERVICE_NAME_USED = -14; + + public static final int ERROR_SOCKET_OPTIONS = -15; + + public static final int ERROR_NFC_ON = -16; + + public static final int ERROR_NOT_INITIALIZED = -17; + + public static final int ERROR_SE_ALREADY_SELECTED = -18; + + public static final int ERROR_SE_CONNECTED = -19; + + public static final int ERROR_NO_SE_CONNECTED = -20; + + public static final int ERROR_NOT_SUPPORTED = -21; + +} diff --git a/nfc/java/android/nfc/FormatException.java b/nfc/java/android/nfc/FormatException.java new file mode 100644 index 000000000000..a57de1e0e21a --- /dev/null +++ b/nfc/java/android/nfc/FormatException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +public class FormatException extends Exception { + public FormatException() { + super(); + } + + public FormatException(String message) { + super(message); + } + + public FormatException(String message, Throwable e) { + super(message, e); + } +} diff --git a/nfc/java/android/nfc/IAppCallback.aidl b/nfc/java/android/nfc/IAppCallback.aidl new file mode 100644 index 000000000000..b06bf06d5197 --- /dev/null +++ b/nfc/java/android/nfc/IAppCallback.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.nfc.Tag; + +/** + * @hide + */ +interface IAppCallback +{ + oneway void onTagDiscovered(in Tag tag); +} diff --git a/nfc/java/android/nfc/INfcAdapter.aidl b/nfc/java/android/nfc/INfcAdapter.aidl new file mode 100644 index 000000000000..85879ac56194 --- /dev/null +++ b/nfc/java/android/nfc/INfcAdapter.aidl @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.app.PendingIntent; +import android.content.IntentFilter; +import android.nfc.NdefMessage; +import android.nfc.Tag; +import android.nfc.TechListParcel; +import android.nfc.IAppCallback; +import android.nfc.INfcAdapterExtras; +import android.nfc.INfcControllerAlwaysOnListener; +import android.nfc.INfcTag; +import android.nfc.INfcCardEmulation; +import android.nfc.INfcFCardEmulation; +import android.nfc.INfcUnlockHandler; +import android.nfc.ITagRemovedCallback; +import android.nfc.INfcDta; +import android.nfc.NfcAntennaInfo; +import android.os.Bundle; + +/** + * @hide + */ +interface INfcAdapter +{ + INfcTag getNfcTagInterface(); + INfcCardEmulation getNfcCardEmulationInterface(); + INfcFCardEmulation getNfcFCardEmulationInterface(); + INfcAdapterExtras getNfcAdapterExtrasInterface(in String pkg); + INfcDta getNfcDtaInterface(in String pkg); + int getState(); + boolean disable(boolean saveState); + boolean enable(); + void pausePolling(int timeoutInMs); + void resumePolling(); + + void setForegroundDispatch(in PendingIntent intent, + in IntentFilter[] filters, in TechListParcel techLists); + void setAppCallback(in IAppCallback callback); + + boolean ignore(int nativeHandle, int debounceMs, ITagRemovedCallback callback); + + void dispatch(in Tag tag); + + void setReaderMode (IBinder b, IAppCallback callback, int flags, in Bundle extras); + + void addNfcUnlockHandler(INfcUnlockHandler unlockHandler, in int[] techList); + void removeNfcUnlockHandler(INfcUnlockHandler unlockHandler); + + void verifyNfcPermission(); + boolean isNfcSecureEnabled(); + boolean deviceSupportsNfcSecure(); + boolean setNfcSecure(boolean enable); + NfcAntennaInfo getNfcAntennaInfo(); + + boolean setControllerAlwaysOn(boolean value); + boolean isControllerAlwaysOn(); + boolean isControllerAlwaysOnSupported(); + void registerControllerAlwaysOnListener(in INfcControllerAlwaysOnListener listener); + void unregisterControllerAlwaysOnListener(in INfcControllerAlwaysOnListener listener); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)") + boolean isTagIntentAppPreferenceSupported(); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)") + Map getTagIntentAppPreferenceForUser(int userId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)") + int setTagIntentAppPreferenceForUser(int userId, String pkg, boolean allow); + + boolean isReaderOptionEnabled(); + boolean isReaderOptionSupported(); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)") + boolean enableReaderOption(boolean enable); + boolean isObserveModeSupported(); + boolean setObserveMode(boolean enabled); + void updateDiscoveryTechnology(IBinder b, int pollFlags, int listenFlags); +} diff --git a/nfc/java/android/nfc/INfcAdapterExtras.aidl b/nfc/java/android/nfc/INfcAdapterExtras.aidl new file mode 100644 index 000000000000..cde57c58ca1f --- /dev/null +++ b/nfc/java/android/nfc/INfcAdapterExtras.aidl @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.os.Bundle; + + +/** + * {@hide} + */ +interface INfcAdapterExtras { + @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) + Bundle open(in String pkg, IBinder b); + @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) + Bundle close(in String pkg, IBinder b); + @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) + Bundle transceive(in String pkg, in byte[] data_in); + @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) + int getCardEmulationRoute(in String pkg); + @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) + void setCardEmulationRoute(in String pkg, int route); + @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) + void authenticate(in String pkg, in byte[] token); + @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) + String getDriverName(in String pkg); +} diff --git a/nfc/java/android/nfc/INfcCardEmulation.aidl b/nfc/java/android/nfc/INfcCardEmulation.aidl new file mode 100644 index 000000000000..f4b46046bc3e --- /dev/null +++ b/nfc/java/android/nfc/INfcCardEmulation.aidl @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.content.ComponentName; +import android.nfc.cardemulation.AidGroup; +import android.nfc.cardemulation.ApduServiceInfo; +import android.os.RemoteCallback; + +/** + * @hide + */ +interface INfcCardEmulation +{ + boolean isDefaultServiceForCategory(int userHandle, in ComponentName service, String category); + boolean isDefaultServiceForAid(int userHandle, in ComponentName service, String aid); + boolean setDefaultServiceForCategory(int userHandle, in ComponentName service, String category); + boolean setDefaultForNextTap(int userHandle, in ComponentName service); + boolean setServiceObserveModeDefault(int userId, in android.content.ComponentName service, boolean enable); + boolean registerAidGroupForService(int userHandle, in ComponentName service, in AidGroup aidGroup); + boolean setOffHostForService(int userHandle, in ComponentName service, in String offHostSecureElement); + boolean unsetOffHostForService(int userHandle, in ComponentName service); + AidGroup getAidGroupForService(int userHandle, in ComponentName service, String category); + boolean removeAidGroupForService(int userHandle, in ComponentName service, String category); + List getServices(int userHandle, in String category); + boolean setPreferredService(in ComponentName service); + boolean unsetPreferredService(); + boolean supportsAidPrefixRegistration(); + ApduServiceInfo getPreferredPaymentService(int userHandle); + boolean setServiceEnabledForCategoryOther(int userHandle, in ComponentName app, boolean status); + boolean isDefaultPaymentRegistered(); + + boolean overrideRoutingTable(int userHandle, String protocol, String technology); + boolean recoverRoutingTable(int userHandle); +} diff --git a/nfc/java/android/nfc/INfcControllerAlwaysOnListener.aidl b/nfc/java/android/nfc/INfcControllerAlwaysOnListener.aidl new file mode 100644 index 000000000000..1bb7680d2fed --- /dev/null +++ b/nfc/java/android/nfc/INfcControllerAlwaysOnListener.aidl @@ -0,0 +1,29 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +/** + * @hide + */ +oneway interface INfcControllerAlwaysOnListener { + /** + * Called whenever the controller always on state changes + * + * @param isEnabled true if the state is enabled, false otherwise + */ + void onControllerAlwaysOnChanged(boolean isEnabled); +} diff --git a/nfc/java/android/nfc/INfcDta.aidl b/nfc/java/android/nfc/INfcDta.aidl new file mode 100644 index 000000000000..4cc59271362b --- /dev/null +++ b/nfc/java/android/nfc/INfcDta.aidl @@ -0,0 +1,34 @@ + /* + * Copyright (C) 2017 NXP Semiconductors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.nfc; + +import android.os.Bundle; + +/** + * {@hide} + */ +interface INfcDta { + + void enableDta(); + void disableDta(); + boolean enableServer(String serviceName, int serviceSap, int miu, + int rwSize,int testCaseId); + void disableServer(); + boolean enableClient(String serviceName, int miu, int rwSize, + int testCaseId); + void disableClient(); + boolean registerMessageService(String msgServiceName); +} diff --git a/nfc/java/android/nfc/INfcFCardEmulation.aidl b/nfc/java/android/nfc/INfcFCardEmulation.aidl new file mode 100644 index 000000000000..124bfac4f0d0 --- /dev/null +++ b/nfc/java/android/nfc/INfcFCardEmulation.aidl @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.content.ComponentName; +import android.nfc.cardemulation.NfcFServiceInfo; + +/** + * @hide + */ +interface INfcFCardEmulation +{ + String getSystemCodeForService(int userHandle, in ComponentName service); + boolean registerSystemCodeForService(int userHandle, in ComponentName service, String systemCode); + boolean removeSystemCodeForService(int userHandle, in ComponentName service); + String getNfcid2ForService(int userHandle, in ComponentName service); + boolean setNfcid2ForService(int userHandle, in ComponentName service, String nfcid2); + boolean enableNfcFForegroundService(in ComponentName service); + boolean disableNfcFForegroundService(); + List getNfcFServices(int userHandle); + int getMaxNumOfRegisterableSystemCodes(); +} diff --git a/nfc/java/android/nfc/INfcTag.aidl b/nfc/java/android/nfc/INfcTag.aidl new file mode 100644 index 000000000000..170df71385bb --- /dev/null +++ b/nfc/java/android/nfc/INfcTag.aidl @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.nfc.NdefMessage; +import android.nfc.Tag; +import android.nfc.TransceiveResult; + +/** + * @hide + */ +interface INfcTag +{ + int connect(int nativeHandle, int technology); + int reconnect(int nativeHandle); + int[] getTechList(int nativeHandle); + boolean isNdef(int nativeHandle); + boolean isPresent(int nativeHandle); + TransceiveResult transceive(int nativeHandle, in byte[] data, boolean raw); + + NdefMessage ndefRead(int nativeHandle); + int ndefWrite(int nativeHandle, in NdefMessage msg); + int ndefMakeReadOnly(int nativeHandle); + boolean ndefIsWritable(int nativeHandle); + int formatNdef(int nativeHandle, in byte[] key); + Tag rediscover(int nativehandle); + + int setTimeout(int technology, int timeout); + int getTimeout(int technology); + void resetTimeouts(); + boolean canMakeReadOnly(int ndefType); + int getMaxTransceiveLength(int technology); + boolean getExtendedLengthApdusSupported(); + + boolean isTagUpToDate(long cookie); +} diff --git a/nfc/java/android/nfc/INfcUnlockHandler.aidl b/nfc/java/android/nfc/INfcUnlockHandler.aidl new file mode 100644 index 000000000000..e1cace987dc3 --- /dev/null +++ b/nfc/java/android/nfc/INfcUnlockHandler.aidl @@ -0,0 +1,12 @@ +package android.nfc; + +import android.nfc.Tag; + +/** + * @hide + */ +interface INfcUnlockHandler { + + boolean onUnlockAttempted(in Tag tag); + +} diff --git a/nfc/java/android/nfc/ITagRemovedCallback.aidl b/nfc/java/android/nfc/ITagRemovedCallback.aidl new file mode 100644 index 000000000000..2a06ff314b22 --- /dev/null +++ b/nfc/java/android/nfc/ITagRemovedCallback.aidl @@ -0,0 +1,8 @@ +package android.nfc; + +/** + * @hide + */ +oneway interface ITagRemovedCallback { + void onTagRemoved(); +} diff --git a/nfc/java/android/nfc/NdefMessage.aidl b/nfc/java/android/nfc/NdefMessage.aidl new file mode 100644 index 000000000000..378b9d05b385 --- /dev/null +++ b/nfc/java/android/nfc/NdefMessage.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +parcelable NdefMessage; diff --git a/nfc/java/android/nfc/NdefMessage.java b/nfc/java/android/nfc/NdefMessage.java new file mode 100644 index 000000000000..553f6c01b016 --- /dev/null +++ b/nfc/java/android/nfc/NdefMessage.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.proto.ProtoOutputStream; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * Represents an immutable NDEF Message. + *

+ * NDEF (NFC Data Exchange Format) is a light-weight binary format, + * used to encapsulate typed data. It is specified by the NFC Forum, + * for transmission and storage with NFC, however it is transport agnostic. + *

+ * NDEF defines messages and records. An NDEF Record contains + * typed data, such as MIME-type media, a URI, or a custom + * application payload. An NDEF Message is a container for + * one or more NDEF Records. + *

+ * When an Android device receives an NDEF Message + * (for example by reading an NFC tag) it processes it through + * a dispatch mechanism to determine an activity to launch. + * The type of the first record in the message has + * special importance for message dispatch, so design this record + * carefully. + *

+ * Use {@link #NdefMessage(byte[])} to construct an NDEF Message from + * binary data, or {@link #NdefMessage(NdefRecord[])} to + * construct from one or more {@link NdefRecord}s. + *

+ * {@link NdefMessage} and {@link NdefRecord} implementations are + * always available, even on Android devices that do not have NFC hardware. + *

+ * {@link NdefRecord}s are intended to be immutable (and thread-safe), + * however they may contain mutable fields. So take care not to modify + * mutable fields passed into constructors, or modify mutable fields + * obtained by getter methods, unless such modification is explicitly + * marked as safe. + * + * @see NfcAdapter#ACTION_NDEF_DISCOVERED + * @see NdefRecord + */ +public final class NdefMessage implements Parcelable { + private final NdefRecord[] mRecords; + + /** + * Construct an NDEF Message by parsing raw bytes.

+ * Strict validation of the NDEF binary structure is performed: + * there must be at least one record, every record flag must + * be correct, and the total length of the message must match + * the length of the input data.

+ * This parser can handle chunked records, and converts them + * into logical {@link NdefRecord}s within the message.

+ * Once the input data has been parsed to one or more logical + * records, basic validation of the tnf, type, id, and payload fields + * of each record is performed, as per the documentation on + * on {@link NdefRecord#NdefRecord(short, byte[], byte[], byte[])}

+ * If either strict validation of the binary format fails, or + * basic validation during record construction fails, a + * {@link FormatException} is thrown

+ * Deep inspection of the type, id and payload fields of + * each record is not performed, so it is possible to parse input + * that has a valid binary format and confirms to the basic + * validation requirements of + * {@link NdefRecord#NdefRecord(short, byte[], byte[], byte[])}, + * but fails more strict requirements as specified by the + * NFC Forum. + * + *

+ * It is safe to re-use the data byte array after construction: + * this constructor will make an internal copy of all necessary fields. + * + * @param data raw bytes to parse + * @throws FormatException if the data cannot be parsed + */ + public NdefMessage(byte[] data) throws FormatException { + if (data == null) throw new NullPointerException("data is null"); + ByteBuffer buffer = ByteBuffer.wrap(data); + + mRecords = NdefRecord.parse(buffer, false); + + if (buffer.remaining() > 0) { + throw new FormatException("trailing data"); + } + } + + /** + * Construct an NDEF Message from one or more NDEF Records. + * + * @param record first record (mandatory) + * @param records additional records (optional) + */ + public NdefMessage(NdefRecord record, NdefRecord ... records) { + // validate + if (record == null) throw new NullPointerException("record cannot be null"); + + for (NdefRecord r : records) { + if (r == null) { + throw new NullPointerException("record cannot be null"); + } + } + + mRecords = new NdefRecord[1 + records.length]; + mRecords[0] = record; + System.arraycopy(records, 0, mRecords, 1, records.length); + } + + /** + * Construct an NDEF Message from one or more NDEF Records. + * + * @param records one or more records + */ + public NdefMessage(NdefRecord[] records) { + // validate + if (records.length < 1) { + throw new IllegalArgumentException("must have at least one record"); + } + for (NdefRecord r : records) { + if (r == null) { + throw new NullPointerException("records cannot contain null"); + } + } + + mRecords = records; + } + + /** + * Get the NDEF Records inside this NDEF Message.

+ * An {@link NdefMessage} always has one or more NDEF Records: so the + * following code to retrieve the first record is always safe + * (no need to check for null or array length >= 1): + *

+     * NdefRecord firstRecord = ndefMessage.getRecords()[0];
+     * 
+ * + * @return array of one or more NDEF records. + */ + public NdefRecord[] getRecords() { + return mRecords; + } + + /** + * Return the length of this NDEF Message if it is written to a byte array + * with {@link #toByteArray}.

+ * An NDEF Message can be formatted to bytes in different ways + * depending on chunking, SR, and ID flags, so the length returned + * by this method may not be equal to the length of the original + * byte array used to construct this NDEF Message. However it will + * always be equal to the length of the byte array produced by + * {@link #toByteArray}. + * + * @return length of this NDEF Message when written to bytes with {@link #toByteArray} + * @see #toByteArray + */ + public int getByteArrayLength() { + int length = 0; + for (NdefRecord r : mRecords) { + length += r.getByteLength(); + } + return length; + } + + /** + * Return this NDEF Message as raw bytes.

+ * The NDEF Message is formatted as per the NDEF 1.0 specification, + * and the byte array is suitable for network transmission or storage + * in an NFC Forum NDEF compatible tag.

+ * This method will not chunk any records, and will always use the + * short record (SR) format and omit the identifier field when possible. + * + * @return NDEF Message in binary format + * @see #getByteArrayLength() + */ + public byte[] toByteArray() { + int length = getByteArrayLength(); + ByteBuffer buffer = ByteBuffer.allocate(length); + + for (int i=0; i CREATOR = + new Parcelable.Creator() { + @Override + public NdefMessage createFromParcel(Parcel in) { + int recordsLength = in.readInt(); + NdefRecord[] records = new NdefRecord[recordsLength]; + in.readTypedArray(records, NdefRecord.CREATOR); + return new NdefMessage(records); + } + @Override + public NdefMessage[] newArray(int size) { + return new NdefMessage[size]; + } + }; + + @Override + public int hashCode() { + return Arrays.hashCode(mRecords); + } + + /** + * Returns true if the specified NDEF Message contains + * identical NDEF Records. + */ + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + NdefMessage other = (NdefMessage) obj; + return Arrays.equals(mRecords, other.mRecords); + } + + @Override + public String toString() { + return "NdefMessage " + Arrays.toString(mRecords); + } + + /** + * Dump debugging information as a NdefMessageProto + * @hide + * + * Note: + * See proto definition in frameworks/base/core/proto/android/nfc/ndef.proto + * When writing a nested message, must call {@link ProtoOutputStream#start(long)} before and + * {@link ProtoOutputStream#end(long)} after. + * Never reuse a proto field number. When removing a field, mark it as reserved. + */ + public void dumpDebug(ProtoOutputStream proto) { + for (NdefRecord record : mRecords) { + long token = proto.start(NdefMessageProto.NDEF_RECORDS); + record.dumpDebug(proto); + proto.end(token); + } + } +} \ No newline at end of file diff --git a/nfc/java/android/nfc/NdefRecord.aidl b/nfc/java/android/nfc/NdefRecord.aidl new file mode 100644 index 000000000000..10f89d0936e4 --- /dev/null +++ b/nfc/java/android/nfc/NdefRecord.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +parcelable NdefRecord; \ No newline at end of file diff --git a/nfc/java/android/nfc/NdefRecord.java b/nfc/java/android/nfc/NdefRecord.java new file mode 100644 index 000000000000..7bf4355d5b35 --- /dev/null +++ b/nfc/java/android/nfc/NdefRecord.java @@ -0,0 +1,1080 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.annotation.Nullable; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.proto.ProtoOutputStream; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** + * Represents an immutable NDEF Record. + *

+ * NDEF (NFC Data Exchange Format) is a light-weight binary format, + * used to encapsulate typed data. It is specified by the NFC Forum, + * for transmission and storage with NFC, however it is transport agnostic. + *

+ * NDEF defines messages and records. An NDEF Record contains + * typed data, such as MIME-type media, a URI, or a custom + * application payload. An NDEF Message is a container for + * one or more NDEF Records. + *

+ * This class represents logical (complete) NDEF Records, and can not be + * used to represent chunked (partial) NDEF Records. However + * {@link NdefMessage#NdefMessage(byte[])} can be used to parse a message + * containing chunked records, and will return a message with unchunked + * (complete) records. + *

+ * A logical NDEF Record always contains a 3-bit TNF (Type Name Field) + * that provides high level typing for the rest of the record. The + * remaining fields are variable length and not always present: + *

    + *
  • type: detailed typing for the payload
  • + *
  • id: identifier meta-data, not commonly used
  • + *
  • payload: the actual payload
  • + *
+ *

+ * Helpers such as {@link NdefRecord#createUri}, {@link NdefRecord#createMime} + * and {@link NdefRecord#createExternal} are included to create well-formatted + * NDEF Records with correctly set tnf, type, id and payload fields, please + * use these helpers whenever possible. + *

+ * Use the constructor {@link #NdefRecord(short, byte[], byte[], byte[])} + * if you know what you are doing and what to set the fields individually. + * Only basic validation is performed with this constructor, so it is possible + * to create records that do not confirm to the strict NFC Forum + * specifications. + *

+ * The binary representation of an NDEF Record includes additional flags to + * indicate location with an NDEF message, provide support for chunking of + * NDEF records, and to pack optional fields. This class does not expose + * those details. To write an NDEF Record as binary you must first put it + * into an {@link NdefMessage}, then call {@link NdefMessage#toByteArray()}. + *

+ * {@link NdefMessage} and {@link NdefRecord} implementations are + * always available, even on Android devices that do not have NFC hardware. + *

+ * {@link NdefRecord}s are intended to be immutable (and thread-safe), + * however they may contain mutable fields. So take care not to modify + * mutable fields passed into constructors, or modify mutable fields + * obtained by getter methods, unless such modification is explicitly + * marked as safe. + * + * @see NfcAdapter#ACTION_NDEF_DISCOVERED + * @see NdefMessage + */ +public final class NdefRecord implements Parcelable { + /** + * Indicates the record is empty.

+ * Type, id and payload fields are empty in a {@literal TNF_EMPTY} record. + */ + public static final short TNF_EMPTY = 0x00; + + /** + * Indicates the type field contains a well-known RTD type name.

+ * Use this tnf with RTD types such as {@link #RTD_TEXT}, {@link #RTD_URI}. + *

+ * The RTD type name format is specified in NFCForum-TS-RTD_1.0. + * + * @see #RTD_URI + * @see #RTD_TEXT + * @see #RTD_SMART_POSTER + * @see #createUri + */ + public static final short TNF_WELL_KNOWN = 0x01; + + /** + * Indicates the type field contains a media-type BNF + * construct, defined by RFC 2046.

+ * Use this with MIME type names such as {@literal "image/jpeg"}, or + * using the helper {@link #createMime}. + * + * @see #createMime + */ + public static final short TNF_MIME_MEDIA = 0x02; + + /** + * Indicates the type field contains an absolute-URI + * BNF construct defined by RFC 3986.

+ * When creating new records prefer {@link #createUri}, + * since it offers more compact URI encoding + * ({@literal #RTD_URI} allows compression of common URI prefixes). + * + * @see #createUri + */ + public static final short TNF_ABSOLUTE_URI = 0x03; + + /** + * Indicates the type field contains an external type name.

+ * Used to encode custom payloads. When creating new records + * use the helper {@link #createExternal}.

+ * The external-type RTD format is specified in NFCForum-TS-RTD_1.0.

+ *

+ * Note this TNF should not be used with RTD_TEXT or RTD_URI constants. + * Those are well known RTD constants, not external RTD constants. + * + * @see #createExternal + */ + public static final short TNF_EXTERNAL_TYPE = 0x04; + + /** + * Indicates the payload type is unknown.

+ * NFC Forum explains this should be treated similarly to the + * "application/octet-stream" MIME type. The payload + * type is not explicitly encoded within the record. + *

+ * The type field is empty in an {@literal TNF_UNKNOWN} record. + */ + public static final short TNF_UNKNOWN = 0x05; + + /** + * Indicates the payload is an intermediate or final chunk of a chunked + * NDEF Record.

+ * {@literal TNF_UNCHANGED} can not be used with this class + * since all {@link NdefRecord}s are already unchunked, however they + * may appear in the binary format. + */ + public static final short TNF_UNCHANGED = 0x06; + + /** + * Reserved TNF type. + *

+ * The NFC Forum NDEF Specification v1.0 suggests for NDEF parsers to treat this + * value like TNF_UNKNOWN. + * @hide + */ + public static final short TNF_RESERVED = 0x07; + + /** + * RTD Text type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN + */ + public static final byte[] RTD_TEXT = {0x54}; // "T" + + /** + * RTD URI type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN + */ + public static final byte[] RTD_URI = {0x55}; // "U" + + /** + * RTD Smart Poster type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN + */ + public static final byte[] RTD_SMART_POSTER = {0x53, 0x70}; // "Sp" + + /** + * RTD Alternative Carrier type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN + */ + public static final byte[] RTD_ALTERNATIVE_CARRIER = {0x61, 0x63}; // "ac" + + /** + * RTD Handover Carrier type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN + */ + public static final byte[] RTD_HANDOVER_CARRIER = {0x48, 0x63}; // "Hc" + + /** + * RTD Handover Request type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN + */ + public static final byte[] RTD_HANDOVER_REQUEST = {0x48, 0x72}; // "Hr" + + /** + * RTD Handover Select type. For use with {@literal TNF_WELL_KNOWN}. + * @see #TNF_WELL_KNOWN + */ + public static final byte[] RTD_HANDOVER_SELECT = {0x48, 0x73}; // "Hs" + + /** + * RTD Android app type. For use with {@literal TNF_EXTERNAL}. + *

+ * The payload of a record with type RTD_ANDROID_APP + * should be the package name identifying an application. + * Multiple RTD_ANDROID_APP records may be included + * in a single {@link NdefMessage}. + *

+ * Use {@link #createApplicationRecord(String)} to create + * RTD_ANDROID_APP records. + * @hide + */ + public static final byte[] RTD_ANDROID_APP = "android.com:pkg".getBytes(); + + private static final byte FLAG_MB = (byte) 0x80; + private static final byte FLAG_ME = (byte) 0x40; + private static final byte FLAG_CF = (byte) 0x20; + private static final byte FLAG_SR = (byte) 0x10; + private static final byte FLAG_IL = (byte) 0x08; + + /** + * NFC Forum "URI Record Type Definition"

+ * This is a mapping of "URI Identifier Codes" to URI string prefixes, + * per section 3.2.2 of the NFC Forum URI Record Type Definition document. + */ + private static final String[] URI_PREFIX_MAP = new String[] { + "", // 0x00 + "http://www.", // 0x01 + "https://www.", // 0x02 + "http://", // 0x03 + "https://", // 0x04 + "tel:", // 0x05 + "mailto:", // 0x06 + "ftp://anonymous:anonymous@", // 0x07 + "ftp://ftp.", // 0x08 + "ftps://", // 0x09 + "sftp://", // 0x0A + "smb://", // 0x0B + "nfs://", // 0x0C + "ftp://", // 0x0D + "dav://", // 0x0E + "news:", // 0x0F + "telnet://", // 0x10 + "imap:", // 0x11 + "rtsp://", // 0x12 + "urn:", // 0x13 + "pop:", // 0x14 + "sip:", // 0x15 + "sips:", // 0x16 + "tftp:", // 0x17 + "btspp://", // 0x18 + "btl2cap://", // 0x19 + "btgoep://", // 0x1A + "tcpobex://", // 0x1B + "irdaobex://", // 0x1C + "file://", // 0x1D + "urn:epc:id:", // 0x1E + "urn:epc:tag:", // 0x1F + "urn:epc:pat:", // 0x20 + "urn:epc:raw:", // 0x21 + "urn:epc:", // 0x22 + "urn:nfc:", // 0x23 + }; + + private static final int MAX_PAYLOAD_SIZE = 10 * (1 << 20); // 10 MB payload limit + + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + private final short mTnf; + private final byte[] mType; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private final byte[] mId; + private final byte[] mPayload; + + /** + * Create a new Android Application Record (AAR). + *

+ * This record indicates to other Android devices the package + * that should be used to handle the entire NDEF message. + * You can embed this record anywhere into your message + * to ensure that the intended package receives the message. + *

+ * When an Android device dispatches an {@link NdefMessage} + * containing one or more Android application records, + * the applications contained in those records will be the + * preferred target for the {@link NfcAdapter#ACTION_NDEF_DISCOVERED} + * intent, in the order in which they appear in the message. + * This dispatch behavior was first added to Android in + * Ice Cream Sandwich. + *

+ * If none of the applications have a are installed on the device, + * a Market link will be opened to the first application. + *

+ * Note that Android application records do not overrule + * applications that have called + * {@link NfcAdapter#enableForegroundDispatch}. + * + * @param packageName Android package name + * @return Android application NDEF record + */ + public static NdefRecord createApplicationRecord(String packageName) { + if (packageName == null) throw new NullPointerException("packageName is null"); + if (packageName.length() == 0) throw new IllegalArgumentException("packageName is empty"); + + return new NdefRecord(TNF_EXTERNAL_TYPE, RTD_ANDROID_APP, null, + packageName.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Create a new NDEF Record containing a URI.

+ * Use this method to encode a URI (or URL) into an NDEF Record.

+ * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN} + * and {@link #RTD_URI}. This is the most efficient encoding + * of a URI into NDEF.

+ * The uri parameter will be normalized with + * {@link Uri#normalizeScheme} to set the scheme to lower case to + * follow Android best practices for intent filtering. + * However the unchecked exception + * {@link IllegalArgumentException} may be thrown if the uri + * parameter has serious problems, for example if it is empty, so always + * catch this exception if you are passing user-generated data into this + * method.

+ * + * Reference specification: NFCForum-TS-RTD_URI_1.0 + * + * @param uri URI to encode. + * @return an NDEF Record containing the URI + * @throws IllegalArugmentException if the uri is empty or invalid + */ + public static NdefRecord createUri(Uri uri) { + if (uri == null) throw new NullPointerException("uri is null"); + + uri = uri.normalizeScheme(); + String uriString = uri.toString(); + if (uriString.length() == 0) throw new IllegalArgumentException("uri is empty"); + + byte prefix = 0; + for (int i = 1; i < URI_PREFIX_MAP.length; i++) { + if (uriString.startsWith(URI_PREFIX_MAP[i])) { + prefix = (byte) i; + uriString = uriString.substring(URI_PREFIX_MAP[i].length()); + break; + } + } + byte[] uriBytes = uriString.getBytes(StandardCharsets.UTF_8); + byte[] recordBytes = new byte[uriBytes.length + 1]; + recordBytes[0] = prefix; + System.arraycopy(uriBytes, 0, recordBytes, 1, uriBytes.length); + return new NdefRecord(TNF_WELL_KNOWN, RTD_URI, null, recordBytes); + } + + /** + * Create a new NDEF Record containing a URI.

+ * Use this method to encode a URI (or URL) into an NDEF Record.

+ * Uses the well known URI type representation: {@link #TNF_WELL_KNOWN} + * and {@link #RTD_URI}. This is the most efficient encoding + * of a URI into NDEF.

+ * The uriString parameter will be normalized with + * {@link Uri#normalizeScheme} to set the scheme to lower case to + * follow Android best practices for intent filtering. + * However the unchecked exception + * {@link IllegalArgumentException} may be thrown if the uriString + * parameter has serious problems, for example if it is empty, so always + * catch this exception if you are passing user-generated data into this + * method.

+ * + * Reference specification: NFCForum-TS-RTD_URI_1.0 + * + * @param uriString string URI to encode. + * @return an NDEF Record containing the URI + * @throws IllegalArugmentException if the uriString is empty or invalid + */ + public static NdefRecord createUri(String uriString) { + return createUri(Uri.parse(uriString)); + } + + /** + * Create a new NDEF Record containing MIME data.

+ * Use this method to encode MIME-typed data into an NDEF Record, + * such as "text/plain", or "image/jpeg".

+ * The mimeType parameter will be normalized with + * {@link Intent#normalizeMimeType} to follow Android best + * practices for intent filtering, for example to force lower-case. + * However the unchecked exception + * {@link IllegalArgumentException} may be thrown + * if the mimeType parameter has serious problems, + * for example if it is empty, so always catch this + * exception if you are passing user-generated data into this method. + *

+ * For efficiency, This method might not make an internal copy of the + * mimeData byte array, so take care not + * to modify the mimeData byte array while still using the returned + * NdefRecord. + * + * @param mimeType a valid MIME type + * @param mimeData MIME data as bytes + * @return an NDEF Record containing the MIME-typed data + * @throws IllegalArugmentException if the mimeType is empty or invalid + * + */ + public static NdefRecord createMime(String mimeType, byte[] mimeData) { + if (mimeType == null) throw new NullPointerException("mimeType is null"); + + // We only do basic MIME type validation: trying to follow the + // RFCs strictly only ends in tears, since there are lots of MIME + // types in common use that are not strictly valid as per RFC rules + mimeType = Intent.normalizeMimeType(mimeType); + if (mimeType.length() == 0) throw new IllegalArgumentException("mimeType is empty"); + int slashIndex = mimeType.indexOf('/'); + if (slashIndex == 0) throw new IllegalArgumentException("mimeType must have major type"); + if (slashIndex == mimeType.length() - 1) { + throw new IllegalArgumentException("mimeType must have minor type"); + } + // missing '/' is allowed + + // MIME RFCs suggest ASCII encoding for content-type + byte[] typeBytes = mimeType.getBytes(StandardCharsets.US_ASCII); + return new NdefRecord(TNF_MIME_MEDIA, typeBytes, null, mimeData); + } + + /** + * Create a new NDEF Record containing external (application-specific) data.

+ * Use this method to encode application specific data into an NDEF Record. + * The data is typed by a domain name (usually your Android package name) and + * a domain-specific type. This data is packaged into a "NFC Forum External + * Type" NDEF Record.

+ * NFC Forum requires that the domain and type used in an external record + * are treated as case insensitive, however Android intent filtering is + * always case sensitive. So this method will force the domain and type to + * lower-case before creating the NDEF Record.

+ * The unchecked exception {@link IllegalArgumentException} will be thrown + * if the domain and type have serious problems, for example if either field + * is empty, so always catch this + * exception if you are passing user-generated data into this method.

+ * There are no such restrictions on the payload data.

+ * For efficiency, This method might not make an internal copy of the + * data byte array, so take care not + * to modify the data byte array while still using the returned + * NdefRecord. + * + * Reference specification: NFCForum-TS-RTD_1.0 + * @param domain domain-name of issuing organization + * @param type domain-specific type of data + * @param data payload as bytes + * @throws IllegalArugmentException if either domain or type are empty or invalid + */ + public static NdefRecord createExternal(String domain, String type, byte[] data) { + if (domain == null) throw new NullPointerException("domain is null"); + if (type == null) throw new NullPointerException("type is null"); + + domain = domain.trim().toLowerCase(Locale.ROOT); + type = type.trim().toLowerCase(Locale.ROOT); + + if (domain.length() == 0) throw new IllegalArgumentException("domain is empty"); + if (type.length() == 0) throw new IllegalArgumentException("type is empty"); + + byte[] byteDomain = domain.getBytes(StandardCharsets.UTF_8); + byte[] byteType = type.getBytes(StandardCharsets.UTF_8); + byte[] b = new byte[byteDomain.length + 1 + byteType.length]; + System.arraycopy(byteDomain, 0, b, 0, byteDomain.length); + b[byteDomain.length] = ':'; + System.arraycopy(byteType, 0, b, byteDomain.length + 1, byteType.length); + + return new NdefRecord(TNF_EXTERNAL_TYPE, b, null, data); + } + + /** + * Create a new NDEF record containing UTF-8 text data.

+ * + * The caller can either specify the language code for the provided text, + * or otherwise the language code corresponding to the current default + * locale will be used. + * + * Reference specification: NFCForum-TS-RTD_Text_1.0 + * @param languageCode The languageCode for the record. If locale is empty or null, + * the language code of the current default locale will be used. + * @param text The text to be encoded in the record. Will be represented in UTF-8 format. + * @throws IllegalArgumentException if text is null + */ + public static NdefRecord createTextRecord(String languageCode, String text) { + if (text == null) throw new NullPointerException("text is null"); + + byte[] textBytes = text.getBytes(StandardCharsets.UTF_8); + + byte[] languageCodeBytes = null; + if (languageCode != null && !languageCode.isEmpty()) { + languageCodeBytes = languageCode.getBytes(StandardCharsets.US_ASCII); + } else { + languageCodeBytes = Locale.getDefault().getLanguage(). + getBytes(StandardCharsets.US_ASCII); + } + // We only have 6 bits to indicate ISO/IANA language code. + if (languageCodeBytes.length >= 64) { + throw new IllegalArgumentException("language code is too long, must be <64 bytes."); + } + ByteBuffer buffer = ByteBuffer.allocate(1 + languageCodeBytes.length + textBytes.length); + + byte status = (byte) (languageCodeBytes.length & 0xFF); + buffer.put(status); + buffer.put(languageCodeBytes); + buffer.put(textBytes); + + return new NdefRecord(TNF_WELL_KNOWN, RTD_TEXT, null, buffer.array()); + } + + /** + * Construct an NDEF Record from its component fields.

+ * Recommend to use helpers such as {#createUri} or + * {{@link #createExternal} where possible, since they perform + * stricter validation that the record is correctly formatted + * as per NDEF specifications. However if you know what you are + * doing then this constructor offers the most flexibility.

+ * An {@link NdefRecord} represents a logical (complete) + * record, and cannot represent NDEF Record chunks.

+ * Basic validation of the tnf, type, id and payload is performed + * as per the following rules: + *

    + *
  • The tnf paramter must be a 3-bit value.
  • + *
  • Records with a tnf of {@link #TNF_EMPTY} cannot have a type, + * id or payload.
  • + *
  • Records with a tnf of {@link #TNF_UNKNOWN} or {@literal 0x07} + * cannot have a type.
  • + *
  • Records with a tnf of {@link #TNF_UNCHANGED} are not allowed + * since this class only represents complete (unchunked) records.
  • + *
+ * This minimal validation is specified by + * NFCForum-TS-NDEF_1.0 section 3.2.6 (Type Name Format).

+ * If any of the above validation + * steps fail then {@link IllegalArgumentException} is thrown.

+ * Deep inspection of the type, id and payload fields is not + * performed, so it is possible to create NDEF Records + * that conform to section 3.2.6 + * but fail other more strict NDEF specification requirements. For + * example, the payload may be invalid given the tnf and type. + *

+ * To omit a type, id or payload field, set the parameter to an + * empty byte array or null. + * + * @param tnf a 3-bit TNF constant + * @param type byte array, containing zero to 255 bytes, or null + * @param id byte array, containing zero to 255 bytes, or null + * @param payload byte array, containing zero to (2 ** 32 - 1) bytes, + * or null + * @throws IllegalArugmentException if a valid record cannot be created + */ + public NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload) { + /* convert nulls */ + if (type == null) type = EMPTY_BYTE_ARRAY; + if (id == null) id = EMPTY_BYTE_ARRAY; + if (payload == null) payload = EMPTY_BYTE_ARRAY; + + String message = validateTnf(tnf, type, id, payload); + if (message != null) { + throw new IllegalArgumentException(message); + } + + mTnf = tnf; + mType = type; + mId = id; + mPayload = payload; + } + + /** + * Construct an NDEF Record from raw bytes.

+ * This method is deprecated, use {@link NdefMessage#NdefMessage(byte[])} + * instead. This is because it does not make sense to parse a record: + * the NDEF binary format is only defined for a message, and the + * record flags MB and ME do not make sense outside of the context of + * an entire message.

+ * This implementation will attempt to parse a single record by ignoring + * the MB and ME flags, and otherwise following the rules of + * {@link NdefMessage#NdefMessage(byte[])}.

+ * + * @param data raw bytes to parse + * @throws FormatException if the data cannot be parsed into a valid record + * @deprecated use {@link NdefMessage#NdefMessage(byte[])} instead. + */ + @Deprecated + public NdefRecord(byte[] data) throws FormatException { + ByteBuffer buffer = ByteBuffer.wrap(data); + NdefRecord[] rs = parse(buffer, true); + + if (buffer.remaining() > 0) { + throw new FormatException("data too long"); + } + + mTnf = rs[0].mTnf; + mType = rs[0].mType; + mId = rs[0].mId; + mPayload = rs[0].mPayload; + } + + /** + * Returns the 3-bit TNF. + *

+ * TNF is the top-level type. + */ + public short getTnf() { + return mTnf; + } + + /** + * Returns the variable length Type field. + *

+ * This should be used in conjunction with the TNF field to determine the + * payload format. + *

+ * Returns an empty byte array if this record + * does not have a type field. + */ + public byte[] getType() { + return mType.clone(); + } + + /** + * Returns the variable length ID. + *

+ * Returns an empty byte array if this record + * does not have an id field. + */ + public byte[] getId() { + return mId.clone(); + } + + /** + * Returns the variable length payload. + *

+ * Returns an empty byte array if this record + * does not have a payload field. + */ + public byte[] getPayload() { + return mPayload.clone(); + } + + /** + * Return this NDEF Record as a byte array.

+ * This method is deprecated, use {@link NdefMessage#toByteArray} + * instead. This is because the NDEF binary format is not defined for + * a record outside of the context of a message: the MB and ME flags + * cannot be set without knowing the location inside a message.

+ * This implementation will attempt to serialize a single record by + * always setting the MB and ME flags (in other words, assume this + * is a single-record NDEF Message).

+ * + * @deprecated use {@link NdefMessage#toByteArray()} instead + */ + @Deprecated + public byte[] toByteArray() { + ByteBuffer buffer = ByteBuffer.allocate(getByteLength()); + writeToByteBuffer(buffer, true, true); + return buffer.array(); + } + + /** + * Map this record to a MIME type, or return null if it cannot be mapped.

+ * Currently this method considers all {@link #TNF_MIME_MEDIA} records to + * be MIME records, as well as some {@link #TNF_WELL_KNOWN} records such as + * {@link #RTD_TEXT}. If this is a MIME record then the MIME type as string + * is returned, otherwise null is returned.

+ * This method does not perform validation that the MIME type is + * actually valid. It always attempts to + * return a string containing the type if this is a MIME record.

+ * The returned MIME type will by normalized to lower-case using + * {@link Intent#normalizeMimeType}.

+ * The MIME payload can be obtained using {@link #getPayload}. + * + * @return MIME type as a string, or null if this is not a MIME record + */ + public String toMimeType() { + switch (mTnf) { + case NdefRecord.TNF_WELL_KNOWN: + if (Arrays.equals(mType, NdefRecord.RTD_TEXT)) { + return "text/plain"; + } + break; + case NdefRecord.TNF_MIME_MEDIA: + String mimeType = new String(mType, StandardCharsets.US_ASCII); + return Intent.normalizeMimeType(mimeType); + } + return null; + } + + /** + * Map this record to a URI, or return null if it cannot be mapped.

+ * Currently this method considers the following to be URI records: + *

    + *
  • {@link #TNF_ABSOLUTE_URI} records.
  • + *
  • {@link #TNF_WELL_KNOWN} with a type of {@link #RTD_URI}.
  • + *
  • {@link #TNF_WELL_KNOWN} with a type of {@link #RTD_SMART_POSTER} + * and containing a URI record in the NDEF message nested in the payload. + *
  • + *
  • {@link #TNF_EXTERNAL_TYPE} records.
  • + *
+ * If this is not a URI record by the above rules, then null is returned.

+ * This method does not perform validation that the URI is + * actually valid: it always attempts to create and return a URI if + * this record appears to be a URI record by the above rules.

+ * The returned URI will be normalized to have a lower case scheme + * using {@link Uri#normalizeScheme}.

+ * + * @return URI, or null if this is not a URI record + */ + public Uri toUri() { + return toUri(false); + } + + private Uri toUri(boolean inSmartPoster) { + switch (mTnf) { + case TNF_WELL_KNOWN: + if (Arrays.equals(mType, RTD_SMART_POSTER) && !inSmartPoster) { + try { + // check payload for a nested NDEF Message containing a URI + NdefMessage nestedMessage = new NdefMessage(mPayload); + for (NdefRecord nestedRecord : nestedMessage.getRecords()) { + Uri uri = nestedRecord.toUri(true); + if (uri != null) { + return uri; + } + } + } catch (FormatException e) { } + } else if (Arrays.equals(mType, RTD_URI)) { + Uri wktUri = parseWktUri(); + return (wktUri != null ? wktUri.normalizeScheme() : null); + } + break; + + case TNF_ABSOLUTE_URI: + Uri uri = Uri.parse(new String(mType, StandardCharsets.UTF_8)); + return uri.normalizeScheme(); + + case TNF_EXTERNAL_TYPE: + if (inSmartPoster) { + break; + } + return Uri.parse("vnd.android.nfc://ext/" + new String(mType, StandardCharsets.US_ASCII)); + } + return null; + } + + /** + * Return complete URI of {@link #TNF_WELL_KNOWN}, {@link #RTD_URI} records. + * @return complete URI, or null if invalid + */ + private Uri parseWktUri() { + if (mPayload.length < 2) { + return null; + } + + // payload[0] contains the URI Identifier Code, as per + // NFC Forum "URI Record Type Definition" section 3.2.2. + int prefixIndex = (mPayload[0] & (byte)0xFF); + if (prefixIndex < 0 || prefixIndex >= URI_PREFIX_MAP.length) { + return null; + } + String prefix = URI_PREFIX_MAP[prefixIndex]; + String suffix = new String(Arrays.copyOfRange(mPayload, 1, mPayload.length), + StandardCharsets.UTF_8); + return Uri.parse(prefix + suffix); + } + + /** + * Main record parsing method.

+ * Expects NdefMessage to begin immediately, allows trailing data.

+ * Currently has strict validation of all fields as per NDEF 1.0 + * specification section 2.5. We will attempt to keep this as strict as + * possible to encourage well-formatted NDEF.

+ * Always returns 1 or more NdefRecord's, or throws FormatException. + * + * @param buffer ByteBuffer to read from + * @param ignoreMbMe ignore MB and ME flags, and read only 1 complete record + * @return one or more records + * @throws FormatException on any parsing error + */ + static NdefRecord[] parse(ByteBuffer buffer, boolean ignoreMbMe) throws FormatException { + List records = new ArrayList(); + + try { + byte[] type = null; + byte[] id = null; + byte[] payload = null; + ArrayList chunks = new ArrayList(); + boolean inChunk = false; + short chunkTnf = -1; + boolean me = false; + + while (!me) { + byte flag = buffer.get(); + + boolean mb = (flag & NdefRecord.FLAG_MB) != 0; + me = (flag & NdefRecord.FLAG_ME) != 0; + boolean cf = (flag & NdefRecord.FLAG_CF) != 0; + boolean sr = (flag & NdefRecord.FLAG_SR) != 0; + boolean il = (flag & NdefRecord.FLAG_IL) != 0; + short tnf = (short)(flag & 0x07); + + if (!mb && records.size() == 0 && !inChunk && !ignoreMbMe) { + throw new FormatException("expected MB flag"); + } else if (mb && (records.size() != 0 || inChunk) && !ignoreMbMe) { + throw new FormatException("unexpected MB flag"); + } else if (inChunk && il) { + throw new FormatException("unexpected IL flag in non-leading chunk"); + } else if (cf && me) { + throw new FormatException("unexpected ME flag in non-trailing chunk"); + } else if (inChunk && tnf != NdefRecord.TNF_UNCHANGED) { + throw new FormatException("expected TNF_UNCHANGED in non-leading chunk"); + } else if (!inChunk && tnf == NdefRecord.TNF_UNCHANGED) { + throw new FormatException("" + + "unexpected TNF_UNCHANGED in first chunk or unchunked record"); + } + + int typeLength = buffer.get() & 0xFF; + long payloadLength = sr ? (buffer.get() & 0xFF) : (buffer.getInt() & 0xFFFFFFFFL); + int idLength = il ? (buffer.get() & 0xFF) : 0; + + if (inChunk && typeLength != 0) { + throw new FormatException("expected zero-length type in non-leading chunk"); + } + + if (!inChunk) { + type = (typeLength > 0 ? new byte[typeLength] : EMPTY_BYTE_ARRAY); + id = (idLength > 0 ? new byte[idLength] : EMPTY_BYTE_ARRAY); + buffer.get(type); + buffer.get(id); + } + + ensureSanePayloadSize(payloadLength); + payload = (payloadLength > 0 ? new byte[(int)payloadLength] : EMPTY_BYTE_ARRAY); + buffer.get(payload); + + if (cf && !inChunk) { + // first chunk + if (typeLength == 0 && tnf != NdefRecord.TNF_UNKNOWN) { + throw new FormatException("expected non-zero type length in first chunk"); + } + chunks.clear(); + chunkTnf = tnf; + } + if (cf || inChunk) { + // any chunk + chunks.add(payload); + } + if (!cf && inChunk) { + // last chunk, flatten the payload + payloadLength = 0; + for (byte[] p : chunks) { + payloadLength += p.length; + } + ensureSanePayloadSize(payloadLength); + payload = new byte[(int)payloadLength]; + int i = 0; + for (byte[] p : chunks) { + System.arraycopy(p, 0, payload, i, p.length); + i += p.length; + } + tnf = chunkTnf; + } + if (cf) { + // more chunks to come + inChunk = true; + continue; + } else { + inChunk = false; + } + + String error = validateTnf(tnf, type, id, payload); + if (error != null) { + throw new FormatException(error); + } + records.add(new NdefRecord(tnf, type, id, payload)); + if (ignoreMbMe) { // for parsing a single NdefRecord + break; + } + } + } catch (BufferUnderflowException e) { + throw new FormatException("expected more data", e); + } + return records.toArray(new NdefRecord[records.size()]); + } + + private static void ensureSanePayloadSize(long size) throws FormatException { + if (size > MAX_PAYLOAD_SIZE) { + throw new FormatException( + "payload above max limit: " + size + " > " + MAX_PAYLOAD_SIZE); + } + } + + /** + * Perform simple validation that the tnf is valid.

+ * Validates the requirements of NFCForum-TS-NDEF_1.0 section + * 3.2.6 (Type Name Format). This just validates that the tnf + * is valid, and that the relevant type, id and payload + * fields are present (or empty) for this tnf. It does not + * perform any deep inspection of the type, id and payload fields.

+ * Also does not allow TNF_UNCHANGED since this class is only used + * to present logical (unchunked) records. + * + * @return null if valid, or a string error if invalid. + */ + static String validateTnf(short tnf, byte[] type, byte[] id, byte[] payload) { + switch (tnf) { + case TNF_EMPTY: + if (type.length != 0 || id.length != 0 || payload.length != 0) { + return "unexpected data in TNF_EMPTY record"; + } + return null; + case TNF_WELL_KNOWN: + case TNF_MIME_MEDIA: + case TNF_ABSOLUTE_URI: + case TNF_EXTERNAL_TYPE: + return null; + case TNF_UNKNOWN: + case TNF_RESERVED: + if (type.length != 0) { + return "unexpected type field in TNF_UNKNOWN or TNF_RESERVEd record"; + } + return null; + case TNF_UNCHANGED: + return "unexpected TNF_UNCHANGED in first chunk or logical record"; + default: + return String.format("unexpected tnf value: 0x%02x", tnf); + } + } + + /** + * Serialize record for network transmission.

+ * Uses specified MB and ME flags.

+ * Does not chunk records. + */ + void writeToByteBuffer(ByteBuffer buffer, boolean mb, boolean me) { + boolean sr = mPayload.length < 256; + boolean il = mTnf == TNF_EMPTY ? true : mId.length > 0; + + byte flags = (byte)((mb ? FLAG_MB : 0) | (me ? FLAG_ME : 0) | + (sr ? FLAG_SR : 0) | (il ? FLAG_IL : 0) | mTnf); + buffer.put(flags); + + buffer.put((byte)mType.length); + if (sr) { + buffer.put((byte)mPayload.length); + } else { + buffer.putInt(mPayload.length); + } + if (il) { + buffer.put((byte)mId.length); + } + + buffer.put(mType); + buffer.put(mId); + buffer.put(mPayload); + } + + /** + * Get byte length of serialized record. + */ + int getByteLength() { + int length = 3 + mType.length + mId.length + mPayload.length; + + boolean sr = mPayload.length < 256; + boolean il = mTnf == TNF_EMPTY ? true : mId.length > 0; + + if (!sr) length += 3; + if (il) length += 1; + + return length; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mTnf); + dest.writeInt(mType.length); + dest.writeByteArray(mType); + dest.writeInt(mId.length); + dest.writeByteArray(mId); + dest.writeInt(mPayload.length); + dest.writeByteArray(mPayload); + } + + public static final @android.annotation.NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public NdefRecord createFromParcel(Parcel in) { + short tnf = (short)in.readInt(); + int typeLength = in.readInt(); + byte[] type = new byte[typeLength]; + in.readByteArray(type); + int idLength = in.readInt(); + byte[] id = new byte[idLength]; + in.readByteArray(id); + int payloadLength = in.readInt(); + byte[] payload = new byte[payloadLength]; + in.readByteArray(payload); + + return new NdefRecord(tnf, type, id, payload); + } + @Override + public NdefRecord[] newArray(int size) { + return new NdefRecord[size]; + } + }; + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(mId); + result = prime * result + Arrays.hashCode(mPayload); + result = prime * result + mTnf; + result = prime * result + Arrays.hashCode(mType); + return result; + } + + /** + * Returns true if the specified NDEF Record contains + * identical tnf, type, id and payload fields. + */ + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + NdefRecord other = (NdefRecord) obj; + if (!Arrays.equals(mId, other.mId)) return false; + if (!Arrays.equals(mPayload, other.mPayload)) return false; + if (mTnf != other.mTnf) return false; + return Arrays.equals(mType, other.mType); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(String.format("NdefRecord tnf=%X", mTnf)); + if (mType.length > 0) b.append(" type=").append(bytesToString(mType)); + if (mId.length > 0) b.append(" id=").append(bytesToString(mId)); + if (mPayload.length > 0) b.append(" payload=").append(bytesToString(mPayload)); + return b.toString(); + } + + /** + * Dump debugging information as a NdefRecordProto + * @hide + * + * Note: + * See proto definition in frameworks/base/core/proto/android/nfc/ndef.proto + * When writing a nested message, must call {@link ProtoOutputStream#start(long)} before and + * {@link ProtoOutputStream#end(long)} after. + * Never reuse a proto field number. When removing a field, mark it as reserved. + */ + public void dumpDebug(ProtoOutputStream proto) { + proto.write(NdefRecordProto.TYPE, mType); + proto.write(NdefRecordProto.ID, mId); + proto.write(NdefRecordProto.PAYLOAD_BYTES, mPayload.length); + } + + private static StringBuilder bytesToString(byte[] bs) { + StringBuilder s = new StringBuilder(); + for (byte b : bs) { + s.append(String.format("%02X", b)); + } + return s; + } +} diff --git a/nfc/java/android/nfc/NfcActivityManager.java b/nfc/java/android/nfc/NfcActivityManager.java new file mode 100644 index 000000000000..f03fc0af86b3 --- /dev/null +++ b/nfc/java/android/nfc/NfcActivityManager.java @@ -0,0 +1,410 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.app.Activity; +import android.app.Application; +import android.compat.annotation.UnsupportedAppUsage; +import android.nfc.NfcAdapter.ReaderCallback; +import android.os.Binder; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * Manages NFC API's that are coupled to the life-cycle of an Activity. + * + *

Uses {@link Application#registerActivityLifecycleCallbacks} to hook + * into activity life-cycle events such as onPause() and onResume(). + * + * @hide + */ +public final class NfcActivityManager extends IAppCallback.Stub + implements Application.ActivityLifecycleCallbacks { + static final String TAG = NfcAdapter.TAG; + static final Boolean DBG = false; + + @UnsupportedAppUsage + final NfcAdapter mAdapter; + + // All objects in the lists are protected by this + final List mApps; // Application(s) that have NFC state. Usually one + final List mActivities; // Activities that have NFC state + + /** + * NFC State associated with an {@link Application}. + */ + class NfcApplicationState { + int refCount = 0; + final Application app; + public NfcApplicationState(Application app) { + this.app = app; + } + public void register() { + refCount++; + if (refCount == 1) { + this.app.registerActivityLifecycleCallbacks(NfcActivityManager.this); + } + } + public void unregister() { + refCount--; + if (refCount == 0) { + this.app.unregisterActivityLifecycleCallbacks(NfcActivityManager.this); + } else if (refCount < 0) { + Log.e(TAG, "-ve refcount for " + app); + } + } + } + + NfcApplicationState findAppState(Application app) { + for (NfcApplicationState appState : mApps) { + if (appState.app == app) { + return appState; + } + } + return null; + } + + void registerApplication(Application app) { + NfcApplicationState appState = findAppState(app); + if (appState == null) { + appState = new NfcApplicationState(app); + mApps.add(appState); + } + appState.register(); + } + + void unregisterApplication(Application app) { + NfcApplicationState appState = findAppState(app); + if (appState == null) { + Log.e(TAG, "app was not registered " + app); + return; + } + appState.unregister(); + } + + /** + * NFC state associated with an {@link Activity} + */ + class NfcActivityState { + boolean resumed = false; + Activity activity; + NfcAdapter.ReaderCallback readerCallback = null; + int readerModeFlags = 0; + Bundle readerModeExtras = null; + Binder token; + + int mPollTech = NfcAdapter.FLAG_USE_ALL_TECH; + int mListenTech = NfcAdapter.FLAG_USE_ALL_TECH; + + public NfcActivityState(Activity activity) { + if (activity.isDestroyed()) { + throw new IllegalStateException("activity is already destroyed"); + } + // Check if activity is resumed right now, as we will not + // immediately get a callback for that. + resumed = activity.isResumed(); + + this.activity = activity; + this.token = new Binder(); + registerApplication(activity.getApplication()); + } + public void destroy() { + unregisterApplication(activity.getApplication()); + resumed = false; + activity = null; + readerCallback = null; + readerModeFlags = 0; + readerModeExtras = null; + token = null; + + mPollTech = NfcAdapter.FLAG_USE_ALL_TECH; + mListenTech = NfcAdapter.FLAG_USE_ALL_TECH; + } + @Override + public String toString() { + StringBuilder s = new StringBuilder("["); + s.append(readerCallback); + s.append("]"); + return s.toString(); + } + } + + /** find activity state from mActivities */ + synchronized NfcActivityState findActivityState(Activity activity) { + for (NfcActivityState state : mActivities) { + if (state.activity == activity) { + return state; + } + } + return null; + } + + /** find or create activity state from mActivities */ + synchronized NfcActivityState getActivityState(Activity activity) { + NfcActivityState state = findActivityState(activity); + if (state == null) { + state = new NfcActivityState(activity); + mActivities.add(state); + } + return state; + } + + synchronized NfcActivityState findResumedActivityState() { + for (NfcActivityState state : mActivities) { + if (state.resumed) { + return state; + } + } + return null; + } + + synchronized void destroyActivityState(Activity activity) { + NfcActivityState activityState = findActivityState(activity); + if (activityState != null) { + activityState.destroy(); + mActivities.remove(activityState); + } + } + + public NfcActivityManager(NfcAdapter adapter) { + mAdapter = adapter; + mActivities = new LinkedList(); + mApps = new ArrayList(1); // Android VM usually has 1 app + } + + public void enableReaderMode(Activity activity, ReaderCallback callback, int flags, + Bundle extras) { + boolean isResumed; + Binder token; + synchronized (NfcActivityManager.this) { + NfcActivityState state = getActivityState(activity); + state.readerCallback = callback; + state.readerModeFlags = flags; + state.readerModeExtras = extras; + token = state.token; + isResumed = state.resumed; + } + if (isResumed) { + setReaderMode(token, flags, extras); + } + } + + public void disableReaderMode(Activity activity) { + boolean isResumed; + Binder token; + synchronized (NfcActivityManager.this) { + NfcActivityState state = getActivityState(activity); + state.readerCallback = null; + state.readerModeFlags = 0; + state.readerModeExtras = null; + token = state.token; + isResumed = state.resumed; + } + if (isResumed) { + setReaderMode(token, 0, null); + } + + } + + public void setReaderMode(Binder token, int flags, Bundle extras) { + if (DBG) Log.d(TAG, "Setting reader mode"); + try { + NfcAdapter.sService.setReaderMode(token, this, flags, extras); + } catch (RemoteException e) { + mAdapter.attemptDeadServiceRecovery(e); + } + } + + /** + * Request or unrequest NFC service callbacks. + * Makes IPC call - do not hold lock. + */ + void requestNfcServiceCallback() { + try { + NfcAdapter.sService.setAppCallback(this); + } catch (RemoteException e) { + mAdapter.attemptDeadServiceRecovery(e); + } + } + + void verifyNfcPermission() { + try { + NfcAdapter.sService.verifyNfcPermission(); + } catch (RemoteException e) { + mAdapter.attemptDeadServiceRecovery(e); + } + } + + @Override + public void onTagDiscovered(Tag tag) throws RemoteException { + NfcAdapter.ReaderCallback callback; + synchronized (NfcActivityManager.this) { + NfcActivityState state = findResumedActivityState(); + if (state == null) return; + + callback = state.readerCallback; + } + + // Make callback without lock + if (callback != null) { + callback.onTagDiscovered(tag); + } + + } + /** Callback from Activity life-cycle, on main thread */ + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { /* NO-OP */ } + + /** Callback from Activity life-cycle, on main thread */ + @Override + public void onActivityStarted(Activity activity) { /* NO-OP */ } + + /** Callback from Activity life-cycle, on main thread */ + @Override + public void onActivityResumed(Activity activity) { + int readerModeFlags = 0; + Bundle readerModeExtras = null; + Binder token; + int pollTech; + int listenTech; + + synchronized (NfcActivityManager.this) { + NfcActivityState state = findActivityState(activity); + if (DBG) Log.d(TAG, "onResume() for " + activity + " " + state); + if (state == null) return; + state.resumed = true; + token = state.token; + readerModeFlags = state.readerModeFlags; + readerModeExtras = state.readerModeExtras; + + pollTech = state.mPollTech; + listenTech = state.mListenTech; + } + if (readerModeFlags != 0) { + setReaderMode(token, readerModeFlags, readerModeExtras); + } else if (listenTech != NfcAdapter.FLAG_USE_ALL_TECH + || pollTech != NfcAdapter.FLAG_USE_ALL_TECH) { + changeDiscoveryTech(token, pollTech, listenTech); + } + requestNfcServiceCallback(); + } + + /** Callback from Activity life-cycle, on main thread */ + @Override + public void onActivityPaused(Activity activity) { + boolean readerModeFlagsSet; + Binder token; + int pollTech; + int listenTech; + + synchronized (NfcActivityManager.this) { + NfcActivityState state = findActivityState(activity); + if (DBG) Log.d(TAG, "onPause() for " + activity + " " + state); + if (state == null) return; + state.resumed = false; + token = state.token; + readerModeFlagsSet = state.readerModeFlags != 0; + + pollTech = state.mPollTech; + listenTech = state.mListenTech; + } + if (readerModeFlagsSet) { + // Restore default p2p modes + setReaderMode(token, 0, null); + } else if (listenTech != NfcAdapter.FLAG_USE_ALL_TECH + || pollTech != NfcAdapter.FLAG_USE_ALL_TECH) { + changeDiscoveryTech(token, + NfcAdapter.FLAG_USE_ALL_TECH, NfcAdapter.FLAG_USE_ALL_TECH); + } + } + + /** Callback from Activity life-cycle, on main thread */ + @Override + public void onActivityStopped(Activity activity) { /* NO-OP */ } + + /** Callback from Activity life-cycle, on main thread */ + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { /* NO-OP */ } + + /** Callback from Activity life-cycle, on main thread */ + @Override + public void onActivityDestroyed(Activity activity) { + synchronized (NfcActivityManager.this) { + NfcActivityState state = findActivityState(activity); + if (DBG) Log.d(TAG, "onDestroy() for " + activity + " " + state); + if (state != null) { + // release all associated references + destroyActivityState(activity); + } + } + } + + /** setDiscoveryTechnology() implementation */ + public void setDiscoveryTech(Activity activity, int pollTech, int listenTech) { + boolean isResumed; + Binder token; + boolean readerModeFlagsSet; + synchronized (NfcActivityManager.this) { + NfcActivityState state = getActivityState(activity); + readerModeFlagsSet = state.readerModeFlags != 0; + state.mListenTech = listenTech; + state.mPollTech = pollTech; + token = state.token; + isResumed = state.resumed; + } + if (!readerModeFlagsSet && isResumed) { + changeDiscoveryTech(token, pollTech, listenTech); + } else if (readerModeFlagsSet) { + throw new IllegalStateException("Cannot be used when the Reader Mode is enabled"); + } + } + + /** resetDiscoveryTechnology() implementation */ + public void resetDiscoveryTech(Activity activity) { + boolean isResumed; + Binder token; + boolean readerModeFlagsSet; + synchronized (NfcActivityManager.this) { + NfcActivityState state = getActivityState(activity); + readerModeFlagsSet = state.readerModeFlags != 0; + state.mListenTech = NfcAdapter.FLAG_USE_ALL_TECH; + state.mPollTech = NfcAdapter.FLAG_USE_ALL_TECH; + token = state.token; + isResumed = state.resumed; + } + if (readerModeFlagsSet) { + disableReaderMode(activity); + } else if (isResumed) { + changeDiscoveryTech(token, NfcAdapter.FLAG_USE_ALL_TECH, NfcAdapter.FLAG_USE_ALL_TECH); + } + + } + + private void changeDiscoveryTech(Binder token, int pollTech, int listenTech) { + try { + NfcAdapter.sService.updateDiscoveryTechnology(token, pollTech, listenTech); + } catch (RemoteException e) { + mAdapter.attemptDeadServiceRecovery(e); + } + } + +} diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java new file mode 100644 index 000000000000..979855e5e25c --- /dev/null +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -0,0 +1,2749 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.annotation.UserIdInt; +import android.app.Activity; +import android.app.PendingIntent; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.Context; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.nfc.tech.MifareClassic; +import android.nfc.tech.Ndef; +import android.nfc.tech.NfcA; +import android.nfc.tech.NfcF; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Represents the local NFC adapter. + *

+ * Use the helper {@link #getDefaultAdapter(Context)} to get the default NFC + * adapter for this Android device. + * + *

+ *

Developer Guides

+ *

For more information about using NFC, read the + * Near Field Communication developer guide.

+ *

To perform basic file sharing between devices, read + * Sharing Files with NFC. + *

+ */ +public final class NfcAdapter { + static final String TAG = "NFC"; + + private final NfcControllerAlwaysOnListener mControllerAlwaysOnListener; + + /** + * Intent to start an activity when a tag with NDEF payload is discovered. + * + *

The system inspects the first {@link NdefRecord} in the first {@link NdefMessage} and + * looks for a URI, SmartPoster, or MIME record. If a URI or SmartPoster record is found the + * intent will contain the URI in its data field. If a MIME record is found the intent will + * contain the MIME type in its type field. This allows activities to register + * {@link IntentFilter}s targeting specific content on tags. Activities should register the + * most specific intent filters possible to avoid the activity chooser dialog, which can + * disrupt the interaction with the tag as the user interacts with the screen. + * + *

If the tag has an NDEF payload this intent is started before + * {@link #ACTION_TECH_DISCOVERED}. If any activities respond to this intent neither + * {@link #ACTION_TECH_DISCOVERED} or {@link #ACTION_TAG_DISCOVERED} will be started. + * + *

The MIME type or data URI of this intent are normalized before dispatch - + * so that MIME, URI scheme and URI host are always lower-case. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_NDEF_DISCOVERED = "android.nfc.action.NDEF_DISCOVERED"; + + /** + * Intent to start an activity when a tag is discovered and activities are registered for the + * specific technologies on the tag. + * + *

To receive this intent an activity must include an intent filter + * for this action and specify the desired tech types in a + * manifest meta-data entry. Here is an example manfiest entry: + *

+     * <activity android:name=".nfc.TechFilter" android:label="NFC/TechFilter">
+     *     <!-- Add a technology filter -->
+     *     <intent-filter>
+     *         <action android:name="android.nfc.action.TECH_DISCOVERED" />
+     *     </intent-filter>
+     *
+     *     <meta-data android:name="android.nfc.action.TECH_DISCOVERED"
+     *         android:resource="@xml/filter_nfc"
+     *     />
+     * </activity>
+ * + *

The meta-data XML file should contain one or more tech-list entries + * each consisting or one or more tech entries. The tech entries refer + * to the qualified class name implementing the technology, for example "android.nfc.tech.NfcA". + * + *

A tag matches if any of the + * tech-list sets is a subset of {@link Tag#getTechList() Tag.getTechList()}. Each + * of the tech-lists is considered independently and the + * activity is considered a match is any single tech-list matches the tag that was + * discovered. This provides AND and OR semantics for filtering desired techs. Here is an + * example that will match any tag using {@link NfcF} or any tag using {@link NfcA}, + * {@link MifareClassic}, and {@link Ndef}: + * + *

+     * <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+     *     <!-- capture anything using NfcF -->
+     *     <tech-list>
+     *         <tech>android.nfc.tech.NfcF</tech>
+     *     </tech-list>
+     *
+     *     <!-- OR -->
+     *
+     *     <!-- capture all MIFARE Classics with NDEF payloads -->
+     *     <tech-list>
+     *         <tech>android.nfc.tech.NfcA</tech>
+     *         <tech>android.nfc.tech.MifareClassic</tech>
+     *         <tech>android.nfc.tech.Ndef</tech>
+     *     </tech-list>
+     * </resources>
+ * + *

This intent is started after {@link #ACTION_NDEF_DISCOVERED} and before + * {@link #ACTION_TAG_DISCOVERED}. If any activities respond to {@link #ACTION_NDEF_DISCOVERED} + * this intent will not be started. If any activities respond to this intent + * {@link #ACTION_TAG_DISCOVERED} will not be started. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_TECH_DISCOVERED = "android.nfc.action.TECH_DISCOVERED"; + + /** + * Intent to start an activity when a tag is discovered. + * + *

This intent will not be started when a tag is discovered if any activities respond to + * {@link #ACTION_NDEF_DISCOVERED} or {@link #ACTION_TECH_DISCOVERED} for the current tag. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_TAG_DISCOVERED = "android.nfc.action.TAG_DISCOVERED"; + + /** + * Broadcast Action: Intent to notify an application that a transaction event has occurred + * on the Secure Element. + * + *

This intent will only be sent if the application has requested permission for + * {@link android.Manifest.permission#NFC_TRANSACTION_EVENT} and if the application has the + * necessary access to Secure Element which witnessed the particular event. + */ + @RequiresPermission(android.Manifest.permission.NFC_TRANSACTION_EVENT) + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_TRANSACTION_DETECTED = + "android.nfc.action.TRANSACTION_DETECTED"; + + /** + * Broadcast Action: Intent to notify if the preferred payment service changed. + * + *

This intent will only be sent to the application has requested permission for + * {@link android.Manifest.permission#NFC_PREFERRED_PAYMENT_INFO} and if the application + * has the necessary access to Secure Element which witnessed the particular event. + */ + @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_PREFERRED_PAYMENT_CHANGED = + "android.nfc.action.PREFERRED_PAYMENT_CHANGED"; + + /** + * Broadcast to only the activity that handles ACTION_TAG_DISCOVERED + * @hide + */ + public static final String ACTION_TAG_LEFT_FIELD = "android.nfc.action.TAG_LOST"; + + /** + * Mandatory extra containing the {@link Tag} that was discovered for the + * {@link #ACTION_NDEF_DISCOVERED}, {@link #ACTION_TECH_DISCOVERED}, and + * {@link #ACTION_TAG_DISCOVERED} intents. + */ + public static final String EXTRA_TAG = "android.nfc.extra.TAG"; + + /** + * Extra containing an array of {@link NdefMessage} present on the discovered tag.

+ * This extra is mandatory for {@link #ACTION_NDEF_DISCOVERED} intents, + * and optional for {@link #ACTION_TECH_DISCOVERED}, and + * {@link #ACTION_TAG_DISCOVERED} intents.

+ * When this extra is present there will always be at least one + * {@link NdefMessage} element. Most NDEF tags have only one NDEF message, + * but we use an array for future compatibility. + */ + public static final String EXTRA_NDEF_MESSAGES = "android.nfc.extra.NDEF_MESSAGES"; + + /** + * Optional extra containing a byte array containing the ID of the discovered tag for + * the {@link #ACTION_NDEF_DISCOVERED}, {@link #ACTION_TECH_DISCOVERED}, and + * {@link #ACTION_TAG_DISCOVERED} intents. + */ + public static final String EXTRA_ID = "android.nfc.extra.ID"; + + /** + * Broadcast Action: The state of the local NFC adapter has been + * changed. + *

For example, NFC has been turned on or off. + *

Always contains the extra field {@link #EXTRA_ADAPTER_STATE} + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_ADAPTER_STATE_CHANGED = + "android.nfc.action.ADAPTER_STATE_CHANGED"; + + /** + * Used as an int extra field in {@link #ACTION_ADAPTER_STATE_CHANGED} + * intents to request the current power state. Possible values are: + * {@link #STATE_OFF}, + * {@link #STATE_TURNING_ON}, + * {@link #STATE_ON}, + * {@link #STATE_TURNING_OFF}, + */ + public static final String EXTRA_ADAPTER_STATE = "android.nfc.extra.ADAPTER_STATE"; + + /** + * Mandatory byte[] extra field in {@link #ACTION_TRANSACTION_DETECTED} + */ + public static final String EXTRA_AID = "android.nfc.extra.AID"; + + /** + * Optional byte[] extra field in {@link #ACTION_TRANSACTION_DETECTED} + */ + public static final String EXTRA_DATA = "android.nfc.extra.DATA"; + + /** + * Mandatory String extra field in {@link #ACTION_TRANSACTION_DETECTED} + * Indicates the Secure Element on which the transaction occurred. + * eSE1...eSEn for Embedded Secure Elements, SIM1...SIMn for UICC, etc. + */ + public static final String EXTRA_SECURE_ELEMENT_NAME = "android.nfc.extra.SECURE_ELEMENT_NAME"; + + /** + * Mandatory String extra field in {@link #ACTION_PREFERRED_PAYMENT_CHANGED} + * Indicates the condition when trigger this event. Possible values are: + * {@link #PREFERRED_PAYMENT_LOADED}, + * {@link #PREFERRED_PAYMENT_CHANGED}, + * {@link #PREFERRED_PAYMENT_UPDATED}, + */ + public static final String EXTRA_PREFERRED_PAYMENT_CHANGED_REASON = + "android.nfc.extra.PREFERRED_PAYMENT_CHANGED_REASON"; + /** + * Nfc is enabled and the preferred payment aids are registered. + */ + public static final int PREFERRED_PAYMENT_LOADED = 1; + /** + * User selected another payment application as the preferred payment. + */ + public static final int PREFERRED_PAYMENT_CHANGED = 2; + /** + * Current preferred payment has issued an update (registered/unregistered new aids or has been + * updated itself). + */ + public static final int PREFERRED_PAYMENT_UPDATED = 3; + + public static final int STATE_OFF = 1; + public static final int STATE_TURNING_ON = 2; + public static final int STATE_ON = 3; + public static final int STATE_TURNING_OFF = 4; + + /** + * Possible states from {@link #getAdapterState}. + * + * @hide + */ + @IntDef(prefix = { "STATE_" }, value = { + STATE_OFF, + STATE_TURNING_ON, + STATE_ON, + STATE_TURNING_OFF + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AdapterState{} + + /** + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. + *

+ * Setting this flag enables polling for Nfc-A technology. + */ + public static final int FLAG_READER_NFC_A = 0x1; + + /** + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. + *

+ * Setting this flag enables polling for Nfc-B technology. + */ + public static final int FLAG_READER_NFC_B = 0x2; + + /** + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. + *

+ * Setting this flag enables polling for Nfc-F technology. + */ + public static final int FLAG_READER_NFC_F = 0x4; + + /** + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. + *

+ * Setting this flag enables polling for Nfc-V (ISO15693) technology. + */ + public static final int FLAG_READER_NFC_V = 0x8; + + /** + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. + *

+ * Setting this flag enables polling for NfcBarcode technology. + */ + public static final int FLAG_READER_NFC_BARCODE = 0x10; + + /** @hide */ + @IntDef(flag = true, prefix = {"FLAG_READER_"}, value = { + FLAG_READER_KEEP, + FLAG_READER_DISABLE, + FLAG_READER_NFC_A, + FLAG_READER_NFC_B, + FLAG_READER_NFC_F, + FLAG_READER_NFC_V, + FLAG_READER_NFC_BARCODE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PollTechnology {} + + /** + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. + *

+ * Setting this flag allows the caller to prevent the + * platform from performing an NDEF check on the tags it + * finds. + */ + public static final int FLAG_READER_SKIP_NDEF_CHECK = 0x80; + + /** + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. + *

+ * Setting this flag allows the caller to prevent the + * platform from playing sounds when it discovers a tag. + */ + public static final int FLAG_READER_NO_PLATFORM_SOUNDS = 0x100; + + /** + * Int Extra for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. + *

+ * Setting this integer extra allows the calling application to specify + * the delay that the platform will use for performing presence checks + * on any discovered tag. + */ + public static final String EXTRA_READER_PRESENCE_CHECK_DELAY = "presence"; + + /** + * Flag for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + *

+ * Setting this flag enables listening for Nfc-A technology. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_LISTEN_NFC_PASSIVE_A = 0x1; + + /** + * Flag for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + *

+ * Setting this flag enables listening for Nfc-B technology. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_LISTEN_NFC_PASSIVE_B = 1 << 1; + + /** + * Flag for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + *

+ * Setting this flag enables listening for Nfc-F technology. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_LISTEN_NFC_PASSIVE_F = 1 << 2; + + /** + * Flags for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + *

+ * Setting this flag disables listening. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_LISTEN_DISABLE = 0x0; + + /** + * Flags for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + *

+ * Setting this flag disables polling. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_READER_DISABLE = 0x0; + + /** + * Flags for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + *

+ * Setting this flag makes listening to use current flags. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_LISTEN_KEEP = -1; + + /** + * Flags for use with {@link #setDiscoveryTechnology(Activity, int, int)}. + *

+ * Setting this flag makes polling to use current flags. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public static final int FLAG_READER_KEEP = -1; + + /** @hide */ + public static final int FLAG_USE_ALL_TECH = 0xff; + + /** @hide */ + @IntDef(flag = true, prefix = {"FLAG_LISTEN_"}, value = { + FLAG_LISTEN_KEEP, + FLAG_LISTEN_DISABLE, + FLAG_LISTEN_NFC_PASSIVE_A, + FLAG_LISTEN_NFC_PASSIVE_B, + FLAG_LISTEN_NFC_PASSIVE_F + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ListenTechnology {} + + /** + * @hide + * @removed + */ + @SystemApi + @UnsupportedAppUsage + public static final int FLAG_NDEF_PUSH_NO_CONFIRM = 0x1; + + /** @hide */ + public static final String ACTION_HANDOVER_TRANSFER_STARTED = + "android.nfc.action.HANDOVER_TRANSFER_STARTED"; + + /** @hide */ + public static final String ACTION_HANDOVER_TRANSFER_DONE = + "android.nfc.action.HANDOVER_TRANSFER_DONE"; + + /** @hide */ + public static final String EXTRA_HANDOVER_TRANSFER_STATUS = + "android.nfc.extra.HANDOVER_TRANSFER_STATUS"; + + /** @hide */ + public static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0; + /** @hide */ + public static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1; + + /** @hide */ + public static final String EXTRA_HANDOVER_TRANSFER_URI = + "android.nfc.extra.HANDOVER_TRANSFER_URI"; + + /** + * Broadcast Action: Notify possible NFC transaction blocked because device is locked. + *

An external NFC field detected when device locked and SecureNfc enabled. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public static final String ACTION_REQUIRE_UNLOCK_FOR_NFC = + "android.nfc.action.REQUIRE_UNLOCK_FOR_NFC"; + + /** + * The requested app is correctly added to the Tag intent app preference. + * + * @see #setTagIntentAppPreferenceForUser(int userId, String pkg, boolean allow) + * @hide + */ + @SystemApi + public static final int TAG_INTENT_APP_PREF_RESULT_SUCCESS = 0; + + /** + * The requested app is not installed on the device. + * + * @see #setTagIntentAppPreferenceForUser(int userId, String pkg, boolean allow) + * @hide + */ + @SystemApi + public static final int TAG_INTENT_APP_PREF_RESULT_PACKAGE_NOT_FOUND = -1; + + /** + * The NfcService is not available. + * + * @see #setTagIntentAppPreferenceForUser(int userId, String pkg, boolean allow) + * @hide + */ + @SystemApi + public static final int TAG_INTENT_APP_PREF_RESULT_UNAVAILABLE = -2; + + /** + * Possible response codes from {@link #setTagIntentAppPreferenceForUser}. + * + * @hide + */ + @IntDef(prefix = { "TAG_INTENT_APP_PREF_RESULT" }, value = { + TAG_INTENT_APP_PREF_RESULT_SUCCESS, + TAG_INTENT_APP_PREF_RESULT_PACKAGE_NOT_FOUND, + TAG_INTENT_APP_PREF_RESULT_UNAVAILABLE}) + @Retention(RetentionPolicy.SOURCE) + public @interface TagIntentAppPreferenceResult {} + + // Guarded by sLock + static boolean sIsInitialized = false; + static boolean sHasNfcFeature; + static boolean sHasCeFeature; + + static Object sLock = new Object(); + + // Final after first constructor, except for + // attemptDeadServiceRecovery() when NFC crashes - we accept a best effort + // recovery + @UnsupportedAppUsage + static INfcAdapter sService; + static NfcServiceManager.ServiceRegisterer sServiceRegisterer; + static INfcTag sTagService; + static INfcCardEmulation sCardEmulationService; + static INfcFCardEmulation sNfcFCardEmulationService; + + /** + * The NfcAdapter object for each application context. + * There is a 1-1 relationship between application context and + * NfcAdapter object. + */ + static HashMap sNfcAdapters = new HashMap(); //guard by NfcAdapter.class + + /** + * NfcAdapter used with a null context. This ctor was deprecated but we have + * to support it for backwards compatibility. New methods that require context + * might throw when called on the null-context NfcAdapter. + */ + static NfcAdapter sNullContextNfcAdapter; // protected by NfcAdapter.class + + final NfcActivityManager mNfcActivityManager; + final Context mContext; + final HashMap mNfcUnlockHandlers; + final Object mLock; + + ITagRemovedCallback mTagRemovedListener; // protected by mLock + + /** + * A callback to be invoked when the system finds a tag while the foreground activity is + * operating in reader mode. + *

Register your {@code ReaderCallback} implementation with {@link + * NfcAdapter#enableReaderMode} and disable it with {@link + * NfcAdapter#disableReaderMode}. + * @see NfcAdapter#enableReaderMode + */ + public interface ReaderCallback { + public void onTagDiscovered(Tag tag); + } + + /** + * A listener to be invoked when NFC controller always on state changes. + *

Register your {@code ControllerAlwaysOnListener} implementation with {@link + * NfcAdapter#registerControllerAlwaysOnListener} and disable it with {@link + * NfcAdapter#unregisterControllerAlwaysOnListener}. + * @see #registerControllerAlwaysOnListener + * @hide + */ + @SystemApi + public interface ControllerAlwaysOnListener { + /** + * Called on NFC controller always on state changes + */ + void onControllerAlwaysOnChanged(boolean isEnabled); + } + + /** + * A callback to be invoked when the system successfully delivers your {@link NdefMessage} + * to another device. + * @deprecated this feature is removed. File sharing can work using other technology like + * Bluetooth. + */ + @java.lang.Deprecated + public interface OnNdefPushCompleteCallback { + /** + * Called on successful NDEF push. + * + *

This callback is usually made on a binder thread (not the UI thread). + * + * @param event {@link NfcEvent} with the {@link NfcEvent#nfcAdapter} field set + */ + public void onNdefPushComplete(NfcEvent event); + } + + /** + * A callback to be invoked when another NFC device capable of NDEF push (Android Beam) + * is within range. + *

Implement this interface and pass it to {@code + * NfcAdapter#setNdefPushMessageCallback setNdefPushMessageCallback()} in order to create an + * {@link NdefMessage} at the moment that another device is within range for NFC. Using this + * callback allows you to create a message with data that might vary based on the + * content currently visible to the user. Alternatively, you can call {@code + * #setNdefPushMessage setNdefPushMessage()} if the {@link NdefMessage} always contains the + * same data. + * @deprecated this feature is removed. File sharing can work using other technology like + * Bluetooth. + */ + @java.lang.Deprecated + public interface CreateNdefMessageCallback { + /** + * Called to provide a {@link NdefMessage} to push. + * + *

This callback is usually made on a binder thread (not the UI thread). + * + *

Called when this device is in range of another device + * that might support NDEF push. It allows the application to + * create the NDEF message only when it is required. + * + *

NDEF push cannot occur until this method returns, so do not + * block for too long. + * + *

The Android operating system will usually show a system UI + * on top of your activity during this time, so do not try to request + * input from the user to complete the callback, or provide custom NDEF + * push UI. The user probably will not see it. + * + * @param event {@link NfcEvent} with the {@link NfcEvent#nfcAdapter} field set + * @return NDEF message to push, or null to not provide a message + */ + public NdefMessage createNdefMessage(NfcEvent event); + } + + + /** + * @deprecated this feature is removed. File sharing can work using other technology like + * Bluetooth. + */ + @java.lang.Deprecated + public interface CreateBeamUrisCallback { + public Uri[] createBeamUris(NfcEvent event); + } + + /** + * A callback that is invoked when a tag is removed from the field. + * @see NfcAdapter#ignore + */ + public interface OnTagRemovedListener { + void onTagRemoved(); + } + + /** + * A callback to be invoked when an application has registered as a + * handler to unlock the device given an NFC tag at the lockscreen. + * @hide + */ + @SystemApi + public interface NfcUnlockHandler { + /** + * Called at the lock screen to attempt to unlock the device with the given tag. + * @param tag the detected tag, to be used to unlock the device + * @return true if the device was successfully unlocked + */ + public boolean onUnlockAttempted(Tag tag); + } + + /** + * Return list of Secure Elements which support off host card emulation. + * + * @return List containing secure elements on the device which supports + * off host card emulation. eSE for Embedded secure element, + * SIM for UICC and so on. + * @hide + */ + public @NonNull List getSupportedOffHostSecureElements() { + if (mContext == null) { + throw new UnsupportedOperationException("You need a context on NfcAdapter to use the " + + " getSupportedOffHostSecureElements APIs"); + } + List offHostSE = new ArrayList(); + PackageManager pm = mContext.getPackageManager(); + if (pm == null) { + Log.e(TAG, "Cannot get package manager, assuming no off-host CE feature"); + return offHostSE; + } + if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC)) { + offHostSE.add("SIM"); + } + if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE)) { + offHostSE.add("eSE"); + } + return offHostSE; + } + + private static void retrieveServiceRegisterer() { + if (sServiceRegisterer == null) { + NfcServiceManager manager = NfcFrameworkInitializer.getNfcServiceManager(); + if (manager == null) { + Log.e(TAG, "NfcServiceManager is null"); + throw new UnsupportedOperationException(); + } + sServiceRegisterer = manager.getNfcManagerServiceRegisterer(); + } + } + + /** + * Returns the NfcAdapter for application context, + * or throws if NFC is not available. + * @hide + */ + @UnsupportedAppUsage + public static synchronized NfcAdapter getNfcAdapter(Context context) { + if (context == null) { + if (sNullContextNfcAdapter == null) { + sNullContextNfcAdapter = new NfcAdapter(null); + } + return sNullContextNfcAdapter; + } + if (!sIsInitialized) { + PackageManager pm; + pm = context.getPackageManager(); + sHasNfcFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC); + sHasCeFeature = + pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION) + || pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF) + || pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC) + || pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE); + /* is this device meant to have NFC */ + if (!sHasNfcFeature && !sHasCeFeature) { + Log.v(TAG, "this device does not have NFC support"); + throw new UnsupportedOperationException(); + } + retrieveServiceRegisterer(); + sService = getServiceInterface(); + if (sService == null) { + Log.e(TAG, "could not retrieve NFC service"); + throw new UnsupportedOperationException(); + } + if (sHasNfcFeature) { + try { + sTagService = sService.getNfcTagInterface(); + } catch (RemoteException e) { + sTagService = null; + Log.e(TAG, "could not retrieve NFC Tag service"); + throw new UnsupportedOperationException(); + } + } + if (sHasCeFeature) { + try { + sNfcFCardEmulationService = sService.getNfcFCardEmulationInterface(); + } catch (RemoteException e) { + sNfcFCardEmulationService = null; + Log.e(TAG, "could not retrieve NFC-F card emulation service"); + throw new UnsupportedOperationException(); + } + try { + sCardEmulationService = sService.getNfcCardEmulationInterface(); + } catch (RemoteException e) { + sCardEmulationService = null; + Log.e(TAG, "could not retrieve card emulation service"); + throw new UnsupportedOperationException(); + } + } + + sIsInitialized = true; + } + NfcAdapter adapter = sNfcAdapters.get(context); + if (adapter == null) { + adapter = new NfcAdapter(context); + sNfcAdapters.put(context, adapter); + } + return adapter; + } + + /** get handle to NFC service interface */ + private static INfcAdapter getServiceInterface() { + /* get a handle to NFC service */ + IBinder b = sServiceRegisterer.get(); + if (b == null) { + return null; + } + return INfcAdapter.Stub.asInterface(b); + } + + /** + * Helper to get the default NFC Adapter. + *

+ * Most Android devices will only have one NFC Adapter (NFC Controller). + *

+ * This helper is the equivalent of: + *

+     * NfcManager manager = (NfcManager) context.getSystemService(Context.NFC_SERVICE);
+     * NfcAdapter adapter = manager.getDefaultAdapter();
+ * @param context the calling application's context + * + * @return the default NFC adapter, or null if no NFC adapter exists + */ + public static NfcAdapter getDefaultAdapter(Context context) { + if (context == null) { + throw new IllegalArgumentException("context cannot be null"); + } + context = context.getApplicationContext(); + if (context == null) { + throw new IllegalArgumentException( + "context not associated with any application (using a mock context?)"); + } + retrieveServiceRegisterer(); + if (sServiceRegisterer.tryGet() == null) { + if (sIsInitialized) { + synchronized (NfcAdapter.class) { + /* Stale sService pointer */ + if (sIsInitialized) sIsInitialized = false; + } + } + return null; + } + /* Try to initialize the service */ + NfcManager manager = (NfcManager) context.getSystemService(Context.NFC_SERVICE); + if (manager == null) { + // NFC not available + return null; + } + return manager.getDefaultAdapter(); + } + + /** + * Legacy NfcAdapter getter, always use {@link #getDefaultAdapter(Context)} instead.

+ * This method was deprecated at API level 10 (Gingerbread MR1) because a context is required + * for many NFC API methods. Those methods will fail when called on an NfcAdapter + * object created from this method.

+ * @deprecated use {@link #getDefaultAdapter(Context)} + * @hide + */ + @Deprecated + @UnsupportedAppUsage + public static NfcAdapter getDefaultAdapter() { + // introduced in API version 9 (GB 2.3) + // deprecated in API version 10 (GB 2.3.3) + // removed from public API in version 16 (ICS MR2) + // should maintain as a hidden API for binary compatibility for a little longer + Log.w(TAG, "WARNING: NfcAdapter.getDefaultAdapter() is deprecated, use " + + "NfcAdapter.getDefaultAdapter(Context) instead", new Exception()); + + return NfcAdapter.getNfcAdapter(null); + } + + NfcAdapter(Context context) { + mContext = context; + mNfcActivityManager = new NfcActivityManager(this); + mNfcUnlockHandlers = new HashMap(); + mTagRemovedListener = null; + mLock = new Object(); + mControllerAlwaysOnListener = new NfcControllerAlwaysOnListener(getService()); + } + + /** + * @hide + */ + @UnsupportedAppUsage + public Context getContext() { + return mContext; + } + + /** + * Returns the binder interface to the service. + * @hide + */ + @UnsupportedAppUsage + public INfcAdapter getService() { + isEnabled(); // NOP call to recover sService if it is stale + return sService; + } + + /** + * Returns the binder interface to the tag service. + * @hide + */ + public INfcTag getTagService() { + isEnabled(); // NOP call to recover sTagService if it is stale + return sTagService; + } + + /** + * Returns the binder interface to the card emulation service. + * @hide + */ + public INfcCardEmulation getCardEmulationService() { + isEnabled(); + return sCardEmulationService; + } + + /** + * Returns the binder interface to the NFC-F card emulation service. + * @hide + */ + public INfcFCardEmulation getNfcFCardEmulationService() { + isEnabled(); + return sNfcFCardEmulationService; + } + + /** + * Returns the binder interface to the NFC-DTA test interface. + * @hide + */ + public INfcDta getNfcDtaInterface() { + if (mContext == null) { + throw new UnsupportedOperationException("You need a context on NfcAdapter to use the " + + " NFC extras APIs"); + } + try { + return sService.getNfcDtaInterface(mContext.getPackageName()); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return null; + } + try { + return sService.getNfcDtaInterface(mContext.getPackageName()); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return null; + } + } + + /** + * NFC service dead - attempt best effort recovery + * @hide + */ + @UnsupportedAppUsage + public void attemptDeadServiceRecovery(Exception e) { + Log.e(TAG, "NFC service dead - attempting to recover", e); + INfcAdapter service = getServiceInterface(); + if (service == null) { + Log.e(TAG, "could not retrieve NFC service during service recovery"); + // nothing more can be done now, sService is still stale, we'll hit + // this recovery path again later + return; + } + // assigning to sService is not thread-safe, but this is best-effort code + // and on a well-behaved system should never happen + sService = service; + if (sHasNfcFeature) { + try { + sTagService = service.getNfcTagInterface(); + } catch (RemoteException ee) { + sTagService = null; + Log.e(TAG, "could not retrieve NFC tag service during service recovery"); + // nothing more can be done now, sService is still stale, we'll hit + // this recovery path again later + return; + } + } + + if (sHasCeFeature) { + try { + sCardEmulationService = service.getNfcCardEmulationInterface(); + } catch (RemoteException ee) { + sCardEmulationService = null; + Log.e(TAG, + "could not retrieve NFC card emulation service during service recovery"); + } + + try { + sNfcFCardEmulationService = service.getNfcFCardEmulationInterface(); + } catch (RemoteException ee) { + sNfcFCardEmulationService = null; + Log.e(TAG, + "could not retrieve NFC-F card emulation service during service recovery"); + } + } + + return; + } + + private boolean isCardEmulationEnabled() { + if (sHasCeFeature) { + return (sCardEmulationService != null || sNfcFCardEmulationService != null); + } + return false; + } + + private boolean isTagReadingEnabled() { + if (sHasNfcFeature) { + return sTagService != null; + } + return false; + } + + + /** + * Return true if this NFC Adapter has any features enabled. + * + *

If this method returns false, the NFC hardware is guaranteed not to + * generate or respond to any NFC communication over its NFC radio. + *

Applications can use this to check if NFC is enabled. Applications + * can request Settings UI allowing the user to toggle NFC using: + *

startActivity(new Intent(Settings.ACTION_NFC_SETTINGS))
+ * + * @see android.provider.Settings#ACTION_NFC_SETTINGS + * @return true if this NFC Adapter has any features enabled + */ + public boolean isEnabled() { + boolean serviceState = false; + try { + serviceState = sService.getState() == STATE_ON; + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + serviceState = sService.getState() == STATE_ON; + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + } + return serviceState && (isTagReadingEnabled() || isCardEmulationEnabled()); + } + + /** + * Return the state of this NFC Adapter. + * + *

Returns one of {@link #STATE_ON}, {@link #STATE_TURNING_ON}, + * {@link #STATE_OFF}, {@link #STATE_TURNING_OFF}. + * + *

{@link #isEnabled()} is equivalent to + * {@link #getAdapterState()} == {@link #STATE_ON} + * + * @return the current state of this NFC adapter + * + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public @AdapterState int getAdapterState() { + try { + return sService.getState(); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return NfcAdapter.STATE_OFF; + } + try { + return sService.getState(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return NfcAdapter.STATE_OFF; + } + } + + /** + * Enable NFC hardware. + * + *

This call is asynchronous. Listen for + * {@link #ACTION_ADAPTER_STATE_CHANGED} broadcasts to find out when the + * operation is complete. + * + *

If this returns true, then either NFC is already on, or + * a {@link #ACTION_ADAPTER_STATE_CHANGED} broadcast will be sent + * to indicate a state transition. If this returns false, then + * there is some problem that prevents an attempt to turn + * NFC on (for example we are in airplane mode and NFC is not + * toggleable in airplane mode on this platform). + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + public boolean enable() { + try { + return sService.enable(); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + return sService.enable(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return false; + } + } + + /** + * Disable NFC hardware. + * + *

No NFC features will work after this call, and the hardware + * will not perform or respond to any NFC communication. + * + *

This call is asynchronous. Listen for + * {@link #ACTION_ADAPTER_STATE_CHANGED} broadcasts to find out when the + * operation is complete. + * + *

If this returns true, then either NFC is already off, or + * a {@link #ACTION_ADAPTER_STATE_CHANGED} broadcast will be sent + * to indicate a state transition. If this returns false, then + * there is some problem that prevents an attempt to turn + * NFC off. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + public boolean disable() { + try { + return sService.disable(true); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + return sService.disable(true); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return false; + } + } + + /** + * Disable NFC hardware. + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + public boolean disable(boolean persist) { + try { + return sService.disable(persist); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + return sService.disable(persist); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return false; + } + } + + /** + * Pauses polling for a {@code timeoutInMs} millis. If polling must be resumed before timeout, + * use {@link #resumePolling()}. + * @hide + */ + public void pausePolling(int timeoutInMs) { + try { + sService.pausePolling(timeoutInMs); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + } + } + + + /** + * Returns whether the device supports observer mode or not. When observe + * mode is enabled, the NFC hardware will listen for NFC readers, but not + * respond to them. When observe mode is disabled, the NFC hardware will + * resoond to the reader and proceed with the transaction. + * @return true if the mode is supported, false otherwise. + */ + @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) + public boolean isObserveModeSupported() { + try { + return sService.isObserveModeSupported(); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + return false; + } + } + + /** + * Disables observe mode to allow the transaction to proceed. See + * {@link #isObserveModeSupported()} for a description of observe mode and + * use {@link #disallowTransaction()} to enable observe mode and block + * transactions again. + * + * @return boolean indicating success or failure. + */ + @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) + public boolean allowTransaction() { + try { + return sService.setObserveMode(false); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + return false; + } + } + + /** + * Signals that the transaction has completed and observe mode may be + * reenabled. See {@link #isObserveModeSupported()} for a description of + * observe mode and use {@link #allowTransaction()} to disable observe + * mode and allow transactions to proceed. + * + * @return boolean indicating success or failure. + */ + + @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) + public boolean disallowTransaction() { + try { + return sService.setObserveMode(true); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + return false; + } + } + + /** + * Resumes default polling for the current device state if polling is paused. Calling + * this while polling is not paused is a no-op. + * + * @hide + */ + public void resumePolling() { + try { + sService.resumePolling(); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + } + } + + /** + * Set one or more {@link Uri}s to send using Android Beam (TM). Every + * Uri you provide must have either scheme 'file' or scheme 'content'. + * + *

For the data provided through this method, Android Beam tries to + * switch to alternate transports such as Bluetooth to achieve a fast + * transfer speed. Hence this method is very suitable + * for transferring large files such as pictures or songs. + * + *

The receiving side will store the content of each Uri in + * a file and present a notification to the user to open the file + * with a {@link android.content.Intent} with action + * {@link android.content.Intent#ACTION_VIEW}. + * If multiple URIs are sent, the {@link android.content.Intent} will refer + * to the first of the stored files. + * + *

This method may be called at any time before {@link Activity#onDestroy}, + * but the URI(s) are only made available for Android Beam when the + * specified activity(s) are in resumed (foreground) state. The recommended + * approach is to call this method during your Activity's + * {@link Activity#onCreate} - see sample + * code below. This method does not immediately perform any I/O or blocking work, + * so is safe to call on your main thread. + * + *

{@link #setBeamPushUris} and {@link #setBeamPushUrisCallback} + * have priority over both {@link #setNdefPushMessage} and + * {@link #setNdefPushMessageCallback}. + * + *

If {@link #setBeamPushUris} is called with a null Uri array, + * and/or {@link #setBeamPushUrisCallback} is called with a null callback, + * then the Uri push will be completely disabled for the specified activity(s). + * + *

Code example: + *

+     * protected void onCreate(Bundle savedInstanceState) {
+     *     super.onCreate(savedInstanceState);
+     *     NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
+     *     if (nfcAdapter == null) return;  // NFC not available on this device
+     *     nfcAdapter.setBeamPushUris(new Uri[] {uri1, uri2}, this);
+     * }
+ * And that is it. Only one call per activity is necessary. The Android + * OS will automatically release its references to the Uri(s) and the + * Activity object when it is destroyed if you follow this pattern. + * + *

If your Activity wants to dynamically supply Uri(s), + * then set a callback using {@link #setBeamPushUrisCallback} instead + * of using this method. + * + *

Do not pass in an Activity that has already been through + * {@link Activity#onDestroy}. This is guaranteed if you call this API + * during {@link Activity#onCreate}. + * + *

If this device does not support alternate transports + * such as Bluetooth or WiFI, calling this method does nothing. + * + *

Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param uris an array of Uri(s) to push over Android Beam + * @param activity activity for which the Uri(s) will be pushed + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @removed this feature is removed. File sharing can work using other technology like + * Bluetooth. + */ + @java.lang.Deprecated + @UnsupportedAppUsage + public void setBeamPushUris(Uri[] uris, Activity activity) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + } + + /** + * Set a callback that will dynamically generate one or more {@link Uri}s + * to send using Android Beam (TM). Every Uri the callback provides + * must have either scheme 'file' or scheme 'content'. + * + *

For the data provided through this callback, Android Beam tries to + * switch to alternate transports such as Bluetooth to achieve a fast + * transfer speed. Hence this method is very suitable + * for transferring large files such as pictures or songs. + * + *

The receiving side will store the content of each Uri in + * a file and present a notification to the user to open the file + * with a {@link android.content.Intent} with action + * {@link android.content.Intent#ACTION_VIEW}. + * If multiple URIs are sent, the {@link android.content.Intent} will refer + * to the first of the stored files. + * + *

This method may be called at any time before {@link Activity#onDestroy}, + * but the URI(s) are only made available for Android Beam when the + * specified activity(s) are in resumed (foreground) state. The recommended + * approach is to call this method during your Activity's + * {@link Activity#onCreate} - see sample + * code below. This method does not immediately perform any I/O or blocking work, + * so is safe to call on your main thread. + * + *

{@link #setBeamPushUris} and {@link #setBeamPushUrisCallback} + * have priority over both {@link #setNdefPushMessage} and + * {@link #setNdefPushMessageCallback}. + * + *

If {@link #setBeamPushUris} is called with a null Uri array, + * and/or {@link #setBeamPushUrisCallback} is called with a null callback, + * then the Uri push will be completely disabled for the specified activity(s). + * + *

Code example: + *

+     * protected void onCreate(Bundle savedInstanceState) {
+     *     super.onCreate(savedInstanceState);
+     *     NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
+     *     if (nfcAdapter == null) return;  // NFC not available on this device
+     *     nfcAdapter.setBeamPushUrisCallback(callback, this);
+     * }
+ * And that is it. Only one call per activity is necessary. The Android + * OS will automatically release its references to the Uri(s) and the + * Activity object when it is destroyed if you follow this pattern. + * + *

Do not pass in an Activity that has already been through + * {@link Activity#onDestroy}. This is guaranteed if you call this API + * during {@link Activity#onCreate}. + * + *

If this device does not support alternate transports + * such as Bluetooth or WiFI, calling this method does nothing. + * + *

Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param callback callback, or null to disable + * @param activity activity for which the Uri(s) will be pushed + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @removed this feature is removed. File sharing can work using other technology like + * Bluetooth. + */ + @java.lang.Deprecated + @UnsupportedAppUsage + public void setBeamPushUrisCallback(CreateBeamUrisCallback callback, Activity activity) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + } + + /** + * Set a static {@link NdefMessage} to send using Android Beam (TM). + * + *

This method may be called at any time before {@link Activity#onDestroy}, + * but the NDEF message is only made available for NDEF push when the + * specified activity(s) are in resumed (foreground) state. The recommended + * approach is to call this method during your Activity's + * {@link Activity#onCreate} - see sample + * code below. This method does not immediately perform any I/O or blocking work, + * so is safe to call on your main thread. + * + *

Only one NDEF message can be pushed by the currently resumed activity. + * If both {@link #setNdefPushMessage} and + * {@link #setNdefPushMessageCallback} are set, then + * the callback will take priority. + * + *

If neither {@link #setNdefPushMessage} or + * {@link #setNdefPushMessageCallback} have been called for your activity, then + * the Android OS may choose to send a default NDEF message on your behalf, + * such as a URI for your application. + * + *

If {@link #setNdefPushMessage} is called with a null NDEF message, + * and/or {@link #setNdefPushMessageCallback} is called with a null callback, + * then NDEF push will be completely disabled for the specified activity(s). + * This also disables any default NDEF message the Android OS would have + * otherwise sent on your behalf for those activity(s). + * + *

If you want to prevent the Android OS from sending default NDEF + * messages completely (for all activities), you can include a + * {@code } element inside the {@code } + * element of your AndroidManifest.xml file, like this: + *

+     * <application ...>
+     *     <meta-data android:name="android.nfc.disable_beam_default"
+     *         android:value="true" />
+     * </application>
+ * + *

The API allows for multiple activities to be specified at a time, + * but it is strongly recommended to just register one at a time, + * and to do so during the activity's {@link Activity#onCreate}. For example: + *

+     * protected void onCreate(Bundle savedInstanceState) {
+     *     super.onCreate(savedInstanceState);
+     *     NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
+     *     if (nfcAdapter == null) return;  // NFC not available on this device
+     *     nfcAdapter.setNdefPushMessage(ndefMessage, this);
+     * }
+ * And that is it. Only one call per activity is necessary. The Android + * OS will automatically release its references to the NDEF message and the + * Activity object when it is destroyed if you follow this pattern. + * + *

If your Activity wants to dynamically generate an NDEF message, + * then set a callback using {@link #setNdefPushMessageCallback} instead + * of a static message. + * + *

Do not pass in an Activity that has already been through + * {@link Activity#onDestroy}. This is guaranteed if you call this API + * during {@link Activity#onCreate}. + * + *

For sending large content such as pictures and songs, + * consider using {@link #setBeamPushUris}, which switches to alternate transports + * such as Bluetooth to achieve a fast transfer rate. + * + *

Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param message NDEF message to push over NFC, or null to disable + * @param activity activity for which the NDEF message will be pushed + * @param activities optional additional activities, however we strongly recommend + * to only register one at a time, and to do so in that activity's + * {@link Activity#onCreate} + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @removed this feature is removed. File sharing can work using other technology like + * Bluetooth. + */ + @java.lang.Deprecated + @UnsupportedAppUsage + public void setNdefPushMessage(NdefMessage message, Activity activity, + Activity ... activities) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + } + + /** + * @hide + * @removed + */ + @SystemApi + @UnsupportedAppUsage + public void setNdefPushMessage(NdefMessage message, Activity activity, int flags) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + } + + /** + * Set a callback that dynamically generates NDEF messages to send using Android Beam (TM). + * + *

This method may be called at any time before {@link Activity#onDestroy}, + * but the NDEF message callback can only occur when the + * specified activity(s) are in resumed (foreground) state. The recommended + * approach is to call this method during your Activity's + * {@link Activity#onCreate} - see sample + * code below. This method does not immediately perform any I/O or blocking work, + * so is safe to call on your main thread. + * + *

Only one NDEF message can be pushed by the currently resumed activity. + * If both {@link #setNdefPushMessage} and + * {@link #setNdefPushMessageCallback} are set, then + * the callback will take priority. + * + *

If neither {@link #setNdefPushMessage} or + * {@link #setNdefPushMessageCallback} have been called for your activity, then + * the Android OS may choose to send a default NDEF message on your behalf, + * such as a URI for your application. + * + *

If {@link #setNdefPushMessage} is called with a null NDEF message, + * and/or {@link #setNdefPushMessageCallback} is called with a null callback, + * then NDEF push will be completely disabled for the specified activity(s). + * This also disables any default NDEF message the Android OS would have + * otherwise sent on your behalf for those activity(s). + * + *

If you want to prevent the Android OS from sending default NDEF + * messages completely (for all activities), you can include a + * {@code } element inside the {@code } + * element of your AndroidManifest.xml file, like this: + *

+     * <application ...>
+     *     <meta-data android:name="android.nfc.disable_beam_default"
+     *         android:value="true" />
+     * </application>
+ * + *

The API allows for multiple activities to be specified at a time, + * but it is strongly recommended to just register one at a time, + * and to do so during the activity's {@link Activity#onCreate}. For example: + *

+     * protected void onCreate(Bundle savedInstanceState) {
+     *     super.onCreate(savedInstanceState);
+     *     NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
+     *     if (nfcAdapter == null) return;  // NFC not available on this device
+     *     nfcAdapter.setNdefPushMessageCallback(callback, this);
+     * }
+ * And that is it. Only one call per activity is necessary. The Android + * OS will automatically release its references to the callback and the + * Activity object when it is destroyed if you follow this pattern. + * + *

Do not pass in an Activity that has already been through + * {@link Activity#onDestroy}. This is guaranteed if you call this API + * during {@link Activity#onCreate}. + *

For sending large content such as pictures and songs, + * consider using {@link #setBeamPushUris}, which switches to alternate transports + * such as Bluetooth to achieve a fast transfer rate. + *

Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param callback callback, or null to disable + * @param activity activity for which the NDEF message will be pushed + * @param activities optional additional activities, however we strongly recommend + * to only register one at a time, and to do so in that activity's + * {@link Activity#onCreate} + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @removed this feature is removed. File sharing can work using other technology like + * Bluetooth. + */ + @java.lang.Deprecated + @UnsupportedAppUsage + public void setNdefPushMessageCallback(CreateNdefMessageCallback callback, Activity activity, + Activity ... activities) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + } + + /** + * Set a callback on successful Android Beam (TM). + * + *

This method may be called at any time before {@link Activity#onDestroy}, + * but the callback can only occur when the + * specified activity(s) are in resumed (foreground) state. The recommended + * approach is to call this method during your Activity's + * {@link Activity#onCreate} - see sample + * code below. This method does not immediately perform any I/O or blocking work, + * so is safe to call on your main thread. + * + *

The API allows for multiple activities to be specified at a time, + * but it is strongly recommended to just register one at a time, + * and to do so during the activity's {@link Activity#onCreate}. For example: + *

+     * protected void onCreate(Bundle savedInstanceState) {
+     *     super.onCreate(savedInstanceState);
+     *     NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
+     *     if (nfcAdapter == null) return;  // NFC not available on this device
+     *     nfcAdapter.setOnNdefPushCompleteCallback(callback, this);
+     * }
+ * And that is it. Only one call per activity is necessary. The Android + * OS will automatically release its references to the callback and the + * Activity object when it is destroyed if you follow this pattern. + * + *

Do not pass in an Activity that has already been through + * {@link Activity#onDestroy}. This is guaranteed if you call this API + * during {@link Activity#onCreate}. + * + *

Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param callback callback, or null to disable + * @param activity activity for which the NDEF message will be pushed + * @param activities optional additional activities, however we strongly recommend + * to only register one at a time, and to do so in that activity's + * {@link Activity#onCreate} + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @removed this feature is removed. File sharing can work using other technology like + * Bluetooth. + */ + @java.lang.Deprecated + @UnsupportedAppUsage + public void setOnNdefPushCompleteCallback(OnNdefPushCompleteCallback callback, + Activity activity, Activity ... activities) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + } + + /** + * Enable foreground dispatch to the given Activity. + * + *

This will give priority to the foreground activity when + * dispatching a discovered {@link Tag} to an application. + * + *

If any IntentFilters are provided to this method they are used to match dispatch Intents + * for both the {@link NfcAdapter#ACTION_NDEF_DISCOVERED} and + * {@link NfcAdapter#ACTION_TAG_DISCOVERED}. Since {@link NfcAdapter#ACTION_TECH_DISCOVERED} + * relies on meta data outside of the IntentFilter matching for that dispatch Intent is handled + * by passing in the tech lists separately. Each first level entry in the tech list represents + * an array of technologies that must all be present to match. If any of the first level sets + * match then the dispatch is routed through the given PendingIntent. In other words, the second + * level is ANDed together and the first level entries are ORed together. + * + *

If you pass {@code null} for both the {@code filters} and {@code techLists} parameters + * that acts a wild card and will cause the foreground activity to receive all tags via the + * {@link NfcAdapter#ACTION_TAG_DISCOVERED} intent. + * + *

This method must be called from the main thread, and only when the activity is in the + * foreground (resumed). Also, activities must call {@link #disableForegroundDispatch} before + * the completion of their {@link Activity#onPause} callback to disable foreground dispatch + * after it has been enabled. + * + *

Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param activity the Activity to dispatch to + * @param intent the PendingIntent to start for the dispatch + * @param filters the IntentFilters to override dispatching for, or null to always dispatch + * @param techLists the tech lists used to perform matching for dispatching of the + * {@link NfcAdapter#ACTION_TECH_DISCOVERED} intent + * @throws IllegalStateException if the Activity is not currently in the foreground + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + */ + public void enableForegroundDispatch(Activity activity, PendingIntent intent, + IntentFilter[] filters, String[][] techLists) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + if (activity == null || intent == null) { + throw new NullPointerException(); + } + try { + TechListParcel parcel = null; + if (techLists != null && techLists.length > 0) { + parcel = new TechListParcel(techLists); + } + sService.setForegroundDispatch(intent, filters, parcel); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + } + } + + /** + * Disable foreground dispatch to the given activity. + * + *

After calling {@link #enableForegroundDispatch}, an activity + * must call this method before its {@link Activity#onPause} callback + * completes. + * + *

This method must be called from the main thread. + * + *

Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param activity the Activity to disable dispatch to + * @throws IllegalStateException if the Activity has already been paused + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + */ + public void disableForegroundDispatch(Activity activity) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + try { + sService.setForegroundDispatch(null, null, null); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + } + } + + /** + * Limit the NFC controller to reader mode while this Activity is in the foreground. + * + *

In this mode the NFC controller will only act as an NFC tag reader/writer, + * thus disabling any peer-to-peer (Android Beam) and card-emulation modes of + * the NFC adapter on this device. + * + *

Use {@link #FLAG_READER_SKIP_NDEF_CHECK} to prevent the platform from + * performing any NDEF checks in reader mode. Note that this will prevent the + * {@link Ndef} tag technology from being enumerated on the tag, and that + * NDEF-based tag dispatch will not be functional. + * + *

For interacting with tags that are emulated on another Android device + * using Android's host-based card-emulation, the recommended flags are + * {@link #FLAG_READER_NFC_A} and {@link #FLAG_READER_SKIP_NDEF_CHECK}. + * + * @param activity the Activity that requests the adapter to be in reader mode + * @param callback the callback to be called when a tag is discovered + * @param flags Flags indicating poll technologies and other optional parameters + * @param extras Additional extras for configuring reader mode. + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + */ + public void enableReaderMode(Activity activity, ReaderCallback callback, int flags, + Bundle extras) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + mNfcActivityManager.enableReaderMode(activity, callback, flags, extras); + } + + /** + * Restore the NFC adapter to normal mode of operation: supporting + * peer-to-peer (Android Beam), card emulation, and polling for + * all supported tag technologies. + * + * @param activity the Activity that currently has reader mode enabled + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + */ + public void disableReaderMode(Activity activity) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + mNfcActivityManager.disableReaderMode(activity); + } + + // Flags arguments to NFC adapter to enable/disable NFC + private static final int DISABLE_POLLING_FLAGS = 0x1000; + private static final int ENABLE_POLLING_FLAGS = 0x0000; + + /** + * Privileged API to enable disable reader polling. + * Note: Use with caution! The app is responsible for ensuring that the polling state is + * returned to normal. + * + * @see #enableReaderMode(Activity, ReaderCallback, int, Bundle) for more detailed + * documentation. + * + * @param enablePolling whether to enable or disable polling. + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @SuppressLint("VisiblySynchronized") + public void setReaderMode(boolean enablePolling) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + Binder token = new Binder(); + int flags = enablePolling ? ENABLE_POLLING_FLAGS : DISABLE_POLLING_FLAGS; + try { + NfcAdapter.sService.setReaderMode(token, null, flags, null); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + } + } + + /** + * Set the NFC controller to enable specific poll/listen technologies, + * as specified in parameters, while this Activity is in the foreground. + * + * Use {@link #FLAG_READER_KEEP} to keep current polling technology. + * Use {@link #FLAG_LISTEN_KEEP} to keep current listenig technology. + * Use {@link #FLAG_READER_DISABLE} to disable polling. + * Use {@link #FLAG_LISTEN_DISABLE} to disable listening. + * Also refer to {@link #resetDiscoveryTechnology(Activity)} to restore these changes. + *

+ * The pollTechnology, listenTechnology parameters can be one or several of below list. + *
+     *                    Poll                    Listen
+     *  Passive A         0x01   (NFC_A)           0x01  (NFC_PASSIVE_A)
+     *  Passive B         0x02   (NFC_B)           0x02  (NFC_PASSIVE_B)
+     *  Passive F         0x04   (NFC_F)           0x04  (NFC_PASSIVE_F)
+     *  ISO 15693         0x08   (NFC_V)             -
+     *  Kovio             0x10   (NFC_BARCODE)       -
+     * 
+ *

Example usage in an Activity that requires to disable poll, + * keep current listen technologies: + *

+     * protected void onResume() {
+     *     mNfcAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext());
+     *     mNfcAdapter.setDiscoveryTechnology(this,
+     *         NfcAdapter.FLAG_READER_DISABLE, NfcAdapter.FLAG_LISTEN_KEEP);
+     * }

+ * @param activity The Activity that requests NFC controller to enable specific technologies. + * @param pollTechnology Flags indicating poll technologies. + * @param listenTechnology Flags indicating listen technologies. + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF are unavailable. + */ + + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public void setDiscoveryTechnology(@NonNull Activity activity, + @PollTechnology int pollTechnology, @ListenTechnology int listenTechnology) { + if (listenTechnology == FLAG_LISTEN_DISABLE) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + mNfcActivityManager.enableReaderMode(activity, null, pollTechnology, null); + return; + } + if (pollTechnology == FLAG_READER_DISABLE) { + synchronized (sLock) { + if (!sHasCeFeature) { + throw new UnsupportedOperationException(); + } + } + } else { + synchronized (sLock) { + if (!sHasNfcFeature || !sHasCeFeature) { + throw new UnsupportedOperationException(); + } + } + } + mNfcActivityManager.setDiscoveryTech(activity, pollTechnology, listenTechnology); + } + + /** + * Restore the poll/listen technologies of NFC controller, + * which were changed by {@link #setDiscoveryTechnology(Activity , int , int)} + * + * @param activity The Activity that requests to changed technologies. + */ + + @FlaggedApi(Flags.FLAG_ENABLE_NFC_SET_DISCOVERY_TECH) + public void resetDiscoveryTechnology(@NonNull Activity activity) { + mNfcActivityManager.resetDiscoveryTech(activity); + } + + /** + * Manually invoke Android Beam to share data. + * + *

The Android Beam animation is normally only shown when two NFC-capable + * devices come into range. + * By calling this method, an Activity can invoke the Beam animation directly + * even if no other NFC device is in range yet. The Beam animation will then + * prompt the user to tap another NFC-capable device to complete the data + * transfer. + * + *

The main advantage of using this method is that it avoids the need for the + * user to tap the screen to complete the transfer, as this method already + * establishes the direction of the transfer and the consent of the user to + * share data. Callers are responsible for making sure that the user has + * consented to sharing data on NFC tap. + * + *

Note that to use this method, the passed in Activity must have already + * set data to share over Beam by using method calls such as + * {@link #setNdefPushMessageCallback} or + * {@link #setBeamPushUrisCallback}. + * + * @param activity the current foreground Activity that has registered data to share + * @return whether the Beam animation was successfully invoked + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @removed this feature is removed. File sharing can work using other technology like + * Bluetooth. + */ + @java.lang.Deprecated + @UnsupportedAppUsage + public boolean invokeBeam(Activity activity) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + return false; + } + + /** + * Enable NDEF message push over NFC while this Activity is in the foreground. + * + *

You must explicitly call this method every time the activity is + * resumed, and you must call {@link #disableForegroundNdefPush} before + * your activity completes {@link Activity#onPause}. + * + *

Strongly recommend to use the new {@link #setNdefPushMessage} + * instead: it automatically hooks into your activity life-cycle, + * so you do not need to call enable/disable in your onResume/onPause. + * + *

For NDEF push to function properly the other NFC device must + * support either NFC Forum's SNEP (Simple Ndef Exchange Protocol), or + * Android's "com.android.npp" (Ndef Push Protocol). This was optional + * on Gingerbread level Android NFC devices, but SNEP is mandatory on + * Ice-Cream-Sandwich and beyond. + * + *

This method must be called from the main thread. + * + *

Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param activity foreground activity + * @param message a NDEF Message to push over NFC + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable + * @removed this feature is removed. File sharing can work using other technology like + * Bluetooth. + */ + @Deprecated + @UnsupportedAppUsage + public void enableForegroundNdefPush(Activity activity, NdefMessage message) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + } + + /** + * Disable NDEF message push over P2P. + * + *

After calling {@link #enableForegroundNdefPush}, an activity + * must call this method before its {@link Activity#onPause} callback + * completes. + * + *

Strongly recommend to use the new {@link #setNdefPushMessage} + * instead: it automatically hooks into your activity life-cycle, + * so you do not need to call enable/disable in your onResume/onPause. + * + *

This method must be called from the main thread. + * + *

Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param activity the Foreground activity + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable + * @removed this feature is removed. File sharing can work using other technology like + * Bluetooth. + */ + @Deprecated + @UnsupportedAppUsage + public void disableForegroundNdefPush(Activity activity) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + } + + /** + * Sets Secure NFC feature. + *

This API is for the Settings application. + * @return True if successful + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + public boolean enableSecureNfc(boolean enable) { + if (!sHasNfcFeature && !sHasCeFeature) { + throw new UnsupportedOperationException(); + } + try { + return sService.setNfcSecure(enable); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + return sService.setNfcSecure(enable); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return false; + } + } + + /** + * Checks if the device supports Secure NFC functionality. + * + * @return True if device supports Secure NFC, false otherwise + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF, + * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE + * are unavailable + */ + public boolean isSecureNfcSupported() { + if (!sHasNfcFeature && !sHasCeFeature) { + throw new UnsupportedOperationException(); + } + try { + return sService.deviceSupportsNfcSecure(); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + return sService.deviceSupportsNfcSecure(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return false; + } + } + + /** + * Returns information regarding Nfc antennas on the device + * such as their relative positioning on the device. + * + * @return Information on the nfc antenna(s) on the device. + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF, + * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE + * are unavailable + */ + @Nullable + public NfcAntennaInfo getNfcAntennaInfo() { + if (!sHasNfcFeature && !sHasCeFeature) { + throw new UnsupportedOperationException(); + } + try { + return sService.getNfcAntennaInfo(); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return null; + } + try { + return sService.getNfcAntennaInfo(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return null; + } + } + + /** + * Checks Secure NFC feature is enabled. + * + * @return True if Secure NFC is enabled, false otherwise + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF, + * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE + * are unavailable + * @throws UnsupportedOperationException if device doesn't support + * Secure NFC functionality. {@link #isSecureNfcSupported} + */ + public boolean isSecureNfcEnabled() { + if (!sHasNfcFeature && !sHasCeFeature) { + throw new UnsupportedOperationException(); + } + try { + return sService.isNfcSecureEnabled(); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + return sService.isNfcSecureEnabled(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return false; + } + } + + /** + * Sets NFC Reader option feature. + *

This API is for the Settings application. + * @return True if successful + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_ENABLE_NFC_READER_OPTION) + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + public boolean enableReaderOption(boolean enable) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + try { + return sService.enableReaderOption(enable); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + return sService.enableReaderOption(enable); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return false; + } + } + + /** + * Checks if the device supports NFC Reader option functionality. + * + * @return True if device supports NFC Reader option, false otherwise + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_READER_OPTION) + public boolean isReaderOptionSupported() { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + try { + return sService.isReaderOptionSupported(); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + return sService.isReaderOptionSupported(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return false; + } + } + + /** + * Checks NFC Reader option feature is enabled. + * + * @return True if NFC Reader option is enabled, false otherwise + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @throws UnsupportedOperationException if device doesn't support + * NFC Reader option functionality. {@link #isReaderOptionSupported} + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_READER_OPTION) + public boolean isReaderOptionEnabled() { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + try { + return sService.isReaderOptionEnabled(); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + return sService.isReaderOptionEnabled(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return false; + } + } + + /** + * Enable NDEF Push feature. + *

This API is for the Settings application. + * @hide + * @removed + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @UnsupportedAppUsage + public boolean enableNdefPush() { + return false; + } + + /** + * Disable NDEF Push feature. + *

This API is for the Settings application. + * @hide + * @removed + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @UnsupportedAppUsage + public boolean disableNdefPush() { + return false; + } + + /** + * Return true if the NDEF Push (Android Beam) feature is enabled. + *

This function will return true only if both NFC is enabled, and the + * NDEF Push feature is enabled. + *

Note that if NFC is enabled but NDEF Push is disabled then this + * device can still receive NDEF messages, it just cannot send them. + *

Applications cannot directly toggle the NDEF Push feature, but they + * can request Settings UI allowing the user to toggle NDEF Push using + * startActivity(new Intent(Settings.ACTION_NFCSHARING_SETTINGS)) + *

Example usage in an Activity that requires NDEF Push: + *

+     * protected void onResume() {
+     *     super.onResume();
+     *     if (!nfcAdapter.isEnabled()) {
+     *         startActivity(new Intent(Settings.ACTION_NFC_SETTINGS));
+     *     } else if (!nfcAdapter.isNdefPushEnabled()) {
+     *         startActivity(new Intent(Settings.ACTION_NFCSHARING_SETTINGS));
+     *     }
+     * }
+ * + * @see android.provider.Settings#ACTION_NFCSHARING_SETTINGS + * @return true if NDEF Push feature is enabled + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + * @removed this feature is removed. File sharing can work using other technology like + * Bluetooth. + */ + @java.lang.Deprecated + @UnsupportedAppUsage + public boolean isNdefPushEnabled() { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + return false; + } + + /** + * Signals that you are no longer interested in communicating with an NFC tag + * for as long as it remains in range. + * + * All future attempted communication to this tag will fail with {@link IOException}. + * The NFC controller will be put in a low-power polling mode, allowing the device + * to save power in cases where it's "attached" to a tag all the time (e.g. a tag in + * car dock). + * + * Additionally the debounceMs parameter allows you to specify for how long the tag needs + * to have gone out of range, before it will be dispatched again. + * + * Note: the NFC controller typically polls at a pretty slow interval (100 - 500 ms). + * This means that if the tag repeatedly goes in and out of range (for example, in + * case of a flaky connection), and the controller happens to poll every time the + * tag is out of range, it *will* re-dispatch the tag after debounceMs, despite the tag + * having been "in range" during the interval. + * + * Note 2: if a tag with another UID is detected after this API is called, its effect + * will be cancelled; if this tag shows up before the amount of time specified in + * debounceMs, it will be dispatched again. + * + * Note 3: some tags have a random UID, in which case this API won't work reliably. + * + * @param tag the {@link android.nfc.Tag Tag} to ignore. + * @param debounceMs minimum amount of time the tag needs to be out of range before being + * dispatched again. + * @param tagRemovedListener listener to be called when the tag is removed from the field. + * Note that this will only be called if the tag has been out of range + * for at least debounceMs, or if another tag came into range before + * debounceMs. May be null in case you don't want a callback. + * @param handler the {@link android.os.Handler Handler} that will be used for delivering + * the callback. if the handler is null, then the thread used for delivering + * the callback is unspecified. + * @return false if the tag couldn't be found (or has already gone out of range), true otherwise + */ + public boolean ignore(final Tag tag, int debounceMs, + final OnTagRemovedListener tagRemovedListener, final Handler handler) { + ITagRemovedCallback.Stub iListener = null; + if (tagRemovedListener != null) { + iListener = new ITagRemovedCallback.Stub() { + @Override + public void onTagRemoved() throws RemoteException { + if (handler != null) { + handler.post(new Runnable() { + @Override + public void run() { + tagRemovedListener.onTagRemoved(); + } + }); + } else { + tagRemovedListener.onTagRemoved(); + } + synchronized (mLock) { + mTagRemovedListener = null; + } + } + }; + } + synchronized (mLock) { + mTagRemovedListener = iListener; + } + try { + return sService.ignore(tag.getServiceHandle(), debounceMs, iListener); + } catch (RemoteException e) { + return false; + } + } + + /** + * Inject a mock NFC tag.

+ * Used for testing purposes. + *

Requires the + * {@link android.Manifest.permission#WRITE_SECURE_SETTINGS} permission. + * @hide + */ + public void dispatch(Tag tag) { + if (tag == null) { + throw new NullPointerException("tag cannot be null"); + } + try { + sService.dispatch(tag); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + } + } + + /** + * Registers a new NFC unlock handler with the NFC service. + * + *

NFC unlock handlers are intended to unlock the keyguard in the presence of a trusted + * NFC device. The handler should return true if it successfully authenticates the user and + * unlocks the keyguard. + * + *

The parameter {@code tagTechnologies} determines which Tag technologies will be polled for + * at the lockscreen. Polling for less tag technologies reduces latency, and so it is + * strongly recommended to only provide the Tag technologies that the handler is expected to + * receive. There must be at least one tag technology provided, otherwise the unlock handler + * is ignored. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + public boolean addNfcUnlockHandler(final NfcUnlockHandler unlockHandler, + String[] tagTechnologies) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + // If there are no tag technologies, don't bother adding unlock handler + if (tagTechnologies.length == 0) { + return false; + } + + try { + synchronized (mLock) { + if (mNfcUnlockHandlers.containsKey(unlockHandler)) { + // update the tag technologies + sService.removeNfcUnlockHandler(mNfcUnlockHandlers.get(unlockHandler)); + mNfcUnlockHandlers.remove(unlockHandler); + } + + INfcUnlockHandler.Stub iHandler = new INfcUnlockHandler.Stub() { + @Override + public boolean onUnlockAttempted(Tag tag) throws RemoteException { + return unlockHandler.onUnlockAttempted(tag); + } + }; + + sService.addNfcUnlockHandler(iHandler, + Tag.getTechCodesFromStrings(tagTechnologies)); + mNfcUnlockHandlers.put(unlockHandler, iHandler); + } + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + return false; + } catch (IllegalArgumentException e) { + Log.e(TAG, "Unable to register LockscreenDispatch", e); + return false; + } + + return true; + } + + /** + * Removes a previously registered unlock handler. Also removes the tag technologies + * associated with the removed unlock handler. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + public boolean removeNfcUnlockHandler(NfcUnlockHandler unlockHandler) { + synchronized (sLock) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + try { + synchronized (mLock) { + if (mNfcUnlockHandlers.containsKey(unlockHandler)) { + sService.removeNfcUnlockHandler(mNfcUnlockHandlers.remove(unlockHandler)); + } + + return true; + } + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + return false; + } + } + + /** + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public INfcAdapterExtras getNfcAdapterExtrasInterface() { + if (mContext == null) { + throw new UnsupportedOperationException("You need a context on NfcAdapter to use the " + + " NFC extras APIs"); + } + try { + return sService.getNfcAdapterExtrasInterface(mContext.getPackageName()); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return null; + } + try { + return sService.getNfcAdapterExtrasInterface(mContext.getPackageName()); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return null; + } + } + + void enforceResumed(Activity activity) { + if (!activity.isResumed()) { + throw new IllegalStateException("API cannot be called while activity is paused"); + } + } + + int getSdkVersion() { + if (mContext == null) { + return android.os.Build.VERSION_CODES.GINGERBREAD; // best guess + } else { + return mContext.getApplicationInfo().targetSdkVersion; + } + } + + /** + * Sets NFC controller always on feature. + *

This API is for the NFCC internal state management. It allows to discriminate + * the controller function from the NFC function by keeping the NFC controller on without + * any NFC RF enabled if necessary. + *

This call is asynchronous. Register a listener {@link #ControllerAlwaysOnListener} + * by {@link #registerControllerAlwaysOnListener} to find out when the operation is + * complete. + *

If this returns true, then either NFCC always on state has been set based on the value, + * or a {@link ControllerAlwaysOnListener#onControllerAlwaysOnChanged(boolean)} will be invoked + * to indicate the state change. + * If this returns false, then there is some problem that prevents an attempt to turn NFCC + * always on. + * @param value if true the NFCC will be kept on (with no RF enabled if NFC adapter is + * disabled), if false the NFCC will follow completely the Nfc adapter state. + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF, + * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE + * are unavailable + * @return void + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) + public boolean setControllerAlwaysOn(boolean value) { + if (!sHasNfcFeature && !sHasCeFeature) { + throw new UnsupportedOperationException(); + } + try { + return sService.setControllerAlwaysOn(value); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + return sService.setControllerAlwaysOn(value); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return false; + } + } + + /** + * Checks NFC controller always on feature is enabled. + * + * @return True if NFC controller always on is enabled, false otherwise + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF, + * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE + * are unavailable + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) + public boolean isControllerAlwaysOn() { + try { + return sService.isControllerAlwaysOn(); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + return sService.isControllerAlwaysOn(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return false; + } + } + + /** + * Checks if the device supports NFC controller always on functionality. + * + * @return True if device supports NFC controller always on, false otherwise + * @throws UnsupportedOperationException if FEATURE_NFC, + * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF, + * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE + * are unavailable + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) + public boolean isControllerAlwaysOnSupported() { + if (!sHasNfcFeature && !sHasCeFeature) { + throw new UnsupportedOperationException(); + } + try { + return sService.isControllerAlwaysOnSupported(); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + return sService.isControllerAlwaysOnSupported(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return false; + } + } + + /** + * Register a {@link ControllerAlwaysOnListener} to listen for NFC controller always on + * state changes + *

The provided listener will be invoked by the given {@link Executor}. + * + * @param executor an {@link Executor} to execute given listener + * @param listener user implementation of the {@link ControllerAlwaysOnListener} + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) + public void registerControllerAlwaysOnListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull ControllerAlwaysOnListener listener) { + mControllerAlwaysOnListener.register(executor, listener); + } + + /** + * Unregister the specified {@link ControllerAlwaysOnListener} + *

The same {@link ControllerAlwaysOnListener} object used when calling + * {@link #registerControllerAlwaysOnListener(Executor, ControllerAlwaysOnListener)} + * must be used. + * + *

Listeners are automatically unregistered when application process goes away + * + * @param listener user implementation of the {@link ControllerAlwaysOnListener} + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) + public void unregisterControllerAlwaysOnListener( + @NonNull ControllerAlwaysOnListener listener) { + mControllerAlwaysOnListener.unregister(listener); + } + + + /** + * Sets whether we dispatch NFC Tag intents to the package. + * + *

{@link #ACTION_NDEF_DISCOVERED}, {@link #ACTION_TECH_DISCOVERED} or + * {@link #ACTION_TAG_DISCOVERED} will not be dispatched to an Activity if its package is + * disallowed. + *

An app is added to the preference list with the allowed flag set to {@code true} + * when a Tag intent is dispatched to the package for the first time. This API is called + * by settings to note that the user wants to change this default preference. + * + * @param userId the user to whom this package name will belong to + * @param pkg the full name (i.e. com.google.android.tag) of the package that will be added to + * the preference list + * @param allow {@code true} to allow dispatching Tag intents to the package's activity, + * {@code false} otherwise + * @return the {@link #TagIntentAppPreferenceResult} value + * @throws UnsupportedOperationException if {@link isTagIntentAppPreferenceSupported} returns + * {@code false} + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @TagIntentAppPreferenceResult + public int setTagIntentAppPreferenceForUser(@UserIdInt int userId, + @NonNull String pkg, boolean allow) { + Objects.requireNonNull(pkg, "pkg cannot be null"); + if (!isTagIntentAppPreferenceSupported()) { + Log.e(TAG, "TagIntentAppPreference is not supported"); + throw new UnsupportedOperationException(); + } + try { + return sService.setTagIntentAppPreferenceForUser(userId, pkg, allow); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + } + try { + return sService.setTagIntentAppPreferenceForUser(userId, pkg, allow); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return TAG_INTENT_APP_PREF_RESULT_UNAVAILABLE; + } + } + + + /** + * Get the Tag dispatch preference list of the UserId. + * + *

This returns a mapping of package names for this user id to whether we dispatch Tag + * intents to the package. {@link #ACTION_NDEF_DISCOVERED}, {@link #ACTION_TECH_DISCOVERED} or + * {@link #ACTION_TAG_DISCOVERED} will not be dispatched to an Activity if its package is + * mapped to {@code false}. + *

There are three different possible cases: + *

A package not being in the preference list. + * It does not contain any Tag intent filters or the user never triggers a Tag detection that + * matches the intent filter of the package. + *

A package being mapped to {@code true}. + * When a package has been launched by a tag detection for the first time, the package name is + * put to the map and by default mapped to {@code true}. The package will receive Tag intents as + * usual. + *

A package being mapped to {@code false}. + * The user chooses to disable this package and it will not receive any Tag intents anymore. + * + * @param userId the user to whom this preference list will belong to + * @return a map of the UserId which indicates the mapping from package name to + * boolean(allow status), otherwise return an empty map + * @throws UnsupportedOperationException if {@link isTagIntentAppPreferenceSupported} returns + * {@code false} + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @NonNull + public Map getTagIntentAppPreferenceForUser(@UserIdInt int userId) { + if (!isTagIntentAppPreferenceSupported()) { + Log.e(TAG, "TagIntentAppPreference is not supported"); + throw new UnsupportedOperationException(); + } + try { + Map result = (Map) sService + .getTagIntentAppPreferenceForUser(userId); + return result; + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return Collections.emptyMap(); + } + try { + Map result = (Map) sService + .getTagIntentAppPreferenceForUser(userId); + return result; + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return Collections.emptyMap(); + } + } + + /** + * Checks if the device supports Tag application preference. + * + * @return {@code true} if the device supports Tag application preference, {@code false} + * otherwise + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + public boolean isTagIntentAppPreferenceSupported() { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + try { + return sService.isTagIntentAppPreferenceSupported(); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + // Try one more time + if (sService == null) { + Log.e(TAG, "Failed to recover NFC Service."); + return false; + } + try { + return sService.isTagIntentAppPreferenceSupported(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover NFC Service."); + } + return false; + } + } +} diff --git a/nfc/java/android/nfc/NfcAntennaInfo.aidl b/nfc/java/android/nfc/NfcAntennaInfo.aidl new file mode 100644 index 000000000000..d5e79fc37282 --- /dev/null +++ b/nfc/java/android/nfc/NfcAntennaInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +parcelable NfcAntennaInfo; diff --git a/nfc/java/android/nfc/NfcAntennaInfo.java b/nfc/java/android/nfc/NfcAntennaInfo.java new file mode 100644 index 000000000000..b002ca21e8e3 --- /dev/null +++ b/nfc/java/android/nfc/NfcAntennaInfo.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.List; + + +/** + * Contains information on all available Nfc + * antennas on an Android device as well as information + * on the device itself in relation positioning of the + * antennas. + */ +public final class NfcAntennaInfo implements Parcelable { + // Width of the device in millimeters. + private final int mDeviceWidth; + // Height of the device in millimeters. + private final int mDeviceHeight; + // Whether the device is foldable. + private final boolean mDeviceFoldable; + // All available Nfc Antennas on the device. + private final List mAvailableNfcAntennas; + + public NfcAntennaInfo(int deviceWidth, int deviceHeight, boolean deviceFoldable, + @NonNull List availableNfcAntennas) { + this.mDeviceWidth = deviceWidth; + this.mDeviceHeight = deviceHeight; + this.mDeviceFoldable = deviceFoldable; + this.mAvailableNfcAntennas = availableNfcAntennas; + } + + /** + * Width of the device in millimeters. + */ + public int getDeviceWidth() { + return mDeviceWidth; + } + + /** + * Height of the device in millimeters. + */ + public int getDeviceHeight() { + return mDeviceHeight; + } + + /** + * Whether the device is foldable. When the device is foldable, + * the 0, 0 is considered to be bottom-left when the device is unfolded and + * the screens are facing the user. For non-foldable devices 0, 0 + * is bottom-left when the user is facing the screen. + */ + public boolean isDeviceFoldable() { + return mDeviceFoldable; + } + + /** + * Get all NFC antennas that exist on the device. + */ + @NonNull + public List getAvailableNfcAntennas() { + return mAvailableNfcAntennas; + } + + private NfcAntennaInfo(Parcel in) { + this.mDeviceWidth = in.readInt(); + this.mDeviceHeight = in.readInt(); + this.mDeviceFoldable = in.readByte() != 0; + this.mAvailableNfcAntennas = new ArrayList<>(); + in.readTypedList(this.mAvailableNfcAntennas, + AvailableNfcAntenna.CREATOR); + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public NfcAntennaInfo createFromParcel(Parcel in) { + return new NfcAntennaInfo(in); + } + + @Override + public NfcAntennaInfo[] newArray(int size) { + return new NfcAntennaInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mDeviceWidth); + dest.writeInt(mDeviceHeight); + dest.writeByte((byte) (mDeviceFoldable ? 1 : 0)); + dest.writeTypedList(mAvailableNfcAntennas, 0); + } +} diff --git a/nfc/java/android/nfc/NfcControllerAlwaysOnListener.java b/nfc/java/android/nfc/NfcControllerAlwaysOnListener.java new file mode 100644 index 000000000000..6ae58fd38cbe --- /dev/null +++ b/nfc/java/android/nfc/NfcControllerAlwaysOnListener.java @@ -0,0 +1,136 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.annotation.NonNull; +import android.nfc.NfcAdapter.ControllerAlwaysOnListener; +import android.os.Binder; +import android.os.RemoteException; +import android.util.Log; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * @hide + */ +public class NfcControllerAlwaysOnListener extends INfcControllerAlwaysOnListener.Stub { + private static final String TAG = NfcControllerAlwaysOnListener.class.getSimpleName(); + + private final INfcAdapter mAdapter; + + private final Map mListenerMap = new HashMap<>(); + + private boolean mCurrentState = false; + private boolean mIsRegistered = false; + + public NfcControllerAlwaysOnListener(@NonNull INfcAdapter adapter) { + mAdapter = adapter; + } + + /** + * Register a {@link ControllerAlwaysOnListener} with this + * {@link NfcControllerAlwaysOnListener} + * + * @param executor an {@link Executor} to execute given listener + * @param listener user implementation of the {@link ControllerAlwaysOnListener} + */ + public void register(@NonNull Executor executor, + @NonNull ControllerAlwaysOnListener listener) { + try { + if (!mAdapter.isControllerAlwaysOnSupported()) { + return; + } + } catch (RemoteException e) { + Log.w(TAG, "Failed to register"); + return; + } + synchronized (this) { + if (mListenerMap.containsKey(listener)) { + return; + } + + mListenerMap.put(listener, executor); + if (!mIsRegistered) { + try { + mAdapter.registerControllerAlwaysOnListener(this); + mIsRegistered = true; + } catch (RemoteException e) { + Log.w(TAG, "Failed to register"); + } + } + } + } + + /** + * Unregister the specified {@link ControllerAlwaysOnListener} + * + * @param listener user implementation of the {@link ControllerAlwaysOnListener} + */ + public void unregister(@NonNull ControllerAlwaysOnListener listener) { + try { + if (!mAdapter.isControllerAlwaysOnSupported()) { + return; + } + } catch (RemoteException e) { + Log.w(TAG, "Failed to unregister"); + return; + } + synchronized (this) { + if (!mListenerMap.containsKey(listener)) { + return; + } + + mListenerMap.remove(listener); + + if (mListenerMap.isEmpty() && mIsRegistered) { + try { + mAdapter.unregisterControllerAlwaysOnListener(this); + } catch (RemoteException e) { + Log.w(TAG, "Failed to unregister"); + } + mIsRegistered = false; + } + } + } + + private void sendCurrentState(@NonNull ControllerAlwaysOnListener listener) { + synchronized (this) { + Executor executor = mListenerMap.get(listener); + + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> listener.onControllerAlwaysOnChanged( + mCurrentState)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + @Override + public void onControllerAlwaysOnChanged(boolean isEnabled) { + synchronized (this) { + mCurrentState = isEnabled; + for (ControllerAlwaysOnListener cb : mListenerMap.keySet()) { + sendCurrentState(cb); + } + } + } +} + diff --git a/nfc/java/android/nfc/NfcEvent.java b/nfc/java/android/nfc/NfcEvent.java new file mode 100644 index 000000000000..aff4f52f2bab --- /dev/null +++ b/nfc/java/android/nfc/NfcEvent.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +/** + * Wraps information associated with any NFC event. + * + *

Immutable object, with direct access to the (final) fields. + * + *

An {@link NfcEvent} object is usually included in callbacks from + * {@link NfcAdapter}. Check the documentation of the callback to see + * which fields may be set. + * + *

This wrapper object is used (instead of parameters + * in the callback) because it allows new fields to be added without breaking + * API compatibility. + * + * @see NfcAdapter.OnNdefPushCompleteCallback#onNdefPushComplete + * @see NfcAdapter.CreateNdefMessageCallback#createNdefMessage + */ +public final class NfcEvent { + /** + * The {@link NfcAdapter} associated with the NFC event. + */ + public final NfcAdapter nfcAdapter; + + /** + * The major LLCP version number of the peer associated with the NFC event. + */ + public final int peerLlcpMajorVersion; + + /** + * The minor LLCP version number of the peer associated with the NFC event. + */ + public final int peerLlcpMinorVersion; + + NfcEvent(NfcAdapter nfcAdapter, byte peerLlcpVersion) { + this.nfcAdapter = nfcAdapter; + this.peerLlcpMajorVersion = (peerLlcpVersion & 0xF0) >> 4; + this.peerLlcpMinorVersion = peerLlcpVersion & 0x0F; + } +} diff --git a/nfc/java/android/nfc/NfcFrameworkInitializer.java b/nfc/java/android/nfc/NfcFrameworkInitializer.java new file mode 100644 index 000000000000..1ab8a1ebd72c --- /dev/null +++ b/nfc/java/android/nfc/NfcFrameworkInitializer.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.nfc; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.app.SystemServiceRegistry; +import android.content.Context; + +/** + * Class for performing registration for Nfc service. + * + * @hide + */ +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public class NfcFrameworkInitializer { + private NfcFrameworkInitializer() {} + + private static volatile NfcServiceManager sNfcServiceManager; + + /** + * Sets an instance of {@link NfcServiceManager} that allows + * the nfc mainline module to register/obtain nfc binder services. This is called + * by the platform during the system initialization. + * + * @param nfcServiceManager instance of {@link NfcServiceManager} that allows + * the nfc mainline module to register/obtain nfcd binder services. + */ + public static void setNfcServiceManager( + @NonNull NfcServiceManager nfcServiceManager) { + if (sNfcServiceManager != null) { + throw new IllegalStateException("setNfcServiceManager called twice!"); + } + + if (nfcServiceManager == null) { + throw new IllegalArgumentException("nfcServiceManager must not be null"); + } + + sNfcServiceManager = nfcServiceManager; + } + + /** @hide */ + public static NfcServiceManager getNfcServiceManager() { + return sNfcServiceManager; + } + + /** + * Called by {@link SystemServiceRegistry}'s static initializer and registers NFC service + * to {@link Context}, so that {@link Context#getSystemService} can return them. + * + * @throws IllegalStateException if this is called from anywhere besides + * {@link SystemServiceRegistry} + */ + public static void registerServiceWrappers() { + SystemServiceRegistry.registerContextAwareService(Context.NFC_SERVICE, + NfcManager.class, context -> new NfcManager(context)); + } +} diff --git a/nfc/java/android/nfc/NfcManager.java b/nfc/java/android/nfc/NfcManager.java new file mode 100644 index 000000000000..644e3122774b --- /dev/null +++ b/nfc/java/android/nfc/NfcManager.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.annotation.SystemService; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.Context; +import android.os.Build; + +/** + * High level manager used to obtain an instance of an {@link NfcAdapter}. + *

+ * Use {@link android.content.Context#getSystemService(java.lang.String)} + * with {@link Context#NFC_SERVICE} to create an {@link NfcManager}, + * then call {@link #getDefaultAdapter} to obtain the {@link NfcAdapter}. + *

+ * Alternately, you can just call the static helper + * {@link NfcAdapter#getDefaultAdapter(android.content.Context)}. + * + *

+ *

Developer Guides

+ *

For more information about using NFC, read the + * Near Field Communication developer guide.

+ *
+ * + * @see NfcAdapter#getDefaultAdapter(android.content.Context) + */ +@SystemService(Context.NFC_SERVICE) +public final class NfcManager { + private final NfcAdapter mAdapter; + + /** + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + public NfcManager(Context context) { + NfcAdapter adapter; + context = context.getApplicationContext(); + if (context == null) { + throw new IllegalArgumentException( + "context not associated with any application (using a mock context?)"); + } + try { + adapter = NfcAdapter.getNfcAdapter(context); + } catch (UnsupportedOperationException e) { + adapter = null; + } + mAdapter = adapter; + } + + /** + * Get the default NFC Adapter for this device. + * + * @return the default NFC Adapter + */ + public NfcAdapter getDefaultAdapter() { + return mAdapter; + } +} diff --git a/nfc/java/android/nfc/NfcServiceManager.java b/nfc/java/android/nfc/NfcServiceManager.java new file mode 100644 index 000000000000..5582f1154cad --- /dev/null +++ b/nfc/java/android/nfc/NfcServiceManager.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/********************************************************************** + * This file is not a part of the NFC mainline modure * + * *******************************************************************/ + +package android.nfc; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.annotation.SystemApi.Client; +import android.content.Context; +import android.os.IBinder; +import android.os.ServiceManager; + +/** + * Provides a way to register and obtain the system service binder objects managed by the nfc + * service. + * + * @hide + */ +@SystemApi(client = Client.MODULE_LIBRARIES) +public class NfcServiceManager { + + /** + * @hide + */ + public NfcServiceManager() { + } + + /** + * A class that exposes the methods to register and obtain each system service. + */ + public static final class ServiceRegisterer { + private final String mServiceName; + + /** + * @hide + */ + public ServiceRegisterer(String serviceName) { + mServiceName = serviceName; + } + + /** + * Register a system server binding object for a service. + */ + public void register(@NonNull IBinder service) { + ServiceManager.addService(mServiceName, service); + } + + /** + * Get the system server binding object for a service. + * + *

This blocks until the service instance is ready, + * or a timeout happens, in which case it returns null. + */ + @Nullable + public IBinder get() { + return ServiceManager.getService(mServiceName); + } + + /** + * Get the system server binding object for a service. + * + *

This blocks until the service instance is ready, + * or a timeout happens, in which case it throws {@link ServiceNotFoundException}. + */ + @NonNull + public IBinder getOrThrow() throws ServiceNotFoundException { + try { + return ServiceManager.getServiceOrThrow(mServiceName); + } catch (ServiceManager.ServiceNotFoundException e) { + throw new ServiceNotFoundException(mServiceName); + } + } + + /** + * Get the system server binding object for a service. If the specified service is + * not available, it returns null. + */ + @Nullable + public IBinder tryGet() { + return ServiceManager.checkService(mServiceName); + } + } + + /** + * See {@link ServiceRegisterer#getOrThrow}. + * + */ + public static class ServiceNotFoundException extends ServiceManager.ServiceNotFoundException { + /** + * Constructor. + * + * @param name the name of the binder service that cannot be found. + * + */ + public ServiceNotFoundException(@NonNull String name) { + super(name); + } + } + + /** + * Returns {@link ServiceRegisterer} for the "nfc" service. + */ + @NonNull + public ServiceRegisterer getNfcManagerServiceRegisterer() { + return new ServiceRegisterer(Context.NFC_SERVICE); + } +} diff --git a/nfc/java/android/nfc/Tag.aidl b/nfc/java/android/nfc/Tag.aidl new file mode 100644 index 000000000000..312261ebe76d --- /dev/null +++ b/nfc/java/android/nfc/Tag.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +parcelable Tag; \ No newline at end of file diff --git a/nfc/java/android/nfc/Tag.java b/nfc/java/android/nfc/Tag.java new file mode 100644 index 000000000000..500038f14d9d --- /dev/null +++ b/nfc/java/android/nfc/Tag.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.compat.annotation.UnsupportedAppUsage; +import android.content.Context; +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.NfcBarcode; +import android.nfc.tech.NfcF; +import android.nfc.tech.NfcV; +import android.nfc.tech.TagTechnology; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; + +/** + * Represents an NFC tag that has been discovered. + *

+ * {@link Tag} is an immutable object that represents the state of a NFC tag at + * the time of discovery. It can be used as a handle to {@link TagTechnology} classes + * to perform advanced operations, or directly queried for its ID via {@link #getId} and the + * set of technologies it contains via {@link #getTechList}. Arrays passed to and + * returned by this class are not cloned, so be careful not to modify them. + *

+ * A new tag object is created every time a tag is discovered (comes into range), even + * if it is the same physical tag. If a tag is removed and then returned into range, then + * only the most recent tag object can be successfully used to create a {@link TagTechnology}. + * + *

Tag Dispatch

+ * When a tag is discovered, a {@link Tag} object is created and passed to a + * single activity via the {@link NfcAdapter#EXTRA_TAG} extra in an + * {@link android.content.Intent} via {@link Context#startActivity}. A four stage dispatch is used + * to select the + * most appropriate activity to handle the tag. The Android OS executes each stage in order, + * and completes dispatch as soon as a single matching activity is found. If there are multiple + * matching activities found at any one stage then the Android activity chooser dialog is shown + * to allow the user to select the activity to receive the tag. + * + *

The Tag dispatch mechanism was designed to give a high probability of dispatching + * a tag to the correct activity without showing the user an activity chooser dialog. + * This is important for NFC interactions because they are very transient -- if a user has to + * move the Android device to choose an application then the connection will likely be broken. + * + *

1. Foreground activity dispatch

+ * A foreground activity that has called + * {@link NfcAdapter#enableForegroundDispatch NfcAdapter.enableForegroundDispatch()} is + * given priority. See the documentation on + * {@link NfcAdapter#enableForegroundDispatch NfcAdapter.enableForegroundDispatch()} for + * its usage. + *

2. NDEF data dispatch

+ * If the tag contains NDEF data the system inspects the first {@link NdefRecord} in the first + * {@link NdefMessage}. If the record is a URI, SmartPoster, or MIME data + * {@link Context#startActivity} is called with {@link NfcAdapter#ACTION_NDEF_DISCOVERED}. For URI + * and SmartPoster records the URI is put into the intent's data field. For MIME records the MIME + * type is put in the intent's type field. This allows activities to register to be launched only + * when data they know how to handle is present on a tag. This is the preferred method of handling + * data on a tag since NDEF data can be stored on many types of tags and doesn't depend on a + * specific tag technology. + * See {@link NfcAdapter#ACTION_NDEF_DISCOVERED} for more detail. If the tag does not contain + * NDEF data, or if no activity is registered + * for {@link NfcAdapter#ACTION_NDEF_DISCOVERED} with a matching data URI or MIME type then dispatch + * moves to stage 3. + *

3. Tag Technology dispatch

+ * {@link Context#startActivity} is called with {@link NfcAdapter#ACTION_TECH_DISCOVERED} to + * dispatch the tag to an activity that can handle the technologies present on the tag. + * Technologies are defined as sub-classes of {@link TagTechnology}, see the package + * {@link android.nfc.tech}. The Android OS looks for an activity that can handle one or + * more technologies in the tag. See {@link NfcAdapter#ACTION_TECH_DISCOVERED} for more detail. + *

4. Fall-back dispatch

+ * If no activity has been matched then {@link Context#startActivity} is called with + * {@link NfcAdapter#ACTION_TAG_DISCOVERED}. This is intended as a fall-back mechanism. + * See {@link NfcAdapter#ACTION_TAG_DISCOVERED}. + * + *

NFC Tag Background

+ * An NFC tag is a passive NFC device, powered by the NFC field of this Android device while + * it is in range. Tag's can come in many forms, such as stickers, cards, key fobs, or + * even embedded in a more sophisticated device. + *

+ * Tags can have a wide range of capabilities. Simple tags just offer read/write semantics, + * and contain some one time + * programmable areas to make read-only. More complex tags offer math operations + * and per-sector access control and authentication. The most sophisticated tags + * contain operating environments allowing complex interactions with the + * code executing on the tag. Use {@link TagTechnology} classes to access a broad + * range of capabilities available in NFC tags. + *

+ */ +public final class Tag implements Parcelable { + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + final byte[] mId; + final int[] mTechList; + final String[] mTechStringList; + final Bundle[] mTechExtras; + final int mServiceHandle; // for use by NFC service, 0 indicates a mock + final long mCookie; // for accessibility checking + final INfcTag mTagService; // interface to NFC service, will be null if mock tag + + int mConnectedTechnology; + + /** + * Hidden constructor to be used by NFC service and internal classes. + * @hide + */ + public Tag(byte[] id, int[] techList, Bundle[] techListExtras, int serviceHandle, + long cookie, INfcTag tagService) { + if (techList == null) { + throw new IllegalArgumentException("rawTargets cannot be null"); + } + mId = id; + mTechList = Arrays.copyOf(techList, techList.length); + mTechStringList = generateTechStringList(techList); + // Ensure mTechExtras is as long as mTechList + mTechExtras = Arrays.copyOf(techListExtras, techList.length); + mServiceHandle = serviceHandle; + mCookie = cookie; + mTagService = tagService; + mConnectedTechnology = -1; + + if (tagService == null) { + return; + } + } + + /** + * Construct a mock Tag. + *

This is an application constructed tag, so NfcAdapter methods on this Tag may fail + * with {@link IllegalArgumentException} since it does not represent a physical Tag. + *

This constructor might be useful for mock testing. + * @param id The tag identifier, can be null + * @param techList must not be null + * @return freshly constructed tag + * @hide + */ + public static Tag createMockTag(byte[] id, int[] techList, Bundle[] techListExtras, + long cookie) { + // set serviceHandle to 0 and tagService to null to indicate mock tag + return new Tag(id, techList, techListExtras, 0, cookie, null); + } + + private String[] generateTechStringList(int[] techList) { + final int size = techList.length; + String[] strings = new String[size]; + for (int i = 0; i < size; i++) { + switch (techList[i]) { + case TagTechnology.ISO_DEP: + strings[i] = IsoDep.class.getName(); + break; + case TagTechnology.MIFARE_CLASSIC: + strings[i] = MifareClassic.class.getName(); + break; + case TagTechnology.MIFARE_ULTRALIGHT: + strings[i] = MifareUltralight.class.getName(); + break; + case TagTechnology.NDEF: + strings[i] = Ndef.class.getName(); + break; + case TagTechnology.NDEF_FORMATABLE: + strings[i] = NdefFormatable.class.getName(); + break; + case TagTechnology.NFC_A: + strings[i] = NfcA.class.getName(); + break; + case TagTechnology.NFC_B: + strings[i] = NfcB.class.getName(); + break; + case TagTechnology.NFC_F: + strings[i] = NfcF.class.getName(); + break; + case TagTechnology.NFC_V: + strings[i] = NfcV.class.getName(); + break; + case TagTechnology.NFC_BARCODE: + strings[i] = NfcBarcode.class.getName(); + break; + default: + throw new IllegalArgumentException("Unknown tech type " + techList[i]); + } + } + return strings; + } + + static int[] getTechCodesFromStrings(String[] techStringList) throws IllegalArgumentException { + if (techStringList == null) { + throw new IllegalArgumentException("List cannot be null"); + } + int[] techIntList = new int[techStringList.length]; + HashMap stringToCodeMap = getTechStringToCodeMap(); + for (int i = 0; i < techStringList.length; i++) { + Integer code = stringToCodeMap.get(techStringList[i]); + + if (code == null) { + throw new IllegalArgumentException("Unknown tech type " + techStringList[i]); + } + + techIntList[i] = code.intValue(); + } + return techIntList; + } + + private static HashMap getTechStringToCodeMap() { + HashMap techStringToCodeMap = new HashMap(); + + techStringToCodeMap.put(IsoDep.class.getName(), TagTechnology.ISO_DEP); + techStringToCodeMap.put(MifareClassic.class.getName(), TagTechnology.MIFARE_CLASSIC); + techStringToCodeMap.put(MifareUltralight.class.getName(), TagTechnology.MIFARE_ULTRALIGHT); + techStringToCodeMap.put(Ndef.class.getName(), TagTechnology.NDEF); + techStringToCodeMap.put(NdefFormatable.class.getName(), TagTechnology.NDEF_FORMATABLE); + techStringToCodeMap.put(NfcA.class.getName(), TagTechnology.NFC_A); + techStringToCodeMap.put(NfcB.class.getName(), TagTechnology.NFC_B); + techStringToCodeMap.put(NfcF.class.getName(), TagTechnology.NFC_F); + techStringToCodeMap.put(NfcV.class.getName(), TagTechnology.NFC_V); + techStringToCodeMap.put(NfcBarcode.class.getName(), TagTechnology.NFC_BARCODE); + + return techStringToCodeMap; + } + + /** + * For use by NfcService only. + * @hide + */ + @UnsupportedAppUsage + public int getServiceHandle() { + return mServiceHandle; + } + + /** + * For use by NfcService only. + * @hide + */ + public int[] getTechCodeList() { + return mTechList; + } + + /** + * Get the Tag Identifier (if it has one). + *

The tag identifier is a low level serial number, used for anti-collision + * and identification. + *

Most tags have a stable unique identifier + * (UID), but some tags will generate a random ID every time they are discovered + * (RID), and there are some tags with no ID at all (the byte array will be zero-sized). + *

The size and format of an ID is specific to the RF technology used by the tag. + *

This function retrieves the ID as determined at discovery time, and does not + * perform any further RF communication or block. + * @return ID as byte array, never null + */ + public byte[] getId() { + return mId; + } + + /** + * Get the technologies available in this tag, as fully qualified class names. + *

+ * A technology is an implementation of the {@link TagTechnology} interface, + * and can be instantiated by calling the static get(Tag) + * method on the implementation with this Tag. The {@link TagTechnology} + * object can then be used to perform advanced, technology-specific operations on a tag. + *

+ * Android defines a mandatory set of technologies that must be correctly + * enumerated by all Android NFC devices, and an optional + * set of proprietary technologies. + * See {@link TagTechnology} for more details. + *

+ * The ordering of the returned array is undefined and should not be relied upon. + * @return an array of fully-qualified {@link TagTechnology} class-names. + */ + public String[] getTechList() { + return mTechStringList; + } + + /** + * Rediscover the technologies available on this tag. + *

+ * The technologies that are available on a tag may change due to + * operations being performed on a tag. For example, formatting a + * tag as NDEF adds the {@link Ndef} technology. The {@link rediscover} + * method reenumerates the available technologies on the tag + * and returns a new {@link Tag} object containing these technologies. + *

+ * You may not be connected to any of this {@link Tag}'s technologies + * when calling this method. + * This method guarantees that you will be returned the same Tag + * if it is still in the field. + *

May cause RF activity and may block. Must not be called + * from the main application thread. A blocked call will be canceled with + * {@link IOException} by calling {@link #close} from another thread. + *

Does not remove power from the RF field, so a tag having a random + * ID should not change its ID. + * @return the rediscovered tag object. + * @throws IOException if the tag cannot be rediscovered + * @hide + */ + // TODO See if we need TagLostException + // TODO Unhide for ICS + // TODO Update documentation to make sure it matches with the final + // implementation. + public Tag rediscover() throws IOException { + if (getConnectedTechnology() != -1) { + throw new IllegalStateException("Close connection to the technology first!"); + } + + if (mTagService == null) { + throw new IOException("Mock tags don't support this operation."); + } + try { + Tag newTag = mTagService.rediscover(getServiceHandle()); + if (newTag != null) { + return newTag; + } else { + throw new IOException("Failed to rediscover tag"); + } + } catch (RemoteException e) { + throw new IOException("NFC service dead"); + } + } + + + /** @hide */ + public boolean hasTech(int techType) { + for (int tech : mTechList) { + if (tech == techType) return true; + } + return false; + } + + /** @hide */ + public Bundle getTechExtras(int tech) { + int pos = -1; + for (int idx = 0; idx < mTechList.length; idx++) { + if (mTechList[idx] == tech) { + pos = idx; + break; + } + } + if (pos < 0) { + return null; + } + + return mTechExtras[pos]; + } + + /** @hide */ + @UnsupportedAppUsage + public INfcTag getTagService() { + if (mTagService == null) { + return null; + } + + try { + if (!mTagService.isTagUpToDate(mCookie)) { + String id_str = ""; + for (int i = 0; i < mId.length; i++) { + id_str = id_str + String.format("%02X ", mId[i]); + } + String msg = "Permission Denial: Tag ( ID: " + id_str + ") is out of date"; + throw new SecurityException(msg); + } + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + return mTagService; + } + + /** + * Human-readable description of the tag, for debugging. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder("TAG: Tech ["); + String[] techList = getTechList(); + int length = techList.length; + for (int i = 0; i < length; i++) { + sb.append(techList[i]); + if (i < length - 1) { + sb.append(", "); + } + } + sb.append("]"); + return sb.toString(); + } + + /*package*/ static byte[] readBytesWithNull(Parcel in) { + int len = in.readInt(); + byte[] result = null; + if (len >= 0) { + result = new byte[len]; + in.readByteArray(result); + } + return result; + } + + /*package*/ static void writeBytesWithNull(Parcel out, byte[] b) { + if (b == null) { + out.writeInt(-1); + return; + } + out.writeInt(b.length); + out.writeByteArray(b); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Null mTagService means this is a mock tag + int isMock = (mTagService == null)?1:0; + + writeBytesWithNull(dest, mId); + dest.writeInt(mTechList.length); + dest.writeIntArray(mTechList); + dest.writeTypedArray(mTechExtras, 0); + dest.writeInt(mServiceHandle); + dest.writeLong(mCookie); + dest.writeInt(isMock); + if (isMock == 0) { + dest.writeStrongBinder(mTagService.asBinder()); + } + } + + public static final @android.annotation.NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public Tag createFromParcel(Parcel in) { + INfcTag tagService; + + // Tag fields + byte[] id = Tag.readBytesWithNull(in); + int[] techList = new int[in.readInt()]; + in.readIntArray(techList); + Bundle[] techExtras = in.createTypedArray(Bundle.CREATOR); + int serviceHandle = in.readInt(); + long cookie = in.readLong(); + int isMock = in.readInt(); + if (isMock == 0) { + tagService = INfcTag.Stub.asInterface(in.readStrongBinder()); + } + else { + tagService = null; + } + + return new Tag(id, techList, techExtras, serviceHandle, cookie, tagService); + } + + @Override + public Tag[] newArray(int size) { + return new Tag[size]; + } + }; + + /** + * For internal use only. + * + * @hide + */ + public synchronized boolean setConnectedTechnology(int technology) { + if (mConnectedTechnology != -1) { + return false; + } + mConnectedTechnology = technology; + return true; + } + + /** + * For internal use only. + * + * @hide + */ + public int getConnectedTechnology() { + return mConnectedTechnology; + } + + /** + * For internal use only. + * + * @hide + */ + public void setTechnologyDisconnected() { + mConnectedTechnology = -1; + } +} diff --git a/nfc/java/android/nfc/TagLostException.java b/nfc/java/android/nfc/TagLostException.java new file mode 100644 index 000000000000..1981d7c0c6e0 --- /dev/null +++ b/nfc/java/android/nfc/TagLostException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2011, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import java.io.IOException; + +public class TagLostException extends IOException { + public TagLostException() { + super(); + } + + public TagLostException(String message) { + super(message); + } +} diff --git a/nfc/java/android/nfc/TechListParcel.aidl b/nfc/java/android/nfc/TechListParcel.aidl new file mode 100644 index 000000000000..92e646f220f0 --- /dev/null +++ b/nfc/java/android/nfc/TechListParcel.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +parcelable TechListParcel; \ No newline at end of file diff --git a/nfc/java/android/nfc/TechListParcel.java b/nfc/java/android/nfc/TechListParcel.java new file mode 100644 index 000000000000..9f01559bd344 --- /dev/null +++ b/nfc/java/android/nfc/TechListParcel.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.nfc; + +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +public class TechListParcel implements Parcelable { + + private String[][] mTechLists; + + public TechListParcel(String[]... strings) { + mTechLists = strings; + } + + public String[][] getTechLists() { + return mTechLists; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + int count = mTechLists.length; + dest.writeInt(count); + for (int i = 0; i < count; i++) { + String[] techList = mTechLists[i]; + dest.writeStringArray(techList); + } + } + + public static final @android.annotation.NonNull Creator CREATOR = new Creator() { + @Override + public TechListParcel createFromParcel(Parcel source) { + int count = source.readInt(); + String[][] techLists = new String[count][]; + for (int i = 0; i < count; i++) { + techLists[i] = source.createStringArray(); + } + return new TechListParcel(techLists); + } + + @Override + public TechListParcel[] newArray(int size) { + return new TechListParcel[size]; + } + }; +} diff --git a/nfc/java/android/nfc/TransceiveResult.aidl b/nfc/java/android/nfc/TransceiveResult.aidl new file mode 100644 index 000000000000..98f92ee03eef --- /dev/null +++ b/nfc/java/android/nfc/TransceiveResult.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +parcelable TransceiveResult; diff --git a/nfc/java/android/nfc/TransceiveResult.java b/nfc/java/android/nfc/TransceiveResult.java new file mode 100644 index 000000000000..7992094e6ba1 --- /dev/null +++ b/nfc/java/android/nfc/TransceiveResult.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2011, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.IOException; + +/** + * Class used to pipe transceive result from the NFC service. + * + * @hide + */ +public final class TransceiveResult implements Parcelable { + public static final int RESULT_SUCCESS = 0; + public static final int RESULT_FAILURE = 1; + public static final int RESULT_TAGLOST = 2; + public static final int RESULT_EXCEEDED_LENGTH = 3; + + final int mResult; + final byte[] mResponseData; + + public TransceiveResult(final int result, final byte[] data) { + mResult = result; + mResponseData = data; + } + + public byte[] getResponseOrThrow() throws IOException { + switch (mResult) { + case RESULT_SUCCESS: + return mResponseData; + case RESULT_TAGLOST: + throw new TagLostException("Tag was lost."); + case RESULT_EXCEEDED_LENGTH: + throw new IOException("Transceive length exceeds supported maximum"); + default: + throw new IOException("Transceive failed"); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mResult); + if (mResult == RESULT_SUCCESS) { + dest.writeInt(mResponseData.length); + dest.writeByteArray(mResponseData); + } + } + + public static final @android.annotation.NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public TransceiveResult createFromParcel(Parcel in) { + int result = in.readInt(); + byte[] responseData; + + if (result == RESULT_SUCCESS) { + int responseLength = in.readInt(); + responseData = new byte[responseLength]; + in.readByteArray(responseData); + } else { + responseData = null; + } + return new TransceiveResult(result, responseData); + } + + @Override + public TransceiveResult[] newArray(int size) { + return new TransceiveResult[size]; + } + }; + +} diff --git a/nfc/java/android/nfc/cardemulation/AidGroup.aidl b/nfc/java/android/nfc/cardemulation/AidGroup.aidl new file mode 100644 index 000000000000..56d6fa559677 --- /dev/null +++ b/nfc/java/android/nfc/cardemulation/AidGroup.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.cardemulation; + +parcelable AidGroup; diff --git a/nfc/java/android/nfc/cardemulation/AidGroup.java b/nfc/java/android/nfc/cardemulation/AidGroup.java new file mode 100644 index 000000000000..ae3e333051d7 --- /dev/null +++ b/nfc/java/android/nfc/cardemulation/AidGroup.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.cardemulation; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.nfc.Flags; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; +import android.util.proto.ProtoOutputStream; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +/********************************************************************** + * This file is not a part of the NFC mainline module * + * *******************************************************************/ + +/** + * The AidGroup class represents a group of Application Identifiers (AIDs). + * + *

The format of AIDs is defined in the ISO/IEC 7816-4 specification. This class + * requires the AIDs to be input as a hexadecimal string, with an even amount of + * hexadecimal characters, e.g. "F014811481". + * + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) +public final class AidGroup implements Parcelable { + /** + * The maximum number of AIDs that can be present in any one group. + */ + private static final int MAX_NUM_AIDS = 256; + + private static final String TAG = "AidGroup"; + + + private final List mAids; + private final String mCategory; + @SuppressWarnings("unused") // Unused as of now, but part of the XML input. + private final String mDescription; + + /** + * Creates a new AidGroup object. + * + * @param aids list of AIDs present in the group + * @param category category of this group, e.g. {@link CardEmulation#CATEGORY_PAYMENT} + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public AidGroup(@NonNull List aids, @Nullable String category) { + if (aids == null || aids.size() == 0) { + throw new IllegalArgumentException("No AIDS in AID group."); + } + if (aids.size() > MAX_NUM_AIDS) { + throw new IllegalArgumentException("Too many AIDs in AID group."); + } + for (String aid : aids) { + if (!isValidAid(aid)) { + throw new IllegalArgumentException("AID " + aid + " is not a valid AID."); + } + } + if (isValidCategory(category)) { + this.mCategory = category; + } else { + this.mCategory = CardEmulation.CATEGORY_OTHER; + } + this.mAids = new ArrayList(aids.size()); + for (String aid : aids) { + this.mAids.add(aid.toUpperCase(Locale.US)); + } + this.mDescription = null; + } + + /** + * Creates a new AidGroup object. + * + * @param category category of this group, e.g. {@link CardEmulation#CATEGORY_PAYMENT} + * @param description description of this group + */ + AidGroup(@NonNull String category, @NonNull String description) { + this.mAids = new ArrayList(); + this.mCategory = category; + this.mDescription = description; + } + + /** + * Returns the category of this group. + * @return the category of this AID group + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public String getCategory() { + return mCategory; + } + + /** + * Returns the list of AIDs in this group. + * + * @return the list of AIDs in this group + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public List getAids() { + return mAids; + } + + @Override + public String toString() { + StringBuilder out = new StringBuilder("Category: " + mCategory + + ", AIDs:"); + for (String aid : mAids) { + out.append(aid); + out.append(", "); + } + return out.toString(); + } + + /** + * Dump debugging info as AidGroupProto. + * + * If the output belongs to a sub message, the caller is responsible for wrapping this function + * between {@link ProtoOutputStream#start(long)} and {@link ProtoOutputStream#end(long)}. + * + * @param proto the ProtoOutputStream to write to + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public void dump(@NonNull ProtoOutputStream proto) { + proto.write(AidGroupProto.CATEGORY, mCategory); + for (String aid : mAids) { + proto.write(AidGroupProto.AIDS, aid); + } + } + + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @Override + public int describeContents() { + return 0; + } + + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mCategory); + dest.writeInt(mAids.size()); + if (mAids.size() > 0) { + dest.writeStringList(mAids); + } + } + + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public AidGroup createFromParcel(Parcel source) { + String category = source.readString8(); + int listSize = source.readInt(); + ArrayList aidList = new ArrayList(); + if (listSize > 0) { + source.readStringList(aidList); + } + return new AidGroup(aidList, category); + } + + @Override + public AidGroup[] newArray(int size) { + return new AidGroup[size]; + } + }; + + /** + * Create an instance of AID group from XML file. + * + * @param parser input xml parser stream + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @Nullable + public static AidGroup createFromXml(@NonNull XmlPullParser parser) + throws XmlPullParserException, IOException { + String category = null; + ArrayList aids = new ArrayList(); + AidGroup group = null; + boolean inGroup = false; + + int eventType = parser.getEventType(); + int minDepth = parser.getDepth(); + while (eventType != XmlPullParser.END_DOCUMENT && parser.getDepth() >= minDepth) { + String tagName = parser.getName(); + if (eventType == XmlPullParser.START_TAG) { + if (tagName.equals("aid")) { + if (inGroup) { + String aid = parser.getAttributeValue(null, "value"); + if (aid != null) { + aids.add(aid.toUpperCase()); + } + } else { + Log.d(TAG, "Ignoring tag while not in group"); + } + } else if (tagName.equals("aid-group")) { + category = parser.getAttributeValue(null, "category"); + if (category == null) { + Log.e(TAG, " tag without valid category"); + return null; + } + inGroup = true; + } else { + Log.d(TAG, "Ignoring unexpected tag: " + tagName); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (tagName.equals("aid-group") && inGroup && aids.size() > 0) { + group = new AidGroup(aids, category); + break; + } + } + eventType = parser.next(); + } + return group; + } + + /** + * Serialize instance of AID group to XML file. + * @param out XML serializer stream + * @throws IOException If an error occurs reading the element. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public void writeAsXml(@NonNull XmlSerializer out) throws IOException { + out.startTag(null, "aid-group"); + out.attribute(null, "category", mCategory); + for (String aid : mAids) { + out.startTag(null, "aid"); + out.attribute(null, "value", aid); + out.endTag(null, "aid"); + } + out.endTag(null, "aid-group"); + } + + private static boolean isValidCategory(String category) { + return CardEmulation.CATEGORY_PAYMENT.equals(category) || + CardEmulation.CATEGORY_OTHER.equals(category); + } + + private static final Pattern AID_PATTERN = Pattern.compile("[0-9A-Fa-f]{10,32}\\*?\\#?"); + /** + * Copied over from {@link CardEmulation#isValidAid(String)} + * @hide + */ + private static boolean isValidAid(String aid) { + if (aid == null) + return false; + + // If a prefix/subset AID, the total length must be odd (even # of AID chars + '*') + if ((aid.endsWith("*") || aid.endsWith("#")) && ((aid.length() % 2) == 0)) { + Log.e(TAG, "AID " + aid + " is not a valid AID."); + return false; + } + + // If not a prefix/subset AID, the total length must be even (even # of AID chars) + if ((!(aid.endsWith("*") || aid.endsWith("#"))) && ((aid.length() % 2) != 0)) { + Log.e(TAG, "AID " + aid + " is not a valid AID."); + return false; + } + + // Verify hex characters + if (!AID_PATTERN.matcher(aid).matches()) { + Log.e(TAG, "AID " + aid + " is not a valid AID."); + return false; + } + + return true; + } +} diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.aidl b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.aidl new file mode 100644 index 000000000000..a62fdd6a6c5c --- /dev/null +++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.cardemulation; + +parcelable ApduServiceInfo; diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java new file mode 100644 index 000000000000..41dee3ab035c --- /dev/null +++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -0,0 +1,912 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/********************************************************************** + * This file is not a part of the NFC mainline module * + * *******************************************************************/ + +package android.nfc.cardemulation; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.nfc.Flags; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; +import android.util.proto.ProtoOutputStream; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Class holding APDU service info. + * + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) +public final class ApduServiceInfo implements Parcelable { + private static final String TAG = "ApduServiceInfo"; + + /** + * The service that implements this + */ + private final ResolveInfo mService; + + /** + * Description of the service + */ + private final String mDescription; + + /** + * Whether this service represents AIDs running on the host CPU + */ + private final boolean mOnHost; + + /** + * Offhost reader name. + * eg: SIM, eSE etc + */ + private String mOffHostName; + + /** + * Offhost reader name from manifest file. + * Used for resetOffHostSecureElement() + */ + private final String mStaticOffHostName; + + /** + * Mapping from category to static AID group + */ + private final HashMap mStaticAidGroups; + + /** + * Mapping from category to dynamic AID group + */ + private final HashMap mDynamicAidGroups; + + /** + * Whether this service should only be started when the device is unlocked. + */ + private final boolean mRequiresDeviceUnlock; + + /** + * Whether this service should only be started when the device is screen on. + */ + private final boolean mRequiresDeviceScreenOn; + + /** + * The id of the service banner specified in XML. + */ + private final int mBannerResourceId; + + /** + * The uid of the package the service belongs to + */ + private final int mUid; + + /** + * Settings Activity for this service + */ + private final String mSettingsActivityName; + + /** + * State of the service for CATEGORY_OTHER selection + */ + private boolean mCategoryOtherServiceEnabled; + + /** + * @hide + */ + @UnsupportedAppUsage + public ApduServiceInfo(ResolveInfo info, boolean onHost, String description, + ArrayList staticAidGroups, ArrayList dynamicAidGroups, + boolean requiresUnlock, int bannerResource, int uid, + String settingsActivityName, String offHost, String staticOffHost) { + this(info, onHost, description, staticAidGroups, dynamicAidGroups, + requiresUnlock, bannerResource, uid, settingsActivityName, + offHost, staticOffHost, false); + } + + /** + * @hide + */ + public ApduServiceInfo(ResolveInfo info, boolean onHost, String description, + ArrayList staticAidGroups, ArrayList dynamicAidGroups, + boolean requiresUnlock, int bannerResource, int uid, + String settingsActivityName, String offHost, String staticOffHost, + boolean isEnabled) { + this(info, onHost, description, staticAidGroups, dynamicAidGroups, + requiresUnlock, onHost ? true : false, bannerResource, uid, + settingsActivityName, offHost, staticOffHost, isEnabled); + } + + /** + * @hide + */ + public ApduServiceInfo(ResolveInfo info, boolean onHost, String description, + List staticAidGroups, List dynamicAidGroups, + boolean requiresUnlock, boolean requiresScreenOn, int bannerResource, int uid, + String settingsActivityName, String offHost, String staticOffHost, boolean isEnabled) { + this.mService = info; + this.mDescription = description; + this.mStaticAidGroups = new HashMap(); + this.mDynamicAidGroups = new HashMap(); + this.mOffHostName = offHost; + this.mStaticOffHostName = staticOffHost; + this.mOnHost = onHost; + this.mRequiresDeviceUnlock = requiresUnlock; + this.mRequiresDeviceScreenOn = requiresScreenOn; + for (AidGroup aidGroup : staticAidGroups) { + this.mStaticAidGroups.put(aidGroup.getCategory(), aidGroup); + } + for (AidGroup aidGroup : dynamicAidGroups) { + this.mDynamicAidGroups.put(aidGroup.getCategory(), aidGroup); + } + this.mBannerResourceId = bannerResource; + this.mUid = uid; + this.mSettingsActivityName = settingsActivityName; + this.mCategoryOtherServiceEnabled = isEnabled; + + } + + /** + * Creates a new ApduServiceInfo object. + * + * @param pm packageManager instance + * @param info app component info + * @param onHost whether service is on host or not (secure element) + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public ApduServiceInfo(@NonNull PackageManager pm, @NonNull ResolveInfo info, boolean onHost) + throws XmlPullParserException, IOException { + ServiceInfo si = info.serviceInfo; + XmlResourceParser parser = null; + try { + if (onHost) { + parser = si.loadXmlMetaData(pm, HostApduService.SERVICE_META_DATA); + if (parser == null) { + throw new XmlPullParserException("No " + HostApduService.SERVICE_META_DATA + + " meta-data"); + } + } else { + parser = si.loadXmlMetaData(pm, OffHostApduService.SERVICE_META_DATA); + if (parser == null) { + throw new XmlPullParserException("No " + OffHostApduService.SERVICE_META_DATA + + " meta-data"); + } + } + + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.START_TAG && eventType != XmlPullParser.END_DOCUMENT) { + eventType = parser.next(); + } + + String tagName = parser.getName(); + if (onHost && !"host-apdu-service".equals(tagName)) { + throw new XmlPullParserException( + "Meta-data does not start with tag"); + } else if (!onHost && !"offhost-apdu-service".equals(tagName)) { + throw new XmlPullParserException( + "Meta-data does not start with tag"); + } + + Resources res = pm.getResourcesForApplication(si.applicationInfo); + AttributeSet attrs = Xml.asAttributeSet(parser); + if (onHost) { + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.HostApduService); + mService = info; + mDescription = sa.getString( + com.android.internal.R.styleable.HostApduService_description); + mRequiresDeviceUnlock = sa.getBoolean( + com.android.internal.R.styleable.HostApduService_requireDeviceUnlock, + false); + mRequiresDeviceScreenOn = sa.getBoolean( + com.android.internal.R.styleable.HostApduService_requireDeviceScreenOn, + true); + mBannerResourceId = sa.getResourceId( + com.android.internal.R.styleable.HostApduService_apduServiceBanner, -1); + mSettingsActivityName = sa.getString( + com.android.internal.R.styleable.HostApduService_settingsActivity); + mOffHostName = null; + mStaticOffHostName = mOffHostName; + sa.recycle(); + } else { + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.OffHostApduService); + mService = info; + mDescription = sa.getString( + com.android.internal.R.styleable.OffHostApduService_description); + mRequiresDeviceUnlock = sa.getBoolean( + com.android.internal.R.styleable.OffHostApduService_requireDeviceUnlock, + false); + mRequiresDeviceScreenOn = sa.getBoolean( + com.android.internal.R.styleable.OffHostApduService_requireDeviceScreenOn, + false); + mBannerResourceId = sa.getResourceId( + com.android.internal.R.styleable.OffHostApduService_apduServiceBanner, -1); + mSettingsActivityName = sa.getString( + com.android.internal.R.styleable.HostApduService_settingsActivity); + mOffHostName = sa.getString( + com.android.internal.R.styleable.OffHostApduService_secureElementName); + if (mOffHostName != null) { + if (mOffHostName.equals("eSE")) { + mOffHostName = "eSE1"; + } else if (mOffHostName.equals("SIM")) { + mOffHostName = "SIM1"; + } + } + mStaticOffHostName = mOffHostName; + sa.recycle(); + } + + mStaticAidGroups = new HashMap(); + mDynamicAidGroups = new HashMap(); + mOnHost = onHost; + + final int depth = parser.getDepth(); + AidGroup currentGroup = null; + + // Parsed values for the current AID group + while (((eventType = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) + && eventType != XmlPullParser.END_DOCUMENT) { + tagName = parser.getName(); + if (eventType == XmlPullParser.START_TAG && "aid-group".equals(tagName) && + currentGroup == null) { + final TypedArray groupAttrs = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AidGroup); + // Get category of AID group + String groupCategory = groupAttrs.getString( + com.android.internal.R.styleable.AidGroup_category); + String groupDescription = groupAttrs.getString( + com.android.internal.R.styleable.AidGroup_description); + if (!CardEmulation.CATEGORY_PAYMENT.equals(groupCategory)) { + groupCategory = CardEmulation.CATEGORY_OTHER; + } + currentGroup = mStaticAidGroups.get(groupCategory); + if (currentGroup != null) { + if (!CardEmulation.CATEGORY_OTHER.equals(groupCategory)) { + Log.e(TAG, "Not allowing multiple aid-groups in the " + + groupCategory + " category"); + currentGroup = null; + } + } else { + currentGroup = new AidGroup(groupCategory, groupDescription); + } + groupAttrs.recycle(); + } else if (eventType == XmlPullParser.END_TAG && "aid-group".equals(tagName) && + currentGroup != null) { + if (currentGroup.getAids().size() > 0) { + if (!mStaticAidGroups.containsKey(currentGroup.getCategory())) { + mStaticAidGroups.put(currentGroup.getCategory(), currentGroup); + } + } else { + Log.e(TAG, "Not adding with empty or invalid AIDs"); + } + currentGroup = null; + } else if (eventType == XmlPullParser.START_TAG && "aid-filter".equals(tagName) && + currentGroup != null) { + final TypedArray a = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AidFilter); + String aid = a.getString(com.android.internal.R.styleable.AidFilter_name). + toUpperCase(); + if (isValidAid(aid) && !currentGroup.getAids().contains(aid)) { + currentGroup.getAids().add(aid); + } else { + Log.e(TAG, "Ignoring invalid or duplicate aid: " + aid); + } + a.recycle(); + } else if (eventType == XmlPullParser.START_TAG && + "aid-prefix-filter".equals(tagName) && currentGroup != null) { + final TypedArray a = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AidFilter); + String aid = a.getString(com.android.internal.R.styleable.AidFilter_name). + toUpperCase(); + // Add wildcard char to indicate prefix + aid = aid.concat("*"); + if (isValidAid(aid) && !currentGroup.getAids().contains(aid)) { + currentGroup.getAids().add(aid); + } else { + Log.e(TAG, "Ignoring invalid or duplicate aid: " + aid); + } + a.recycle(); + } else if (eventType == XmlPullParser.START_TAG && + tagName.equals("aid-suffix-filter") && currentGroup != null) { + final TypedArray a = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AidFilter); + String aid = a.getString(com.android.internal.R.styleable.AidFilter_name). + toUpperCase(); + // Add wildcard char to indicate suffix + aid = aid.concat("#"); + if (isValidAid(aid) && !currentGroup.getAids().contains(aid)) { + currentGroup.getAids().add(aid); + } else { + Log.e(TAG, "Ignoring invalid or duplicate aid: " + aid); + } + a.recycle(); + } + } + } catch (NameNotFoundException e) { + throw new XmlPullParserException("Unable to create context for: " + si.packageName); + } finally { + if (parser != null) parser.close(); + } + // Set uid + mUid = si.applicationInfo.uid; + + mCategoryOtherServiceEnabled = true; // support other category + + } + + /** + * Returns the app component corresponding to this APDU service. + * + * @return app component for this service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public ComponentName getComponent() { + return new ComponentName(mService.serviceInfo.packageName, + mService.serviceInfo.name); + } + + /** + * Returns the offhost secure element name (if the service is offhost). + * + * @return offhost secure element name for offhost services + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @Nullable + public String getOffHostSecureElement() { + return mOffHostName; + } + + /** + * Returns a consolidated list of AIDs from the AID groups + * registered by this service. Note that if a service has both + * a static (manifest-based) AID group for a category and a dynamic + * AID group, only the dynamically registered AIDs will be returned + * for that category. + * @return List of AIDs registered by the service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public List getAids() { + final ArrayList aids = new ArrayList(); + for (AidGroup group : getAidGroups()) { + aids.addAll(group.getAids()); + } + return aids; + } + + /** + * Returns a consolidated list of AIDs with prefixes from the AID groups + * registered by this service. Note that if a service has both + * a static (manifest-based) AID group for a category and a dynamic + * AID group, only the dynamically registered AIDs will be returned + * for that category. + * @return List of prefix AIDs registered by the service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public List getPrefixAids() { + final ArrayList prefixAids = new ArrayList(); + for (AidGroup group : getAidGroups()) { + for (String aid : group.getAids()) { + if (aid.endsWith("*")) { + prefixAids.add(aid); + } + } + } + return prefixAids; + } + + /** + * Returns a consolidated list of AIDs with subsets from the AID groups + * registered by this service. Note that if a service has both + * a static (manifest-based) AID group for a category and a dynamic + * AID group, only the dynamically registered AIDs will be returned + * for that category. + * @return List of prefix AIDs registered by the service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public List getSubsetAids() { + final ArrayList subsetAids = new ArrayList(); + for (AidGroup group : getAidGroups()) { + for (String aid : group.getAids()) { + if (aid.endsWith("#")) { + subsetAids.add(aid); + } + } + } + return subsetAids; + } + + /** + * Returns the registered AID group for this category. + * + * @param category category name + * @return {@link AidGroup} instance for the provided category + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public AidGroup getDynamicAidGroupForCategory(@NonNull String category) { + return mDynamicAidGroups.get(category); + } + + /** + * Removes the registered AID group for this category. + * + * @param category category name + * @return {@code true} if an AID group existed + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public boolean removeDynamicAidGroupForCategory(@NonNull String category) { + return (mDynamicAidGroups.remove(category) != null); + } + + /** + * Returns a consolidated list of AID groups + * registered by this service. Note that if a service has both + * a static (manifest-based) AID group for a category and a dynamic + * AID group, only the dynamically registered AID group will be returned + * for that category. + * @return List of AIDs registered by the service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public List getAidGroups() { + final ArrayList groups = new ArrayList(); + for (Map.Entry entry : mDynamicAidGroups.entrySet()) { + groups.add(entry.getValue()); + } + for (Map.Entry entry : mStaticAidGroups.entrySet()) { + if (!mDynamicAidGroups.containsKey(entry.getKey())) { + // Consolidate AID groups - don't return static ones + // if a dynamic group exists for the category. + groups.add(entry.getValue()); + } + } + return groups; + } + + /** + * Returns the category to which this service has attributed the AID that is passed in, + * or null if we don't know this AID. + * @param aid AID to lookup for + * @return category name corresponding to this AID + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public String getCategoryForAid(@NonNull String aid) { + List groups = getAidGroups(); + for (AidGroup group : groups) { + if (group.getAids().contains(aid.toUpperCase())) { + return group.getCategory(); + } + } + return null; + } + + /** + * Returns whether there is any AID group for this category. + * @param category category name + * @return {@code true} if an AID group exists + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public boolean hasCategory(@NonNull String category) { + return (mStaticAidGroups.containsKey(category) || mDynamicAidGroups.containsKey(category)); + } + + /** + * Returns whether the service is on host or not. + * @return true if the service is on host (not secure element) + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public boolean isOnHost() { + return mOnHost; + } + + /** + * Returns whether the service requires device unlock. + * @return whether the service requires device unlock + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public boolean requiresUnlock() { + return mRequiresDeviceUnlock; + } + + /** + * Returns whether this service should only be started when the device is screen on. + * @return whether the service requires screen on + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public boolean requiresScreenOn() { + return mRequiresDeviceScreenOn; + } + + /** + * Returns description of service. + * @return user readable description of service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public String getDescription() { + return mDescription; + } + + /** + * Returns uid of service. + * @return uid of the service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public int getUid() { + return mUid; + } + + /** + * Add or replace an AID group to this service. + * @param aidGroup instance of aid group to set or replace + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public void setDynamicAidGroup(@NonNull AidGroup aidGroup) { + mDynamicAidGroups.put(aidGroup.getCategory(), aidGroup); + } + + /** + * Sets the off host Secure Element. + * @param offHost Secure Element to set. Only accept strings with prefix SIM or prefix eSE. + * Ref: GSMA TS.26 - NFC Handset Requirements + * TS26_NFC_REQ_069: For UICC, Secure Element Name SHALL be SIM[smartcard slot] + * (e.g. SIM/SIM1, SIM2… SIMn). + * TS26_NFC_REQ_070: For embedded SE, Secure Element Name SHALL be eSE[number] + * (e.g. eSE/eSE1, eSE2, etc.). + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public void setOffHostSecureElement(@NonNull String offHost) { + mOffHostName = offHost; + } + + /** + * Resets the off host Secure Element to statically defined + * by the service in the manifest file. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public void resetOffHostSecureElement() { + mOffHostName = mStaticOffHostName; + } + + /** + * Load label for this service. + * @param pm packagemanager instance + * @return label name corresponding to service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public CharSequence loadLabel(@NonNull PackageManager pm) { + return mService.loadLabel(pm); + } + + /** + * Load application label for this service. + * @param pm packagemanager instance + * @return app label name corresponding to service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public CharSequence loadAppLabel(@NonNull PackageManager pm) { + try { + return pm.getApplicationLabel(pm.getApplicationInfo( + mService.resolvePackageName, PackageManager.GET_META_DATA)); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + /** + * Load application icon for this service. + * @param pm packagemanager instance + * @return app icon corresponding to service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public Drawable loadIcon(@NonNull PackageManager pm) { + return mService.loadIcon(pm); + } + + /** + * Load application banner for this service. + * @param pm packagemanager instance + * @return app banner corresponding to service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public Drawable loadBanner(@NonNull PackageManager pm) { + Resources res; + try { + res = pm.getResourcesForApplication(mService.serviceInfo.packageName); + Drawable banner = res.getDrawable(mBannerResourceId); + return banner; + } catch (NotFoundException e) { + Log.e(TAG, "Could not load banner."); + return null; + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not load banner."); + return null; + } + } + + /** + * Load activity name for this service. + * @return activity name for this service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public String getSettingsActivityName() { return mSettingsActivityName; } + + @Override + public String toString() { + StringBuilder out = new StringBuilder("ApduService: "); + out.append(getComponent()); + out.append(", UID: " + mUid); + out.append(", description: " + mDescription); + out.append(", Static AID Groups: "); + for (AidGroup aidGroup : mStaticAidGroups.values()) { + out.append(aidGroup.toString()); + } + out.append(", Dynamic AID Groups: "); + for (AidGroup aidGroup : mDynamicAidGroups.values()) { + out.append(aidGroup.toString()); + } + return out.toString(); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof ApduServiceInfo)) return false; + ApduServiceInfo thatService = (ApduServiceInfo) o; + + return thatService.getComponent().equals(this.getComponent()) + && thatService.getUid() == this.getUid(); + } + + @Override + public int hashCode() { + return getComponent().hashCode(); + } + + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @Override + public int describeContents() { + return 0; + } + + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + mService.writeToParcel(dest, flags); + dest.writeString(mDescription); + dest.writeInt(mOnHost ? 1 : 0); + dest.writeString(mOffHostName); + dest.writeString(mStaticOffHostName); + dest.writeInt(mStaticAidGroups.size()); + if (mStaticAidGroups.size() > 0) { + dest.writeTypedList(new ArrayList(mStaticAidGroups.values())); + } + dest.writeInt(mDynamicAidGroups.size()); + if (mDynamicAidGroups.size() > 0) { + dest.writeTypedList(new ArrayList(mDynamicAidGroups.values())); + } + dest.writeInt(mRequiresDeviceUnlock ? 1 : 0); + dest.writeInt(mRequiresDeviceScreenOn ? 1 : 0); + dest.writeInt(mBannerResourceId); + dest.writeInt(mUid); + dest.writeString(mSettingsActivityName); + + dest.writeInt(mCategoryOtherServiceEnabled ? 1 : 0); + }; + + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public ApduServiceInfo createFromParcel(Parcel source) { + ResolveInfo info = ResolveInfo.CREATOR.createFromParcel(source); + String description = source.readString(); + boolean onHost = source.readInt() != 0; + String offHostName = source.readString(); + String staticOffHostName = source.readString(); + ArrayList staticAidGroups = new ArrayList(); + int numStaticGroups = source.readInt(); + if (numStaticGroups > 0) { + source.readTypedList(staticAidGroups, AidGroup.CREATOR); + } + ArrayList dynamicAidGroups = new ArrayList(); + int numDynamicGroups = source.readInt(); + if (numDynamicGroups > 0) { + source.readTypedList(dynamicAidGroups, AidGroup.CREATOR); + } + boolean requiresUnlock = source.readInt() != 0; + boolean requiresScreenOn = source.readInt() != 0; + int bannerResource = source.readInt(); + int uid = source.readInt(); + String settingsActivityName = source.readString(); + boolean isEnabled = source.readInt() != 0; + return new ApduServiceInfo(info, onHost, description, staticAidGroups, + dynamicAidGroups, requiresUnlock, requiresScreenOn, bannerResource, uid, + settingsActivityName, offHostName, staticOffHostName, + isEnabled); + } + + @Override + public ApduServiceInfo[] newArray(int size) { + return new ApduServiceInfo[size]; + } + }; + + /** + * Dump contents for debugging. + * @param fd parcelfiledescriptor instance + * @param pw printwriter instance + * @param args args for dumping + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public void dump(@NonNull ParcelFileDescriptor fd, @NonNull PrintWriter pw, + @NonNull String[] args) { + pw.println(" " + getComponent() + + " (Description: " + getDescription() + ")" + + " (UID: " + getUid() + ")"); + if (mOnHost) { + pw.println(" On Host Service"); + } else { + pw.println(" Off-host Service"); + pw.println(" " + "Current off-host SE:" + mOffHostName + + " static off-host SE:" + mStaticOffHostName); + } + pw.println(" Static AID groups:"); + for (AidGroup group : mStaticAidGroups.values()) { + pw.println(" Category: " + group.getCategory() + + "(enabled: " + mCategoryOtherServiceEnabled + ")"); + for (String aid : group.getAids()) { + pw.println(" AID: " + aid); + } + } + pw.println(" Dynamic AID groups:"); + for (AidGroup group : mDynamicAidGroups.values()) { + pw.println(" Category: " + group.getCategory() + + "(enabled: " + mCategoryOtherServiceEnabled + ")"); + for (String aid : group.getAids()) { + pw.println(" AID: " + aid); + } + } + pw.println(" Settings Activity: " + mSettingsActivityName); + pw.println(" Requires Device Unlock: " + mRequiresDeviceUnlock); + pw.println(" Requires Device ScreenOn: " + mRequiresDeviceScreenOn); + } + + + /** + * Enable or disable this CATEGORY_OTHER service. + * + * @param enabled true to indicate if user has enabled this service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public void setCategoryOtherServiceEnabled(boolean enabled) { + mCategoryOtherServiceEnabled = enabled; + } + + + /** + * Returns whether this CATEGORY_OTHER service is enabled or not. + * + * @return true to indicate if user has enabled this service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public boolean isCategoryOtherServiceEnabled() { + return mCategoryOtherServiceEnabled; + } + + /** + * Dump debugging info as ApduServiceInfoProto. + * + * If the output belongs to a sub message, the caller is responsible for wrapping this function + * between {@link ProtoOutputStream#start(long)} and {@link ProtoOutputStream#end(long)}. + * See proto definition in frameworks/base/core/proto/android/nfc/apdu_service_info.proto + * + * @param proto the ProtoOutputStream to write to + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public void dumpDebug(@NonNull ProtoOutputStream proto) { + getComponent().dumpDebug(proto, ApduServiceInfoProto.COMPONENT_NAME); + proto.write(ApduServiceInfoProto.DESCRIPTION, getDescription()); + proto.write(ApduServiceInfoProto.ON_HOST, mOnHost); + if (!mOnHost) { + proto.write(ApduServiceInfoProto.OFF_HOST_NAME, mOffHostName); + proto.write(ApduServiceInfoProto.STATIC_OFF_HOST_NAME, mStaticOffHostName); + } + for (AidGroup group : mStaticAidGroups.values()) { + long token = proto.start(ApduServiceInfoProto.STATIC_AID_GROUPS); + group.dump(proto); + proto.end(token); + } + for (AidGroup group : mDynamicAidGroups.values()) { + long token = proto.start(ApduServiceInfoProto.STATIC_AID_GROUPS); + group.dump(proto); + proto.end(token); + } + proto.write(ApduServiceInfoProto.SETTINGS_ACTIVITY_NAME, mSettingsActivityName); + } + + private static final Pattern AID_PATTERN = Pattern.compile("[0-9A-Fa-f]{10,32}\\*?\\#?"); + /** + * Copied over from {@link CardEmulation#isValidAid(String)} + * @hide + */ + private static boolean isValidAid(String aid) { + if (aid == null) + return false; + + // If a prefix/subset AID, the total length must be odd (even # of AID chars + '*') + if ((aid.endsWith("*") || aid.endsWith("#")) && ((aid.length() % 2) == 0)) { + Log.e(TAG, "AID " + aid + " is not a valid AID."); + return false; + } + + // If not a prefix/subset AID, the total length must be even (even # of AID chars) + if ((!(aid.endsWith("*") || aid.endsWith("#"))) && ((aid.length() % 2) != 0)) { + Log.e(TAG, "AID " + aid + " is not a valid AID."); + return false; + } + + // Verify hex characters + if (!AID_PATTERN.matcher(aid).matches()) { + Log.e(TAG, "AID " + aid + " is not a valid AID."); + return false; + } + + return true; + } +} diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java new file mode 100644 index 000000000000..ad86d70db967 --- /dev/null +++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java @@ -0,0 +1,1087 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.cardemulation; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SystemApi; +import android.annotation.UserIdInt; +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.nfc.Constants; +import android.nfc.Flags; +import android.nfc.INfcCardEmulation; +import android.nfc.NfcAdapter; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.util.Log; + +import java.util.HashMap; +import java.util.List; +import java.util.regex.Pattern; + +/** + * This class can be used to query the state of + * NFC card emulation services. + * + * For a general introduction into NFC card emulation, + * please read the + * NFC card emulation developer guide.

+ * + *

Use of this class requires the + * {@link PackageManager#FEATURE_NFC_HOST_CARD_EMULATION} to be present + * on the device. + */ +public final class CardEmulation { + private static final Pattern AID_PATTERN = Pattern.compile("[0-9A-Fa-f]{10,32}\\*?\\#?"); + static final String TAG = "CardEmulation"; + + /** + * Activity action: ask the user to change the default + * card emulation service for a certain category. This will + * show a dialog that asks the user whether they want to + * replace the current default service with the service + * identified with the ComponentName specified in + * {@link #EXTRA_SERVICE_COMPONENT}, for the category + * specified in {@link #EXTRA_CATEGORY}. There is an optional + * extra field using {@link Intent#EXTRA_USER} to specify + * the {@link UserHandle} of the user that owns the app. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CHANGE_DEFAULT = + "android.nfc.cardemulation.action.ACTION_CHANGE_DEFAULT"; + + /** + * The category extra for {@link #ACTION_CHANGE_DEFAULT}. + * + * @see #ACTION_CHANGE_DEFAULT + */ + public static final String EXTRA_CATEGORY = "category"; + + /** + * The service {@link ComponentName} object passed in as an + * extra for {@link #ACTION_CHANGE_DEFAULT}. + * + * @see #ACTION_CHANGE_DEFAULT + */ + public static final String EXTRA_SERVICE_COMPONENT = "component"; + + /** + * Category used for NFC payment services. + */ + public static final String CATEGORY_PAYMENT = "payment"; + + /** + * Category that can be used for all other card emulation + * services. + */ + public static final String CATEGORY_OTHER = "other"; + + /** + * Return value for {@link #getSelectionModeForCategory(String)}. + * + *

In this mode, the user has set a default service for this + * category. + * + *

When using ISO-DEP card emulation with {@link HostApduService} + * or {@link OffHostApduService}, if a remote NFC device selects + * any of the Application IDs (AIDs) + * that the default service has registered in this category, + * that service will automatically be bound to to handle + * the transaction. + */ + public static final int SELECTION_MODE_PREFER_DEFAULT = 0; + + /** + * Return value for {@link #getSelectionModeForCategory(String)}. + * + *

In this mode, when using ISO-DEP card emulation with {@link HostApduService} + * or {@link OffHostApduService}, whenever an Application ID (AID) of this category + * is selected, the user is asked which service they want to use to handle + * the transaction, even if there is only one matching service. + */ + public static final int SELECTION_MODE_ALWAYS_ASK = 1; + + /** + * Return value for {@link #getSelectionModeForCategory(String)}. + * + *

In this mode, when using ISO-DEP card emulation with {@link HostApduService} + * or {@link OffHostApduService}, the user will only be asked to select a service + * if the Application ID (AID) selected by the reader has been registered by multiple + * services. If there is only one service that has registered for the AID, + * that service will be invoked directly. + */ + public static final int SELECTION_MODE_ASK_IF_CONFLICT = 2; + + static boolean sIsInitialized = false; + static HashMap sCardEmus = new HashMap(); + static INfcCardEmulation sService; + + final Context mContext; + + private CardEmulation(Context context, INfcCardEmulation service) { + mContext = context.getApplicationContext(); + sService = service; + } + + /** + * Helper to get an instance of this class. + * + * @param adapter A reference to an NfcAdapter object. + * @return + */ + public static synchronized CardEmulation getInstance(NfcAdapter adapter) { + if (adapter == null) throw new NullPointerException("NfcAdapter is null"); + Context context = adapter.getContext(); + if (context == null) { + Log.e(TAG, "NfcAdapter context is null."); + throw new UnsupportedOperationException(); + } + if (!sIsInitialized) { + PackageManager pm = context.getPackageManager(); + if (pm == null) { + Log.e(TAG, "Cannot get PackageManager"); + throw new UnsupportedOperationException(); + } + if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)) { + Log.e(TAG, "This device does not support card emulation"); + throw new UnsupportedOperationException(); + } + sIsInitialized = true; + } + CardEmulation manager = sCardEmus.get(context); + if (manager == null) { + // Get card emu service + INfcCardEmulation service = adapter.getCardEmulationService(); + if (service == null) { + Log.e(TAG, "This device does not implement the INfcCardEmulation interface."); + throw new UnsupportedOperationException(); + } + manager = new CardEmulation(context, service); + sCardEmus.put(context, manager); + } + return manager; + } + + /** + * Allows an application to query whether a service is currently + * the default service to handle a card emulation category. + * + *

Note that if {@link #getSelectionModeForCategory(String)} + * returns {@link #SELECTION_MODE_ALWAYS_ASK} or {@link #SELECTION_MODE_ASK_IF_CONFLICT}, + * this method will always return false. That is because in these + * selection modes a default can't be set at the category level. For categories where + * the selection mode is {@link #SELECTION_MODE_ALWAYS_ASK} or + * {@link #SELECTION_MODE_ASK_IF_CONFLICT}, use + * {@link #isDefaultServiceForAid(ComponentName, String)} to determine whether a service + * is the default for a specific AID. + * + * @param service The ComponentName of the service + * @param category The category + * @return whether service is currently the default service for the category. + * + *

Requires the {@link android.Manifest.permission#NFC} permission. + */ + public boolean isDefaultServiceForCategory(ComponentName service, String category) { + try { + return sService.isDefaultServiceForCategory(mContext.getUser().getIdentifier(), + service, category); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.isDefaultServiceForCategory(mContext.getUser().getIdentifier(), + service, category); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + } + } + + /** + * + * Allows an application to query whether a service is currently + * the default handler for a specified ISO7816-4 Application ID. + * + * @param service The ComponentName of the service + * @param aid The ISO7816-4 Application ID + * @return whether the service is the default handler for the specified AID + * + *

Requires the {@link android.Manifest.permission#NFC} permission. + */ + public boolean isDefaultServiceForAid(ComponentName service, String aid) { + try { + return sService.isDefaultServiceForAid(mContext.getUser().getIdentifier(), + service, aid); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.isDefaultServiceForAid(mContext.getUser().getIdentifier(), + service, aid); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Returns whether the user has allowed AIDs registered in the + * specified category to be handled by a service that is preferred + * by the foreground application, instead of by a pre-configured default. + * + * Foreground applications can set such preferences using the + * {@link #setPreferredService(Activity, ComponentName)} method. + * + * @param category The category, e.g. {@link #CATEGORY_PAYMENT} + * @return whether AIDs in the category can be handled by a service + * specified by the foreground app. + */ + @SuppressWarnings("NonUserGetterCalled") + public boolean categoryAllowsForegroundPreference(String category) { + if (CATEGORY_PAYMENT.equals(category)) { + boolean preferForeground = false; + Context contextAsUser = mContext.createContextAsUser( + UserHandle.of(UserHandle.myUserId()), 0); + try { + preferForeground = Settings.Secure.getInt( + contextAsUser.getContentResolver(), + Constants.SETTINGS_SECURE_NFC_PAYMENT_FOREGROUND) != 0; + } catch (SettingNotFoundException e) { + } + return preferForeground; + } else { + // Allowed for all other categories + return true; + } + } + + /** + * Returns the service selection mode for the passed in category. + * Valid return values are: + *

{@link #SELECTION_MODE_PREFER_DEFAULT} the user has requested a default + * service for this category, which will be preferred. + *

{@link #SELECTION_MODE_ALWAYS_ASK} the user has requested to be asked + * every time what service they would like to use in this category. + *

{@link #SELECTION_MODE_ASK_IF_CONFLICT} the user will only be asked + * to pick a service if there is a conflict. + * @param category The category, for example {@link #CATEGORY_PAYMENT} + * @return the selection mode for the passed in category + */ + public int getSelectionModeForCategory(String category) { + if (CATEGORY_PAYMENT.equals(category)) { + boolean paymentRegistered = false; + try { + paymentRegistered = sService.isDefaultPaymentRegistered(); + } catch (RemoteException e) { + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return SELECTION_MODE_ALWAYS_ASK; + } + try { + paymentRegistered = sService.isDefaultPaymentRegistered(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return SELECTION_MODE_ALWAYS_ASK; + } + } + if (paymentRegistered) { + return SELECTION_MODE_PREFER_DEFAULT; + } else { + return SELECTION_MODE_ALWAYS_ASK; + } + } else { + return SELECTION_MODE_ASK_IF_CONFLICT; + } + } + /** + * Sets whether the system should default to observe mode or not when + * the service is in the foreground or the default payment service. + * + * @param service The component name of the service + * @param enable Whether the servic should default to observe mode or not + * @return whether the change was successful. + */ + @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) + public boolean setServiceObserveModeDefault(@NonNull ComponentName service, boolean enable) { + try { + return sService.setServiceObserveModeDefault(mContext.getUser().getIdentifier(), + service, enable); + } catch (RemoteException e) { + Log.e(TAG, "Failed to reach CardEmulationService."); + } + return false; + } + + /** + * Registers a list of AIDs for a specific category for the + * specified service. + * + *

If a list of AIDs for that category was previously + * registered for this service (either statically + * through the manifest, or dynamically by using this API), + * that list of AIDs will be replaced with this one. + * + *

Note that you can only register AIDs for a service that + * is running under the same UID as the caller of this API. Typically + * this means you need to call this from the same + * package as the service itself, though UIDs can also + * be shared between packages using shared UIDs. + * + * @param service The component name of the service + * @param category The category of AIDs to be registered + * @param aids A list containing the AIDs to be registered + * @return whether the registration was successful. + */ + public boolean registerAidsForService(ComponentName service, String category, + List aids) { + AidGroup aidGroup = new AidGroup(aids, category); + try { + return sService.registerAidGroupForService(mContext.getUser().getIdentifier(), + service, aidGroup); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.registerAidGroupForService(mContext.getUser().getIdentifier(), + service, aidGroup); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Unsets the off-host Secure Element for the given service. + * + *

Note that this will only remove Secure Element that was dynamically + * set using the {@link #setOffHostForService(ComponentName, String)} + * and resets it to a value that was statically assigned using manifest. + * + *

Note that you can only unset off-host SE for a service that + * is running under the same UID as the caller of this API. Typically + * this means you need to call this from the same + * package as the service itself, though UIDs can also + * be shared between packages using shared UIDs. + * + * @param service The component name of the service + * @return whether the registration was successful. + */ + @RequiresPermission(android.Manifest.permission.NFC) + @NonNull + public boolean unsetOffHostForService(@NonNull ComponentName service) { + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext); + if (adapter == null) { + return false; + } + + try { + return sService.unsetOffHostForService(mContext.getUser().getIdentifier(), service); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.unsetOffHostForService(mContext.getUser().getIdentifier(), service); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Sets the off-host Secure Element for the given service. + * + *

If off-host SE was initially set (either statically + * through the manifest, or dynamically by using this API), + * it will be replaced with this one. All AIDs registered by + * this service will be re-routed to this Secure Element if + * successful. AIDs that was statically assigned using manifest + * will re-route to off-host SE that stated in manifest after NFC + * toggle. + * + *

Note that you can only set off-host SE for a service that + * is running under the same UID as the caller of this API. Typically + * this means you need to call this from the same + * package as the service itself, though UIDs can also + * be shared between packages using shared UIDs. + * + *

Registeration will be successful only if the Secure Element + * exists on the device. + * + * @param service The component name of the service + * @param offHostSecureElement Secure Element to register the AID to. Only accept strings with + * prefix SIM or prefix eSE. + * Ref: GSMA TS.26 - NFC Handset Requirements + * TS26_NFC_REQ_069: For UICC, Secure Element Name SHALL be + * SIM[smartcard slot] + * (e.g. SIM/SIM1, SIM2… SIMn). + * TS26_NFC_REQ_070: For embedded SE, Secure Element Name SHALL be + * eSE[number] + * (e.g. eSE/eSE1, eSE2, etc.). + * @return whether the registration was successful. + */ + @RequiresPermission(android.Manifest.permission.NFC) + @NonNull + public boolean setOffHostForService(@NonNull ComponentName service, + @NonNull String offHostSecureElement) { + boolean validSecureElement = false; + + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext); + if (adapter == null || offHostSecureElement == null) { + return false; + } + + List validSE = adapter.getSupportedOffHostSecureElements(); + if ((offHostSecureElement.startsWith("eSE") && !validSE.contains("eSE")) + || (offHostSecureElement.startsWith("SIM") && !validSE.contains("SIM"))) { + return false; + } + + if (!offHostSecureElement.startsWith("eSE") && !offHostSecureElement.startsWith("SIM")) { + return false; + } + + if (offHostSecureElement.equals("eSE")) { + offHostSecureElement = "eSE1"; + } else if (offHostSecureElement.equals("SIM")) { + offHostSecureElement = "SIM1"; + } + + try { + return sService.setOffHostForService(mContext.getUser().getIdentifier(), service, + offHostSecureElement); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.setOffHostForService(mContext.getUser().getIdentifier(), service, + offHostSecureElement); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Retrieves the currently registered AIDs for the specified + * category for a service. + * + *

Note that this will only return AIDs that were dynamically + * registered using {@link #registerAidsForService(ComponentName, String, List)} + * method. It will *not* return AIDs that were statically registered + * in the manifest. + * + * @param service The component name of the service + * @param category The category for which the AIDs were registered, + * e.g. {@link #CATEGORY_PAYMENT} + * @return The list of AIDs registered for this category, or null if it couldn't be found. + */ + public List getAidsForService(ComponentName service, String category) { + try { + AidGroup group = sService.getAidGroupForService(mContext.getUser().getIdentifier(), + service, category); + return (group != null ? group.getAids() : null); + } catch (RemoteException e) { + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return null; + } + try { + AidGroup group = sService.getAidGroupForService(mContext.getUser().getIdentifier(), + service, category); + return (group != null ? group.getAids() : null); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return null; + } + } + } + + /** + * Removes a previously registered list of AIDs for the specified category for the + * service provided. + * + *

Note that this will only remove AIDs that were dynamically + * registered using the {@link #registerAidsForService(ComponentName, String, List)} + * method. It will *not* remove AIDs that were statically registered in + * the manifest. If dynamically registered AIDs are removed using + * this method, and a statically registered AID group for the same category + * exists in the manifest, the static AID group will become active again. + * + * @param service The component name of the service + * @param category The category of the AIDs to be removed, e.g. {@link #CATEGORY_PAYMENT} + * @return whether the group was successfully removed. + */ + public boolean removeAidsForService(ComponentName service, String category) { + try { + return sService.removeAidGroupForService(mContext.getUser().getIdentifier(), service, + category); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.removeAidGroupForService(mContext.getUser().getIdentifier(), + service, category); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Allows a foreground application to specify which card emulation service + * should be preferred while a specific Activity is in the foreground. + * + *

The specified Activity must currently be in resumed state. A good + * paradigm is to call this method in your {@link Activity#onResume}, and to call + * {@link #unsetPreferredService(Activity)} in your {@link Activity#onPause}. + * + *

This method call will fail in two specific scenarios: + *

    + *
  • If the service registers one or more AIDs in the {@link #CATEGORY_PAYMENT} + * category, but the user has indicated that foreground apps are not allowed + * to override the default payment service. + *
  • If the service registers one or more AIDs in the {@link #CATEGORY_OTHER} + * category that are also handled by the default payment service, and the + * user has indicated that foreground apps are not allowed to override the + * default payment service. + *
+ * + *

Use {@link #categoryAllowsForegroundPreference(String)} to determine + * whether foreground apps can override the default payment service. + * + *

Note that this preference is not persisted by the OS, and hence must be + * called every time the Activity is resumed. + * + * @param activity The activity which prefers this service to be invoked + * @param service The service to be preferred while this activity is in the foreground + * @return whether the registration was successful + */ + public boolean setPreferredService(Activity activity, ComponentName service) { + // Verify the activity is in the foreground before calling into NfcService + if (activity == null || service == null) { + throw new NullPointerException("activity or service or category is null"); + } + try { + return sService.setPreferredService(service); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.setPreferredService(service); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Unsets the preferred service for the specified Activity. + * + *

Note that the specified Activity must still be in resumed + * state at the time of this call. A good place to call this method + * is in your {@link Activity#onPause} implementation. + * + * @param activity The activity which the service was registered for + * @return true when successful + */ + public boolean unsetPreferredService(Activity activity) { + if (activity == null) { + throw new NullPointerException("activity is null"); + } + try { + return sService.unsetPreferredService(); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.unsetPreferredService(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Some devices may allow an application to register all + * AIDs that starts with a certain prefix, e.g. + * "A000000004*" to register all MasterCard AIDs. + * + * Use this method to determine whether this device + * supports registering AID prefixes. + * + * @return whether AID prefix registering is supported on this device. + */ + public boolean supportsAidPrefixRegistration() { + try { + return sService.supportsAidPrefixRegistration(); + } catch (RemoteException e) { + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.supportsAidPrefixRegistration(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Retrieves the registered AIDs for the preferred payment service. + * + * @return The list of AIDs registered for this category, or null if it couldn't be found. + */ + @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) + @Nullable + public List getAidsForPreferredPaymentService() { + try { + ApduServiceInfo serviceInfo = sService.getPreferredPaymentService( + mContext.getUser().getIdentifier()); + return (serviceInfo != null ? serviceInfo.getAids() : null); + } catch (RemoteException e) { + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + throw e.rethrowFromSystemServer(); + } + try { + ApduServiceInfo serviceInfo = + sService.getPreferredPaymentService(mContext.getUser().getIdentifier()); + return (serviceInfo != null ? serviceInfo.getAids() : null); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover CardEmulationService."); + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Retrieves the route destination for the preferred payment service. + * + * @return The route destination secure element name of the preferred payment service. + * HCE payment: "Host" + * OffHost payment: 1. String with prefix SIM or prefix eSE string. + * Ref: GSMA TS.26 - NFC Handset Requirements + * TS26_NFC_REQ_069: For UICC, Secure Element Name SHALL be + * SIM[smartcard slot] + * (e.g. SIM/SIM1, SIM2… SIMn). + * TS26_NFC_REQ_070: For embedded SE, Secure Element Name SHALL be + * eSE[number] + * (e.g. eSE/eSE1, eSE2, etc.). + * 2. "OffHost" if the payment service does not specify secure element + * name. + */ + @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) + @Nullable + public String getRouteDestinationForPreferredPaymentService() { + try { + ApduServiceInfo serviceInfo = sService.getPreferredPaymentService( + mContext.getUser().getIdentifier()); + if (serviceInfo != null) { + if (!serviceInfo.isOnHost()) { + return serviceInfo.getOffHostSecureElement() == null ? + "OffHost" : serviceInfo.getOffHostSecureElement(); + } + return "Host"; + } + return null; + } catch (RemoteException e) { + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + throw e.rethrowFromSystemServer(); + } + try { + ApduServiceInfo serviceInfo = + sService.getPreferredPaymentService(mContext.getUser().getIdentifier()); + if (serviceInfo != null) { + if (!serviceInfo.isOnHost()) { + return serviceInfo.getOffHostSecureElement() == null ? + "Offhost" : serviceInfo.getOffHostSecureElement(); + } + return "Host"; + } + return null; + + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover CardEmulationService."); + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Returns a user-visible description of the preferred payment service. + * + * @return the preferred payment service description + */ + @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) + @Nullable + public CharSequence getDescriptionForPreferredPaymentService() { + try { + ApduServiceInfo serviceInfo = sService.getPreferredPaymentService( + mContext.getUser().getIdentifier()); + return (serviceInfo != null ? serviceInfo.getDescription() : null); + } catch (RemoteException e) { + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + throw e.rethrowFromSystemServer(); + } + try { + ApduServiceInfo serviceInfo = + sService.getPreferredPaymentService(mContext.getUser().getIdentifier()); + return (serviceInfo != null ? serviceInfo.getDescription() : null); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover CardEmulationService."); + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * @hide + */ + public boolean setDefaultServiceForCategory(ComponentName service, String category) { + try { + return sService.setDefaultServiceForCategory(mContext.getUser().getIdentifier(), + service, category); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.setDefaultServiceForCategory(mContext.getUser().getIdentifier(), + service, category); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * @hide + */ + public boolean setDefaultForNextTap(ComponentName service) { + try { + return sService.setDefaultForNextTap(mContext.getUser().getIdentifier(), service); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.setDefaultForNextTap(mContext.getUser().getIdentifier(), service); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * @hide + */ + public boolean setDefaultForNextTap(int userId, ComponentName service) { + try { + return sService.setDefaultForNextTap(userId, service); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.setDefaultForNextTap(userId, service); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * @hide + */ + public List getServices(String category) { + try { + return sService.getServices(mContext.getUser().getIdentifier(), category); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return null; + } + try { + return sService.getServices(mContext.getUser().getIdentifier(), category); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return null; + } + } + } + + /** + * Retrieves list of services registered of the provided category for the provided user. + * + * @param category Category string, one of {@link #CATEGORY_PAYMENT} or {@link #CATEGORY_OTHER} + * @param userId the user handle of the user whose information is being requested. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public List getServices(@NonNull String category, @UserIdInt int userId) { + try { + return sService.getServices(userId, category); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return null; + } + try { + return sService.getServices(userId, category); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return null; + } + } + } + + /** + * A valid AID according to ISO/IEC 7816-4: + *

    + *
  • Has >= 5 bytes and <=16 bytes (>=10 hex chars and <= 32 hex chars) + *
  • Consist of only hex characters + *
  • Additionally, we allow an asterisk at the end, to indicate + * a prefix + *
  • Additinally we allow an (#) at symbol at the end, to indicate + * a subset + *
+ * + * @hide + */ + public static boolean isValidAid(String aid) { + if (aid == null) + return false; + + // If a prefix/subset AID, the total length must be odd (even # of AID chars + '*') + if ((aid.endsWith("*") || aid.endsWith("#")) && ((aid.length() % 2) == 0)) { + Log.e(TAG, "AID " + aid + " is not a valid AID."); + return false; + } + + // If not a prefix/subset AID, the total length must be even (even # of AID chars) + if ((!(aid.endsWith("*") || aid.endsWith("#"))) && ((aid.length() % 2) != 0)) { + Log.e(TAG, "AID " + aid + " is not a valid AID."); + return false; + } + + // Verify hex characters + if (!AID_PATTERN.matcher(aid).matches()) { + Log.e(TAG, "AID " + aid + " is not a valid AID."); + return false; + } + + return true; + } + + /** + * Allows to set or unset preferred service (category other) to avoid AID Collision. + * + * @param service The ComponentName of the service + * @param status true to enable, false to disable + * @return set service for the category and true if service is already set return false. + * + * @hide + */ + public boolean setServiceEnabledForCategoryOther(ComponentName service, boolean status) { + if (service == null) { + throw new NullPointerException("activity or service or category is null"); + } + int userId = mContext.getUser().getIdentifier(); + + try { + return sService.setServiceEnabledForCategoryOther(userId, service, status); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.setServiceEnabledForCategoryOther(userId, service, status); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Setting NFC controller routing table, which includes Protocol Route and Technology Route, + * while this Activity is in the foreground. + * + * The parameter set to null can be used to keep current values for that entry. + *

+ * Example usage in an Activity that requires to set proto route to "ESE" and keep tech route: + *

+      * protected void onResume() {
+      *     mNfcAdapter.overrideRoutingTable(this , "ESE" , null);
+      * }
+ *

+ * Also activities must call this method when it goes to the background, + * with all parameters set to null. + * @param activity The Activity that requests NFC controller routing table to be changed. + * @param protocol ISO-DEP route destination, which can be "DH" or "UICC" or "ESE". + * @param technology Tech-A, Tech-B route destination, which can be "DH" or "UICC" or "ESE". + * @return true if operation is successful and false otherwise + * + * This is a high risk API and only included to support mainline effort + * @hide + */ + public boolean overrideRoutingTable(Activity activity, String protocol, String technology) { + if (activity == null) { + throw new NullPointerException("activity or service or category is null"); + } + if (!activity.isResumed()) { + throw new IllegalArgumentException("Activity must be resumed."); + } + try { + return sService.overrideRoutingTable(UserHandle.myUserId(), protocol, technology); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.overrideRoutingTable(UserHandle.myUserId(), protocol, technology); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Restore the NFC controller routing table, + * which was changed by {@link #overrideRoutingTable(Activity, String, String)} + * + * @param activity The Activity that requested NFC controller routing table to be changed. + * @return true if operation is successful and false otherwise + * + * @hide + */ + public boolean recoverRoutingTable(Activity activity) { + if (activity == null) { + throw new NullPointerException("activity is null"); + } + if (!activity.isResumed()) { + throw new IllegalArgumentException("Activity must be resumed."); + } + try { + return sService.recoverRoutingTable(UserHandle.myUserId()); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.recoverRoutingTable(UserHandle.myUserId()); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + void recoverService() { + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext); + sService = adapter.getCardEmulationService(); + } + +} diff --git a/nfc/java/android/nfc/cardemulation/HostApduService.java b/nfc/java/android/nfc/cardemulation/HostApduService.java new file mode 100644 index 000000000000..7cd2533a7dbf --- /dev/null +++ b/nfc/java/android/nfc/cardemulation/HostApduService.java @@ -0,0 +1,519 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.cardemulation; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.nfc.NfcAdapter; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + *

HostApduService is a convenience {@link Service} class that can be + * extended to emulate an NFC card inside an Android + * service component. + * + *

+ *

Developer Guide

+ * For a general introduction to card emulation, see + * + * Host-based Card Emulation.

+ *
+ * + *

NFC Protocols

+ *

Cards emulated by this class are based on the NFC-Forum ISO-DEP + * protocol (based on ISO/IEC 14443-4) and support processing + * command Application Protocol Data Units (APDUs) as + * defined in the ISO/IEC 7816-4 specification. + * + *

Service selection

+ *

When a remote NFC device wants to talk to your + * service, it sends a so-called + * "SELECT AID" APDU as defined in the ISO/IEC 7816-4 specification. + * The AID is an application identifier defined in ISO/IEC 7816-4. + * + *

The registration procedure for AIDs is defined in the + * ISO/IEC 7816-5 specification. If you don't want to register an + * AID, you are free to use AIDs in the proprietary range: + * bits 8-5 of the first byte must each be set to '1'. For example, + * "0xF00102030405" is a proprietary AID. If you do use proprietary + * AIDs, it is recommended to choose an AID of at least 6 bytes, + * to reduce the risk of collisions with other applications that + * might be using proprietary AIDs as well. + * + *

AID groups

+ *

In some cases, a service may need to register multiple AIDs + * to implement a certain application, and it needs to be sure + * that it is the default handler for all of these AIDs (as opposed + * to some AIDs in the group going to another service). + * + *

An AID group is a list of AIDs that should be considered as + * belonging together by the OS. For all AIDs in an AID group, the + * OS will guarantee one of the following: + *

    + *
  • All AIDs in the group are routed to this service + *
  • No AIDs in the group are routed to this service + *
+ * In other words, there is no in-between state, where some AIDs + * in the group can be routed to this service, and some to another. + *

AID groups and categories

+ *

Each AID group can be associated with a category. This allows + * the Android OS to classify services, and it allows the user to + * set defaults at the category level instead of the AID level. + * + *

You can use + * {@link CardEmulation#isDefaultServiceForCategory(android.content.ComponentName, String)} + * to determine if your service is the default handler for a category. + * + *

In this version of the platform, the only known categories + * are {@link CardEmulation#CATEGORY_PAYMENT} and {@link CardEmulation#CATEGORY_OTHER}. + * AID groups without a category, or with a category that is not recognized + * by the current platform version, will automatically be + * grouped into the {@link CardEmulation#CATEGORY_OTHER} category. + *

Service AID registration

+ *

To tell the platform which AIDs groups + * are requested by this service, a {@link #SERVICE_META_DATA} + * entry must be included in the declaration of the service. An + * example of a HostApduService manifest declaration is shown below: + *

 <service android:name=".MyHostApduService" android:exported="true" android:permission="android.permission.BIND_NFC_SERVICE">
+ *     <intent-filter>
+ *         <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
+ *     </intent-filter>
+ *     <meta-data android:name="android.nfc.cardemulation.host_apdu_ervice" android:resource="@xml/apduservice"/>
+ * </service>
+ * + * This meta-data tag points to an apduservice.xml file. + * An example of this file with a single AID group declaration is shown below: + *
+ * <host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
+ *           android:description="@string/servicedesc" android:requireDeviceUnlock="false">
+ *       <aid-group android:description="@string/aiddescription" android:category="other">
+ *           <aid-filter android:name="F0010203040506"/>
+ *           <aid-filter android:name="F0394148148100"/>
+ *       </aid-group>
+ * </host-apdu-service>
+ * 
+ * + *

The {@link android.R.styleable#HostApduService <host-apdu-service>} is required + * to contain a + * {@link android.R.styleable#HostApduService_description <android:description>} + * attribute that contains a user-friendly description of the service that may be shown in UI. + * The + * {@link android.R.styleable#HostApduService_requireDeviceUnlock <requireDeviceUnlock>} + * attribute can be used to specify that the device must be unlocked before this service + * can be invoked to handle APDUs. + *

The {@link android.R.styleable#HostApduService <host-apdu-service>} must + * contain one or more {@link android.R.styleable#AidGroup <aid-group>} tags. + * Each {@link android.R.styleable#AidGroup <aid-group>} must contain one or + * more {@link android.R.styleable#AidFilter <aid-filter>} tags, each of which + * contains a single AID. The AID must be specified in hexadecimal format, and contain + * an even number of characters. + *

AID conflict resolution

+ * Multiple HostApduServices may be installed on a single device, and the same AID + * can be registered by more than one service. The Android platform resolves AID + * conflicts depending on which category an AID belongs to. Each category may + * have a different conflict resolution policy. For example, for some categories + * the user may be able to select a default service in the Android settings UI. + * For other categories, to policy may be to always ask the user which service + * is to be invoked in case of conflict. + * + * To query the conflict resolution policy for a certain category, see + * {@link CardEmulation#getSelectionModeForCategory(String)}. + * + *

Data exchange

+ *

Once the platform has resolved a "SELECT AID" command APDU to a specific + * service component, the "SELECT AID" command APDU and all subsequent + * command APDUs will be sent to that service through + * {@link #processCommandApdu(byte[], Bundle)}, until either: + *

    + *
  • The NFC link is broken
  • + *
  • A "SELECT AID" APDU is received which resolves to another service
  • + *
+ * These two scenarios are indicated by a call to {@link #onDeactivated(int)}. + * + *

Use of this class requires the + * {@link PackageManager#FEATURE_NFC_HOST_CARD_EMULATION} to be present + * on the device. + * + */ +public abstract class HostApduService extends Service { + /** + * The {@link Intent} action that must be declared as handled by the service. + */ + @SdkConstant(SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = + "android.nfc.cardemulation.action.HOST_APDU_SERVICE"; + + /** + * The name of the meta-data element that contains + * more information about this service. + */ + public static final String SERVICE_META_DATA = + "android.nfc.cardemulation.host_apdu_service"; + + /** + * Reason for {@link #onDeactivated(int)}. + * Indicates deactivation was due to the NFC link + * being lost. + */ + public static final int DEACTIVATION_LINK_LOSS = 0; + + /** + * Reason for {@link #onDeactivated(int)}. + * + *

Indicates deactivation was due to a different AID + * being selected (which implicitly deselects the AID + * currently active on the logical channel). + * + *

Note that this next AID may still be resolved to this + * service, in which case {@link #processCommandApdu(byte[], Bundle)} + * will be called again. + */ + public static final int DEACTIVATION_DESELECTED = 1; + + static final String TAG = "ApduService"; + + /** + * MSG_COMMAND_APDU is sent by NfcService when + * a 7816-4 command APDU has been received. + * + * @hide + */ + public static final int MSG_COMMAND_APDU = 0; + + /** + * MSG_RESPONSE_APDU is sent to NfcService to send + * a response APDU back to the remote device. + * + * @hide + */ + public static final int MSG_RESPONSE_APDU = 1; + + /** + * MSG_DEACTIVATED is sent by NfcService when + * the current session is finished; either because + * another AID was selected that resolved to + * another service, or because the NFC link + * was deactivated. + * + * @hide + */ + public static final int MSG_DEACTIVATED = 2; + + /** + * + * @hide + */ + public static final int MSG_UNHANDLED = 3; + + /** + * @hide + */ + public static final int MSG_POLLING_LOOP = 4; + + /** + * @hide + */ + public static final String KEY_DATA = "data"; + + /** + * POLLING_LOOP_TYPE_KEY is the Bundle key for the type of + * polling loop frame in the Bundle passed to {@link #processPollingFrames(List)} + */ + @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP) + public static final String POLLING_LOOP_TYPE_KEY = "android.nfc.cardemulation.TYPE"; + + /** + * POLLING_LOOP_TYPE_A is the value associated with the key + * POLLING_LOOP_TYPE in the Bundle passed to {@link #processPollingFrames(List)} + * when the polling loop is for NFC-A. + */ + @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP) + public static final char POLLING_LOOP_TYPE_A = 'A'; + + /** + * POLLING_LOOP_TYPE_B is the value associated with the key + * POLLING_LOOP_TYPE in the Bundle passed to {@link #processPollingFrames(List)} + * when the polling loop is for NFC-B. + */ + @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP) + public static final char POLLING_LOOP_TYPE_B = 'B'; + + /** + * POLLING_LOOP_TYPE_F is the value associated with the key + * POLLING_LOOP_TYPE in the Bundle passed to {@link #processPollingFrames(List)} + * when the polling loop is for NFC-F. + */ + @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP) + public static final char POLLING_LOOP_TYPE_F = 'F'; + + /** + * POLLING_LOOP_TYPE_ON is the value associated with the key + * POLLING_LOOP_TYPE in the Bundle passed to {@link #processPollingFrames(List)} + * when the polling loop turns on. + */ + @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP) + public static final char POLLING_LOOP_TYPE_ON = 'O'; + + /** + * POLLING_LOOP_TYPE_OFF is the value associated with the key + * POLLING_LOOP_TYPE in the Bundle passed to {@link #processPollingFrames(List)} + * when the polling loop turns off. + */ + @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP) + public static final char POLLING_LOOP_TYPE_OFF = 'X'; + + /** + * POLLING_LOOP_TYPE_UNKNOWN is the value associated with the key + * POLLING_LOOP_TYPE in the Bundle passed to {@link #processPollingFrames(List)} + * when the polling loop frame isn't recognized. + */ + @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP) + public static final char POLLING_LOOP_TYPE_UNKNOWN = 'U'; + + /** + * POLLING_LOOP_DATA is the Bundle key for the raw data of captured from + * the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)} + * when the frame type isn't recognized. + */ + @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP) + public static final String POLLING_LOOP_DATA_KEY = "android.nfc.cardemulation.DATA"; + + /** + * POLLING_LOOP_GAIN_KEY is the Bundle key for the field strength of + * the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)} + * when the frame type isn't recognized. + */ + @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP) + public static final String POLLING_LOOP_GAIN_KEY = "android.nfc.cardemulation.GAIN"; + + /** + * POLLING_LOOP_TIMESTAMP_KEY is the Bundle key for the timestamp of + * the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)} + * when the frame type isn't recognized. + */ + @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP) + public static final String POLLING_LOOP_TIMESTAMP_KEY = "android.nfc.cardemulation.TIMESTAMP"; + + /** + * @hide + */ + public static final String POLLING_LOOP_FRAMES_BUNDLE_KEY = + "android.nfc.cardemulation.POLLING_FRAMES"; + + /** + * Messenger interface to NfcService for sending responses. + * Only accessed on main thread by the message handler. + * + * @hide + */ + Messenger mNfcService = null; + + final Messenger mMessenger = new Messenger(new MsgHandler()); + + final class MsgHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_COMMAND_APDU: + Bundle dataBundle = msg.getData(); + if (dataBundle == null) { + return; + } + if (mNfcService == null) mNfcService = msg.replyTo; + + byte[] apdu = dataBundle.getByteArray(KEY_DATA); + if (apdu != null) { + HostApduService has = HostApduService.this; + byte[] responseApdu = processCommandApdu(apdu, null); + if (responseApdu != null) { + if (mNfcService == null) { + Log.e(TAG, "Response not sent; service was deactivated."); + return; + } + Message responseMsg = Message.obtain(null, MSG_RESPONSE_APDU); + Bundle responseBundle = new Bundle(); + responseBundle.putByteArray(KEY_DATA, responseApdu); + responseMsg.setData(responseBundle); + responseMsg.replyTo = mMessenger; + try { + mNfcService.send(responseMsg); + } catch (RemoteException e) { + Log.e("TAG", "Response not sent; RemoteException calling into " + + "NfcService."); + } + } + } else { + Log.e(TAG, "Received MSG_COMMAND_APDU without data."); + } + break; + case MSG_RESPONSE_APDU: + if (mNfcService == null) { + Log.e(TAG, "Response not sent; service was deactivated."); + return; + } + try { + msg.replyTo = mMessenger; + mNfcService.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException calling into NfcService."); + } + break; + case MSG_DEACTIVATED: + // Make sure we won't call into NfcService again + mNfcService = null; + onDeactivated(msg.arg1); + break; + case MSG_UNHANDLED: + if (mNfcService == null) { + Log.e(TAG, "notifyUnhandled not sent; service was deactivated."); + return; + } + try { + msg.replyTo = mMessenger; + mNfcService.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException calling into NfcService."); + } + break; + case MSG_POLLING_LOOP: + ArrayList frames = + msg.getData().getParcelableArrayList(POLLING_LOOP_FRAMES_BUNDLE_KEY, + Bundle.class); + processPollingFrames(frames); + break; + default: + super.handleMessage(msg); + } + } + } + + @Override + public final IBinder onBind(Intent intent) { + return mMessenger.getBinder(); + } + + /** + * Sends a response APDU back to the remote device. + * + *

Note: this method may be called from any thread and will not block. + * @param responseApdu A byte-array containing the reponse APDU. + */ + public final void sendResponseApdu(byte[] responseApdu) { + Message responseMsg = Message.obtain(null, MSG_RESPONSE_APDU); + Bundle dataBundle = new Bundle(); + dataBundle.putByteArray(KEY_DATA, responseApdu); + responseMsg.setData(dataBundle); + try { + mMessenger.send(responseMsg); + } catch (RemoteException e) { + Log.e("TAG", "Local messenger has died."); + } + } + + /** + * Calling this method allows the service to tell the OS + * that it won't be able to complete this transaction - + * for example, because it requires data connectivity + * that is not present at that moment. + * + * The OS may use this indication to give the user a list + * of alternative applications that can handle the last + * AID that was selected. If the user would select an + * application from the list, that action by itself + * will not cause the default to be changed; the selected + * application will be invoked for the next tap only. + * + * If there are no other applications that can handle + * this transaction, the OS will show an error dialog + * indicating your service could not complete the + * transaction. + * + *

Note: this method may be called anywhere between + * the first {@link #processCommandApdu(byte[], Bundle)} + * call and a {@link #onDeactivated(int)} call. + */ + public final void notifyUnhandled() { + Message unhandledMsg = Message.obtain(null, MSG_UNHANDLED); + try { + mMessenger.send(unhandledMsg); + } catch (RemoteException e) { + Log.e("TAG", "Local messenger has died."); + } + } + + /** + * This method is called when a polling frame has been received from a + * remote device. If the device is in observe mode, the service should + * call {@link NfcAdapter#allowTransaction()} once it is ready to proceed + * with the transaction. If the device is not in observe mode, the service + * can use this polling frame information to determine how to proceed if it + * subsequently has {@link #processCommandApdu(byte[], Bundle)} called. The + * service must override this method inorder to receive polling frames, + * otherwise the base implementation drops the frame. + * + * @param frame A description of the polling frame. + */ + @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP) + public void processPollingFrames(@NonNull List frame) { + } + + /** + *

This method will be called when a command APDU has been received + * from a remote device. A response APDU can be provided directly + * by returning a byte-array in this method. Note that in general + * response APDUs must be sent as quickly as possible, given the fact + * that the user is likely holding their device over an NFC reader + * when this method is called. + * + *

If there are multiple services that have registered for the same + * AIDs in their meta-data entry, you will only get called if the user has + * explicitly selected your service, either as a default or just for the next tap. + * + *

This method is running on the main thread of your application. + * If you cannot return a response APDU immediately, return null + * and use the {@link #sendResponseApdu(byte[])} method later. + * + * @param commandApdu The APDU that was received from the remote device + * @param extras A bundle containing extra data. May be null. + * @return a byte-array containing the response APDU, or null if no + * response APDU can be sent at this point. + */ + public abstract byte[] processCommandApdu(byte[] commandApdu, Bundle extras); + + /** + * This method will be called in two possible scenarios: + *

  • The NFC link has been deactivated or lost + *
  • A different AID has been selected and was resolved to a different + * service component + * @param reason Either {@link #DEACTIVATION_LINK_LOSS} or {@link #DEACTIVATION_DESELECTED} + */ + public abstract void onDeactivated(int reason); +} diff --git a/nfc/java/android/nfc/cardemulation/HostNfcFService.java b/nfc/java/android/nfc/cardemulation/HostNfcFService.java new file mode 100644 index 000000000000..65b5ca77de62 --- /dev/null +++ b/nfc/java/android/nfc/cardemulation/HostNfcFService.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.cardemulation; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.app.Service; +import android.content.ComponentName; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.util.Log; + +/** + *

    HostNfcFService is a convenience {@link Service} class that can be + * extended to emulate an NFC-F card inside an Android service component. + * + *

    NFC Protocols

    + *

    Cards emulated by this class are based on the NFC-Forum NFC-F + * protocol (based on the JIS-X 6319-4 specification.)

    + * + *

    System Code and NFCID2 registration

    + *

    A {@link HostNfcFService HostNfcFService service} can register + * exactly one System Code and one NFCID2. For details about the use of + * System Code and NFCID2, see the NFC Forum Digital specification.

    + *

    To statically register a System Code and NFCID2 with the service, a {@link #SERVICE_META_DATA} + * entry must be included in the declaration of the service. + * + *

    All {@link HostNfcFService HostNfcFService} declarations in the manifest must require the + * {@link android.Manifest.permission#BIND_NFC_SERVICE} permission + * in their <service> tag, to ensure that only the platform can bind to your service.

    + * + *

    An example of a HostNfcFService manifest declaration is shown below: + * + *

     <service android:name=".MyHostNfcFService" android:exported="true" android:permission="android.permission.BIND_NFC_SERVICE">
    + *     <intent-filter>
    + *         <action android:name="android.nfc.cardemulation.action.HOST_NFCF_SERVICE"/>
    + *     </intent-filter>
    + *     <meta-data android:name="android.nfc.cardemulation.host_nfcf_service" android:resource="@xml/nfcfservice"/>
    + * </service>
    + * + * This meta-data tag points to an nfcfservice.xml file. + * An example of this file with a System Code and NFCID2 declaration is shown below: + *
    + * <host-nfcf-service xmlns:android="http://schemas.android.com/apk/res/android"
    + *           android:description="@string/servicedesc">
    + *       <system-code-filter android:name="4000"/>
    + *       <nfcid2-filter android:name="02FE000000000000"/>
    +         <t3tPmm-filter android:name="FFFFFFFFFFFFFFFF"/>
    + * </host-nfcf-service>
    + * 
    + * + *

    The {@link android.R.styleable#HostNfcFService <host-nfcf-service>} is required + * to contain a + * {@link android.R.styleable#HostApduService_description <android:description>} + * attribute that contains a user-friendly description of the service that may be shown in UI. + *

    The {@link android.R.styleable#HostNfcFService <host-nfcf-service>} must + * contain: + *

      + *
    • Exactly one {@link android.R.styleable#SystemCodeFilter <system-code-filter>} tag.
    • + *
    • Exactly one {@link android.R.styleable#Nfcid2Filter <nfcid2-filter>} tag.
    • + *
    • Zero or one {@link android.R.styleable#T3tPmmFilter <t3tPmm-filter>} tag.
    • + *
    + *

    + * + *

    Alternatively, the System Code and NFCID2 can be dynamically registererd for a service + * by using the {@link NfcFCardEmulation#registerSystemCodeForService(ComponentName, String)} and + * {@link NfcFCardEmulation#setNfcid2ForService(ComponentName, String)} methods. + *

    + * + *

    Service selection

    + *

    When a remote NFC devices wants to communicate with your service, it + * sends a SENSF_REQ command to the NFC controller, requesting a System Code. + * If a {@link NfcFCardEmulation NfcFCardEmulation service} has registered + * this system code and has been enabled by the foreground application, the + * NFC controller will respond with the NFCID2 that is registered for this service. + * The reader can then continue data exchange with this service by using the NFCID2.

    + * + *

    Data exchange

    + *

    After service selection, all frames addressed to the NFCID2 of this service will + * be sent through {@link #processNfcFPacket(byte[], Bundle)}, until the NFC link is + * broken.

    + * + *

    When the NFC link is broken, {@link #onDeactivated(int)} will be called.

    + */ +public abstract class HostNfcFService extends Service { + /** + * The {@link Intent} action that must be declared as handled by the service. + */ + @SdkConstant(SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = + "android.nfc.cardemulation.action.HOST_NFCF_SERVICE"; + + /** + * The name of the meta-data element that contains + * more information about this service. + */ + public static final String SERVICE_META_DATA = + "android.nfc.cardemulation.host_nfcf_service"; + + /** + * Reason for {@link #onDeactivated(int)}. + * Indicates deactivation was due to the NFC link + * being lost. + */ + public static final int DEACTIVATION_LINK_LOSS = 0; + + static final String TAG = "NfcFService"; + + /** + * MSG_COMMAND_PACKET is sent by NfcService when + * a NFC-F command packet has been received. + * + * @hide + */ + public static final int MSG_COMMAND_PACKET = 0; + + /** + * MSG_RESPONSE_PACKET is sent to NfcService to send + * a response packet back to the remote device. + * + * @hide + */ + public static final int MSG_RESPONSE_PACKET = 1; + + /** + * MSG_DEACTIVATED is sent by NfcService when + * the current session is finished; because + * the NFC link was deactivated. + * + * @hide + */ + public static final int MSG_DEACTIVATED = 2; + + /** + * @hide + */ + public static final String KEY_DATA = "data"; + + /** + * @hide + */ + public static final String KEY_MESSENGER = "messenger"; + + /** + * Messenger interface to NfcService for sending responses. + * Only accessed on main thread by the message handler. + * + * @hide + */ + Messenger mNfcService = null; + + final Messenger mMessenger = new Messenger(new MsgHandler()); + + final class MsgHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_COMMAND_PACKET: + Bundle dataBundle = msg.getData(); + if (dataBundle == null) { + return; + } + if (mNfcService == null) mNfcService = msg.replyTo; + + byte[] packet = dataBundle.getByteArray(KEY_DATA); + if (packet != null) { + byte[] responsePacket = processNfcFPacket(packet, null); + if (responsePacket != null) { + if (mNfcService == null) { + Log.e(TAG, "Response not sent; service was deactivated."); + return; + } + Message responseMsg = Message.obtain(null, MSG_RESPONSE_PACKET); + Bundle responseBundle = new Bundle(); + responseBundle.putByteArray(KEY_DATA, responsePacket); + responseMsg.setData(responseBundle); + responseMsg.replyTo = mMessenger; + try { + mNfcService.send(responseMsg); + } catch (RemoteException e) { + Log.e("TAG", "Response not sent; RemoteException calling into " + + "NfcService."); + } + } + } else { + Log.e(TAG, "Received MSG_COMMAND_PACKET without data."); + } + break; + case MSG_RESPONSE_PACKET: + if (mNfcService == null) { + Log.e(TAG, "Response not sent; service was deactivated."); + return; + } + try { + msg.replyTo = mMessenger; + mNfcService.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException calling into NfcService."); + } + break; + case MSG_DEACTIVATED: + // Make sure we won't call into NfcService again + mNfcService = null; + onDeactivated(msg.arg1); + break; + default: + super.handleMessage(msg); + } + } + } + + @Override + public final IBinder onBind(Intent intent) { + return mMessenger.getBinder(); + } + + /** + * Sends a response packet back to the remote device. + * + *

    Note: this method may be called from any thread and will not block. + * @param responsePacket A byte-array containing the response packet. + */ + public final void sendResponsePacket(byte[] responsePacket) { + Message responseMsg = Message.obtain(null, MSG_RESPONSE_PACKET); + Bundle dataBundle = new Bundle(); + dataBundle.putByteArray(KEY_DATA, responsePacket); + responseMsg.setData(dataBundle); + try { + mMessenger.send(responseMsg); + } catch (RemoteException e) { + Log.e("TAG", "Local messenger has died."); + } + } + + /** + *

    This method will be called when a NFC-F packet has been received + * from a remote device. A response packet can be provided directly + * by returning a byte-array in this method. Note that in general + * response packets must be sent as quickly as possible, given the fact + * that the user is likely holding their device over an NFC reader + * when this method is called. + * + *

    This method is running on the main thread of your application. + * If you cannot return a response packet immediately, return null + * and use the {@link #sendResponsePacket(byte[])} method later. + * + * @param commandPacket The NFC-F packet that was received from the remote device + * @param extras A bundle containing extra data. May be null. + * @return a byte-array containing the response packet, or null if no + * response packet can be sent at this point. + */ + public abstract byte[] processNfcFPacket(byte[] commandPacket, Bundle extras); + + /** + * This method will be called in following possible scenarios: + *

  • The NFC link has been lost + * @param reason {@link #DEACTIVATION_LINK_LOSS} + */ + public abstract void onDeactivated(int reason); +} diff --git a/nfc/java/android/nfc/cardemulation/NfcFCardEmulation.java b/nfc/java/android/nfc/cardemulation/NfcFCardEmulation.java new file mode 100644 index 000000000000..48bbf5b61052 --- /dev/null +++ b/nfc/java/android/nfc/cardemulation/NfcFCardEmulation.java @@ -0,0 +1,473 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.cardemulation; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.nfc.INfcFCardEmulation; +import android.nfc.NfcAdapter; +import android.os.RemoteException; +import android.util.Log; + +import java.util.HashMap; +import java.util.List; + +/** + * This class can be used to query the state of + * NFC-F card emulation services. + * + * For a general introduction into NFC card emulation, + * please read the + * NFC card emulation developer guide.

    + * + *

    Use of this class requires the + * {@link PackageManager#FEATURE_NFC_HOST_CARD_EMULATION_NFCF} + * to be present on the device. + */ +public final class NfcFCardEmulation { + static final String TAG = "NfcFCardEmulation"; + + static boolean sIsInitialized = false; + static HashMap sCardEmus = new HashMap(); + static INfcFCardEmulation sService; + + final Context mContext; + + private NfcFCardEmulation(Context context, INfcFCardEmulation service) { + mContext = context.getApplicationContext(); + sService = service; + } + + /** + * Helper to get an instance of this class. + * + * @param adapter A reference to an NfcAdapter object. + * @return + */ + public static synchronized NfcFCardEmulation getInstance(NfcAdapter adapter) { + if (adapter == null) throw new NullPointerException("NfcAdapter is null"); + Context context = adapter.getContext(); + if (context == null) { + Log.e(TAG, "NfcAdapter context is null."); + throw new UnsupportedOperationException(); + } + if (!sIsInitialized) { + PackageManager pm = context.getPackageManager(); + if (pm == null) { + Log.e(TAG, "Cannot get PackageManager"); + throw new UnsupportedOperationException(); + } + if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF)) { + Log.e(TAG, "This device does not support NFC-F card emulation"); + throw new UnsupportedOperationException(); + } + sIsInitialized = true; + } + NfcFCardEmulation manager = sCardEmus.get(context); + if (manager == null) { + // Get card emu service + INfcFCardEmulation service = adapter.getNfcFCardEmulationService(); + if (service == null) { + Log.e(TAG, "This device does not implement the INfcFCardEmulation interface."); + throw new UnsupportedOperationException(); + } + manager = new NfcFCardEmulation(context, service); + sCardEmus.put(context, manager); + } + return manager; + } + + /** + * Retrieves the current System Code for the specified service. + * + *

    Before calling {@link #registerSystemCodeForService(ComponentName, String)}, + * the System Code contained in the Manifest file is returned. After calling + * {@link #registerSystemCodeForService(ComponentName, String)}, the System Code + * registered there is returned. After calling + * {@link #unregisterSystemCodeForService(ComponentName)}, "null" is returned. + * + * @param service The component name of the service + * @return the current System Code + */ + public String getSystemCodeForService(ComponentName service) throws RuntimeException { + if (service == null) { + throw new NullPointerException("service is null"); + } + try { + return sService.getSystemCodeForService(mContext.getUser().getIdentifier(), service); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return null; + } + try { + return sService.getSystemCodeForService(mContext.getUser().getIdentifier(), + service); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + ee.rethrowAsRuntimeException(); + return null; + } + } + } + + /** + * Registers a System Code for the specified service. + * + *

    The System Code must be in range from "4000" to "4FFF" (excluding "4*FF"). + * + *

    If a System Code was previously registered for this service + * (either statically through the manifest, or dynamically by using this API), + * it will be replaced with this one. + * + *

    Even if the same System Code is already registered for another service, + * this method succeeds in registering the System Code. + * + *

    Note that you can only register a System Code for a service that + * is running under the same UID as the caller of this API. Typically + * this means you need to call this from the same + * package as the service itself, though UIDs can also + * be shared between packages using shared UIDs. + * + * @param service The component name of the service + * @param systemCode The System Code to be registered + * @return whether the registration was successful. + */ + public boolean registerSystemCodeForService(ComponentName service, String systemCode) + throws RuntimeException { + if (service == null || systemCode == null) { + throw new NullPointerException("service or systemCode is null"); + } + try { + return sService.registerSystemCodeForService(mContext.getUser().getIdentifier(), + service, systemCode); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.registerSystemCodeForService(mContext.getUser().getIdentifier(), + service, systemCode); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + ee.rethrowAsRuntimeException(); + return false; + } + } + } + + /** + * Removes a registered System Code for the specified service. + * + * @param service The component name of the service + * @return whether the System Code was successfully removed. + */ + public boolean unregisterSystemCodeForService(ComponentName service) throws RuntimeException { + if (service == null) { + throw new NullPointerException("service is null"); + } + try { + return sService.removeSystemCodeForService(mContext.getUser().getIdentifier(), service); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.removeSystemCodeForService(mContext.getUser().getIdentifier(), + service); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + ee.rethrowAsRuntimeException(); + return false; + } + } + } + + /** + * Retrieves the current NFCID2 for the specified service. + * + *

    Before calling {@link #setNfcid2ForService(ComponentName, String)}, + * the NFCID2 contained in the Manifest file is returned. If "random" is specified + * in the Manifest file, a random number assigned by the system at installation time + * is returned. After setting an NFCID2 + * with {@link #setNfcid2ForService(ComponentName, String)}, this NFCID2 is returned. + * + * @param service The component name of the service + * @return the current NFCID2 + */ + public String getNfcid2ForService(ComponentName service) throws RuntimeException { + if (service == null) { + throw new NullPointerException("service is null"); + } + try { + return sService.getNfcid2ForService(mContext.getUser().getIdentifier(), service); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return null; + } + try { + return sService.getNfcid2ForService(mContext.getUser().getIdentifier(), service); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + ee.rethrowAsRuntimeException(); + return null; + } + } + } + + /** + * Set a NFCID2 for the specified service. + * + *

    The NFCID2 must be in range from "02FE000000000000" to "02FEFFFFFFFFFFFF". + * + *

    If a NFCID2 was previously set for this service + * (either statically through the manifest, or dynamically by using this API), + * it will be replaced. + * + *

    Note that you can only set the NFCID2 for a service that + * is running under the same UID as the caller of this API. Typically + * this means you need to call this from the same + * package as the service itself, though UIDs can also + * be shared between packages using shared UIDs. + * + * @param service The component name of the service + * @param nfcid2 The NFCID2 to be registered + * @return whether the setting was successful. + */ + public boolean setNfcid2ForService(ComponentName service, String nfcid2) + throws RuntimeException { + if (service == null || nfcid2 == null) { + throw new NullPointerException("service or nfcid2 is null"); + } + try { + return sService.setNfcid2ForService(mContext.getUser().getIdentifier(), + service, nfcid2); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.setNfcid2ForService(mContext.getUser().getIdentifier(), + service, nfcid2); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + ee.rethrowAsRuntimeException(); + return false; + } + } + } + + /** + * Allows a foreground application to specify which card emulation service + * should be enabled while a specific Activity is in the foreground. + * + *

    The specified HCE-F service is only enabled when the corresponding application is + * in the foreground and this method has been called. When the application is moved to + * the background, {@link #disableService(Activity)} is called, or + * NFCID2 or System Code is replaced, the HCE-F service is disabled. + * + *

    The specified Activity must currently be in resumed state. A good + * paradigm is to call this method in your {@link Activity#onResume}, and to call + * {@link #disableService(Activity)} in your {@link Activity#onPause}. + * + *

    Note that this preference is not persisted by the OS, and hence must be + * called every time the Activity is resumed. + * + * @param activity The activity which prefers this service to be invoked + * @param service The service to be preferred while this activity is in the foreground + * @return whether the registration was successful + */ + public boolean enableService(Activity activity, ComponentName service) throws RuntimeException { + if (activity == null || service == null) { + throw new NullPointerException("activity or service is null"); + } + // Verify the activity is in the foreground before calling into NfcService + if (!activity.isResumed()) { + throw new IllegalArgumentException("Activity must be resumed."); + } + try { + return sService.enableNfcFForegroundService(service); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.enableNfcFForegroundService(service); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + ee.rethrowAsRuntimeException(); + return false; + } + } + } + + /** + * Disables the service for the specified Activity. + * + *

    Note that the specified Activity must still be in resumed + * state at the time of this call. A good place to call this method + * is in your {@link Activity#onPause} implementation. + * + * @param activity The activity which the service was registered for + * @return true when successful + */ + public boolean disableService(Activity activity) throws RuntimeException { + if (activity == null) { + throw new NullPointerException("activity is null"); + } + if (!activity.isResumed()) { + throw new IllegalArgumentException("Activity must be resumed."); + } + try { + return sService.disableNfcFForegroundService(); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.disableNfcFForegroundService(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + ee.rethrowAsRuntimeException(); + return false; + } + } + } + + /** + * @hide + */ + public List getNfcFServices() { + try { + return sService.getNfcFServices(mContext.getUser().getIdentifier()); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return null; + } + try { + return sService.getNfcFServices(mContext.getUser().getIdentifier()); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return null; + } + } + } + + /** + * @hide + */ + public int getMaxNumOfRegisterableSystemCodes() { + try { + return sService.getMaxNumOfRegisterableSystemCodes(); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return -1; + } + try { + return sService.getMaxNumOfRegisterableSystemCodes(); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return -1; + } + } + } + + /** + * @hide + */ + public static boolean isValidSystemCode(String systemCode) { + if (systemCode == null) { + return false; + } + if (systemCode.length() != 4) { + Log.e(TAG, "System Code " + systemCode + " is not a valid System Code."); + return false; + } + // check if the value is between "4000" and "4FFF" (excluding "4*FF") + if (!systemCode.startsWith("4") || systemCode.toUpperCase().endsWith("FF")) { + Log.e(TAG, "System Code " + systemCode + " is not a valid System Code."); + return false; + } + try { + Integer.parseInt(systemCode, 16); + } catch (NumberFormatException e) { + Log.e(TAG, "System Code " + systemCode + " is not a valid System Code."); + return false; + } + return true; + } + + /** + * @hide + */ + public static boolean isValidNfcid2(String nfcid2) { + if (nfcid2 == null) { + return false; + } + if (nfcid2.length() != 16) { + Log.e(TAG, "NFCID2 " + nfcid2 + " is not a valid NFCID2."); + return false; + } + // check if the the value starts with "02FE" + if (!nfcid2.toUpperCase().startsWith("02FE")) { + Log.e(TAG, "NFCID2 " + nfcid2 + " is not a valid NFCID2."); + return false; + } + try { + Long.parseLong(nfcid2, 16); + } catch (NumberFormatException e) { + Log.e(TAG, "NFCID2 " + nfcid2 + " is not a valid NFCID2."); + return false; + } + return true; + } + + void recoverService() { + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext); + sService = adapter.getNfcFCardEmulationService(); + } + +} + diff --git a/nfc/java/android/nfc/cardemulation/NfcFServiceInfo.aidl b/nfc/java/android/nfc/cardemulation/NfcFServiceInfo.aidl new file mode 100644 index 000000000000..56b98ebd90fa --- /dev/null +++ b/nfc/java/android/nfc/cardemulation/NfcFServiceInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.cardemulation; + +parcelable NfcFServiceInfo; diff --git a/nfc/java/android/nfc/cardemulation/NfcFServiceInfo.java b/nfc/java/android/nfc/cardemulation/NfcFServiceInfo.java new file mode 100644 index 000000000000..33bc16978721 --- /dev/null +++ b/nfc/java/android/nfc/cardemulation/NfcFServiceInfo.java @@ -0,0 +1,497 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/********************************************************************** + * This file is not a part of the NFC mainline module * + * *******************************************************************/ + +package android.nfc.cardemulation; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.nfc.Flags; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; +import android.util.proto.ProtoOutputStream; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.PrintWriter; + +/** + * Class to hold NfcF service info. + * + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) +public final class NfcFServiceInfo implements Parcelable { + static final String TAG = "NfcFServiceInfo"; + + private static final String DEFAULT_T3T_PMM = "FFFFFFFFFFFFFFFF"; + + /** + * The service that implements this + */ + private final ResolveInfo mService; + + /** + * Description of the service + */ + private final String mDescription; + + /** + * System Code of the service + */ + private final String mSystemCode; + + /** + * System Code of the service registered by API + */ + private String mDynamicSystemCode; + + /** + * NFCID2 of the service + */ + private final String mNfcid2; + + /** + * NFCID2 of the service registered by API + */ + private String mDynamicNfcid2; + + /** + * The uid of the package the service belongs to + */ + private final int mUid; + + /** + * LF_T3T_PMM of the service + */ + private final String mT3tPmm; + + /** + * @hide + */ + public NfcFServiceInfo(ResolveInfo info, String description, + String systemCode, String dynamicSystemCode, String nfcid2, String dynamicNfcid2, + int uid, String t3tPmm) { + this.mService = info; + this.mDescription = description; + this.mSystemCode = systemCode; + this.mDynamicSystemCode = dynamicSystemCode; + this.mNfcid2 = nfcid2; + this.mDynamicNfcid2 = dynamicNfcid2; + this.mUid = uid; + this.mT3tPmm = t3tPmm; + } + + /** + * Creates a new NfcFServiceInfo object. + * + * @param pm packageManager instance + * @param info app component info + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public NfcFServiceInfo(@NonNull PackageManager pm, @NonNull ResolveInfo info) + throws XmlPullParserException, IOException { + ServiceInfo si = info.serviceInfo; + XmlResourceParser parser = null; + try { + parser = si.loadXmlMetaData(pm, HostNfcFService.SERVICE_META_DATA); + if (parser == null) { + throw new XmlPullParserException("No " + HostNfcFService.SERVICE_META_DATA + + " meta-data"); + } + + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.START_TAG && + eventType != XmlPullParser.END_DOCUMENT) { + eventType = parser.next(); + } + + String tagName = parser.getName(); + if (!"host-nfcf-service".equals(tagName)) { + throw new XmlPullParserException( + "Meta-data does not start with tag"); + } + + Resources res = pm.getResourcesForApplication(si.applicationInfo); + AttributeSet attrs = Xml.asAttributeSet(parser); + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.HostNfcFService); + mService = info; + mDescription = sa.getString( + com.android.internal.R.styleable.HostNfcFService_description); + mDynamicSystemCode = null; + mDynamicNfcid2 = null; + sa.recycle(); + + String systemCode = null; + String nfcid2 = null; + String t3tPmm = null; + final int depth = parser.getDepth(); + + while (((eventType = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > depth) && eventType != XmlPullParser.END_DOCUMENT) { + tagName = parser.getName(); + if (eventType == XmlPullParser.START_TAG && + "system-code-filter".equals(tagName) && systemCode == null) { + final TypedArray a = res.obtainAttributes(attrs, + com.android.internal.R.styleable.SystemCodeFilter); + systemCode = a.getString( + com.android.internal.R.styleable.SystemCodeFilter_name).toUpperCase(); + if (!isValidSystemCode(systemCode) && + !systemCode.equalsIgnoreCase("NULL")) { + Log.e(TAG, "Invalid System Code: " + systemCode); + systemCode = null; + } + a.recycle(); + } else if (eventType == XmlPullParser.START_TAG && + "nfcid2-filter".equals(tagName) && nfcid2 == null) { + final TypedArray a = res.obtainAttributes(attrs, + com.android.internal.R.styleable.Nfcid2Filter); + nfcid2 = a.getString( + com.android.internal.R.styleable.Nfcid2Filter_name).toUpperCase(); + if (!nfcid2.equalsIgnoreCase("RANDOM") && + !nfcid2.equalsIgnoreCase("NULL") && + !isValidNfcid2(nfcid2)) { + Log.e(TAG, "Invalid NFCID2: " + nfcid2); + nfcid2 = null; + } + a.recycle(); + } else if (eventType == XmlPullParser.START_TAG && tagName.equals("t3tPmm-filter") + && t3tPmm == null) { + final TypedArray a = res.obtainAttributes(attrs, + com.android.internal.R.styleable.T3tPmmFilter); + t3tPmm = a.getString( + com.android.internal.R.styleable.T3tPmmFilter_name).toUpperCase(); + a.recycle(); + } + } + mSystemCode = (systemCode == null ? "NULL" : systemCode); + mNfcid2 = (nfcid2 == null ? "NULL" : nfcid2); + mT3tPmm = (t3tPmm == null ? DEFAULT_T3T_PMM : t3tPmm); + } catch (NameNotFoundException e) { + throw new XmlPullParserException("Unable to create context for: " + si.packageName); + } finally { + if (parser != null) parser.close(); + } + // Set uid + mUid = si.applicationInfo.uid; + } + + /** + * Returns the app component corresponding to this NFCF service. + * + * @return app component for this service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public ComponentName getComponent() { + return new ComponentName(mService.serviceInfo.packageName, + mService.serviceInfo.name); + } + + /** + * Returns the system code corresponding to this service. + * + * @return system code for this service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public String getSystemCode() { + return (mDynamicSystemCode == null ? mSystemCode : mDynamicSystemCode); + } + + /** + * Add or replace a system code to this service. + * @param systemCode system code to set or replace + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public void setDynamicSystemCode(@NonNull String systemCode) { + mDynamicSystemCode = systemCode; + } + + /** + * Returns NFC ID2. + * + * @return nfc id2 to return + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public String getNfcid2() { + return (mDynamicNfcid2 == null ? mNfcid2 : mDynamicNfcid2); + } + + /** + * Set or replace NFC ID2 + * + * @param nfcid2 NFC ID2 string + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public void setDynamicNfcid2(@NonNull String nfcid2) { + mDynamicNfcid2 = nfcid2; + } + + /** + * Returns description of service. + * @return user readable description of service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public String getDescription() { + return mDescription; + } + + /** + * Returns uid of service. + * @return uid of the service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public int getUid() { + return mUid; + } + + /** + * Returns LF_T3T_PMM of the service + * @return returns LF_T3T_PMM of the service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public String getT3tPmm() { + return mT3tPmm; + } + + /** + * Load application label for this service. + * @param pm packagemanager instance + * @return label name corresponding to service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public CharSequence loadLabel(@NonNull PackageManager pm) { + return mService.loadLabel(pm); + } + + /** + * Load application icon for this service. + * @param pm packagemanager instance + * @return app icon corresponding to service + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @NonNull + public Drawable loadIcon(@NonNull PackageManager pm) { + return mService.loadIcon(pm); + } + + @Override + public String toString() { + StringBuilder out = new StringBuilder("NfcFService: "); + out.append(getComponent()); + out.append(", UID: " + mUid); + out.append(", description: " + mDescription); + out.append(", System Code: " + mSystemCode); + if (mDynamicSystemCode != null) { + out.append(", dynamic System Code: " + mDynamicSystemCode); + } + out.append(", NFCID2: " + mNfcid2); + if (mDynamicNfcid2 != null) { + out.append(", dynamic NFCID2: " + mDynamicNfcid2); + } + out.append(", T3T PMM:" + mT3tPmm); + return out.toString(); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof NfcFServiceInfo)) return false; + NfcFServiceInfo thatService = (NfcFServiceInfo) o; + + if (!thatService.getComponent().equals(this.getComponent())) return false; + if (thatService.getUid() != this.getUid()) return false; + if (!thatService.mSystemCode.equalsIgnoreCase(this.mSystemCode)) return false; + if (!thatService.mNfcid2.equalsIgnoreCase(this.mNfcid2)) return false; + if (!thatService.mT3tPmm.equalsIgnoreCase(this.mT3tPmm)) return false; + return true; + } + + @Override + public int hashCode() { + return getComponent().hashCode(); + } + + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @Override + public int describeContents() { + return 0; + } + + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + mService.writeToParcel(dest, flags); + dest.writeString(mDescription); + dest.writeString(mSystemCode); + dest.writeInt(mDynamicSystemCode != null ? 1 : 0); + if (mDynamicSystemCode != null) { + dest.writeString(mDynamicSystemCode); + } + dest.writeString(mNfcid2); + dest.writeInt(mDynamicNfcid2 != null ? 1 : 0); + if (mDynamicNfcid2 != null) { + dest.writeString(mDynamicNfcid2); + } + dest.writeInt(mUid); + dest.writeString(mT3tPmm); + }; + + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public NfcFServiceInfo createFromParcel(Parcel source) { + ResolveInfo info = ResolveInfo.CREATOR.createFromParcel(source); + String description = source.readString(); + String systemCode = source.readString(); + String dynamicSystemCode = null; + if (source.readInt() != 0) { + dynamicSystemCode = source.readString(); + } + String nfcid2 = source.readString(); + String dynamicNfcid2 = null; + if (source.readInt() != 0) { + dynamicNfcid2 = source.readString(); + } + int uid = source.readInt(); + String t3tPmm = source.readString(); + NfcFServiceInfo service = new NfcFServiceInfo(info, description, + systemCode, dynamicSystemCode, nfcid2, dynamicNfcid2, uid, t3tPmm); + return service; + } + + @Override + public NfcFServiceInfo[] newArray(int size) { + return new NfcFServiceInfo[size]; + } + }; + + /** + * Dump contents of the service for debugging. + * @param fd parcelfiledescriptor instance + * @param pw printwriter instance + * @param args args for dumping + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public void dump(@NonNull ParcelFileDescriptor fd, @NonNull PrintWriter pw, + @NonNull String[] args) { + pw.println(" " + getComponent() + + " (Description: " + getDescription() + ")" + + " (UID: " + getUid() + ")"); + pw.println(" System Code: " + getSystemCode()); + pw.println(" NFCID2: " + getNfcid2()); + pw.println(" T3tPmm: " + getT3tPmm()); + } + + /** + * Dump debugging info as NfcFServiceInfoProto. + * + * If the output belongs to a sub message, the caller is responsible for wrapping this function + * between {@link ProtoOutputStream#start(long)} and {@link ProtoOutputStream#end(long)}. + * + * @param proto the ProtoOutputStream to write to + */ + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + public void dumpDebug(@NonNull ProtoOutputStream proto) { + getComponent().dumpDebug(proto, NfcFServiceInfoProto.COMPONENT_NAME); + proto.write(NfcFServiceInfoProto.DESCRIPTION, getDescription()); + proto.write(NfcFServiceInfoProto.SYSTEM_CODE, getSystemCode()); + proto.write(NfcFServiceInfoProto.NFCID2, getNfcid2()); + proto.write(NfcFServiceInfoProto.T3T_PMM, getT3tPmm()); + } + + /** + * Copied over from {@link NfcFCardEmulation#isValidSystemCode(String)} + * @hide + */ + private static boolean isValidSystemCode(String systemCode) { + if (systemCode == null) { + return false; + } + if (systemCode.length() != 4) { + Log.e(TAG, "System Code " + systemCode + " is not a valid System Code."); + return false; + } + // check if the value is between "4000" and "4FFF" (excluding "4*FF") + if (!systemCode.startsWith("4") || systemCode.toUpperCase().endsWith("FF")) { + Log.e(TAG, "System Code " + systemCode + " is not a valid System Code."); + return false; + } + try { + Integer.parseInt(systemCode, 16); + } catch (NumberFormatException e) { + Log.e(TAG, "System Code " + systemCode + " is not a valid System Code."); + return false; + } + return true; + } + + /** + * Copied over from {@link NfcFCardEmulation#isValidNfcid2(String)} + * @hide + */ + private static boolean isValidNfcid2(String nfcid2) { + if (nfcid2 == null) { + return false; + } + if (nfcid2.length() != 16) { + Log.e(TAG, "NFCID2 " + nfcid2 + " is not a valid NFCID2."); + return false; + } + // check if the the value starts with "02FE" + if (!nfcid2.toUpperCase().startsWith("02FE")) { + Log.e(TAG, "NFCID2 " + nfcid2 + " is not a valid NFCID2."); + return false; + } + try { + Long.parseLong(nfcid2, 16); + } catch (NumberFormatException e) { + Log.e(TAG, "NFCID2 " + nfcid2 + " is not a valid NFCID2."); + return false; + } + return true; + } +} diff --git a/nfc/java/android/nfc/cardemulation/OWNERS b/nfc/java/android/nfc/cardemulation/OWNERS new file mode 100644 index 000000000000..35e9713f5715 --- /dev/null +++ b/nfc/java/android/nfc/cardemulation/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 48448 +include platform/packages/apps/Nfc:/OWNERS diff --git a/nfc/java/android/nfc/cardemulation/OffHostApduService.java b/nfc/java/android/nfc/cardemulation/OffHostApduService.java new file mode 100644 index 000000000000..2286e8476d94 --- /dev/null +++ b/nfc/java/android/nfc/cardemulation/OffHostApduService.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.cardemulation; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.IBinder; + +/** + *

    OffHostApduService is a convenience {@link Service} class that can be + * extended to describe one or more NFC applications that are residing + * off-host, for example on an embedded secure element or a UICC. + * + *

    + *

    Developer Guide

    + * For a general introduction into the topic of card emulation, + * please read the + * NFC card emulation developer guide.

    + *
    + * + *

    NFC Protocols

    + *

    Off-host applications represented by this class are based on the NFC-Forum ISO-DEP + * protocol (based on ISO/IEC 14443-4) and support processing + * command Application Protocol Data Units (APDUs) as + * defined in the ISO/IEC 7816-4 specification. + * + *

    Service selection

    + *

    When a remote NFC device wants to talk to your + * off-host NFC application, it sends a so-called + * "SELECT AID" APDU as defined in the ISO/IEC 7816-4 specification. + * The AID is an application identifier defined in ISO/IEC 7816-4. + * + *

    The registration procedure for AIDs is defined in the + * ISO/IEC 7816-5 specification. If you don't want to register an + * AID, you are free to use AIDs in the proprietary range: + * bits 8-5 of the first byte must each be set to '1'. For example, + * "0xF00102030405" is a proprietary AID. If you do use proprietary + * AIDs, it is recommended to choose an AID of at least 6 bytes, + * to reduce the risk of collisions with other applications that + * might be using proprietary AIDs as well. + * + *

    AID groups

    + *

    In some cases, an off-host environment may need to register multiple AIDs + * to implement a certain application, and it needs to be sure + * that it is the default handler for all of these AIDs (as opposed + * to some AIDs in the group going to another service). + * + *

    An AID group is a list of AIDs that should be considered as + * belonging together by the OS. For all AIDs in an AID group, the + * OS will guarantee one of the following: + *

      + *
    • All AIDs in the group are routed to the off-host execution environment + *
    • No AIDs in the group are routed to the off-host execution environment + *
    + * In other words, there is no in-between state, where some AIDs + * in the group can be routed to this off-host execution environment, + * and some to another or a host-based {@link HostApduService}. + *

    AID groups and categories

    + *

    Each AID group can be associated with a category. This allows + * the Android OS to classify services, and it allows the user to + * set defaults at the category level instead of the AID level. + * + *

    You can use + * {@link CardEmulation#isDefaultServiceForCategory(android.content.ComponentName, String)} + * to determine if your off-host service is the default handler for a category. + * + *

    In this version of the platform, the only known categories + * are {@link CardEmulation#CATEGORY_PAYMENT} and {@link CardEmulation#CATEGORY_OTHER}. + * AID groups without a category, or with a category that is not recognized + * by the current platform version, will automatically be + * grouped into the {@link CardEmulation#CATEGORY_OTHER} category. + * + *

    Service AID registration

    + *

    To tell the platform which AIDs + * reside off-host and are managed by this service, a {@link #SERVICE_META_DATA} + * entry must be included in the declaration of the service. An + * example of a OffHostApduService manifest declaration is shown below: + *

     <service android:name=".MyOffHostApduService" android:exported="true" android:permission="android.permission.BIND_NFC_SERVICE">
    + *     <intent-filter>
    + *         <action android:name="android.nfc.cardemulation.action.OFF_HOST_APDU_SERVICE"/>
    + *     </intent-filter>
    + *     <meta-data android:name="android.nfc.cardemulation.off_host_apdu_ervice" android:resource="@xml/apduservice"/>
    + * </service>
    + * + * This meta-data tag points to an apduservice.xml file. + * An example of this file with a single AID group declaration is shown below: + *
    + * <offhost-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    + *           android:description="@string/servicedesc">
    + *       <aid-group android:description="@string/subscription" android:category="other">
    + *           <aid-filter android:name="F0010203040506"/>
    + *           <aid-filter android:name="F0394148148100"/>
    + *       </aid-group>
    + * </offhost-apdu-service>
    + * 
    + * + *

    The {@link android.R.styleable#OffHostApduService <offhost-apdu-service>} is required + * to contain a + * {@link android.R.styleable#OffHostApduService_description <android:description>} + * attribute that contains a user-friendly description of the service that may be shown in UI. + * + *

    The {@link android.R.styleable#OffHostApduService <offhost-apdu-service>} must + * contain one or more {@link android.R.styleable#AidGroup <aid-group>} tags. + * Each {@link android.R.styleable#AidGroup <aid-group>} must contain one or + * more {@link android.R.styleable#AidFilter <aid-filter>} tags, each of which + * contains a single AID. The AID must be specified in hexadecimal format, and contain + * an even number of characters. + * + *

    This registration will allow the service to be included + * as an option for being the default handler for categories. + * The Android OS will take care of correctly + * routing the AIDs to the off-host execution environment, + * based on which service the user has selected to be the handler for a certain category. + * + *

    The service may define additional actions outside of the + * Android namespace that provide further interaction with + * the off-host execution environment. + * + *

    Use of this class requires the + * {@link PackageManager#FEATURE_NFC_HOST_CARD_EMULATION} to be present + * on the device. + */ +public abstract class OffHostApduService extends Service { + /** + * The {@link Intent} action that must be declared as handled by the service. + */ + @SdkConstant(SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = + "android.nfc.cardemulation.action.OFF_HOST_APDU_SERVICE"; + + /** + * The name of the meta-data element that contains + * more information about this service. + */ + public static final String SERVICE_META_DATA = + "android.nfc.cardemulation.off_host_apdu_service"; + + /** + * The Android platform itself will not bind to this service, + * but merely uses its declaration to keep track of what AIDs + * the service is interested in. This information is then used + * to present the user with a list of applications that can handle + * an AID, as well as correctly route those AIDs either to the host (in case + * the user preferred a {@link HostApduService}), or to an off-host + * execution environment (in case the user preferred a {@link OffHostApduService}. + * + * Implementers may define additional actions outside of the + * Android namespace that allow further interactions with + * the off-host execution environment. Such implementations + * would need to override this method. + */ + public abstract IBinder onBind(Intent intent); +} diff --git a/nfc/java/android/nfc/cardemulation/Utils.java b/nfc/java/android/nfc/cardemulation/Utils.java new file mode 100644 index 000000000000..202e1cfb48f6 --- /dev/null +++ b/nfc/java/android/nfc/cardemulation/Utils.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.cardemulation; + +import android.annotation.NonNull; +import android.content.ComponentName; +import android.content.ComponentNameProto; +import android.util.proto.ProtoOutputStream; + +/** @hide */ +public final class Utils { + private Utils() { + } + + /** Copied from {@link ComponentName#dumpDebug(ProtoOutputStream, long)} */ + public static void dumpDebugComponentName( + @NonNull ComponentName componentName, @NonNull ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + proto.write(ComponentNameProto.PACKAGE_NAME, componentName.getPackageName()); + proto.write(ComponentNameProto.CLASS_NAME, componentName.getClassName()); + proto.end(token); + } +} diff --git a/nfc/java/android/nfc/dta/NfcDta.java b/nfc/java/android/nfc/dta/NfcDta.java new file mode 100644 index 000000000000..88016623434d --- /dev/null +++ b/nfc/java/android/nfc/dta/NfcDta.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.dta; + +import android.content.Context; +import android.nfc.INfcDta; +import android.nfc.NfcAdapter; +import android.os.RemoteException; +import android.util.Log; + +import java.util.HashMap; + +/** + * This class provides the primary API for DTA operations. + * @hide + */ +public final class NfcDta { + private static final String TAG = "NfcDta"; + + private static INfcDta sService; + private static HashMap sNfcDtas = new HashMap(); + + private final Context mContext; + + private NfcDta(Context context, INfcDta service) { + mContext = context.getApplicationContext(); + sService = service; + } + + /** + * Helper to get an instance of this class. + * + * @param adapter A reference to an NfcAdapter object. + * @return + */ + public static synchronized NfcDta getInstance(NfcAdapter adapter) { + if (adapter == null) throw new NullPointerException("NfcAdapter is null"); + Context context = adapter.getContext(); + if (context == null) { + Log.e(TAG, "NfcAdapter context is null."); + throw new UnsupportedOperationException(); + } + + NfcDta manager = sNfcDtas.get(context); + if (manager == null) { + INfcDta service = adapter.getNfcDtaInterface(); + if (service == null) { + Log.e(TAG, "This device does not implement the INfcDta interface."); + throw new UnsupportedOperationException(); + } + manager = new NfcDta(context, service); + sNfcDtas.put(context, manager); + } + return manager; + } + + /** + * Enables DTA mode + * + * @return true/false if enabling was successful + */ + public boolean enableDta() { + try { + sService.enableDta(); + } catch (RemoteException e) { + return false; + } + return true; + } + + /** + * Disables DTA mode + * + * @return true/false if disabling was successful + */ + public boolean disableDta() { + try { + sService.disableDta(); + } catch (RemoteException e) { + return false; + } + return true; + } + + /** + * Enables Server + * + * @return true/false if enabling was successful + */ + public boolean enableServer(String serviceName, int serviceSap, int miu, + int rwSize, int testCaseId) { + try { + return sService.enableServer(serviceName, serviceSap, miu, rwSize, testCaseId); + } catch (RemoteException e) { + return false; + } + } + + /** + * Disables Server + * + * @return true/false if disabling was successful + */ + public boolean disableServer() { + try { + sService.disableServer(); + } catch (RemoteException e) { + return false; + } + return true; + } + + /** + * Enables Client + * + * @return true/false if enabling was successful + */ + public boolean enableClient(String serviceName, int miu, int rwSize, + int testCaseId) { + try { + return sService.enableClient(serviceName, miu, rwSize, testCaseId); + } catch (RemoteException e) { + return false; + } + } + + /** + * Disables client + * + * @return true/false if disabling was successful + */ + public boolean disableClient() { + try { + sService.disableClient(); + } catch (RemoteException e) { + return false; + } + return true; + } + + /** + * Registers Message Service + * + * @return true/false if registration was successful + */ + public boolean registerMessageService(String msgServiceName) { + try { + return sService.registerMessageService(msgServiceName); + } catch (RemoteException e) { + return false; + } + } +} diff --git a/nfc/java/android/nfc/dta/OWNERS b/nfc/java/android/nfc/dta/OWNERS new file mode 100644 index 000000000000..35e9713f5715 --- /dev/null +++ b/nfc/java/android/nfc/dta/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 48448 +include platform/packages/apps/Nfc:/OWNERS diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig new file mode 100644 index 000000000000..11be905c41ca --- /dev/null +++ b/nfc/java/android/nfc/flags.aconfig @@ -0,0 +1,64 @@ +package: "android.nfc" + +flag { + name: "enable_nfc_mainline" + namespace: "nfc" + description: "Flag for NFC mainline changes" + bug: "292140387" +} + +flag { + name: "enable_nfc_reader_option" + namespace: "nfc" + description: "Flag for NFC reader option API changes" + bug: "291187960" +} + +flag { + name: "enable_nfc_user_restriction" + namespace: "nfc" + description: "Flag for NFC user restriction" + bug: "291187960" +} + +flag { + name: "nfc_observe_mode" + namespace: "nfc" + description: "Enable NFC Observe Mode" + bug: "294217286" +} + +flag { + name: "nfc_read_polling_loop" + namespace: "nfc" + description: "Enable NFC Polling Loop Notifications" + bug: "294217286" +} + +flag { + name: "nfc_observe_mode_st_shim" + namespace: "nfc" + description: "Enable NFC Observe Mode ST shim" + bug: "294217286" +} + +flag { + name: "nfc_read_polling_loop_st_shim" + namespace: "nfc" + description: "Enable NFC Polling Loop Notifications ST shim" + bug: "294217286" +} + +flag { + name: "enable_tag_detection_broadcasts" + namespace: "nfc" + description: "Enable sending broadcasts to Wallet role holder when a tag enters/leaves the field." + bug: "306203494" +} + +flag { + name: "enable_nfc_set_discovery_tech" + namespace: "nfc" + description: "Flag for NFC set discovery tech API" + bug: "300351519" +} diff --git a/nfc/java/android/nfc/package.html b/nfc/java/android/nfc/package.html new file mode 100644 index 000000000000..55c1d1650aa9 --- /dev/null +++ b/nfc/java/android/nfc/package.html @@ -0,0 +1,33 @@ + + +

    Provides access to Near Field Communication (NFC) functionality, allowing applications to read +NDEF message in NFC tags. A "tag" may actually be another device that appears as a tag.

    + +

    For more information, see the +Near Field Communication guide.

    +{@more} + +

    Here's a summary of the classes:

    + +
    +
    {@link android.nfc.NfcManager}
    +
    This is the high level manager, used to obtain this device's {@link android.nfc.NfcAdapter}. You can +acquire an instance using {@link android.content.Context#getSystemService}.
    +
    {@link android.nfc.NfcAdapter}
    +
    This represents the device's NFC adapter, which is your entry-point to performing NFC +operations. You can acquire an instance with {@link android.nfc.NfcManager#getDefaultAdapter}, or +{@link android.nfc.NfcAdapter#getDefaultAdapter(android.content.Context)}.
    +
    {@link android.nfc.NdefMessage}
    +
    Represents an NDEF data message, which is the standard format in which "records" +carrying data are transmitted between devices and tags. Your application can receive these +messages from an {@link android.nfc.NfcAdapter#ACTION_TAG_DISCOVERED} intent.
    +
    {@link android.nfc.NdefRecord}
    +
    Represents a record, which is delivered in a {@link android.nfc.NdefMessage} and describes the +type of data being shared and carries the data itself.
    +
    + +

    Note: +Not all Android-powered devices provide NFC functionality.

    + + + diff --git a/nfc/java/android/nfc/tech/BasicTagTechnology.java b/nfc/java/android/nfc/tech/BasicTagTechnology.java new file mode 100644 index 000000000000..ae468fead7a2 --- /dev/null +++ b/nfc/java/android/nfc/tech/BasicTagTechnology.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.tech; + +import android.nfc.ErrorCodes; +import android.nfc.Tag; +import android.nfc.TransceiveResult; +import android.os.RemoteException; +import android.util.Log; + +import java.io.IOException; + +/** + * A base class for tag technologies that are built on top of transceive(). + */ +abstract class BasicTagTechnology implements TagTechnology { + private static final String TAG = "NFC"; + + final Tag mTag; + + boolean mIsConnected; + int mSelectedTechnology; + + BasicTagTechnology(Tag tag, int tech) throws RemoteException { + mTag = tag; + mSelectedTechnology = tech; + } + + @Override + public Tag getTag() { + return mTag; + } + + /** Internal helper to throw IllegalStateException if the technology isn't connected */ + void checkConnected() { + if ((mTag.getConnectedTechnology() != mSelectedTechnology) || + (mTag.getConnectedTechnology() == -1)) { + throw new IllegalStateException("Call connect() first!"); + } + } + + @Override + public boolean isConnected() { + if (!mIsConnected) { + return false; + } + + try { + return mTag.getTagService().isPresent(mTag.getServiceHandle()); + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + return false; + } + } + + @Override + public void connect() throws IOException { + try { + int errorCode = mTag.getTagService().connect(mTag.getServiceHandle(), + mSelectedTechnology); + + if (errorCode == ErrorCodes.SUCCESS) { + // Store this in the tag object + if (!mTag.setConnectedTechnology(mSelectedTechnology)) { + Log.e(TAG, "Close other technology first!"); + throw new IOException("Only one TagTechnology can be connected at a time."); + } + mIsConnected = true; + } else if (errorCode == ErrorCodes.ERROR_NOT_SUPPORTED) { + throw new UnsupportedOperationException("Connecting to " + + "this technology is not supported by the NFC " + + "adapter."); + } else { + throw new IOException(); + } + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + throw new IOException("NFC service died"); + } + } + + /** @hide */ + @Override + public void reconnect() throws IOException { + if (!mIsConnected) { + throw new IllegalStateException("Technology not connected yet"); + } + + try { + int errorCode = mTag.getTagService().reconnect(mTag.getServiceHandle()); + + if (errorCode != ErrorCodes.SUCCESS) { + mIsConnected = false; + mTag.setTechnologyDisconnected(); + throw new IOException(); + } + } catch (RemoteException e) { + mIsConnected = false; + mTag.setTechnologyDisconnected(); + Log.e(TAG, "NFC service dead", e); + throw new IOException("NFC service died"); + } + } + + @Override + public void close() throws IOException { + try { + /* Note that we don't want to physically disconnect the tag, + * but just reconnect to it to reset its state + */ + mTag.getTagService().resetTimeouts(); + mTag.getTagService().reconnect(mTag.getServiceHandle()); + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + } finally { + mIsConnected = false; + mTag.setTechnologyDisconnected(); + } + } + + /** Internal getMaxTransceiveLength() */ + int getMaxTransceiveLengthInternal() { + try { + return mTag.getTagService().getMaxTransceiveLength(mSelectedTechnology); + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + return 0; + } + } + /** Internal transceive */ + byte[] transceive(byte[] data, boolean raw) throws IOException { + checkConnected(); + + try { + TransceiveResult result = mTag.getTagService().transceive(mTag.getServiceHandle(), + data, raw); + if (result == null) { + throw new IOException("transceive failed"); + } else { + return result.getResponseOrThrow(); + } + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + throw new IOException("NFC service died"); + } + } +} diff --git a/nfc/java/android/nfc/tech/IsoDep.java b/nfc/java/android/nfc/tech/IsoDep.java new file mode 100644 index 000000000000..0ba0c5a8d13b --- /dev/null +++ b/nfc/java/android/nfc/tech/IsoDep.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.tech; + +import android.nfc.ErrorCodes; +import android.nfc.Tag; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import java.io.IOException; + +/** + * Provides access to ISO-DEP (ISO 14443-4) properties and I/O operations on a {@link Tag}. + * + *

    Acquire an {@link IsoDep} object using {@link #get}. + *

    The primary ISO-DEP I/O operation is {@link #transceive}. Applications must + * implement their own protocol stack on top of {@link #transceive}. + *

    Tags that enumerate the {@link IsoDep} technology in {@link Tag#getTechList} + * will also enumerate + * {@link NfcA} or {@link NfcB} (since IsoDep builds on top of either of these). + * + *

    Note: Methods that perform I/O operations + * require the {@link android.Manifest.permission#NFC} permission. + */ +public final class IsoDep extends BasicTagTechnology { + private static final String TAG = "NFC"; + + /** @hide */ + public static final String EXTRA_HI_LAYER_RESP = "hiresp"; + /** @hide */ + public static final String EXTRA_HIST_BYTES = "histbytes"; + + private byte[] mHiLayerResponse = null; + private byte[] mHistBytes = null; + + /** + * Get an instance of {@link IsoDep} for the given tag. + *

    Does not cause any RF activity and does not block. + *

    Returns null if {@link IsoDep} was not enumerated in {@link Tag#getTechList}. + * This indicates the tag does not support ISO-DEP. + * + * @param tag an ISO-DEP compatible tag + * @return ISO-DEP object + */ + public static IsoDep get(Tag tag) { + if (!tag.hasTech(TagTechnology.ISO_DEP)) return null; + try { + return new IsoDep(tag); + } catch (RemoteException e) { + return null; + } + } + + /** @hide */ + public IsoDep(Tag tag) + throws RemoteException { + super(tag, TagTechnology.ISO_DEP); + Bundle extras = tag.getTechExtras(TagTechnology.ISO_DEP); + if (extras != null) { + mHiLayerResponse = extras.getByteArray(EXTRA_HI_LAYER_RESP); + mHistBytes = extras.getByteArray(EXTRA_HIST_BYTES); + } + } + + /** + * Set the timeout of {@link #transceive} in milliseconds. + *

    The timeout only applies to ISO-DEP {@link #transceive}, and is + * reset to a default value when {@link #close} is called. + *

    Setting a longer timeout may be useful when performing + * transactions that require a long processing time on the tag + * such as key generation. + * + *

    Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param timeout timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public void setTimeout(int timeout) { + try { + int err = mTag.getTagService().setTimeout(TagTechnology.ISO_DEP, timeout); + if (err != ErrorCodes.SUCCESS) { + throw new IllegalArgumentException("The supplied timeout is not valid"); + } + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + } + } + + /** + * Get the current timeout for {@link #transceive} in milliseconds. + * + *

    Requires the {@link android.Manifest.permission#NFC} permission. + * + * @return timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public int getTimeout() { + try { + return mTag.getTagService().getTimeout(TagTechnology.ISO_DEP); + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + return 0; + } + } + + /** + * Return the ISO-DEP historical bytes for {@link NfcA} tags. + *

    Does not cause any RF activity and does not block. + *

    The historical bytes can be used to help identify a tag. They are present + * only on {@link IsoDep} tags that are based on {@link NfcA} RF technology. + * If this tag is not {@link NfcA} then null is returned. + *

    In ISO 14443-4 terminology, the historical bytes are a subset of the RATS + * response. + * + * @return ISO-DEP historical bytes, or null if this is not a {@link NfcA} tag + */ + public byte[] getHistoricalBytes() { + return mHistBytes; + } + + /** + * Return the higher layer response bytes for {@link NfcB} tags. + *

    Does not cause any RF activity and does not block. + *

    The higher layer response bytes can be used to help identify a tag. + * They are present only on {@link IsoDep} tags that are based on {@link NfcB} + * RF technology. If this tag is not {@link NfcB} then null is returned. + *

    In ISO 14443-4 terminology, the higher layer bytes are a subset of the + * ATTRIB response. + * + * @return ISO-DEP historical bytes, or null if this is not a {@link NfcB} tag + */ + public byte[] getHiLayerResponse() { + return mHiLayerResponse; + } + + /** + * Send raw ISO-DEP data to the tag and receive the response. + * + *

    Applications must only send the INF payload, and not the start of frame and + * end of frame indicators. Applications do not need to fragment the payload, it + * will be automatically fragmented and defragmented by {@link #transceive} if + * it exceeds FSD/FSC limits. + * + *

    Use {@link #getMaxTransceiveLength} to retrieve the maximum number of bytes + * that can be sent with {@link #transceive}. + * + *

    This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

    Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param data command bytes to send, must not be null + * @return response bytes received, will not be null + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or this operation is canceled + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public byte[] transceive(byte[] data) throws IOException { + return transceive(data, true); + } + + /** + * Return the maximum number of bytes that can be sent with {@link #transceive}. + * @return the maximum number of bytes that can be sent with {@link #transceive}. + */ + public int getMaxTransceiveLength() { + return getMaxTransceiveLengthInternal(); + } + + /** + *

    Standard APDUs have a 1-byte length field, allowing a maximum of + * 255 payload bytes, which results in a maximum APDU length of 261 bytes. + * + *

    Extended length APDUs have a 3-byte length field, allowing 65535 + * payload bytes. + * + *

    Some NFC adapters, like the one used in the Nexus S and the Galaxy Nexus + * do not support extended length APDUs. They are expected to be well-supported + * in the future though. Use this method to check for extended length APDU + * support. + * + * @return whether the NFC adapter on this device supports extended length APDUs. + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public boolean isExtendedLengthApduSupported() { + try { + return mTag.getTagService().getExtendedLengthApdusSupported(); + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + return false; + } + } +} diff --git a/nfc/java/android/nfc/tech/MifareClassic.java b/nfc/java/android/nfc/tech/MifareClassic.java new file mode 100644 index 000000000000..26f54e692289 --- /dev/null +++ b/nfc/java/android/nfc/tech/MifareClassic.java @@ -0,0 +1,655 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.tech; + +import android.nfc.ErrorCodes; +import android.nfc.Tag; +import android.nfc.TagLostException; +import android.os.RemoteException; +import android.util.Log; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Provides access to MIFARE Classic properties and I/O operations on a {@link Tag}. + * + *

    Acquire a {@link MifareClassic} object using {@link #get}. + * + *

    MIFARE Classic is also known as MIFARE Standard. + *

    MIFARE Classic tags are divided into sectors, and each sector is sub-divided into + * blocks. Block size is always 16 bytes ({@link #BLOCK_SIZE}. Sector size varies. + *

      + *
    • MIFARE Classic Mini are 320 bytes ({@link #SIZE_MINI}), with 5 sectors each of 4 blocks. + *
    • MIFARE Classic 1k are 1024 bytes ({@link #SIZE_1K}), with 16 sectors each of 4 blocks. + *
    • MIFARE Classic 2k are 2048 bytes ({@link #SIZE_2K}), with 32 sectors each of 4 blocks. + *
    • MIFARE Classic 4k are 4096 bytes ({@link #SIZE_4K}). The first 32 sectors contain 4 blocks + * and the last 8 sectors contain 16 blocks. + *
    + * + *

    MIFARE Classic tags require authentication on a per-sector basis before any + * other I/O operations on that sector can be performed. There are two keys per sector, + * and ACL bits determine what I/O operations are allowed on that sector after + * authenticating with a key. {@see #authenticateSectorWithKeyA} and + * {@see #authenticateSectorWithKeyB}. + * + *

    Three well-known authentication keys are defined in this class: + * {@link #KEY_DEFAULT}, {@link #KEY_MIFARE_APPLICATION_DIRECTORY}, + * {@link #KEY_NFC_FORUM}. + *

      + *
    • {@link #KEY_DEFAULT} is the default factory key for MIFARE Classic. + *
    • {@link #KEY_MIFARE_APPLICATION_DIRECTORY} is the well-known key for + * MIFARE Classic cards that have been formatted according to the + * MIFARE Application Directory (MAD) specification. + *
    • {@link #KEY_NFC_FORUM} is the well-known key for MIFARE Classic cards that + * have been formatted according to the NXP specification for NDEF on MIFARE Classic. + * + *

      Implementation of this class on a Android NFC device is optional. + * If it is not implemented, then + * {@link MifareClassic} will never be enumerated in {@link Tag#getTechList}. + * If it is enumerated, then all {@link MifareClassic} I/O operations will be supported, + * and {@link Ndef#MIFARE_CLASSIC} NDEF tags will also be supported. In either case, + * {@link NfcA} will also be enumerated on the tag, because all MIFARE Classic tags are also + * {@link NfcA}. + * + *

      Note: Methods that perform I/O operations + * require the {@link android.Manifest.permission#NFC} permission. + */ +public final class MifareClassic extends BasicTagTechnology { + private static final String TAG = "NFC"; + + /** + * The default factory key. + */ + public static final byte[] KEY_DEFAULT = + {(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF}; + /** + * The well-known key for tags formatted according to the + * MIFARE Application Directory (MAD) specification. + */ + public static final byte[] KEY_MIFARE_APPLICATION_DIRECTORY = + {(byte)0xA0,(byte)0xA1,(byte)0xA2,(byte)0xA3,(byte)0xA4,(byte)0xA5}; + /** + * The well-known key for tags formatted according to the + * NDEF on MIFARE Classic specification. + */ + public static final byte[] KEY_NFC_FORUM = + {(byte)0xD3,(byte)0xF7,(byte)0xD3,(byte)0xF7,(byte)0xD3,(byte)0xF7}; + + /** A MIFARE Classic compatible card of unknown type */ + public static final int TYPE_UNKNOWN = -1; + /** A MIFARE Classic tag */ + public static final int TYPE_CLASSIC = 0; + /** A MIFARE Plus tag */ + public static final int TYPE_PLUS = 1; + /** A MIFARE Pro tag */ + public static final int TYPE_PRO = 2; + + /** Tag contains 16 sectors, each with 4 blocks. */ + public static final int SIZE_1K = 1024; + /** Tag contains 32 sectors, each with 4 blocks. */ + public static final int SIZE_2K = 2048; + /** + * Tag contains 40 sectors. The first 32 sectors contain 4 blocks and the last 8 sectors + * contain 16 blocks. + */ + public static final int SIZE_4K = 4096; + /** Tag contains 5 sectors, each with 4 blocks. */ + public static final int SIZE_MINI = 320; + + /** Size of a MIFARE Classic block (in bytes) */ + public static final int BLOCK_SIZE = 16; + + private static final int MAX_BLOCK_COUNT = 256; + private static final int MAX_SECTOR_COUNT = 40; + + private boolean mIsEmulated; + private int mType; + private int mSize; + + /** + * Get an instance of {@link MifareClassic} for the given tag. + *

      Does not cause any RF activity and does not block. + *

      Returns null if {@link MifareClassic} was not enumerated in {@link Tag#getTechList}. + * This indicates the tag is not MIFARE Classic compatible, or this Android + * device does not support MIFARE Classic. + * + * @param tag an MIFARE Classic compatible tag + * @return MIFARE Classic object + */ + public static MifareClassic get(Tag tag) { + if (!tag.hasTech(TagTechnology.MIFARE_CLASSIC)) return null; + try { + return new MifareClassic(tag); + } catch (RemoteException e) { + return null; + } + } + + /** @hide */ + public MifareClassic(Tag tag) throws RemoteException { + super(tag, TagTechnology.MIFARE_CLASSIC); + + NfcA a = NfcA.get(tag); // MIFARE Classic is always based on NFC a + + mIsEmulated = false; + + switch (a.getSak()) { + case 0x01: + case 0x08: + mType = TYPE_CLASSIC; + mSize = SIZE_1K; + break; + case 0x09: + mType = TYPE_CLASSIC; + mSize = SIZE_MINI; + break; + case 0x10: + mType = TYPE_PLUS; + mSize = SIZE_2K; + // SecLevel = SL2 + break; + case 0x11: + mType = TYPE_PLUS; + mSize = SIZE_4K; + // Seclevel = SL2 + break; + case 0x18: + mType = TYPE_CLASSIC; + mSize = SIZE_4K; + break; + case 0x28: + mType = TYPE_CLASSIC; + mSize = SIZE_1K; + mIsEmulated = true; + break; + case 0x38: + mType = TYPE_CLASSIC; + mSize = SIZE_4K; + mIsEmulated = true; + break; + case 0x88: + mType = TYPE_CLASSIC; + mSize = SIZE_1K; + // NXP-tag: false + break; + case 0x98: + case 0xB8: + mType = TYPE_PRO; + mSize = SIZE_4K; + break; + default: + // Stack incorrectly reported a MifareClassic. We cannot handle this + // gracefully - we have no idea of the memory layout. Bail. + throw new RuntimeException( + "Tag incorrectly enumerated as MIFARE Classic, SAK = " + a.getSak()); + } + } + + /** + * Return the type of this MIFARE Classic compatible tag. + *

      One of {@link #TYPE_UNKNOWN}, {@link #TYPE_CLASSIC}, {@link #TYPE_PLUS} or + * {@link #TYPE_PRO}. + *

      Does not cause any RF activity and does not block. + * + * @return type + */ + public int getType() { + return mType; + } + + /** + * Return the size of the tag in bytes + *

      One of {@link #SIZE_MINI}, {@link #SIZE_1K}, {@link #SIZE_2K}, {@link #SIZE_4K}. + * These constants are equal to their respective size in bytes. + *

      Does not cause any RF activity and does not block. + * @return size in bytes + */ + public int getSize() { + return mSize; + } + + /** + * Return true if the tag is emulated, determined at discovery time. + * These are actually smart-cards that emulate a MIFARE Classic interface. + * They can be treated identically to a MIFARE Classic tag. + * @hide + */ + public boolean isEmulated() { + return mIsEmulated; + } + + /** + * Return the number of MIFARE Classic sectors. + *

      Does not cause any RF activity and does not block. + * @return number of sectors + */ + public int getSectorCount() { + switch (mSize) { + case SIZE_1K: + return 16; + case SIZE_2K: + return 32; + case SIZE_4K: + return 40; + case SIZE_MINI: + return 5; + default: + return 0; + } + } + + /** + * Return the total number of MIFARE Classic blocks. + *

      Does not cause any RF activity and does not block. + * @return total number of blocks + */ + public int getBlockCount() { + return mSize / BLOCK_SIZE; + } + + /** + * Return the number of blocks in the given sector. + *

      Does not cause any RF activity and does not block. + * + * @param sectorIndex index of sector, starting from 0 + * @return number of blocks in the sector + */ + public int getBlockCountInSector(int sectorIndex) { + validateSector(sectorIndex); + + if (sectorIndex < 32) { + return 4; + } else { + return 16; + } + } + + /** + * Return the sector that contains a given block. + *

      Does not cause any RF activity and does not block. + * + * @param blockIndex index of block to lookup, starting from 0 + * @return sector index that contains the block + */ + public int blockToSector(int blockIndex) { + validateBlock(blockIndex); + + if (blockIndex < 32 * 4) { + return blockIndex / 4; + } else { + return 32 + (blockIndex - 32 * 4) / 16; + } + } + + /** + * Return the first block of a given sector. + *

      Does not cause any RF activity and does not block. + * + * @param sectorIndex index of sector to lookup, starting from 0 + * @return block index of first block in sector + */ + public int sectorToBlock(int sectorIndex) { + if (sectorIndex < 32) { + return sectorIndex * 4; + } else { + return 32 * 4 + (sectorIndex - 32) * 16; + } + } + + /** + * Authenticate a sector with key A. + * + *

      Successful authentication of a sector with key A enables other + * I/O operations on that sector. The set of operations granted by key A + * key depends on the ACL bits set in that sector. For more information + * see the MIFARE Classic specification on http://www.nxp.com. + * + *

      A failed authentication attempt causes an implicit reconnection to the + * tag, so authentication to other sectors will be lost. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param sectorIndex index of sector to authenticate, starting from 0 + * @param key 6-byte authentication key + * @return true on success, false on authentication failure + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + */ + public boolean authenticateSectorWithKeyA(int sectorIndex, byte[] key) throws IOException { + return authenticate(sectorIndex, key, true); + } + + /** + * Authenticate a sector with key B. + * + *

      Successful authentication of a sector with key B enables other + * I/O operations on that sector. The set of operations granted by key B + * depends on the ACL bits set in that sector. For more information + * see the MIFARE Classic specification on http://www.nxp.com. + * + *

      A failed authentication attempt causes an implicit reconnection to the + * tag, so authentication to other sectors will be lost. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param sectorIndex index of sector to authenticate, starting from 0 + * @param key 6-byte authentication key + * @return true on success, false on authentication failure + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + */ + public boolean authenticateSectorWithKeyB(int sectorIndex, byte[] key) throws IOException { + return authenticate(sectorIndex, key, false); + } + + private boolean authenticate(int sector, byte[] key, boolean keyA) throws IOException { + validateSector(sector); + checkConnected(); + + byte[] cmd = new byte[12]; + + // First byte is the command + if (keyA) { + cmd[0] = 0x60; // phHal_eMifareAuthentA + } else { + cmd[0] = 0x61; // phHal_eMifareAuthentB + } + + // Second byte is block address + // Authenticate command takes a block address. Authenticating a block + // of a sector will authenticate the entire sector. + cmd[1] = (byte) sectorToBlock(sector); + + // Next 4 bytes are last 4 bytes of UID + byte[] uid = getTag().getId(); + System.arraycopy(uid, uid.length - 4, cmd, 2, 4); + + // Next 6 bytes are key + System.arraycopy(key, 0, cmd, 6, 6); + + try { + if (transceive(cmd, false) != null) { + return true; + } + } catch (TagLostException e) { + throw e; + } catch (IOException e) { + // No need to deal with, will return false anyway + } + return false; + } + + /** + * Read 16-byte block. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param blockIndex index of block to read, starting from 0 + * @return 16 byte block + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + */ + public byte[] readBlock(int blockIndex) throws IOException { + validateBlock(blockIndex); + checkConnected(); + + byte[] cmd = { 0x30, (byte) blockIndex }; + return transceive(cmd, false); + } + + /** + * Write 16-byte block. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param blockIndex index of block to write, starting from 0 + * @param data 16 bytes of data to write + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + */ + public void writeBlock(int blockIndex, byte[] data) throws IOException { + validateBlock(blockIndex); + checkConnected(); + if (data.length != 16) { + throw new IllegalArgumentException("must write 16-bytes"); + } + + byte[] cmd = new byte[data.length + 2]; + cmd[0] = (byte) 0xA0; // MF write command + cmd[1] = (byte) blockIndex; + System.arraycopy(data, 0, cmd, 2, data.length); + + transceive(cmd, false); + } + + /** + * Increment a value block, storing the result in the temporary block on the tag. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param blockIndex index of block to increment, starting from 0 + * @param value non-negative to increment by + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + */ + public void increment(int blockIndex, int value) throws IOException { + validateBlock(blockIndex); + validateValueOperand(value); + checkConnected(); + + ByteBuffer cmd = ByteBuffer.allocate(6); + cmd.order(ByteOrder.LITTLE_ENDIAN); + cmd.put( (byte) 0xC1 ); + cmd.put( (byte) blockIndex ); + cmd.putInt(value); + + transceive(cmd.array(), false); + } + + /** + * Decrement a value block, storing the result in the temporary block on the tag. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param blockIndex index of block to decrement, starting from 0 + * @param value non-negative to decrement by + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + */ + public void decrement(int blockIndex, int value) throws IOException { + validateBlock(blockIndex); + validateValueOperand(value); + checkConnected(); + + ByteBuffer cmd = ByteBuffer.allocate(6); + cmd.order(ByteOrder.LITTLE_ENDIAN); + cmd.put( (byte) 0xC0 ); + cmd.put( (byte) blockIndex ); + cmd.putInt(value); + + transceive(cmd.array(), false); + } + + /** + * Copy from the temporary block to a value block. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param blockIndex index of block to copy to + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + */ + public void transfer(int blockIndex) throws IOException { + validateBlock(blockIndex); + checkConnected(); + + byte[] cmd = { (byte) 0xB0, (byte) blockIndex }; + + transceive(cmd, false); + } + + /** + * Copy from a value block to the temporary block. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param blockIndex index of block to copy from + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + */ + public void restore(int blockIndex) throws IOException { + validateBlock(blockIndex); + checkConnected(); + + byte[] cmd = { (byte) 0xC2, (byte) blockIndex }; + + transceive(cmd, false); + } + + /** + * Send raw NfcA data to a tag and receive the response. + * + *

      This is equivalent to connecting to this tag via {@link NfcA} + * and calling {@link NfcA#transceive}. Note that all MIFARE Classic + * tags are based on {@link NfcA} technology. + * + *

      Use {@link #getMaxTransceiveLength} to retrieve the maximum number of bytes + * that can be sent with {@link #transceive}. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @see NfcA#transceive + */ + public byte[] transceive(byte[] data) throws IOException { + return transceive(data, true); + } + + /** + * Return the maximum number of bytes that can be sent with {@link #transceive}. + * @return the maximum number of bytes that can be sent with {@link #transceive}. + */ + public int getMaxTransceiveLength() { + return getMaxTransceiveLengthInternal(); + } + + /** + * Set the {@link #transceive} timeout in milliseconds. + * + *

      The timeout only applies to {@link #transceive} on this object, + * and is reset to a default value when {@link #close} is called. + * + *

      Setting a longer timeout may be useful when performing + * transactions that require a long processing time on the tag + * such as key generation. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param timeout timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public void setTimeout(int timeout) { + try { + int err = mTag.getTagService().setTimeout(TagTechnology.MIFARE_CLASSIC, timeout); + if (err != ErrorCodes.SUCCESS) { + throw new IllegalArgumentException("The supplied timeout is not valid"); + } + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + } + } + + /** + * Get the current {@link #transceive} timeout in milliseconds. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @return timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public int getTimeout() { + try { + return mTag.getTagService().getTimeout(TagTechnology.MIFARE_CLASSIC); + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + return 0; + } + } + + private static void validateSector(int sector) { + // Do not be too strict on upper bounds checking, since some cards + // have more addressable memory than they report. For example, + // MIFARE Plus 2k cards will appear as MIFARE Classic 1k cards when in + // MIFARE Classic compatibility mode. + // Note that issuing a command to an out-of-bounds block is safe - the + // tag should report error causing IOException. This validation is a + // helper to guard against obvious programming mistakes. + if (sector < 0 || sector >= MAX_SECTOR_COUNT) { + throw new IndexOutOfBoundsException("sector out of bounds: " + sector); + } + } + + private static void validateBlock(int block) { + // Just looking for obvious out of bounds... + if (block < 0 || block >= MAX_BLOCK_COUNT) { + throw new IndexOutOfBoundsException("block out of bounds: " + block); + } + } + + private static void validateValueOperand(int value) { + if (value < 0) { + throw new IllegalArgumentException("value operand negative"); + } + } +} diff --git a/nfc/java/android/nfc/tech/MifareUltralight.java b/nfc/java/android/nfc/tech/MifareUltralight.java new file mode 100644 index 000000000000..c0416a39ba76 --- /dev/null +++ b/nfc/java/android/nfc/tech/MifareUltralight.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.tech; + +import android.nfc.ErrorCodes; +import android.nfc.Tag; +import android.nfc.TagLostException; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import java.io.IOException; + +//TOOD: Ultralight C 3-DES authentication, one-way counter + +/** + * Provides access to MIFARE Ultralight properties and I/O operations on a {@link Tag}. + * + *

      Acquire a {@link MifareUltralight} object using {@link #get}. + * + *

      MIFARE Ultralight compatible tags have 4 byte pages {@link #PAGE_SIZE}. + * The primary operations on an Ultralight tag are {@link #readPages} and + * {@link #writePage}. + * + *

      The original MIFARE Ultralight consists of a 64 byte EEPROM. The first + * 4 pages are for the OTP area, manufacturer data, and locking bits. They are + * readable and some bits are writable. The final 12 pages are the user + * read/write area. For more information see the NXP data sheet MF0ICU1. + * + *

      The MIFARE Ultralight C consists of a 192 byte EEPROM. The first 4 pages + * are for OTP, manufacturer data, and locking bits. The next 36 pages are the + * user read/write area. The next 4 pages are additional locking bits, counters + * and authentication configuration and are readable. The final 4 pages are for + * the authentication key and are not readable. For more information see the + * NXP data sheet MF0ICU2. + * + *

      Implementation of this class on a Android NFC device is optional. + * If it is not implemented, then + * {@link MifareUltralight} will never be enumerated in {@link Tag#getTechList}. + * If it is enumerated, then all {@link MifareUltralight} I/O operations will be supported. + * In either case, {@link NfcA} will also be enumerated on the tag, + * because all MIFARE Ultralight tags are also {@link NfcA} tags. + * + *

      Note: Methods that perform I/O operations + * require the {@link android.Manifest.permission#NFC} permission. + */ +public final class MifareUltralight extends BasicTagTechnology { + private static final String TAG = "NFC"; + + /** A MIFARE Ultralight compatible tag of unknown type */ + public static final int TYPE_UNKNOWN = -1; + /** A MIFARE Ultralight tag */ + public static final int TYPE_ULTRALIGHT = 1; + /** A MIFARE Ultralight C tag */ + public static final int TYPE_ULTRALIGHT_C = 2; + + /** Size of a MIFARE Ultralight page in bytes */ + public static final int PAGE_SIZE = 4; + + private static final int NXP_MANUFACTURER_ID = 0x04; + private static final int MAX_PAGE_COUNT = 256; + + /** @hide */ + public static final String EXTRA_IS_UL_C = "isulc"; + + private int mType; + + /** + * Get an instance of {@link MifareUltralight} for the given tag. + *

      Returns null if {@link MifareUltralight} was not enumerated in + * {@link Tag#getTechList} - this indicates the tag is not MIFARE + * Ultralight compatible, or that this Android + * device does not implement MIFARE Ultralight. + *

      Does not cause any RF activity and does not block. + * + * @param tag an MIFARE Ultralight compatible tag + * @return MIFARE Ultralight object + */ + public static MifareUltralight get(Tag tag) { + if (!tag.hasTech(TagTechnology.MIFARE_ULTRALIGHT)) return null; + try { + return new MifareUltralight(tag); + } catch (RemoteException e) { + return null; + } + } + + /** @hide */ + public MifareUltralight(Tag tag) throws RemoteException { + super(tag, TagTechnology.MIFARE_ULTRALIGHT); + + // Check if this could actually be a MIFARE + NfcA a = NfcA.get(tag); + + mType = TYPE_UNKNOWN; + + if (a.getSak() == 0x00 && tag.getId()[0] == NXP_MANUFACTURER_ID) { + Bundle extras = tag.getTechExtras(TagTechnology.MIFARE_ULTRALIGHT); + if (extras.getBoolean(EXTRA_IS_UL_C)) { + mType = TYPE_ULTRALIGHT_C; + } else { + mType = TYPE_ULTRALIGHT; + } + } + } + + /** + * Return the MIFARE Ultralight type of the tag. + *

      One of {@link #TYPE_ULTRALIGHT} or {@link #TYPE_ULTRALIGHT_C} or + * {@link #TYPE_UNKNOWN}. + *

      Depending on how the tag has been formatted, it can be impossible + * to accurately classify between original MIFARE Ultralight and + * Ultralight C. So treat this method as a hint. + *

      Does not cause any RF activity and does not block. + * + * @return the type + */ + public int getType() { + return mType; + } + + /** + * Read 4 pages (16 bytes). + * + *

      The MIFARE Ultralight protocol always reads 4 pages at a time, to + * reduce the number of commands required to read an entire tag. + *

      If a read spans past the last readable block, then the tag will + * return pages that have been wrapped back to the first blocks. MIFARE + * Ultralight tags have readable blocks 0x00 through 0x0F. So a read to + * block offset 0x0E would return blocks 0x0E, 0x0F, 0x00, 0x01. MIFARE + * Ultralight C tags have readable blocks 0x00 through 0x2B. So a read to + * block 0x2A would return blocks 0x2A, 0x2B, 0x00, 0x01. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param pageOffset index of first page to read, starting from 0 + * @return 4 pages (16 bytes) + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + */ + public byte[] readPages(int pageOffset) throws IOException { + validatePageIndex(pageOffset); + checkConnected(); + + byte[] cmd = { 0x30, (byte) pageOffset}; + return transceive(cmd, false); + } + + /** + * Write 1 page (4 bytes). + * + *

      The MIFARE Ultralight protocol always writes 1 page at a time, to + * minimize EEPROM write cycles. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param pageOffset index of page to write, starting from 0 + * @param data 4 bytes to write + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + */ + public void writePage(int pageOffset, byte[] data) throws IOException { + validatePageIndex(pageOffset); + checkConnected(); + + byte[] cmd = new byte[data.length + 2]; + cmd[0] = (byte) 0xA2; + cmd[1] = (byte) pageOffset; + System.arraycopy(data, 0, cmd, 2, data.length); + + transceive(cmd, false); + } + + /** + * Send raw NfcA data to a tag and receive the response. + * + *

      This is equivalent to connecting to this tag via {@link NfcA} + * and calling {@link NfcA#transceive}. Note that all MIFARE Classic + * tags are based on {@link NfcA} technology. + * + *

      Use {@link #getMaxTransceiveLength} to retrieve the maximum number of bytes + * that can be sent with {@link #transceive}. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @see NfcA#transceive + */ + public byte[] transceive(byte[] data) throws IOException { + return transceive(data, true); + } + + /** + * Return the maximum number of bytes that can be sent with {@link #transceive}. + * @return the maximum number of bytes that can be sent with {@link #transceive}. + */ + public int getMaxTransceiveLength() { + return getMaxTransceiveLengthInternal(); + } + + /** + * Set the {@link #transceive} timeout in milliseconds. + * + *

      The timeout only applies to {@link #transceive} on this object, + * and is reset to a default value when {@link #close} is called. + * + *

      Setting a longer timeout may be useful when performing + * transactions that require a long processing time on the tag + * such as key generation. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param timeout timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public void setTimeout(int timeout) { + try { + int err = mTag.getTagService().setTimeout( + TagTechnology.MIFARE_ULTRALIGHT, timeout); + if (err != ErrorCodes.SUCCESS) { + throw new IllegalArgumentException("The supplied timeout is not valid"); + } + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + } + } + + /** + * Get the current {@link #transceive} timeout in milliseconds. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @return timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public int getTimeout() { + try { + return mTag.getTagService().getTimeout(TagTechnology.MIFARE_ULTRALIGHT); + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + return 0; + } + } + + private static void validatePageIndex(int pageIndex) { + // Do not be too strict on upper bounds checking, since some cards + // may have more addressable memory than they report. + // Note that issuing a command to an out-of-bounds block is safe - the + // tag will wrap the read to an addressable area. This validation is a + // helper to guard against obvious programming mistakes. + if (pageIndex < 0 || pageIndex >= MAX_PAGE_COUNT) { + throw new IndexOutOfBoundsException("page out of bounds: " + pageIndex); + } + } +} diff --git a/nfc/java/android/nfc/tech/Ndef.java b/nfc/java/android/nfc/tech/Ndef.java new file mode 100644 index 000000000000..7d83f157a314 --- /dev/null +++ b/nfc/java/android/nfc/tech/Ndef.java @@ -0,0 +1,408 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.tech; + +import android.nfc.ErrorCodes; +import android.nfc.FormatException; +import android.nfc.INfcTag; +import android.nfc.NdefMessage; +import android.nfc.Tag; +import android.nfc.TagLostException; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import java.io.IOException; + +/** + * Provides access to NDEF content and operations on a {@link Tag}. + * + *

      Acquire a {@link Ndef} object using {@link #get}. + * + *

      NDEF is an NFC Forum data format. The data formats are implemented in + * {@link android.nfc.NdefMessage} and + * {@link android.nfc.NdefRecord}. This class provides methods to + * retrieve and modify the {@link android.nfc.NdefMessage} + * on a tag. + * + *

      There are currently four NFC Forum standardized tag types that can be + * formatted to contain NDEF data. + *

        + *
      • NFC Forum Type 1 Tag ({@link #NFC_FORUM_TYPE_1}), such as the Innovision Topaz + *
      • NFC Forum Type 2 Tag ({@link #NFC_FORUM_TYPE_2}), such as the NXP MIFARE Ultralight + *
      • NFC Forum Type 3 Tag ({@link #NFC_FORUM_TYPE_3}), such as Sony Felica + *
      • NFC Forum Type 4 Tag ({@link #NFC_FORUM_TYPE_4}), such as NXP MIFARE Desfire + *
      + * It is mandatory for all Android devices with NFC to correctly enumerate + * {@link Ndef} on NFC Forum Tag Types 1-4, and implement all NDEF operations + * as defined in this class. + * + *

      Some vendors have their own well defined specifications for storing NDEF data + * on tags that do not fall into the above categories. Android devices with NFC + * should enumerate and implement {@link Ndef} under these vendor specifications + * where possible, but it is not mandatory. {@link #getType} returns a String + * describing this specification, for example {@link #MIFARE_CLASSIC} is + * com.nxp.ndef.mifareclassic. + * + *

      Android devices that support MIFARE Classic must also correctly + * implement {@link Ndef} on MIFARE Classic tags formatted to NDEF. + * + *

      For guaranteed compatibility across all Android devices with NFC, it is + * recommended to use NFC Forum Types 1-4 in new deployments of NFC tags + * with NDEF payload. Vendor NDEF formats will not work on all Android devices. + * + *

      Note: Methods that perform I/O operations + * require the {@link android.Manifest.permission#NFC} permission. + */ +public final class Ndef extends BasicTagTechnology { + private static final String TAG = "NFC"; + + /** @hide */ + public static final int NDEF_MODE_READ_ONLY = 1; + /** @hide */ + public static final int NDEF_MODE_READ_WRITE = 2; + /** @hide */ + public static final int NDEF_MODE_UNKNOWN = 3; + + /** @hide */ + public static final String EXTRA_NDEF_MSG = "ndefmsg"; + + /** @hide */ + public static final String EXTRA_NDEF_MAXLENGTH = "ndefmaxlength"; + + /** @hide */ + public static final String EXTRA_NDEF_CARDSTATE = "ndefcardstate"; + + /** @hide */ + public static final String EXTRA_NDEF_TYPE = "ndeftype"; + + /** @hide */ + public static final int TYPE_OTHER = -1; + /** @hide */ + public static final int TYPE_1 = 1; + /** @hide */ + public static final int TYPE_2 = 2; + /** @hide */ + public static final int TYPE_3 = 3; + /** @hide */ + public static final int TYPE_4 = 4; + /** @hide */ + public static final int TYPE_MIFARE_CLASSIC = 101; + /** @hide */ + public static final int TYPE_ICODE_SLI = 102; + + /** @hide */ + public static final String UNKNOWN = "android.ndef.unknown"; + + /** NFC Forum Tag Type 1 */ + public static final String NFC_FORUM_TYPE_1 = "org.nfcforum.ndef.type1"; + /** NFC Forum Tag Type 2 */ + public static final String NFC_FORUM_TYPE_2 = "org.nfcforum.ndef.type2"; + /** NFC Forum Tag Type 3 */ + public static final String NFC_FORUM_TYPE_3 = "org.nfcforum.ndef.type3"; + /** NFC Forum Tag Type 4 */ + public static final String NFC_FORUM_TYPE_4 = "org.nfcforum.ndef.type4"; + /** NDEF on MIFARE Classic */ + public static final String MIFARE_CLASSIC = "com.nxp.ndef.mifareclassic"; + /** + * NDEF on iCODE SLI + * @hide + */ + public static final String ICODE_SLI = "com.nxp.ndef.icodesli"; + + private final int mMaxNdefSize; + private final int mCardState; + private final NdefMessage mNdefMsg; + private final int mNdefType; + + /** + * Get an instance of {@link Ndef} for the given tag. + * + *

      Returns null if {@link Ndef} was not enumerated in {@link Tag#getTechList}. + * This indicates the tag is not NDEF formatted, or that this tag + * is NDEF formatted but under a vendor specification that this Android + * device does not implement. + * + *

      Does not cause any RF activity and does not block. + * + * @param tag an NDEF compatible tag + * @return Ndef object + */ + public static Ndef get(Tag tag) { + if (!tag.hasTech(TagTechnology.NDEF)) return null; + try { + return new Ndef(tag); + } catch (RemoteException e) { + return null; + } + } + + /** + * Internal constructor, to be used by NfcAdapter + * @hide + */ + public Ndef(Tag tag) throws RemoteException { + super(tag, TagTechnology.NDEF); + Bundle extras = tag.getTechExtras(TagTechnology.NDEF); + if (extras != null) { + mMaxNdefSize = extras.getInt(EXTRA_NDEF_MAXLENGTH); + mCardState = extras.getInt(EXTRA_NDEF_CARDSTATE); + mNdefMsg = extras.getParcelable(EXTRA_NDEF_MSG, android.nfc.NdefMessage.class); + mNdefType = extras.getInt(EXTRA_NDEF_TYPE); + } else { + throw new NullPointerException("NDEF tech extras are null."); + } + + } + + /** + * Get the {@link NdefMessage} that was read from the tag at discovery time. + * + *

      If the NDEF Message is modified by an I/O operation then it + * will not be updated here, this function only returns what was discovered + * when the tag entered the field. + *

      Note that this method may return null if the tag was in the + * INITIALIZED state as defined by NFC Forum, as in this state the + * tag is formatted to support NDEF but does not contain a message yet. + *

      Does not cause any RF activity and does not block. + * @return NDEF Message read from the tag at discovery time, can be null + */ + public NdefMessage getCachedNdefMessage() { + return mNdefMsg; + } + + /** + * Get the NDEF tag type. + * + *

      Returns one of {@link #NFC_FORUM_TYPE_1}, {@link #NFC_FORUM_TYPE_2}, + * {@link #NFC_FORUM_TYPE_3}, {@link #NFC_FORUM_TYPE_4}, + * {@link #MIFARE_CLASSIC} or another NDEF tag type that has not yet been + * formalized in this Android API. + * + *

      Does not cause any RF activity and does not block. + * + * @return a string representing the NDEF tag type + */ + public String getType() { + switch (mNdefType) { + case TYPE_1: + return NFC_FORUM_TYPE_1; + case TYPE_2: + return NFC_FORUM_TYPE_2; + case TYPE_3: + return NFC_FORUM_TYPE_3; + case TYPE_4: + return NFC_FORUM_TYPE_4; + case TYPE_MIFARE_CLASSIC: + return MIFARE_CLASSIC; + case TYPE_ICODE_SLI: + return ICODE_SLI; + default: + return UNKNOWN; + } + } + + /** + * Get the maximum NDEF message size in bytes. + * + *

      Does not cause any RF activity and does not block. + * + * @return size in bytes + */ + public int getMaxSize() { + return mMaxNdefSize; + } + + /** + * Determine if the tag is writable. + * + *

      NFC Forum tags can be in read-only or read-write states. + * + *

      Does not cause any RF activity and does not block. + * + *

      Requires {@link android.Manifest.permission#NFC} permission. + * + * @return true if the tag is writable + */ + public boolean isWritable() { + return (mCardState == NDEF_MODE_READ_WRITE); + } + + /** + * Read the current {@link android.nfc.NdefMessage} on this tag. + * + *

      This always reads the current NDEF Message stored on the tag. + * + *

      Note that this method may return null if the tag was in the + * INITIALIZED state as defined by NFC Forum, as in that state the + * tag is formatted to support NDEF but does not contain a message yet. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @return the NDEF Message, can be null + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + * @throws FormatException if the NDEF Message on the tag is malformed + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public NdefMessage getNdefMessage() throws IOException, FormatException { + checkConnected(); + + try { + INfcTag tagService = mTag.getTagService(); + if (tagService == null) { + throw new IOException("Mock tags don't support this operation."); + } + int serviceHandle = mTag.getServiceHandle(); + if (tagService.isNdef(serviceHandle)) { + NdefMessage msg = tagService.ndefRead(serviceHandle); + if (msg == null && !tagService.isPresent(serviceHandle)) { + throw new TagLostException(); + } + return msg; + } else if (!tagService.isPresent(serviceHandle)) { + throw new TagLostException(); + } else { + return null; + } + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + return null; + } + } + + /** + * Overwrite the {@link NdefMessage} on this tag. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param msg the NDEF Message to write, must not be null + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + * @throws FormatException if the NDEF Message to write is malformed + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public void writeNdefMessage(NdefMessage msg) throws IOException, FormatException { + checkConnected(); + + try { + INfcTag tagService = mTag.getTagService(); + if (tagService == null) { + throw new IOException("Mock tags don't support this operation."); + } + int serviceHandle = mTag.getServiceHandle(); + if (tagService.isNdef(serviceHandle)) { + int errorCode = tagService.ndefWrite(serviceHandle, msg); + switch (errorCode) { + case ErrorCodes.SUCCESS: + break; + case ErrorCodes.ERROR_IO: + throw new IOException(); + case ErrorCodes.ERROR_INVALID_PARAM: + throw new FormatException(); + default: + // Should not happen + throw new IOException(); + } + } + else { + throw new IOException("Tag is not ndef"); + } + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + } + } + + /** + * Indicates whether a tag can be made read-only with {@link #makeReadOnly()}. + * + *

      Does not cause any RF activity and does not block. + * + * @return true if it is possible to make this tag read-only + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public boolean canMakeReadOnly() { + INfcTag tagService = mTag.getTagService(); + if (tagService == null) { + return false; + } + try { + return tagService.canMakeReadOnly(mNdefType); + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + return false; + } + } + + /** + * Make a tag read-only. + * + *

      This sets the CC field to indicate the tag is read-only, + * and where possible permanently sets the lock bits to prevent + * any further modification of the memory. + *

      This is a one-way process and cannot be reverted! + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @return true on success, false if it is not possible to make this tag read-only + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public boolean makeReadOnly() throws IOException { + checkConnected(); + + try { + INfcTag tagService = mTag.getTagService(); + if (tagService == null) { + return false; + } + if (tagService.isNdef(mTag.getServiceHandle())) { + int errorCode = tagService.ndefMakeReadOnly(mTag.getServiceHandle()); + switch (errorCode) { + case ErrorCodes.SUCCESS: + return true; + case ErrorCodes.ERROR_IO: + throw new IOException(); + case ErrorCodes.ERROR_INVALID_PARAM: + return false; + default: + // Should not happen + throw new IOException(); + } + } + else { + throw new IOException("Tag is not ndef"); + } + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + return false; + } + } +} diff --git a/nfc/java/android/nfc/tech/NdefFormatable.java b/nfc/java/android/nfc/tech/NdefFormatable.java new file mode 100644 index 000000000000..2240fe7f7d3b --- /dev/null +++ b/nfc/java/android/nfc/tech/NdefFormatable.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.tech; + +import android.nfc.ErrorCodes; +import android.nfc.FormatException; +import android.nfc.INfcTag; +import android.nfc.NdefMessage; +import android.nfc.Tag; +import android.nfc.TagLostException; +import android.os.RemoteException; +import android.util.Log; + +import java.io.IOException; + +/** + * Provide access to NDEF format operations on a {@link Tag}. + * + *

      Acquire a {@link NdefFormatable} object using {@link #get}. + * + *

      Android devices with NFC must only enumerate and implement this + * class for tags for which it can format to NDEF. + * + *

      Unfortunately the procedures to convert unformated tags to NDEF formatted + * tags are not specified by NFC Forum, and are not generally well-known. So + * there is no mandatory set of tags for which all Android devices with NFC + * must support {@link NdefFormatable}. + * + *

      Note: Methods that perform I/O operations + * require the {@link android.Manifest.permission#NFC} permission. + */ +public final class NdefFormatable extends BasicTagTechnology { + private static final String TAG = "NFC"; + + /** + * Get an instance of {@link NdefFormatable} for the given tag. + *

      Does not cause any RF activity and does not block. + *

      Returns null if {@link NdefFormatable} was not enumerated in {@link Tag#getTechList}. + * This indicates the tag is not NDEF formatable by this Android device. + * + * @param tag an NDEF formatable tag + * @return NDEF formatable object + */ + public static NdefFormatable get(Tag tag) { + if (!tag.hasTech(TagTechnology.NDEF_FORMATABLE)) return null; + try { + return new NdefFormatable(tag); + } catch (RemoteException e) { + return null; + } + } + + /** + * Internal constructor, to be used by NfcAdapter + * @hide + */ + public NdefFormatable(Tag tag) throws RemoteException { + super(tag, TagTechnology.NDEF_FORMATABLE); + } + + /** + * Format a tag as NDEF, and write a {@link NdefMessage}. + * + *

      This is a multi-step process, an IOException is thrown + * if any one step fails. + *

      The card is left in a read-write state after this operation. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param firstMessage the NDEF message to write after formatting, can be null + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + * @throws FormatException if the NDEF Message to write is malformed + */ + public void format(NdefMessage firstMessage) throws IOException, FormatException { + format(firstMessage, false); + } + + /** + * Formats a tag as NDEF, write a {@link NdefMessage}, and make read-only. + * + *

      This is a multi-step process, an IOException is thrown + * if any one step fails. + *

      The card is left in a read-only state if this method returns successfully. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param firstMessage the NDEF message to write after formatting + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or the operation is canceled + * @throws FormatException if the NDEF Message to write is malformed + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public void formatReadOnly(NdefMessage firstMessage) throws IOException, FormatException { + format(firstMessage, true); + } + + /*package*/ void format(NdefMessage firstMessage, boolean makeReadOnly) throws IOException, + FormatException { + checkConnected(); + + try { + int serviceHandle = mTag.getServiceHandle(); + INfcTag tagService = mTag.getTagService(); + if (tagService == null) { + throw new IOException(); + } + int errorCode = tagService.formatNdef(serviceHandle, MifareClassic.KEY_DEFAULT); + switch (errorCode) { + case ErrorCodes.SUCCESS: + break; + case ErrorCodes.ERROR_IO: + throw new IOException(); + case ErrorCodes.ERROR_INVALID_PARAM: + throw new FormatException(); + default: + // Should not happen + throw new IOException(); + } + // Now check and see if the format worked + if (!tagService.isNdef(serviceHandle)) { + throw new IOException(); + } + + // Write a message, if one was provided + if (firstMessage != null) { + errorCode = tagService.ndefWrite(serviceHandle, firstMessage); + switch (errorCode) { + case ErrorCodes.SUCCESS: + break; + case ErrorCodes.ERROR_IO: + throw new IOException(); + case ErrorCodes.ERROR_INVALID_PARAM: + throw new FormatException(); + default: + // Should not happen + throw new IOException(); + } + } + + // optionally make read-only + if (makeReadOnly) { + errorCode = tagService.ndefMakeReadOnly(serviceHandle); + switch (errorCode) { + case ErrorCodes.SUCCESS: + break; + case ErrorCodes.ERROR_IO: + throw new IOException(); + case ErrorCodes.ERROR_INVALID_PARAM: + throw new IOException(); + default: + // Should not happen + throw new IOException(); + } + } + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + } + } +} diff --git a/nfc/java/android/nfc/tech/NfcA.java b/nfc/java/android/nfc/tech/NfcA.java new file mode 100644 index 000000000000..7e6648361670 --- /dev/null +++ b/nfc/java/android/nfc/tech/NfcA.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.tech; + +import android.nfc.ErrorCodes; +import android.nfc.Tag; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import java.io.IOException; + +/** + * Provides access to NFC-A (ISO 14443-3A) properties and I/O operations on a {@link Tag}. + * + *

      Acquire a {@link NfcA} object using {@link #get}. + *

      The primary NFC-A I/O operation is {@link #transceive}. Applications must + * implement their own protocol stack on top of {@link #transceive}. + * + *

      Note: Methods that perform I/O operations + * require the {@link android.Manifest.permission#NFC} permission. + */ +public final class NfcA extends BasicTagTechnology { + private static final String TAG = "NFC"; + + /** @hide */ + public static final String EXTRA_SAK = "sak"; + /** @hide */ + public static final String EXTRA_ATQA = "atqa"; + + private short mSak; + private byte[] mAtqa; + + /** + * Get an instance of {@link NfcA} for the given tag. + *

      Returns null if {@link NfcA} was not enumerated in {@link Tag#getTechList}. + * This indicates the tag does not support NFC-A. + *

      Does not cause any RF activity and does not block. + * + * @param tag an NFC-A compatible tag + * @return NFC-A object + */ + public static NfcA get(Tag tag) { + if (!tag.hasTech(TagTechnology.NFC_A)) return null; + try { + return new NfcA(tag); + } catch (RemoteException e) { + return null; + } + } + + /** @hide */ + public NfcA(Tag tag) throws RemoteException { + super(tag, TagTechnology.NFC_A); + Bundle extras = tag.getTechExtras(TagTechnology.NFC_A); + mSak = extras.getShort(EXTRA_SAK); + mAtqa = extras.getByteArray(EXTRA_ATQA); + } + + /** + * Return the ATQA/SENS_RES bytes from tag discovery. + * + *

      Does not cause any RF activity and does not block. + * + * @return ATQA/SENS_RES bytes + */ + public byte[] getAtqa() { + return mAtqa; + } + + /** + * Return the SAK/SEL_RES bytes from tag discovery. + * + *

      Does not cause any RF activity and does not block. + * + * @return SAK bytes + */ + public short getSak() { + return mSak; + } + + /** + * Send raw NFC-A commands to the tag and receive the response. + * + *

      Applications must not append the EoD (CRC) to the payload, + * it will be automatically calculated. + *

      Applications must only send commands that are complete bytes, + * for example a SENS_REQ is not possible (these are used to + * manage tag polling and initialization). + * + *

      Use {@link #getMaxTransceiveLength} to retrieve the maximum number of bytes + * that can be sent with {@link #transceive}. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param data bytes to send + * @return bytes received in response + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or this operation is canceled + */ + public byte[] transceive(byte[] data) throws IOException { + return transceive(data, true); + } + + /** + * Return the maximum number of bytes that can be sent with {@link #transceive}. + * @return the maximum number of bytes that can be sent with {@link #transceive}. + */ + public int getMaxTransceiveLength() { + return getMaxTransceiveLengthInternal(); + } + + /** + * Set the {@link #transceive} timeout in milliseconds. + * + *

      The timeout only applies to {@link #transceive} on this object, + * and is reset to a default value when {@link #close} is called. + * + *

      Setting a longer timeout may be useful when performing + * transactions that require a long processing time on the tag + * such as key generation. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param timeout timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public void setTimeout(int timeout) { + try { + int err = mTag.getTagService().setTimeout(TagTechnology.NFC_A, timeout); + if (err != ErrorCodes.SUCCESS) { + throw new IllegalArgumentException("The supplied timeout is not valid"); + } + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + } + } + + /** + * Get the current {@link #transceive} timeout in milliseconds. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @return timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public int getTimeout() { + try { + return mTag.getTagService().getTimeout(TagTechnology.NFC_A); + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + return 0; + } + } +} diff --git a/nfc/java/android/nfc/tech/NfcB.java b/nfc/java/android/nfc/tech/NfcB.java new file mode 100644 index 000000000000..3ebd47f610c0 --- /dev/null +++ b/nfc/java/android/nfc/tech/NfcB.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.tech; + +import android.nfc.Tag; +import android.os.Bundle; +import android.os.RemoteException; + +import java.io.IOException; + +/** + * Provides access to NFC-B (ISO 14443-3B) properties and I/O operations on a {@link Tag}. + * + *

      Acquire a {@link NfcB} object using {@link #get}. + *

      The primary NFC-B I/O operation is {@link #transceive}. Applications must + * implement their own protocol stack on top of {@link #transceive}. + * + *

      Note: Methods that perform I/O operations + * require the {@link android.Manifest.permission#NFC} permission. + */ +public final class NfcB extends BasicTagTechnology { + /** @hide */ + public static final String EXTRA_APPDATA = "appdata"; + /** @hide */ + public static final String EXTRA_PROTINFO = "protinfo"; + + private byte[] mAppData; + private byte[] mProtInfo; + + /** + * Get an instance of {@link NfcB} for the given tag. + *

      Returns null if {@link NfcB} was not enumerated in {@link Tag#getTechList}. + * This indicates the tag does not support NFC-B. + *

      Does not cause any RF activity and does not block. + * + * @param tag an NFC-B compatible tag + * @return NFC-B object + */ + public static NfcB get(Tag tag) { + if (!tag.hasTech(TagTechnology.NFC_B)) return null; + try { + return new NfcB(tag); + } catch (RemoteException e) { + return null; + } + } + + /** @hide */ + public NfcB(Tag tag) throws RemoteException { + super(tag, TagTechnology.NFC_B); + Bundle extras = tag.getTechExtras(TagTechnology.NFC_B); + mAppData = extras.getByteArray(EXTRA_APPDATA); + mProtInfo = extras.getByteArray(EXTRA_PROTINFO); + } + + /** + * Return the Application Data bytes from ATQB/SENSB_RES at tag discovery. + * + *

      Does not cause any RF activity and does not block. + * + * @return Application Data bytes from ATQB/SENSB_RES bytes + */ + public byte[] getApplicationData() { + return mAppData; + } + + /** + * Return the Protocol Info bytes from ATQB/SENSB_RES at tag discovery. + * + *

      Does not cause any RF activity and does not block. + * + * @return Protocol Info bytes from ATQB/SENSB_RES bytes + */ + public byte[] getProtocolInfo() { + return mProtInfo; + } + + /** + * Send raw NFC-B commands to the tag and receive the response. + * + *

      Applications must not append the EoD (CRC) to the payload, + * it will be automatically calculated. + *

      Applications must not send commands that manage the polling + * loop and initialization (SENSB_REQ, SLOT_MARKER etc). + * + *

      Use {@link #getMaxTransceiveLength} to retrieve the maximum number of bytes + * that can be sent with {@link #transceive}. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param data bytes to send + * @return bytes received in response + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or this operation is canceled + */ + public byte[] transceive(byte[] data) throws IOException { + return transceive(data, true); + } + + /** + * Return the maximum number of bytes that can be sent with {@link #transceive}. + * @return the maximum number of bytes that can be sent with {@link #transceive}. + */ + public int getMaxTransceiveLength() { + return getMaxTransceiveLengthInternal(); + } +} diff --git a/nfc/java/android/nfc/tech/NfcBarcode.java b/nfc/java/android/nfc/tech/NfcBarcode.java new file mode 100644 index 000000000000..421ba7827cb1 --- /dev/null +++ b/nfc/java/android/nfc/tech/NfcBarcode.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.tech; + +import android.nfc.Tag; +import android.os.Bundle; +import android.os.RemoteException; + +/** + * Provides access to tags containing just a barcode. + * + *

      Acquire an {@link NfcBarcode} object using {@link #get}. + * + */ +public final class NfcBarcode extends BasicTagTechnology { + + /** Kovio Tags */ + public static final int TYPE_KOVIO = 1; + public static final int TYPE_UNKNOWN = -1; + + /** @hide */ + public static final String EXTRA_BARCODE_TYPE = "barcodetype"; + + private int mType; + + /** + * Get an instance of {@link NfcBarcode} for the given tag. + * + *

      Returns null if {@link NfcBarcode} was not enumerated in {@link Tag#getTechList}. + * + *

      Does not cause any RF activity and does not block. + * + * @param tag an NfcBarcode compatible tag + * @return NfcBarcode object + */ + public static NfcBarcode get(Tag tag) { + if (!tag.hasTech(TagTechnology.NFC_BARCODE)) return null; + try { + return new NfcBarcode(tag); + } catch (RemoteException e) { + return null; + } + } + + /** + * Internal constructor, to be used by NfcAdapter + * @hide + */ + public NfcBarcode(Tag tag) throws RemoteException { + super(tag, TagTechnology.NFC_BARCODE); + Bundle extras = tag.getTechExtras(TagTechnology.NFC_BARCODE); + if (extras != null) { + mType = extras.getInt(EXTRA_BARCODE_TYPE); + } else { + throw new NullPointerException("NfcBarcode tech extras are null."); + } + } + + /** + * Returns the NFC Barcode tag type. + * + *

      Currently only one of {@link #TYPE_KOVIO} or {@link #TYPE_UNKNOWN}. + * + *

      Does not cause any RF activity and does not block. + * + * @return the NFC Barcode tag type + */ + public int getType() { + return mType; + } + + /** + * Returns the barcode of an NfcBarcode tag. + * + *

      Tags of {@link #TYPE_KOVIO} return 16 bytes: + *

        + *

        The first byte is 0x80 ORd with a manufacturer ID, corresponding + * to ISO/IEC 7816-6. + *

        The second byte describes the payload data format. Defined data + * format types include the following:

          + *
        • 0x00: Reserved for manufacturer assignment
        • + *
        • 0x01: 96-bit URL with "http://www." prefix
        • + *
        • 0x02: 96-bit URL with "https://www." prefix
        • + *
        • 0x03: 96-bit URL with "http://" prefix
        • + *
        • 0x04: 96-bit URL with "https://" prefix
        • + *
        • 0x05: 96-bit GS1 EPC
        • + *
        • 0x06-0xFF: reserved
        • + *
        + *

        The following 12 bytes are payload:

        + *

        The last 2 bytes comprise the CRC. + *

      + *

      Does not cause any RF activity and does not block. + * + * @return a byte array containing the barcode + * @see + * Thinfilm NFC Barcode tag specification (previously Kovio NFC Barcode) + * @see + * Thinfilm NFC Barcode data format (previously Kovio NFC Barcode) + */ + public byte[] getBarcode() { + switch (mType) { + case TYPE_KOVIO: + // For Kovio tags the barcode matches the ID + return mTag.getId(); + default: + return null; + } + } +} diff --git a/nfc/java/android/nfc/tech/NfcF.java b/nfc/java/android/nfc/tech/NfcF.java new file mode 100644 index 000000000000..2ccd38875f07 --- /dev/null +++ b/nfc/java/android/nfc/tech/NfcF.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.tech; + +import android.nfc.ErrorCodes; +import android.nfc.Tag; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import java.io.IOException; + +/** + * Provides access to NFC-F (JIS 6319-4) properties and I/O operations on a {@link Tag}. + * + *

      Acquire a {@link NfcF} object using {@link #get}. + *

      The primary NFC-F I/O operation is {@link #transceive}. Applications must + * implement their own protocol stack on top of {@link #transceive}. + * + *

      Note: Methods that perform I/O operations + * require the {@link android.Manifest.permission#NFC} permission. + */ +public final class NfcF extends BasicTagTechnology { + private static final String TAG = "NFC"; + + /** @hide */ + public static final String EXTRA_SC = "systemcode"; + /** @hide */ + public static final String EXTRA_PMM = "pmm"; + + private byte[] mSystemCode = null; + private byte[] mManufacturer = null; + + /** + * Get an instance of {@link NfcF} for the given tag. + *

      Returns null if {@link NfcF} was not enumerated in {@link Tag#getTechList}. + * This indicates the tag does not support NFC-F. + *

      Does not cause any RF activity and does not block. + * + * @param tag an NFC-F compatible tag + * @return NFC-F object + */ + public static NfcF get(Tag tag) { + if (!tag.hasTech(TagTechnology.NFC_F)) return null; + try { + return new NfcF(tag); + } catch (RemoteException e) { + return null; + } + } + + /** @hide */ + public NfcF(Tag tag) throws RemoteException { + super(tag, TagTechnology.NFC_F); + Bundle extras = tag.getTechExtras(TagTechnology.NFC_F); + if (extras != null) { + mSystemCode = extras.getByteArray(EXTRA_SC); + mManufacturer = extras.getByteArray(EXTRA_PMM); + } + } + + /** + * Return the System Code bytes from tag discovery. + * + *

      Does not cause any RF activity and does not block. + * + * @return System Code bytes + */ + public byte[] getSystemCode() { + return mSystemCode; + } + + /** + * Return the Manufacturer bytes from tag discovery. + * + *

      Does not cause any RF activity and does not block. + * + * @return Manufacturer bytes + */ + public byte[] getManufacturer() { + return mManufacturer; + } + + /** + * Send raw NFC-F commands to the tag and receive the response. + * + *

      Applications must not prefix the SoD (preamble and sync code) + * and/or append the EoD (CRC) to the payload, it will be automatically calculated. + * + *

      A typical NFC-F frame for this method looks like: + *

      +     * LENGTH (1 byte) --- CMD (1 byte) -- IDm (8 bytes) -- PARAMS (LENGTH - 10 bytes)
      +     * 
      + * + *

      Use {@link #getMaxTransceiveLength} to retrieve the maximum amount of bytes + * that can be sent with {@link #transceive}. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param data bytes to send + * @return bytes received in response + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or this operation is canceled + */ + public byte[] transceive(byte[] data) throws IOException { + return transceive(data, true); + } + + /** + * Return the maximum number of bytes that can be sent with {@link #transceive}. + * @return the maximum number of bytes that can be sent with {@link #transceive}. + */ + public int getMaxTransceiveLength() { + return getMaxTransceiveLengthInternal(); + } + + /** + * Set the {@link #transceive} timeout in milliseconds. + * + *

      The timeout only applies to {@link #transceive} on this object, + * and is reset to a default value when {@link #close} is called. + * + *

      Setting a longer timeout may be useful when performing + * transactions that require a long processing time on the tag + * such as key generation. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param timeout timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public void setTimeout(int timeout) { + try { + int err = mTag.getTagService().setTimeout(TagTechnology.NFC_F, timeout); + if (err != ErrorCodes.SUCCESS) { + throw new IllegalArgumentException("The supplied timeout is not valid"); + } + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + } + } + + /** + * Get the current {@link #transceive} timeout in milliseconds. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @return timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public int getTimeout() { + try { + return mTag.getTagService().getTimeout(TagTechnology.NFC_F); + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + return 0; + } + } +} diff --git a/nfc/java/android/nfc/tech/NfcV.java b/nfc/java/android/nfc/tech/NfcV.java new file mode 100644 index 000000000000..186c63bf07fd --- /dev/null +++ b/nfc/java/android/nfc/tech/NfcV.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.tech; + +import android.nfc.Tag; +import android.os.Bundle; +import android.os.RemoteException; + +import java.io.IOException; + +/** + * Provides access to NFC-V (ISO 15693) properties and I/O operations on a {@link Tag}. + * + *

      Acquire a {@link NfcV} object using {@link #get}. + *

      The primary NFC-V I/O operation is {@link #transceive}. Applications must + * implement their own protocol stack on top of {@link #transceive}. + * + *

      Note: Methods that perform I/O operations + * require the {@link android.Manifest.permission#NFC} permission. + */ +public final class NfcV extends BasicTagTechnology { + /** @hide */ + public static final String EXTRA_RESP_FLAGS = "respflags"; + + /** @hide */ + public static final String EXTRA_DSFID = "dsfid"; + + private byte mRespFlags; + private byte mDsfId; + + /** + * Get an instance of {@link NfcV} for the given tag. + *

      Returns null if {@link NfcV} was not enumerated in {@link Tag#getTechList}. + * This indicates the tag does not support NFC-V. + *

      Does not cause any RF activity and does not block. + * + * @param tag an NFC-V compatible tag + * @return NFC-V object + */ + public static NfcV get(Tag tag) { + if (!tag.hasTech(TagTechnology.NFC_V)) return null; + try { + return new NfcV(tag); + } catch (RemoteException e) { + return null; + } + } + + /** @hide */ + public NfcV(Tag tag) throws RemoteException { + super(tag, TagTechnology.NFC_V); + Bundle extras = tag.getTechExtras(TagTechnology.NFC_V); + mRespFlags = extras.getByte(EXTRA_RESP_FLAGS); + mDsfId = extras.getByte(EXTRA_DSFID); + } + + /** + * Return the Response Flag bytes from tag discovery. + * + *

      Does not cause any RF activity and does not block. + * + * @return Response Flag bytes + */ + public byte getResponseFlags() { + return mRespFlags; + } + + /** + * Return the DSF ID bytes from tag discovery. + * + *

      Does not cause any RF activity and does not block. + * + * @return DSF ID bytes + */ + public byte getDsfId() { + return mDsfId; + } + + /** + * Send raw NFC-V commands to the tag and receive the response. + * + *

      Applications must not append the CRC to the payload, + * it will be automatically calculated. The application does + * provide FLAGS, CMD and PARAMETER bytes. + * + *

      Use {@link #getMaxTransceiveLength} to retrieve the maximum amount of bytes + * that can be sent with {@link #transceive}. + * + *

      This is an I/O operation and will block until complete. It must + * not be called from the main application thread. A blocked call will be canceled with + * {@link IOException} if {@link #close} is called from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param data bytes to send + * @return bytes received in response + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or this operation is canceled + */ + public byte[] transceive(byte[] data) throws IOException { + return transceive(data, true); + } + + + /** + * Return the maximum number of bytes that can be sent with {@link #transceive}. + * @return the maximum number of bytes that can be sent with {@link #transceive}. + */ + public int getMaxTransceiveLength() { + return getMaxTransceiveLengthInternal(); + } +} diff --git a/nfc/java/android/nfc/tech/OWNERS b/nfc/java/android/nfc/tech/OWNERS new file mode 100644 index 000000000000..35e9713f5715 --- /dev/null +++ b/nfc/java/android/nfc/tech/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 48448 +include platform/packages/apps/Nfc:/OWNERS diff --git a/nfc/java/android/nfc/tech/TagTechnology.java b/nfc/java/android/nfc/tech/TagTechnology.java new file mode 100644 index 000000000000..839fe429b338 --- /dev/null +++ b/nfc/java/android/nfc/tech/TagTechnology.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.tech; + +import android.nfc.Tag; + +import java.io.Closeable; +import java.io.IOException; + +/** + * {@link TagTechnology} is an interface to a technology in a {@link Tag}. + *

      + * Obtain a {@link TagTechnology} implementation by calling the static method get() + * on the implementation class. + *

      + * NFC tags are based on a number of independently developed technologies and offer a + * wide range of capabilities. The + * {@link TagTechnology} implementations provide access to these different + * technologies and capabilities. Some sub-classes map to technology + * specification (for example {@link NfcA}, {@link IsoDep}, others map to + * pseudo-technologies or capabilities (for example {@link Ndef}, {@link NdefFormatable}). + *

      + * It is mandatory for all Android NFC devices to provide the following + * {@link TagTechnology} implementations. + *

        + *
      • {@link NfcA} (also known as ISO 14443-3A) + *
      • {@link NfcB} (also known as ISO 14443-3B) + *
      • {@link NfcF} (also known as JIS 6319-4) + *
      • {@link NfcV} (also known as ISO 15693) + *
      • {@link IsoDep} + *
      • {@link Ndef} on NFC Forum Type 1, Type 2, Type 3 or Type 4 compliant tags + *
      + * It is optional for Android NFC devices to provide the following + * {@link TagTechnology} implementations. If it is not provided, the + * Android device will never enumerate that class via {@link Tag#getTechList}. + *
        + *
      • {@link MifareClassic} + *
      • {@link MifareUltralight} + *
      • {@link NfcBarcode} + *
      • {@link NdefFormatable} must only be enumerated on tags for which this Android device + * is capable of formatting. Proprietary knowledge is often required to format a tag + * to make it NDEF compatible. + *
      + *

      + * {@link TagTechnology} implementations provide methods that fall into two classes: + * cached getters and I/O operations. + *

      Cached getters

      + * These methods (usually prefixed by get or is) return + * properties of the tag, as determined at discovery time. These methods will never + * block or cause RF activity, and do not require {@link #connect} to have been called. + * They also never update, for example if a property is changed by an I/O operation with a tag + * then the cached getter will still return the result from tag discovery time. + *

      I/O operations

      + * I/O operations may require RF activity, and may block. They have the following semantics. + *
        + *
      • {@link #connect} must be called before using any other I/O operation. + *
      • {@link #close} must be called after completing I/O operations with a + * {@link TagTechnology}, and it will cancel all other blocked I/O operations on other threads + * (including {@link #connect} with {@link IOException}. + *
      • Only one {@link TagTechnology} can be connected at a time. Other calls to + * {@link #connect} will return {@link IOException}. + *
      • I/O operations may block, and should never be called on the main application + * thread. + *
      + * + *

      Note: Methods that perform I/O operations + * require the {@link android.Manifest.permission#NFC} permission. + */ +public interface TagTechnology extends Closeable { + /** + * This technology is an instance of {@link NfcA}. + *

      Support for this technology type is mandatory. + * @hide + */ + public static final int NFC_A = 1; + + /** + * This technology is an instance of {@link NfcB}. + *

      Support for this technology type is mandatory. + * @hide + */ + public static final int NFC_B = 2; + + /** + * This technology is an instance of {@link IsoDep}. + *

      Support for this technology type is mandatory. + * @hide + */ + public static final int ISO_DEP = 3; + + /** + * This technology is an instance of {@link NfcF}. + *

      Support for this technology type is mandatory. + * @hide + */ + public static final int NFC_F = 4; + + /** + * This technology is an instance of {@link NfcV}. + *

      Support for this technology type is mandatory. + * @hide + */ + public static final int NFC_V = 5; + + /** + * This technology is an instance of {@link Ndef}. + *

      Support for this technology type is mandatory. + * @hide + */ + public static final int NDEF = 6; + + /** + * This technology is an instance of {@link NdefFormatable}. + *

      Support for this technology type is mandatory. + * @hide + */ + public static final int NDEF_FORMATABLE = 7; + + /** + * This technology is an instance of {@link MifareClassic}. + *

      Support for this technology type is optional. If a stack doesn't support this technology + * type tags using it must still be discovered and present the lower level radio interface + * technologies in use. + * @hide + */ + public static final int MIFARE_CLASSIC = 8; + + /** + * This technology is an instance of {@link MifareUltralight}. + *

      Support for this technology type is optional. If a stack doesn't support this technology + * type tags using it must still be discovered and present the lower level radio interface + * technologies in use. + * @hide + */ + public static final int MIFARE_ULTRALIGHT = 9; + + /** + * This technology is an instance of {@link NfcBarcode}. + *

      Support for this technology type is optional. If a stack doesn't support this technology + * type tags using it must still be discovered and present the lower level radio interface + * technologies in use. + * @hide + */ + public static final int NFC_BARCODE = 10; + + /** + * Get the {@link Tag} object backing this {@link TagTechnology} object. + * @return the {@link Tag} backing this {@link TagTechnology} object. + */ + public Tag getTag(); + + /** + * Enable I/O operations to the tag from this {@link TagTechnology} object. + *

      May cause RF activity and may block. Must not be called + * from the main application thread. A blocked call will be canceled with + * {@link IOException} by calling {@link #close} from another thread. + *

      Only one {@link TagTechnology} object can be connected to a {@link Tag} at a time. + *

      Applications must call {@link #close} when I/O operations are complete. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @see #close() + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or connect is canceled + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public void connect() throws IOException; + + /** + * Re-connect to the {@link Tag} associated with this connection. Reconnecting to a tag can be + * used to reset the state of the tag itself. + * + *

      May cause RF activity and may block. Must not be called + * from the main application thread. A blocked call will be canceled with + * {@link IOException} by calling {@link #close} from another thread. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @see #connect() + * @see #close() + * @throws TagLostException if the tag leaves the field + * @throws IOException if there is an I/O failure, or connect is canceled + * @throws SecurityException if the tag object is reused after the tag has left the field + * @hide + */ + public void reconnect() throws IOException; + + /** + * Disable I/O operations to the tag from this {@link TagTechnology} object, and release resources. + *

      Also causes all blocked I/O operations on other thread to be canceled and + * return with {@link IOException}. + * + *

      Requires the {@link android.Manifest.permission#NFC} permission. + * + * @see #connect() + * @throws SecurityException if the tag object is reused after the tag has left the field + */ + public void close() throws IOException; + + /** + * Helper to indicate if I/O operations should be possible. + * + *

      Returns true if {@link #connect} has completed, and {@link #close} has not been + * called, and the {@link Tag} is not known to be out of range. + *

      Does not cause RF activity, and does not block. + * + * @return true if I/O operations should be possible + */ + public boolean isConnected(); +} diff --git a/nfc/java/android/nfc/tech/package.html b/nfc/java/android/nfc/tech/package.html new file mode 100644 index 000000000000..a99828f90c5b --- /dev/null +++ b/nfc/java/android/nfc/tech/package.html @@ -0,0 +1,13 @@ + + +

      +These classes provide access to a tag technology's features, which vary by the type +of tag that is scanned. A scanned tag can support multiple technologies, and you can find +out what they are by calling {@link android.nfc.Tag#getTechList getTechList()}.

      + +

      For more information on dealing with tag technologies and handling the ones that you care about, see +The Tag Dispatch System. +The {@link android.nfc.tech.TagTechnology} interface provides an overview of the +supported technologies.

      + + -- cgit v1.2.3-59-g8ed1b