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