1 /*
2  * Copyright (C) 2024 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 
17 package com.android.nfc;
18 
19 import android.content.Context;
20 import android.os.Handler;
21 import android.os.Looper;
22 import android.util.AtomicFile;
23 import android.util.Log;
24 
25 import com.android.internal.annotations.VisibleForTesting;
26 import com.android.nfc.proto.NfcEventProto;
27 
28 import libcore.util.HexEncoding;
29 
30 import com.google.protobuf.InvalidProtocolBufferException;
31 
32 import java.io.File;
33 import java.io.FileDescriptor;
34 import java.io.FileOutputStream;
35 import java.io.IOException;
36 import java.io.PrintWriter;
37 import java.time.LocalDateTime;
38 import java.time.format.DateTimeFormatter;
39 import java.util.ArrayDeque;
40 
41 /**
42  * Used to store important NFC event logs persistently for debugging purposes.
43  */
44 public final class NfcEventLog {
45     private static final String TAG = "NfcEventLog";
46     @VisibleForTesting
47     public static final DateTimeFormatter FORMATTER =
48             DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
49     private final Context mContext;
50     private final NfcInjector mNfcInjector;
51     private final Handler mHander;
52     private final int mMaxEventNum;
53     private final AtomicFile mLogFile;
54     private final ArrayDeque<NfcEventProto.Event> mEventList;
55 
NfcEventLog(Context context, NfcInjector nfcInjector, Looper looper, AtomicFile logFile)56     public NfcEventLog(Context context, NfcInjector nfcInjector, Looper looper,
57             AtomicFile logFile) {
58         mContext = context;
59         mNfcInjector = nfcInjector;
60         mMaxEventNum = context.getResources().getInteger(R.integer.max_event_log_num);
61         mEventList = new ArrayDeque<>(0);
62         mHander = new Handler(looper);
63         mLogFile = logFile;
64         mHander.post(() -> readListFromLogFile());
65     }
66 
readLogFile()67     private byte[] readLogFile() {
68         byte[] bytes;
69         try {
70             bytes = mLogFile.readFully();
71         } catch (IOException e) {
72             return null;
73         }
74         return bytes;
75     }
76 
writeLogFile(byte[] bytes)77     private void writeLogFile(byte[] bytes) throws IOException {
78         FileOutputStream out = null;
79         try {
80             out = mLogFile.startWrite();
81             out.write(bytes);
82             mLogFile.finishWrite(out);
83         } catch (IOException e) {
84             if (out != null) {
85                 mLogFile.failWrite(out);
86             }
87             throw e;
88         }
89     }
90 
readListFromLogFile()91     private void readListFromLogFile() {
92         byte[] bytes = readLogFile();
93         if (bytes == null) {
94             Log.i(TAG, "No NFC events found in log file");
95             return;
96         }
97         NfcEventProto.EventList eventList;
98         try {
99             eventList = NfcEventProto.EventList.parseFrom(bytes);
100         } catch (InvalidProtocolBufferException e) {
101             Log.e(TAG, "Failed to deserialize events from log file", e);
102             return;
103         }
104         synchronized (mEventList) {
105             for (NfcEventProto.Event event : eventList.getEventsList()) {
106                 mEventList.add(event);
107             }
108         }
109     }
110 
writeListToLogFile()111     private void writeListToLogFile() {
112         NfcEventProto.EventList.Builder eventListBuilder =
113                 NfcEventProto.EventList.newBuilder();
114         synchronized (mEventList) {
115             for (NfcEventProto.Event event:  mEventList) {
116                 eventListBuilder.addEvents(event);
117             }
118         }
119         byte[] bytes = eventListBuilder.build().toByteArray();
120         Log.d(TAG, "writeListToLogFile: " + HexEncoding.encodeToString(bytes));
121         try {
122             writeLogFile(bytes);
123         } catch (IOException e) {
124             Log.e(TAG, "Failed to write to log file", e);
125         }
126     }
127 
addAndWriteListToLogFile(NfcEventProto.Event event)128     private void addAndWriteListToLogFile(NfcEventProto.Event event) {
129         synchronized (mEventList) {
130             // Trim the list to MAX_EVENTS.
131             if (mEventList.size() == mMaxEventNum) {
132                 mEventList.remove();
133             }
134             mEventList.add(event);
135             writeListToLogFile();
136         }
137     }
138 
139     /**
140      * Log NFC event
141      * Does not block the main NFC thread for logging, posts it to the logging thraead.
142      */
logEvent(NfcEventProto.EventType eventType)143     public void logEvent(NfcEventProto.EventType eventType) {
144         mHander.post(() -> {
145             NfcEventProto.Event event = NfcEventProto.Event.newBuilder()
146                     .setTimestamp(mNfcInjector.getLocalDateTime().format(FORMATTER))
147                     .setEventType(eventType)
148                     .build();
149             addAndWriteListToLogFile(event);
150         });
151     }
152 
dump(FileDescriptor fd, PrintWriter pw, String[] args)153     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
154         pw.println("===== Nfc Event Log =====");
155         synchronized (mEventList) {
156             for (NfcEventProto.Event event: mEventList) {
157                 // Cleanup the proto string output to make it more readable.
158                 String eventTypeString = event.getEventType().toString()
159                     .replaceAll("# com.android.nfc.proto.*", "")
160                     .replaceAll("\n", "");
161                 pw.println(event.getTimestamp() + ": " + eventTypeString);
162             }
163         }
164         pw.println("===== Nfc Event Log =====");
165     }
166 
167     @VisibleForTesting
getEventsList()168     public ArrayDeque<NfcEventProto.Event> getEventsList() {
169         return mEventList;
170     }
171 }
172