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