diff options
| -rw-r--r-- | core/api/system-current.txt | 3 | ||||
| -rw-r--r-- | core/res/res/values/attrs.xml | 15 | ||||
| -rw-r--r-- | nfc/api/current.txt | 3 | ||||
| -rw-r--r-- | nfc/java/android/nfc/INfcCardEmulation.aidl | 3 | ||||
| -rw-r--r-- | nfc/java/android/nfc/cardemulation/ApduServiceInfo.java | 81 | ||||
| -rw-r--r-- | nfc/java/android/nfc/cardemulation/CardEmulation.java | 147 |
6 files changed, 244 insertions, 8 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 9039bf169f8b..97d69f5c4176 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -10438,6 +10438,7 @@ package android.nfc.cardemulation { @FlaggedApi("android.nfc.enable_nfc_mainline") public final class ApduServiceInfo implements android.os.Parcelable { ctor @FlaggedApi("android.nfc.enable_nfc_mainline") public ApduServiceInfo(@NonNull android.content.pm.PackageManager, @NonNull android.content.pm.ResolveInfo, boolean) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void addPollingLoopFilter(@NonNull String, boolean); + method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void addPollingLoopPatternFilter(@NonNull String, boolean); method @FlaggedApi("android.nfc.enable_nfc_mainline") public int describeContents(); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dump(@NonNull android.os.ParcelFileDescriptor, @NonNull java.io.PrintWriter, @NonNull String[]); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dumpDebug(@NonNull android.util.proto.ProtoOutputStream); @@ -10449,6 +10450,7 @@ package android.nfc.cardemulation { method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public android.nfc.cardemulation.AidGroup getDynamicAidGroupForCategory(@NonNull String); method @FlaggedApi("android.nfc.enable_nfc_mainline") @Nullable public String getOffHostSecureElement(); method @FlaggedApi("android.nfc.nfc_read_polling_loop") @NonNull public java.util.List<java.lang.String> getPollingLoopFilters(); + method @FlaggedApi("android.nfc.nfc_read_polling_loop") @NonNull public java.util.List<java.util.regex.Pattern> getPollingLoopPatternFilters(); method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<java.lang.String> getPrefixAids(); method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public String getSettingsActivityName(); method @FlaggedApi("android.nfc.nfc_read_polling_loop") public boolean getShouldAutoTransact(@NonNull String); @@ -10463,6 +10465,7 @@ package android.nfc.cardemulation { method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public CharSequence loadLabel(@NonNull android.content.pm.PackageManager); method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public boolean removeDynamicAidGroupForCategory(@NonNull String); method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void removePollingLoopFilter(@NonNull String); + method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void removePollingLoopPatternFilter(@NonNull String); method @FlaggedApi("android.nfc.enable_nfc_mainline") public boolean requiresScreenOn(); method @FlaggedApi("android.nfc.enable_nfc_mainline") public boolean requiresUnlock(); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void resetOffHostSecureElement(); diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index c77ee4326471..81c79f23af46 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -4484,6 +4484,21 @@ <attr name="autoTransact" format="boolean"/> </declare-styleable> + <!-- Specify one or more <code>polling-loop-pattern-filter</code> elements inside a + <code>host-apdu-service</code> to indicate polling loop frames that + your service can handle. --> + <!-- @FlaggedApi("android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP") --> + <declare-styleable name="PollingLoopPatternFilter"> + <!-- The patter to match polling loop frames to, must to be compatible with + {@link java.util.regex.Pattern#compile(String)} and only contain hexadecimal numbers and + `.`, `?` and `*` operators. This attribute is mandatory. --> + <attr name="name" /> + <!-- Whether or not the system should automatically start a transaction when this polling + loop filter matches. If not set, default value is false. --> + <!-- @FlaggedApi("android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP") --> + <attr name="autoTransact" format="boolean"/> + </declare-styleable> + <!-- Use <code>host-nfcf-service</code> as the root tag of the XML resource that describes an {@link android.nfc.cardemulation.HostNfcFService} service, which is referenced from its {@link android.nfc.cardemulation.HostNfcFService#SERVICE_META_DATA} diff --git a/nfc/api/current.txt b/nfc/api/current.txt index 9e0bb86f46a5..da292a818396 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -205,7 +205,10 @@ package android.nfc.cardemulation { method public boolean isDefaultServiceForCategory(android.content.ComponentName, String); method public boolean registerAidsForService(android.content.ComponentName, String, java.util.List<java.lang.String>); method @FlaggedApi("android.nfc.nfc_read_polling_loop") public boolean registerPollingLoopFilterForService(@NonNull android.content.ComponentName, @NonNull String, boolean); + method @FlaggedApi("android.nfc.nfc_read_polling_loop") public boolean registerPollingLoopPatternFilterForService(@NonNull android.content.ComponentName, @NonNull String, boolean); method public boolean removeAidsForService(android.content.ComponentName, String); + method @FlaggedApi("android.nfc.nfc_read_polling_loop") public boolean removePollingLoopFilterForService(@NonNull android.content.ComponentName, @NonNull String); + method @FlaggedApi("android.nfc.nfc_read_polling_loop") public boolean removePollingLoopPatternFilterForService(@NonNull android.content.ComponentName, @NonNull String); method @NonNull @RequiresPermission(android.Manifest.permission.NFC) public boolean setOffHostForService(@NonNull android.content.ComponentName, @NonNull String); method public boolean setPreferredService(android.app.Activity, android.content.ComponentName); method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean setShouldDefaultToObserveModeForService(@NonNull android.content.ComponentName, boolean); diff --git a/nfc/java/android/nfc/INfcCardEmulation.aidl b/nfc/java/android/nfc/INfcCardEmulation.aidl index 85a07b74871b..cb97f23e813c 100644 --- a/nfc/java/android/nfc/INfcCardEmulation.aidl +++ b/nfc/java/android/nfc/INfcCardEmulation.aidl @@ -33,10 +33,13 @@ interface INfcCardEmulation boolean setShouldDefaultToObserveModeForService(int userId, in android.content.ComponentName service, boolean enable); boolean registerAidGroupForService(int userHandle, in ComponentName service, in AidGroup aidGroup); boolean registerPollingLoopFilterForService(int userHandle, in ComponentName service, in String pollingLoopFilter, boolean autoTransact); + boolean registerPollingLoopPatternFilterForService(int userHandle, in ComponentName service, in String pollingLoopPatternFilter, boolean autoTransact); 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); + boolean removePollingLoopFilterForService(int userHandle, in ComponentName service, in String pollingLoopFilter); + boolean removePollingLoopPatternFilterForService(int userHandle, in ComponentName service, in String pollingLoopPatternFilter); List<ApduServiceInfo> getServices(int userHandle, in String category); boolean setPreferredService(in ComponentName service); boolean unsetPreferredService(); diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java index 2c7d61eea777..be3c24806c5b 100644 --- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -108,6 +108,8 @@ public final class ApduServiceInfo implements Parcelable { private final Map<String, Boolean> mAutoTransact; + private final Map<Pattern, Boolean> mAutoTransactPatterns; + /** * Whether this service should only be started when the device is unlocked. */ @@ -179,7 +181,7 @@ public final class ApduServiceInfo implements Parcelable { this(info, onHost, description, staticAidGroups, dynamicAidGroups, requiresUnlock, requiresScreenOn, bannerResource, uid, settingsActivityName, offHost, staticOffHost, isEnabled, - new HashMap<String, Boolean>()); + new HashMap<String, Boolean>(), new HashMap<Pattern, Boolean>()); } /** @@ -189,12 +191,13 @@ public final class ApduServiceInfo implements Parcelable { List<AidGroup> staticAidGroups, List<AidGroup> dynamicAidGroups, boolean requiresUnlock, boolean requiresScreenOn, int bannerResource, int uid, String settingsActivityName, String offHost, String staticOffHost, boolean isEnabled, - HashMap<String, Boolean> autoTransact) { + Map<String, Boolean> autoTransact, Map<Pattern, Boolean> autoTransactPatterns) { this.mService = info; this.mDescription = description; this.mStaticAidGroups = new HashMap<String, AidGroup>(); this.mDynamicAidGroups = new HashMap<String, AidGroup>(); this.mAutoTransact = autoTransact; + this.mAutoTransactPatterns = autoTransactPatterns; this.mOffHostName = offHost; this.mStaticOffHostName = staticOffHost; this.mOnHost = onHost; @@ -314,6 +317,7 @@ public final class ApduServiceInfo implements Parcelable { mStaticAidGroups = new HashMap<String, AidGroup>(); mDynamicAidGroups = new HashMap<String, AidGroup>(); mAutoTransact = new HashMap<String, Boolean>(); + mAutoTransactPatterns = new HashMap<Pattern, Boolean>(); mOnHost = onHost; final int depth = parser.getDepth(); @@ -408,6 +412,18 @@ public final class ApduServiceInfo implements Parcelable { false); mAutoTransact.put(plf, autoTransact); a.recycle(); + } else if (eventType == XmlPullParser.START_TAG + && "polling-loop-pattern-filter".equals(tagName) && currentGroup == null) { + final TypedArray a = res.obtainAttributes(attrs, + com.android.internal.R.styleable.PollingLoopPatternFilter); + String plf = a.getString( + com.android.internal.R.styleable.PollingLoopPatternFilter_name) + .toUpperCase(Locale.ROOT); + boolean autoTransact = a.getBoolean( + com.android.internal.R.styleable.PollingLoopFilter_autoTransact, + false); + mAutoTransactPatterns.put(Pattern.compile(plf), autoTransact); + a.recycle(); } } } catch (NameNotFoundException e) { @@ -481,7 +497,30 @@ public final class ApduServiceInfo implements Parcelable { */ @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public boolean getShouldAutoTransact(@NonNull String plf) { - return mAutoTransact.getOrDefault(plf.toUpperCase(Locale.ROOT), false); + if (mAutoTransact.getOrDefault(plf.toUpperCase(Locale.ROOT), false)) { + return true; + } + List<Pattern> patternMatches = mAutoTransactPatterns.keySet().stream() + .filter(p -> p.matcher(plf).matches()).toList(); + if (patternMatches == null || patternMatches.size() == 0) { + return false; + } + for (Pattern patternMatch : patternMatches) { + if (mAutoTransactPatterns.get(patternMatch)) { + return true; + } + } + return false; + } + + /** + * Returns the current polling loop pattern filters for this service. + * @return List of polling loop pattern filters. + */ + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + @NonNull + public List<Pattern> getPollingLoopPatternFilters() { + return new ArrayList<>(mAutoTransactPatterns.keySet()); } /** @@ -683,7 +722,7 @@ public final class ApduServiceInfo implements Parcelable { * Add a Polling Loop Filter. Custom NFC polling frames that match this filter will be * delivered to {@link HostApduService#processPollingFrames(List)}. Adding a key with this * multiple times will cause the value to be overwritten each time. - * @param pollingLoopFilter the polling loop filter to add, must be a valide hexadecimal string + * @param pollingLoopFilter the polling loop filter to add, must be a valid hexadecimal string */ @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public void addPollingLoopFilter(@NonNull String pollingLoopFilter, @@ -703,6 +742,31 @@ public final class ApduServiceInfo implements Parcelable { } /** + * Add a Polling Loop Pattern Filter. Custom NFC polling frames that match this filter will be + * delivered to {@link HostApduService#processPollingFrames(List)}. Adding a key with this + * multiple times will cause the value to be overwritten each time. + * @param pollingLoopPatternFilter the polling loop pattern filter to add, must be a valid + * regex to match a hexadecimal string + */ + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + public void addPollingLoopPatternFilter(@NonNull String pollingLoopPatternFilter, + boolean autoTransact) { + mAutoTransactPatterns.put(Pattern.compile(pollingLoopPatternFilter), autoTransact); + + } + + /** + * Remove a Polling Loop Pattern Filter. Custom NFC polling frames that match this filter will + * no longer be delivered to {@link HostApduService#processPollingFrames(List)}. + * @param pollingLoopPatternFilter this polling loop filter to add. + */ + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + public void removePollingLoopPatternFilter(@NonNull String pollingLoopPatternFilter) { + mAutoTransactPatterns.remove( + Pattern.compile(pollingLoopPatternFilter.toUpperCase(Locale.ROOT))); + } + + /** * 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 @@ -856,6 +920,8 @@ public final class ApduServiceInfo implements Parcelable { dest.writeInt(mCategoryOtherServiceEnabled ? 1 : 0); dest.writeInt(mAutoTransact.size()); dest.writeMap(mAutoTransact); + dest.writeInt(mAutoTransactPatterns.size()); + dest.writeMap(mAutoTransactPatterns); }; @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @@ -889,10 +955,15 @@ public final class ApduServiceInfo implements Parcelable { new HashMap<String, Boolean>(autoTransactSize); source.readMap(autoTransact, getClass().getClassLoader(), String.class, Boolean.class); + int autoTransactPatternSize = source.readInt(); + HashMap<Pattern, Boolean> autoTransactPatterns = + new HashMap<Pattern, Boolean>(autoTransactSize); + source.readMap(autoTransactPatterns, getClass().getClassLoader(), + Pattern.class, Boolean.class); return new ApduServiceInfo(info, onHost, description, staticAidGroups, dynamicAidGroups, requiresUnlock, requiresScreenOn, bannerResource, uid, settingsActivityName, offHostName, staticOffHostName, - isEnabled, autoTransact); + isEnabled, autoTransact, autoTransactPatterns); } @Override diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java index e55f5403ed83..67697a429a32 100644 --- a/nfc/java/android/nfc/cardemulation/CardEmulation.java +++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java @@ -45,6 +45,7 @@ import android.util.Log; import java.util.HashMap; import java.util.HexFormat; import java.util.List; +import java.util.Locale; import java.util.regex.Pattern; /** @@ -61,6 +62,7 @@ import java.util.regex.Pattern; */ public final class CardEmulation { private static final Pattern AID_PATTERN = Pattern.compile("[0-9A-Fa-f]{10,32}\\*?\\#?"); + private static final Pattern PLPF_PATTERN = Pattern.compile("[0-9A-Fa-f,\\?,\\*\\.]*"); static final String TAG = "CardEmulation"; @@ -379,9 +381,9 @@ public final class CardEmulation { * auto-transact or not. The PLF can be sequence of an * even number of at least 2 hexadecimal numbers (0-9, A-F or a-f), representing a series of * bytes. When non-standard polling loop frame matches this sequence exactly, it may be - * delivered to {@link HostApduService#processPollingFrames(List)}. If auto-transact is set to - * true, then observe mode will also be disabled. if this service is currently preferred or - * there are no other services registered for this filter. + * delivered to {@link HostApduService#processPollingFrames(List)}. If auto-transact + * is set to true and this service is currently preferred or there are no other services + * registered for this filter then observe mode will also be disabled. * @param service The HostApduService to register the filter for * @param pollingLoopFilter The filter to register * @param autoTransact true to have the NFC stack automatically disable observe mode and allow @@ -416,6 +418,128 @@ public final class CardEmulation { } /** + * Unregister a polling loop filter (PLF) for a HostApduService. If the PLF had previously been + * registered via {@link #registerPollingLoopFilterForService(ComponentName, String, boolean)} + * for this service it will be removed. + * @param service The HostApduService to unregister the filter for + * @param pollingLoopFilter The filter to unregister + * @return true if the filter was removed, false otherwise + * @throws IllegalArgumentException if the passed in string doesn't parse to at least one byte + */ + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + public boolean removePollingLoopFilterForService(@NonNull ComponentName service, + @NonNull String pollingLoopFilter) { + pollingLoopFilter = validatePollingLoopFilter(pollingLoopFilter); + + try { + return sService.removePollingLoopFilterForService(mContext.getUser().getIdentifier(), + service, pollingLoopFilter); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.removePollingLoopFilterForService( + mContext.getUser().getIdentifier(), service, + pollingLoopFilter); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + + /** + * Register a polling loop pattern filter (PLPF) for a HostApduService and indicate whether it + * should auto-transact or not. The pattern may include the characters 0-9 and A-F as well as + * the regular expression operators `.`, `?` and `*`. When the beginning of anon-standard + * polling loop frame matches this sequence exactly, it may be delivered to + * {@link HostApduService#processPollingFrames(List)}. If auto-transact is set to true and this + * service is currently preferred or there are no other services registered for this filter + * then observe mode will also be disabled. + * @param service The HostApduService to register the filter for + * @param pollingLoopPatternFilter The pattern filter to register, must to be compatible with + * {@link java.util.regex.Pattern#compile(String)} and only contain hexadecimal numbers + * and `.`, `?` and `*` operators + * @param autoTransact true to have the NFC stack automatically disable observe mode and allow + * transactions to proceed when this filter matches, false otherwise + * @return true if the filter was registered, false otherwise + * @throws IllegalArgumentException if the filter containst elements other than hexadecimal + * numbers and `.`, `?` and `*` operators + * @throws java.util.regex.PatternSyntaxException if the regex syntax is invalid + */ + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + public boolean registerPollingLoopPatternFilterForService(@NonNull ComponentName service, + @NonNull String pollingLoopPatternFilter, boolean autoTransact) { + pollingLoopPatternFilter = validatePollingLoopPatternFilter(pollingLoopPatternFilter); + + try { + return sService.registerPollingLoopPatternFilterForService( + mContext.getUser().getIdentifier(), + service, pollingLoopPatternFilter, autoTransact); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.registerPollingLoopPatternFilterForService( + mContext.getUser().getIdentifier(), service, + pollingLoopPatternFilter, autoTransact); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Unregister a polling loop pattern filter (PLPF) for a HostApduService. If the PLF had + * previously been registered via + * {@link #registerPollingLoopFilterForService(ComponentName, String, boolean)} for this + * service it will be removed. + * @param service The HostApduService to unregister the filter for + * @param pollingLoopPatternFilter The filter to unregister, must to be compatible with + * {@link java.util.regex.Pattern#compile(String)} and only contain hexadecimal numbers + * and`.`, `?` and `*` operators + * @return true if the filter was removed, false otherwise + * @throws IllegalArgumentException if the filter containst elements other than hexadecimal + * numbers and `.`, `?` and `*` operators + * @throws java.util.regex.PatternSyntaxException if the regex syntax is invalid + */ + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + public boolean removePollingLoopPatternFilterForService(@NonNull ComponentName service, + @NonNull String pollingLoopPatternFilter) { + pollingLoopPatternFilter = validatePollingLoopPatternFilter(pollingLoopPatternFilter); + + try { + return sService.removePollingLoopPatternFilterForService( + mContext.getUser().getIdentifier(), service, pollingLoopPatternFilter); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.removePollingLoopPatternFilterForService( + mContext.getUser().getIdentifier(), service, + pollingLoopPatternFilter); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** * Registers a list of AIDs for a specific category for the * specified service. * @@ -1027,6 +1151,23 @@ public final class CardEmulation { } /** + * Tests the validity of the polling loop pattern filter. + * @param pollingLoopPatternFilter The polling loop filter to test. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) + public static @NonNull String validatePollingLoopPatternFilter( + @NonNull String pollingLoopPatternFilter) { + // Verify hex characters + if (!PLPF_PATTERN.matcher(pollingLoopPatternFilter).matches()) { + throw new IllegalArgumentException( + "Polling loop pattern filters may only contain hexadecimal numbers, ?s and *s"); + } + return Pattern.compile(pollingLoopPatternFilter.toUpperCase(Locale.ROOT)).toString(); + } + + /** * A valid AID according to ISO/IEC 7816-4: * <ul> * <li>Has >= 5 bytes and <=16 bytes (>=10 hex chars and <= 32 hex chars) |