1 /*
2  * Copyright (C) 2020 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.eventlib;
18 
19 import android.content.Context;
20 import android.util.Log;
21 
22 import java.io.FileInputStream;
23 import java.io.FileNotFoundException;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.nio.ByteBuffer;
27 import java.time.Duration;
28 import java.time.Instant;
29 import java.util.ArrayDeque;
30 import java.util.Collections;
31 import java.util.Deque;
32 import java.util.Queue;
33 import java.util.Set;
34 import java.util.WeakHashMap;
35 import java.util.concurrent.ConcurrentLinkedDeque;
36 import java.util.concurrent.ExecutorService;
37 import java.util.concurrent.Executors;
38 import java.util.concurrent.atomic.AtomicBoolean;
39 
40 /** Event store for the current package. */
41 class Events {
42 
43     private static final String TAG = "EventLibEvents";
44     private static final String EVENT_LOG_FILE_NAME = "Events";
45     private static final Duration MAX_LOG_AGE = Duration.ofMinutes(5);
46     private static final int BYTES_PER_INT = 4;
47 
48     private static final ExecutorService sExecutor = Executors.newSingleThreadExecutor();
49     private AtomicBoolean mLoadedHistory = new AtomicBoolean(false);
50 
51     /** Interface used to be informed when new events are logged. */
52     interface EventListener {
onNewEvent(Event e)53         void onNewEvent(Event e);
54     }
55 
56     private static Events mInstance;
57 
getInstance(Context context, boolean needsHistory)58     static Events getInstance(Context context, boolean needsHistory) {
59         if (mInstance == null) {
60             synchronized (Events.class) {
61                 if (mInstance == null) {
62                     mInstance = new Events(context.getApplicationContext());
63                 }
64             }
65         }
66 
67         if (needsHistory) {
68             mInstance.loadHistory();
69         }
70 
71         return mInstance;
72     }
73 
74     private final Context mContext; // ApplicationContext
75     private FileOutputStream mOutputStream;
76 
Events(Context context)77     private Events(Context context) {
78         this.mContext = context;
79     }
80 
loadHistory()81     private void loadHistory() {
82         if (mLoadedHistory.getAndSet(true)) {
83             return;
84         }
85 
86         loadEventsFromFile();
87     }
88 
loadEventsFromFile()89     private void loadEventsFromFile() {
90         mEventList.clear();
91         Instant now = Instant.now();
92         Deque<Event> eventQueue = new ArrayDeque<>();
93         try (FileInputStream fileInputStream = mContext.openFileInput(EVENT_LOG_FILE_NAME)) {
94             Event event = readEvent(fileInputStream);
95 
96             while (event != null) {
97                 // I'm not sure if we need this
98                 if (event.mTimestamp.plus(MAX_LOG_AGE).isAfter(now)) {
99                     eventQueue.addFirst(event);
100                 }
101                 event = readEvent(fileInputStream);
102             }
103 
104             for (Event e : eventQueue) {
105                 mEventList.addFirst(e);
106             }
107         } catch (FileNotFoundException e) {
108             // Ignore this exception as if there's no file there's nothing to load
109             Log.i(TAG, "No existing event file");
110         } catch (IOException e) {
111             Log.e(TAG, "Error when loading events from file", e);
112         }
113     }
114 
readEvent(FileInputStream fileInputStream)115     private Event readEvent(FileInputStream fileInputStream) throws IOException {
116         if (fileInputStream.available() < BYTES_PER_INT) {
117             return null;
118         }
119         byte[] sizeBytes = new byte[BYTES_PER_INT];
120         fileInputStream.read(sizeBytes);
121 
122         int size = ByteBuffer.wrap(sizeBytes).getInt();
123 
124         byte[] eventBytes = new byte[size];
125         fileInputStream.read(eventBytes);
126 
127         return Event.fromBytes(eventBytes);
128     }
129 
130     /** Saves the event so it can be queried. */
log(Event event)131     void log(Event event) {
132         sExecutor.execute(() -> {
133             Log.d(TAG, event.toString());
134             synchronized (mEventList) {
135                 mEventList.add(event); // TODO: This should be made immutable before adding
136                 writeEventToFile(event);
137             }
138             triggerEventListeners(event);
139         });
140     }
141 
writeEventToFile(Event event)142     private void writeEventToFile(Event event) {
143         try {
144             if (mOutputStream == null) {
145                 mOutputStream = mContext.openFileOutput(
146                         EVENT_LOG_FILE_NAME, Context.MODE_PRIVATE | Context.MODE_APPEND);
147             }
148 
149             Log.e(TAG, "writing event to file: " + event);
150             byte[] eventBytes = event.toBytes();
151             mOutputStream.write(
152                     ByteBuffer.allocate(BYTES_PER_INT).putInt(eventBytes.length).array());
153             mOutputStream.write(eventBytes);
154         } catch (IOException e) {
155             throw new IllegalStateException("Error writing event to log", e);
156         }
157     }
158 
159     private final Deque<Event> mEventList = new ConcurrentLinkedDeque<>();
160     // This is a weak set so we don't retain listeners from old tests
161     private final Set<EventListener> mEventListeners
162             = Collections.newSetFromMap(new WeakHashMap<>());
163 
164     /** Get all logged events. */
getEvents()165     public Queue<Event> getEvents() {
166             return mEventList;
167     }
168 
169     /** Register an {@link EventListener} to be called when a new {@link Event} is logged. */
registerEventListener(EventListener listener)170     public void registerEventListener(EventListener listener) {
171         synchronized (mEventListeners) {
172             mEventListeners.add(listener);
173         }
174     }
175 
triggerEventListeners(Event event)176     private void triggerEventListeners(Event event) {
177         synchronized (mEventListeners) {
178             for (EventListener listener : mEventListeners) {
179                 listener.onNewEvent(event);
180             }
181         }
182     }
183 
184 }
185