• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 final 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         synchronized (mEventList) {
91             mEventList.clear();
92             Instant now = Instant.now();
93             Deque<Event> eventQueue = new ArrayDeque<>();
94             try (FileInputStream fileInputStream = mContext.openFileInput(EVENT_LOG_FILE_NAME)) {
95                 Event event = readEvent(fileInputStream);
96 
97                 while (event != null) {
98                     // I'm not sure if we need this
99                     if (event.mTimestamp.plus(MAX_LOG_AGE).isAfter(now)) {
100                         eventQueue.addFirst(event);
101                     }
102                     event = readEvent(fileInputStream);
103                 }
104 
105                 for (Event e : eventQueue) {
106                     mEventList.addFirst(e);
107                 }
108             } catch (FileNotFoundException e) {
109                 // Ignore this exception as if there's no file there's nothing to load
110                 Log.i(TAG, "No existing event file");
111             } catch (IOException e) {
112                 Log.e(TAG, "Error when loading events from file", e);
113             }
114         }
115     }
116 
readEvent(FileInputStream fileInputStream)117     private Event readEvent(FileInputStream fileInputStream) throws IOException {
118         if (fileInputStream.available() < BYTES_PER_INT) {
119             return null;
120         }
121         byte[] sizeBytes = new byte[BYTES_PER_INT];
122         fileInputStream.read(sizeBytes);
123 
124         int size = ByteBuffer.wrap(sizeBytes).getInt();
125 
126         byte[] eventBytes = new byte[size];
127         fileInputStream.read(eventBytes);
128 
129         return Event.fromBytes(eventBytes);
130     }
131 
132     /** Saves the event so it can be queried. */
log(Event event)133     void log(Event event) {
134         sExecutor.execute(() -> {
135             Log.d(TAG, event.toString());
136             synchronized (mEventList) {
137                 mEventList.add(event); // TODO: This should be made immutable before adding
138                 writeEventToFile(event);
139             }
140             triggerEventListeners(event);
141         });
142     }
143 
writeEventToFile(Event event)144     private void writeEventToFile(Event event) {
145         try {
146             if (mOutputStream == null) {
147                 mOutputStream = mContext.openFileOutput(
148                         EVENT_LOG_FILE_NAME, Context.MODE_PRIVATE | Context.MODE_APPEND);
149             }
150 
151             Log.e(TAG, "writing event to file: " + event);
152             try {
153                 byte[] eventBytes = event.toBytes();
154                 mOutputStream.write(
155                         ByteBuffer.allocate(BYTES_PER_INT).putInt(eventBytes.length).array());
156                 mOutputStream.write(eventBytes);
157             } catch (Throwable e) {
158                 // This will happen if the event contains a Binder - can't be written to disk
159                 Log.e(TAG, "We can't write this event to disk because it contains a Binder "
160                         + "(this may cause errors in tests after this point - particularly related"
161                         + " to EventLib)", e);
162             }
163         } catch (IOException e) {
164             throw new IllegalStateException("Error writing event to log", e);
165         }
166     }
167 
168     private final Deque<Event> mEventList = new ConcurrentLinkedDeque<>();
169     // This is a weak set so we don't retain listeners from old tests
170     private final Set<EventListener> mEventListeners
171             = Collections.newSetFromMap(new WeakHashMap<>());
172 
173     /** Get all logged events. */
getEvents()174     public Queue<Event> getEvents() {
175         return mEventList;
176     }
177 
178     /** Register an {@link EventListener} to be called when a new {@link Event} is logged. */
registerEventListener(EventListener listener)179     public Queue<Event> registerEventListener(EventListener listener) {
180         synchronized (mEventList) {
181             synchronized (mEventListeners) {
182                 mEventListeners.add(listener);
183 
184                 return getEvents();
185             }
186         }
187     }
188 
triggerEventListeners(Event event)189     private void triggerEventListeners(Event event) {
190         synchronized (mEventListeners) {
191             for (EventListener listener : mEventListeners) {
192                 listener.onNewEvent(event);
193             }
194         }
195     }
196 
197 }
198