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.nfc.cardemulation.CardEmulation; 24 import android.nfc.cardemulation.PollingFrame; 25 import android.os.Bundle; 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 /** Name of SE terminal to log in statsd */ 79 private String mSeName = ""; 80 /** Timestamp in millis when app binding starts */ 81 private long mBindingStartTimeMillis = 0; 82 /** Flag to indicate that the service has not sent the first response */ 83 private boolean mWaitingForFirstResponse = false; 84 /** Current transaction's category to log in statsd */ 85 private String mTransactionCategory = CardEmulation.EXTRA_CATEGORY; 86 /** Current transaction's uid to log in statsd */ 87 private int mTransactionUid = -1; 88 89 private static final byte FRAME_HEADER_ECP = 0x6A; 90 private static final byte FRAME_ECP_V1 = 0x01; 91 private static final byte FRAME_ECP_V2 = 0x02; 92 private static final int FRAME_ECP_MIN_SIZE = 5; 93 94 private static final int NO_GAIN_INFORMATION = -1; 95 private int mLastGainLevel = NO_GAIN_INFORMATION; 96 97 /** Result constants for statsd usage */ 98 static enum StatsdResult { 99 SUCCESS, 100 NO_ROUTING_FOR_AID, 101 WRONG_APP_AND_DEVICE_SETTINGS, 102 DISCONNECTED_BEFORE_BOUND, 103 DISCONNECTED_BEFORE_RESPONSE 104 } 105 StatsdUtils(String seName)106 public StatsdUtils(String seName) { 107 mSeName = seName; 108 109 // HCEF has no category, default it to PAYMENT category to record every call 110 if (seName.equals(SE_NAME_HCEF)) mTransactionCategory = CardEmulation.CATEGORY_PAYMENT; 111 } 112 StatsdUtils()113 public StatsdUtils() {} 114 resetCardEmulationEvent()115 private void resetCardEmulationEvent() { 116 // Reset mTransactionCategory value to prevent accidental triggers in general 117 // except for HCEF, which is always intentional because it only works in foreground 118 if (!mSeName.equals(SE_NAME_HCEF)) mTransactionCategory = CardEmulation.EXTRA_CATEGORY; 119 mBindingStartTimeMillis = 0; 120 mWaitingForFirstResponse = false; 121 mTransactionUid = -1; 122 } 123 getCardEmulationStatsdCategory( StatsdResult transactionResult, String transactionCategory)124 private int getCardEmulationStatsdCategory( 125 StatsdResult transactionResult, String transactionCategory) { 126 switch (transactionResult) { 127 case SUCCESS: 128 switch (transactionCategory) { 129 case CardEmulation.CATEGORY_PAYMENT: 130 return CE_HCE_PAYMENT; 131 case CardEmulation.CATEGORY_OTHER: 132 return CE_HCE_OTHER; 133 default: 134 return CE_UNKNOWN; 135 } 136 137 case NO_ROUTING_FOR_AID: 138 return CE_NO_ROUTING; 139 140 case WRONG_APP_AND_DEVICE_SETTINGS: 141 switch (transactionCategory) { 142 case CardEmulation.CATEGORY_PAYMENT: 143 return CE_PAYMENT_WRONG_SETTING; 144 case CardEmulation.CATEGORY_OTHER: 145 return CE_OTHER_WRONG_SETTING; 146 default: 147 return CE_UNKNOWN; 148 } 149 150 case DISCONNECTED_BEFORE_BOUND: 151 switch (transactionCategory) { 152 case CardEmulation.CATEGORY_PAYMENT: 153 return CE_PAYMENT_DC_BOUND; 154 case CardEmulation.CATEGORY_OTHER: 155 return CE_OTHER_DC_BOUND; 156 default: 157 return CE_UNKNOWN; 158 } 159 160 case DISCONNECTED_BEFORE_RESPONSE: 161 switch (transactionCategory) { 162 case CardEmulation.CATEGORY_PAYMENT: 163 return CE_PAYMENT_DC_RESPONSE; 164 case CardEmulation.CATEGORY_OTHER: 165 return CE_OTHER_DC_RESPONSE; 166 default: 167 return CE_UNKNOWN; 168 } 169 } 170 return CE_UNKNOWN; 171 } 172 logCardEmulationEvent(int statsdCategory)173 private void logCardEmulationEvent(int statsdCategory) { 174 NfcStatsLog.write( 175 NfcStatsLog.NFC_CARDEMULATION_OCCURRED, statsdCategory, mSeName, mTransactionUid); 176 resetCardEmulationEvent(); 177 } 178 logErrorEvent(int errorType, int nciCmd, int ntfStatusCode)179 public void logErrorEvent(int errorType, int nciCmd, int ntfStatusCode) { 180 NfcStatsLog.write(NfcStatsLog.NFC_ERROR_OCCURRED, errorType, nciCmd, ntfStatusCode); 181 } 182 logErrorEvent(int errorType)183 public void logErrorEvent(int errorType) { 184 logErrorEvent(errorType, 0, 0); 185 } 186 setCardEmulationEventCategory(String category)187 public void setCardEmulationEventCategory(String category) { 188 mTransactionCategory = category; 189 } 190 setCardEmulationEventUid(int uid)191 public void setCardEmulationEventUid(int uid) { 192 mTransactionUid = uid; 193 } 194 notifyCardEmulationEventWaitingForResponse()195 public void notifyCardEmulationEventWaitingForResponse() { 196 mWaitingForFirstResponse = true; 197 } 198 notifyCardEmulationEventResponseReceived()199 public void notifyCardEmulationEventResponseReceived() { 200 mWaitingForFirstResponse = false; 201 } 202 notifyCardEmulationEventWaitingForService()203 public void notifyCardEmulationEventWaitingForService() { 204 mBindingStartTimeMillis = SystemClock.elapsedRealtime(); 205 } 206 notifyCardEmulationEventServiceBound()207 public void notifyCardEmulationEventServiceBound() { 208 int bindingLimitMillis = 500; 209 if (mBindingStartTimeMillis > 0) { 210 long bindingElapsedTimeMillis = SystemClock.elapsedRealtime() - mBindingStartTimeMillis; 211 if (DBG) Log.d(TAG, "binding took " + bindingElapsedTimeMillis + " millis"); 212 if (bindingElapsedTimeMillis >= bindingLimitMillis) { 213 logErrorEvent(NfcStatsLog.NFC_ERROR_OCCURRED__TYPE__HCE_LATE_BINDING); 214 } 215 mBindingStartTimeMillis = 0; 216 } 217 } 218 logCardEmulationWrongSettingEvent()219 public void logCardEmulationWrongSettingEvent() { 220 int statsdCategory = 221 getCardEmulationStatsdCategory( 222 StatsdResult.WRONG_APP_AND_DEVICE_SETTINGS, mTransactionCategory); 223 logCardEmulationEvent(statsdCategory); 224 } 225 logCardEmulationNoRoutingEvent()226 public void logCardEmulationNoRoutingEvent() { 227 int statsdCategory = 228 getCardEmulationStatsdCategory( 229 StatsdResult.NO_ROUTING_FOR_AID, mTransactionCategory); 230 logCardEmulationEvent(statsdCategory); 231 } 232 logCardEmulationDeactivatedEvent()233 public void logCardEmulationDeactivatedEvent() { 234 if (mTransactionCategory.equals(CardEmulation.EXTRA_CATEGORY)) { 235 // Skip deactivation calls without select apdu 236 resetCardEmulationEvent(); 237 return; 238 } 239 240 StatsdResult transactionResult; 241 if (mBindingStartTimeMillis > 0) { 242 transactionResult = StatsdResult.DISCONNECTED_BEFORE_BOUND; 243 } else if (mWaitingForFirstResponse) { 244 transactionResult = StatsdResult.DISCONNECTED_BEFORE_RESPONSE; 245 } else { 246 transactionResult = StatsdResult.SUCCESS; 247 } 248 int statsdCategory = 249 getCardEmulationStatsdCategory(transactionResult, mTransactionCategory); 250 logCardEmulationEvent(statsdCategory); 251 } 252 logCardEmulationOffhostEvent(String seName)253 public void logCardEmulationOffhostEvent(String seName) { 254 mSeName = seName; 255 256 int statsdCategory; 257 switch (mTransactionCategory) { 258 case CardEmulation.CATEGORY_PAYMENT: 259 statsdCategory = CE_OFFHOST_PAYMENT; 260 break; 261 case CardEmulation.CATEGORY_OTHER: 262 statsdCategory = CE_OFFHOST_OTHER; 263 break; 264 default: 265 statsdCategory = CE_OFFHOST; 266 }; 267 logCardEmulationEvent(statsdCategory); 268 } 269 logFieldChanged(boolean isOn, int fieldStrength)270 public void logFieldChanged(boolean isOn, int fieldStrength) { 271 NfcStatsLog.write(NfcStatsLog.NFC_FIELD_CHANGED, 272 isOn ? NfcStatsLog.NFC_FIELD_CHANGED__FIELD_STATUS__FIELD_ON 273 : NfcStatsLog.NFC_FIELD_CHANGED__FIELD_STATUS__FIELD_OFF, fieldStrength); 274 275 if (!isOn) { 276 mLastGainLevel = NO_GAIN_INFORMATION; 277 } 278 } 279 logObserveModeStateChanged(boolean enabled, int triggerSource, int latency)280 public void logObserveModeStateChanged(boolean enabled, int triggerSource, int latency) { 281 NfcStatsLog.write(NfcStatsLog.NFC_OBSERVE_MODE_STATE_CHANGED, 282 enabled ? NfcStatsLog.NFC_OBSERVE_MODE_STATE_CHANGED__STATE__OBSERVE_MODE_ENABLED 283 : NfcStatsLog.NFC_OBSERVE_MODE_STATE_CHANGED__STATE__OBSERVE_MODE_DISABLED, 284 triggerSource, latency); 285 } 286 287 private final HashMap<String, PollingFrameLog> pollingFrameMap = new HashMap<>(); 288 tallyPollingFrame(String frameDataHex, PollingFrame frame)289 public void tallyPollingFrame(String frameDataHex, PollingFrame frame) { 290 int type = frame.getType(); 291 292 int gainLevel = frame.getVendorSpecificGain(); 293 if (gainLevel != -1) { 294 if (mLastGainLevel != gainLevel) { 295 logFieldChanged(true, gainLevel); 296 mLastGainLevel = gainLevel; 297 } 298 } 299 300 if (type == PollingFrame.POLLING_LOOP_TYPE_UNKNOWN) { 301 byte[] data = frame.getData(); 302 303 PollingFrameLog log = pollingFrameMap.getOrDefault(frameDataHex, null); 304 305 if (log == null) { 306 PollingFrameLog frameLog = new PollingFrameLog(data); 307 308 pollingFrameMap.put(frameDataHex, frameLog); 309 } else { 310 log.repeatCount++; 311 } 312 } 313 } 314 logPollingFrames()315 public void logPollingFrames() { 316 for (PollingFrameLog log : pollingFrameMap.values()) { 317 writeToStatsd(log); 318 } 319 pollingFrameMap.clear(); 320 } 321 getFrameType(byte[] data)322 protected static int getFrameType(byte[] data) { 323 int frameType = 324 NFC_POLLING_LOOP_NOTIFICATION_REPORTED__PROPRIETARY_FRAME_TYPE__PROPRIETARY_FRAME_UNKNOWN; 325 326 if (data != null && data.length >= FRAME_ECP_MIN_SIZE && data[0] == FRAME_HEADER_ECP) { 327 frameType = switch (data[1]) { 328 case FRAME_ECP_V1 -> 329 NFC_POLLING_LOOP_NOTIFICATION_REPORTED__PROPRIETARY_FRAME_TYPE__ECP_V1; 330 case FRAME_ECP_V2 -> 331 NFC_POLLING_LOOP_NOTIFICATION_REPORTED__PROPRIETARY_FRAME_TYPE__ECP_V2; 332 default -> frameType; 333 }; 334 } 335 return frameType; 336 } 337 writeToStatsd(PollingFrameLog frameLog)338 protected void writeToStatsd(PollingFrameLog frameLog) { 339 NfcStatsLog.write(NfcStatsLog.NFC_POLLING_LOOP_NOTIFICATION_REPORTED, 340 frameLog.frameType, 341 frameLog.repeatCount); 342 } 343 344 protected static class PollingFrameLog { 345 int repeatCount = 1; 346 final int frameType; 347 PollingFrameLog(byte[] data)348 public PollingFrameLog(byte[] data) { 349 frameType = getFrameType(data); 350 } 351 352 @Override equals(Object o)353 public boolean equals(Object o) { 354 if (this == o) return true; 355 if (!(o instanceof PollingFrameLog)) return false; 356 PollingFrameLog that = (PollingFrameLog) o; 357 return repeatCount == that.repeatCount && frameType == that.frameType; 358 } 359 360 @Override hashCode()361 public int hashCode() { 362 return Objects.hash(repeatCount, frameType); 363 } 364 365 @Override toString()366 public String toString() { 367 return "PollingFrameLog{" + 368 "repeatCount=" + repeatCount + 369 ", frameType=" + frameType + 370 '}'; 371 } 372 } 373 } 374