1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.nfc.cardemulation.util; 17 18 import static com.android.nfc.NfcStatsLog.NFC_POLLING_LOOP_NOTIFICATION_REPORTED__PROPRIETARY_FRAME_TYPE__ECP_V1; 19 import static com.android.nfc.NfcStatsLog.NFC_POLLING_LOOP_NOTIFICATION_REPORTED__PROPRIETARY_FRAME_TYPE__ECP_V2; 20 import static com.android.nfc.NfcStatsLog.NFC_POLLING_LOOP_NOTIFICATION_REPORTED__PROPRIETARY_FRAME_TYPE__PROPRIETARY_FRAME_UNKNOWN; 21 22 import android.annotation.FlaggedApi; 23 import android.annotation.NonNull; 24 import android.nfc.cardemulation.CardEmulation; 25 import android.nfc.cardemulation.PollingFrame; 26 import android.os.SystemClock; 27 import android.sysprop.NfcProperties; 28 import android.util.Log; 29 30 import com.android.nfc.NfcStatsLog; 31 import com.android.nfc.flags.Flags; 32 33 import java.util.HashMap; 34 import java.util.Objects; 35 36 @FlaggedApi(Flags.FLAG_STATSD_CE_EVENTS_FLAG) 37 public class StatsdUtils { 38 static final boolean DBG = NfcProperties.debug_enabled().orElse(true); 39 private final String TAG = "StatsdUtils"; 40 41 public static final String SE_NAME_HCE = "HCE"; 42 public static final String SE_NAME_HCEF = "HCEF"; 43 44 /** Wrappers for Category values */ 45 public static final int CE_UNKNOWN = 46 NfcStatsLog.NFC_CARDEMULATION_OCCURRED__CATEGORY__UNKNOWN; 47 /** Successful cases */ 48 public static final int CE_HCE_PAYMENT = 49 NfcStatsLog.NFC_CARDEMULATION_OCCURRED__CATEGORY__HCE_PAYMENT; 50 public static final int CE_HCE_OTHER = 51 NfcStatsLog.NFC_CARDEMULATION_OCCURRED__CATEGORY__HCE_OTHER; 52 public static final int CE_OFFHOST = 53 NfcStatsLog.NFC_CARDEMULATION_OCCURRED__CATEGORY__OFFHOST; 54 public static final int CE_OFFHOST_PAYMENT = 55 NfcStatsLog.NFC_CARDEMULATION_OCCURRED__CATEGORY__OFFHOST_PAYMENT; 56 public static final int CE_OFFHOST_OTHER = 57 NfcStatsLog.NFC_CARDEMULATION_OCCURRED__CATEGORY__OFFHOST_OTHER; 58 /** NO_ROUTING */ 59 public static final int CE_NO_ROUTING = 60 NfcStatsLog.NFC_CARDEMULATION_OCCURRED__CATEGORY__FAILED_NO_ROUTING; 61 /** WRONG_SETTING */ 62 public static final int CE_PAYMENT_WRONG_SETTING = 63 NfcStatsLog.NFC_CARDEMULATION_OCCURRED__CATEGORY__FAILED_HCE_PAYMENT_WRONG_SETTING; 64 public static final int CE_OTHER_WRONG_SETTING = 65 NfcStatsLog.NFC_CARDEMULATION_OCCURRED__CATEGORY__FAILED_HCE_OTHER_WRONG_SETTING; 66 /** DISCONNECTED_BEFORE_BOUND */ 67 public static final int CE_PAYMENT_DC_BOUND = NfcStatsLog 68 .NFC_CARDEMULATION_OCCURRED__CATEGORY__FAILED_HCE_PAYMENT_DISCONNECTED_BEFORE_BOUND; 69 public static final int CE_OTHER_DC_BOUND = NfcStatsLog 70 .NFC_CARDEMULATION_OCCURRED__CATEGORY__FAILED_HCE_OTHER_DISCONNECTED_BEFORE_BOUND; 71 /** DISCONNECTED_BEFORE_RESPONSE */ 72 public static final int CE_PAYMENT_DC_RESPONSE = NfcStatsLog 73 .NFC_CARDEMULATION_OCCURRED__CATEGORY__FAILED_HCE_PAYMENT_DISCONNECTED_BEFORE_RESPONSE; 74 public static final int CE_OTHER_DC_RESPONSE = NfcStatsLog 75 .NFC_CARDEMULATION_OCCURRED__CATEGORY__FAILED_HCE_OTHER_DISCONNECTED_BEFORE_RESPONSE; 76 /** Wrappers for Category values */ 77 78 public static final int TRIGGER_SOURCE_UNKNOWN = 79 NfcStatsLog.NFC_OBSERVE_MODE_STATE_CHANGED__TRIGGER_SOURCE__TRIGGER_SOURCE_UNKNOWN; 80 public static final int TRIGGER_SOURCE_WALLET_ROLE_HOLDER = 81 NfcStatsLog.NFC_OBSERVE_MODE_STATE_CHANGED__TRIGGER_SOURCE__WALLET_ROLE_HOLDER; 82 public static final int TRIGGER_SOURCE_FOREGROUND_APP = 83 NfcStatsLog.NFC_OBSERVE_MODE_STATE_CHANGED__TRIGGER_SOURCE__FOREGROUND_APP; 84 public static final int TRIGGER_SOURCE_AUTO_TRANSACT = 85 NfcStatsLog.NFC_OBSERVE_MODE_STATE_CHANGED__TRIGGER_SOURCE__AUTO_TRANSACT; 86 87 public static final int PROCESSOR_UNKNOWN = 88 NfcStatsLog.NFC_AUTO_TRANSACT_REPORTED__AUTO_TRANSACT_PROCESSOR__PROCESSOR_UNKNOWN; 89 public static final int PROCESSOR_HOST = 90 NfcStatsLog.NFC_AUTO_TRANSACT_REPORTED__AUTO_TRANSACT_PROCESSOR__HOST; 91 public static final int PROCESSOR_NFCC = 92 NfcStatsLog.NFC_AUTO_TRANSACT_REPORTED__AUTO_TRANSACT_PROCESSOR__NFCC; 93 94 /** Name of SE terminal to log in statsd */ 95 private String mSeName = ""; 96 /** Timestamp in millis when app binding starts */ 97 private long mBindingStartTimeMillis = 0; 98 /** Flag to indicate that the service has not sent the first response */ 99 private boolean mWaitingForFirstResponse = false; 100 /** Current transaction's category to log in statsd */ 101 private String mTransactionCategory = CardEmulation.EXTRA_CATEGORY; 102 /** Current transaction's uid to log in statsd */ 103 private int mTransactionUid = -1; 104 /** Shared context between instances of StatsdUtils */ 105 @NonNull 106 private StatsdUtilsContext mStatsdUtilsContext; 107 108 private static final byte FRAME_HEADER_ECP = 0x6A; 109 private static final byte FRAME_ECP_V1 = 0x01; 110 private static final byte FRAME_ECP_V2 = 0x02; 111 private static final int FRAME_ECP_MIN_SIZE = 5; 112 113 private static final int NO_GAIN_INFORMATION = -1; 114 private int mLastGainLevel = NO_GAIN_INFORMATION; 115 116 /** Result constants for statsd usage */ 117 static enum StatsdResult { 118 SUCCESS, 119 NO_ROUTING_FOR_AID, 120 WRONG_APP_AND_DEVICE_SETTINGS, 121 DISCONNECTED_BEFORE_BOUND, 122 DISCONNECTED_BEFORE_RESPONSE 123 } 124 StatsdUtils(String seName, @NonNull StatsdUtilsContext statsdUtilsContext)125 public StatsdUtils(String seName, @NonNull StatsdUtilsContext statsdUtilsContext) { 126 this(statsdUtilsContext); 127 mSeName = seName; 128 129 // HCEF has no category, default it to PAYMENT category to record every call 130 if (seName.equals(SE_NAME_HCEF)) mTransactionCategory = CardEmulation.CATEGORY_PAYMENT; 131 } 132 StatsdUtils(@onNull StatsdUtilsContext statsdUtilsContext)133 public StatsdUtils(@NonNull StatsdUtilsContext statsdUtilsContext) { 134 mStatsdUtilsContext = statsdUtilsContext; 135 } 136 resetCardEmulationEvent()137 private void resetCardEmulationEvent() { 138 // Reset mTransactionCategory value to prevent accidental triggers in general 139 // except for HCEF, which is always intentional because it only works in foreground 140 if (!mSeName.equals(SE_NAME_HCEF)) mTransactionCategory = CardEmulation.EXTRA_CATEGORY; 141 mBindingStartTimeMillis = 0; 142 mWaitingForFirstResponse = false; 143 mTransactionUid = -1; 144 } 145 getCardEmulationStatsdCategory( StatsdResult transactionResult, String transactionCategory)146 private int getCardEmulationStatsdCategory( 147 StatsdResult transactionResult, String transactionCategory) { 148 switch (transactionResult) { 149 case SUCCESS: 150 switch (transactionCategory) { 151 case CardEmulation.CATEGORY_PAYMENT: 152 return CE_HCE_PAYMENT; 153 case CardEmulation.CATEGORY_OTHER: 154 return CE_HCE_OTHER; 155 default: 156 return CE_UNKNOWN; 157 } 158 159 case NO_ROUTING_FOR_AID: 160 return CE_NO_ROUTING; 161 162 case WRONG_APP_AND_DEVICE_SETTINGS: 163 switch (transactionCategory) { 164 case CardEmulation.CATEGORY_PAYMENT: 165 return CE_PAYMENT_WRONG_SETTING; 166 case CardEmulation.CATEGORY_OTHER: 167 return CE_OTHER_WRONG_SETTING; 168 default: 169 return CE_UNKNOWN; 170 } 171 172 case DISCONNECTED_BEFORE_BOUND: 173 switch (transactionCategory) { 174 case CardEmulation.CATEGORY_PAYMENT: 175 return CE_PAYMENT_DC_BOUND; 176 case CardEmulation.CATEGORY_OTHER: 177 return CE_OTHER_DC_BOUND; 178 default: 179 return CE_UNKNOWN; 180 } 181 182 case DISCONNECTED_BEFORE_RESPONSE: 183 switch (transactionCategory) { 184 case CardEmulation.CATEGORY_PAYMENT: 185 return CE_PAYMENT_DC_RESPONSE; 186 case CardEmulation.CATEGORY_OTHER: 187 return CE_OTHER_DC_RESPONSE; 188 default: 189 return CE_UNKNOWN; 190 } 191 } 192 return CE_UNKNOWN; 193 } 194 logCardEmulationEvent(int statsdCategory)195 void logCardEmulationEvent(int statsdCategory) { 196 NfcStatsLog.write( 197 NfcStatsLog.NFC_CARDEMULATION_OCCURRED, statsdCategory, mSeName, mTransactionUid); 198 resetCardEmulationEvent(); 199 } 200 logErrorEvent(int errorType, int nciCmd, int ntfStatusCode)201 public void logErrorEvent(int errorType, int nciCmd, int ntfStatusCode) { 202 NfcStatsLog.write(NfcStatsLog.NFC_ERROR_OCCURRED, errorType, nciCmd, ntfStatusCode); 203 } 204 logErrorEvent(int errorType)205 public void logErrorEvent(int errorType) { 206 logErrorEvent(errorType, 0, 0); 207 } 208 setCardEmulationEventCategory(String category)209 public void setCardEmulationEventCategory(String category) { 210 mTransactionCategory = category; 211 } 212 setCardEmulationEventUid(int uid)213 public void setCardEmulationEventUid(int uid) { 214 mTransactionUid = uid; 215 } 216 notifyCardEmulationEventWaitingForResponse()217 public void notifyCardEmulationEventWaitingForResponse() { 218 mWaitingForFirstResponse = true; 219 } 220 notifyCardEmulationEventResponseReceived()221 public void notifyCardEmulationEventResponseReceived() { 222 mWaitingForFirstResponse = false; 223 } 224 notifyCardEmulationEventWaitingForService()225 public void notifyCardEmulationEventWaitingForService() { 226 mBindingStartTimeMillis = SystemClock.elapsedRealtime(); 227 } 228 notifyCardEmulationEventServiceBound()229 public void notifyCardEmulationEventServiceBound() { 230 int bindingLimitMillis = 500; 231 if (mBindingStartTimeMillis > 0) { 232 long bindingElapsedTimeMillis = SystemClock.elapsedRealtime() - mBindingStartTimeMillis; 233 if (DBG) { 234 Log.d(TAG, "notifyCardEmulationEventServiceBound: binding took " 235 + bindingElapsedTimeMillis + " millis"); 236 } 237 if (bindingElapsedTimeMillis >= bindingLimitMillis) { 238 logErrorEvent(NfcStatsLog.NFC_ERROR_OCCURRED__TYPE__HCE_LATE_BINDING); 239 } 240 mBindingStartTimeMillis = 0; 241 } 242 } 243 logCardEmulationWrongSettingEvent()244 public void logCardEmulationWrongSettingEvent() { 245 int statsdCategory = 246 getCardEmulationStatsdCategory( 247 StatsdResult.WRONG_APP_AND_DEVICE_SETTINGS, mTransactionCategory); 248 logCardEmulationEvent(statsdCategory); 249 } 250 logCardEmulationNoRoutingEvent()251 public void logCardEmulationNoRoutingEvent() { 252 int statsdCategory = 253 getCardEmulationStatsdCategory( 254 StatsdResult.NO_ROUTING_FOR_AID, mTransactionCategory); 255 logCardEmulationEvent(statsdCategory); 256 } 257 logCardEmulationDeactivatedEvent()258 public void logCardEmulationDeactivatedEvent() { 259 if (mTransactionCategory.equals(CardEmulation.EXTRA_CATEGORY)) { 260 // Skip deactivation calls without select apdu 261 resetCardEmulationEvent(); 262 return; 263 } 264 265 StatsdResult transactionResult; 266 if (mBindingStartTimeMillis > 0) { 267 transactionResult = StatsdResult.DISCONNECTED_BEFORE_BOUND; 268 } else if (mWaitingForFirstResponse) { 269 transactionResult = StatsdResult.DISCONNECTED_BEFORE_RESPONSE; 270 } else { 271 transactionResult = StatsdResult.SUCCESS; 272 } 273 int statsdCategory = 274 getCardEmulationStatsdCategory(transactionResult, mTransactionCategory); 275 logCardEmulationEvent(statsdCategory); 276 } 277 logCardEmulationOffhostEvent(String seName)278 public void logCardEmulationOffhostEvent(String seName) { 279 mSeName = seName; 280 281 int statsdCategory; 282 switch (mTransactionCategory) { 283 case CardEmulation.CATEGORY_PAYMENT: 284 statsdCategory = CE_OFFHOST_PAYMENT; 285 break; 286 case CardEmulation.CATEGORY_OTHER: 287 statsdCategory = CE_OFFHOST_OTHER; 288 break; 289 default: 290 statsdCategory = CE_OFFHOST; 291 } 292 logCardEmulationEvent(statsdCategory); 293 } 294 logFieldChanged(boolean isOn, int fieldStrength)295 public void logFieldChanged(boolean isOn, int fieldStrength) { 296 NfcStatsLog.write(NfcStatsLog.NFC_FIELD_CHANGED, 297 isOn ? NfcStatsLog.NFC_FIELD_CHANGED__FIELD_STATUS__FIELD_ON 298 : NfcStatsLog.NFC_FIELD_CHANGED__FIELD_STATUS__FIELD_OFF, fieldStrength); 299 300 if (!isOn) { 301 mLastGainLevel = NO_GAIN_INFORMATION; 302 } 303 } 304 305 /** 306 * Set the trigger source of the next observe mode state change. Use this when the source of the 307 * change is not known at the time of the change. This value will be cleared after the next 308 * call to logObserveModeStateChanged(). 309 */ setNextObserveModeTriggerSource(int triggerSource)310 public void setNextObserveModeTriggerSource(int triggerSource) { 311 mStatsdUtilsContext.setObserveModeTriggerSource(triggerSource); 312 } 313 314 /** 315 * Log when observe mode is enabled or disabled. 316 * 317 * @param enabled true if observe mode is enabled, false if it is disabled 318 * @param triggerSource the source of the change, if known. This value will be ignored if 319 * setNextObserveModeTriggerSource() has been called. 320 * @param latencyMs the amount of time that it took to enable or disable observe mode in 321 * milliseconds. 322 */ logObserveModeStateChanged(boolean enabled, int triggerSource, int latencyMs)323 public void logObserveModeStateChanged(boolean enabled, int triggerSource, int latencyMs) { 324 Integer overrideTriggerSource = mStatsdUtilsContext.getObserveModeTriggerSource(); 325 326 NfcStatsLog.write(NfcStatsLog.NFC_OBSERVE_MODE_STATE_CHANGED, 327 enabled ? NfcStatsLog.NFC_OBSERVE_MODE_STATE_CHANGED__STATE__OBSERVE_MODE_ENABLED 328 : NfcStatsLog.NFC_OBSERVE_MODE_STATE_CHANGED__STATE__OBSERVE_MODE_DISABLED, 329 overrideTriggerSource != null ? overrideTriggerSource : triggerSource, latencyMs); 330 mStatsdUtilsContext.setObserveModeTriggerSource(null); 331 } 332 333 /** 334 * Log when the exit frame table is changed. 335 * 336 * @param tableSize the number of entries in the exit frame table 337 * @param timeoutMs the timeout, in milliseconds, chosen to restore observe mode 338 */ logExitFrameTableChanged(int tableSize, int timeoutMs)339 public void logExitFrameTableChanged(int tableSize, int timeoutMs) { 340 NfcStatsLog.write(NfcStatsLog.NFC_EXIT_FRAME_TABLE_CHANGED, tableSize, timeoutMs); 341 } 342 logAutoTransactReported(int processor, byte[] data)343 public void logAutoTransactReported(int processor, byte[] data) { 344 NfcStatsLog.write(NfcStatsLog.NFC_AUTO_TRANSACT_REPORTED, processor, data.length, 345 getFrameType(data)); 346 } 347 348 private final HashMap<String, PollingFrameLog> pollingFrameMap = new HashMap<>(); 349 tallyPollingFrame(String frameDataHex, PollingFrame frame)350 public void tallyPollingFrame(String frameDataHex, PollingFrame frame) { 351 int type = frame.getType(); 352 353 int gainLevel = frame.getVendorSpecificGain(); 354 if (gainLevel != -1) { 355 if (mLastGainLevel != gainLevel) { 356 logFieldChanged(true, gainLevel); 357 mLastGainLevel = gainLevel; 358 } 359 } 360 361 if (type == PollingFrame.POLLING_LOOP_TYPE_UNKNOWN) { 362 byte[] data = frame.getData(); 363 364 PollingFrameLog log = pollingFrameMap.getOrDefault(frameDataHex, null); 365 366 if (log == null) { 367 PollingFrameLog frameLog = new PollingFrameLog(data); 368 369 pollingFrameMap.put(frameDataHex, frameLog); 370 } else { 371 log.repeatCount++; 372 } 373 } 374 } 375 logPollingFrames()376 public void logPollingFrames() { 377 for (PollingFrameLog log : pollingFrameMap.values()) { 378 writeToStatsd(log); 379 } 380 pollingFrameMap.clear(); 381 } 382 getFrameType(byte[] data)383 protected static int getFrameType(byte[] data) { 384 int frameType = 385 NFC_POLLING_LOOP_NOTIFICATION_REPORTED__PROPRIETARY_FRAME_TYPE__PROPRIETARY_FRAME_UNKNOWN; 386 387 if (data != null && data.length >= FRAME_ECP_MIN_SIZE && data[0] == FRAME_HEADER_ECP) { 388 frameType = switch (data[1]) { 389 case FRAME_ECP_V1 -> 390 NFC_POLLING_LOOP_NOTIFICATION_REPORTED__PROPRIETARY_FRAME_TYPE__ECP_V1; 391 case FRAME_ECP_V2 -> 392 NFC_POLLING_LOOP_NOTIFICATION_REPORTED__PROPRIETARY_FRAME_TYPE__ECP_V2; 393 default -> frameType; 394 }; 395 } 396 return frameType; 397 } 398 writeToStatsd(PollingFrameLog frameLog)399 protected void writeToStatsd(PollingFrameLog frameLog) { 400 NfcStatsLog.write(NfcStatsLog.NFC_POLLING_LOOP_NOTIFICATION_REPORTED, 401 frameLog.frameType, 402 frameLog.repeatCount); 403 } 404 405 protected static class PollingFrameLog { 406 int repeatCount = 1; 407 final int frameType; 408 PollingFrameLog(byte[] data)409 public PollingFrameLog(byte[] data) { 410 frameType = getFrameType(data); 411 } 412 413 @Override equals(Object o)414 public boolean equals(Object o) { 415 if (this == o) return true; 416 if (!(o instanceof PollingFrameLog)) return false; 417 PollingFrameLog that = (PollingFrameLog) o; 418 return repeatCount == that.repeatCount && frameType == that.frameType; 419 } 420 421 @Override hashCode()422 public int hashCode() { 423 return Objects.hash(repeatCount, frameType); 424 } 425 426 @Override toString()427 public String toString() { 428 return "PollingFrameLog{" + 429 "repeatCount=" + repeatCount + 430 ", frameType=" + frameType + 431 '}'; 432 } 433 } 434 } 435