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