blob: 8b92537a97896f8abd6ad3928edc4b3d8cc330d7 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.people.data;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.net.Uri;
import android.os.FileUtils;
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.Slog;
import android.util.SparseArray;
import android.util.proto.ProtoInputStream;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.people.PeopleEventIndexesProto;
import com.android.server.people.PeopleEventsProto;
import com.android.server.people.TypedPeopleEventIndexProto;
import com.google.android.collect.Lists;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
class EventHistoryImpl implements EventHistory {
private static final long MAX_EVENTS_AGE = 4L * DateUtils.HOUR_IN_MILLIS;
private static final long PRUNE_OLD_EVENTS_DELAY = 15L * DateUtils.MINUTE_IN_MILLIS;
private static final String EVENTS_DIR = "events";
private static final String INDEXES_DIR = "indexes";
private final Injector mInjector;
private final ScheduledExecutorService mScheduledExecutorService;
private final EventsProtoDiskReadWriter mEventsProtoDiskReadWriter;
private final EventIndexesProtoDiskReadWriter mEventIndexesProtoDiskReadWriter;
private final File mRootDir;
// Event Type -> Event Index
@GuardedBy("this")
private final SparseArray<EventIndex> mEventIndexArray = new SparseArray<>();
@GuardedBy("this")
private final EventList mRecentEvents = new EventList();
private long mLastPruneTime;
EventHistoryImpl(@NonNull File rootDir,
@NonNull ScheduledExecutorService scheduledExecutorService) {
this(new Injector(), rootDir, scheduledExecutorService);
}
@VisibleForTesting
EventHistoryImpl(@NonNull Injector injector, @NonNull File rootDir,
@NonNull ScheduledExecutorService scheduledExecutorService) {
mInjector = injector;
mScheduledExecutorService = scheduledExecutorService;
mLastPruneTime = injector.currentTimeMillis();
mRootDir = rootDir;
File eventsDir = new File(mRootDir, EVENTS_DIR);
mEventsProtoDiskReadWriter = new EventsProtoDiskReadWriter(eventsDir,
mScheduledExecutorService);
File indexesDir = new File(mRootDir, INDEXES_DIR);
mEventIndexesProtoDiskReadWriter = new EventIndexesProtoDiskReadWriter(indexesDir,
scheduledExecutorService);
}
@WorkerThread
@NonNull
static Map<String, EventHistoryImpl> eventHistoriesImplFromDisk(File categoryDir,
ScheduledExecutorService scheduledExecutorService) {
return eventHistoriesImplFromDisk(new Injector(), categoryDir, scheduledExecutorService);
}
@VisibleForTesting
@NonNull
static Map<String, EventHistoryImpl> eventHistoriesImplFromDisk(Injector injector,
File categoryDir, ScheduledExecutorService scheduledExecutorService) {
Map<String, EventHistoryImpl> results = new ArrayMap<>();
File[] keyDirs = categoryDir.listFiles(File::isDirectory);
if (keyDirs == null) {
return results;
}
for (File keyDir : keyDirs) {
File[] dirContents = keyDir.listFiles(
(dir, name) -> EVENTS_DIR.equals(name) || INDEXES_DIR.equals(name));
if (dirContents != null && dirContents.length == 2) {
EventHistoryImpl eventHistory = new EventHistoryImpl(injector, keyDir,
scheduledExecutorService);
eventHistory.loadFromDisk();
results.put(Uri.decode(keyDir.getName()), eventHistory);
}
}
return results;
}
/**
* Loads recent events and indexes from disk to memory in a background thread. This should be
* called after the device powers on and the user has been unlocked.
*/
@VisibleForTesting
@MainThread
synchronized void loadFromDisk() {
mScheduledExecutorService.execute(() -> {
synchronized (this) {
EventList diskEvents = mEventsProtoDiskReadWriter.loadRecentEventsFromDisk();
if (diskEvents != null) {
diskEvents.removeOldEvents(mInjector.currentTimeMillis() - MAX_EVENTS_AGE);
mRecentEvents.addAll(diskEvents.getAllEvents());
}
SparseArray<EventIndex> diskIndexes =
mEventIndexesProtoDiskReadWriter.loadIndexesFromDisk();
if (diskIndexes != null) {
for (int i = 0; i < diskIndexes.size(); i++) {
mEventIndexArray.put(diskIndexes.keyAt(i), diskIndexes.valueAt(i));
}
}
}
});
}
/**
* Flushes events and indexes immediately. This should be called when device is powering off.
*/
@MainThread
synchronized void saveToDisk() {
mEventsProtoDiskReadWriter.saveEventsImmediately(mRecentEvents);
mEventIndexesProtoDiskReadWriter.saveIndexesImmediately(mEventIndexArray);
}
@Override
@NonNull
public synchronized EventIndex getEventIndex(@Event.EventType int eventType) {
EventIndex eventIndex = mEventIndexArray.get(eventType);
return eventIndex != null ? new EventIndex(eventIndex) : mInjector.createEventIndex();
}
@Override
@NonNull
public synchronized EventIndex getEventIndex(Set<Integer> eventTypes) {
EventIndex combined = mInjector.createEventIndex();
for (@Event.EventType int eventType : eventTypes) {
EventIndex eventIndex = mEventIndexArray.get(eventType);
if (eventIndex != null) {
combined = EventIndex.combine(combined, eventIndex);
}
}
return combined;
}
@Override
@NonNull
public synchronized List<Event> queryEvents(Set<Integer> eventTypes, long startTime,
long endTime) {
return mRecentEvents.queryEvents(eventTypes, startTime, endTime);
}
synchronized void addEvent(Event event) {
pruneOldEvents();
addEventInMemory(event);
mEventsProtoDiskReadWriter.scheduleEventsSave(mRecentEvents);
mEventIndexesProtoDiskReadWriter.scheduleIndexesSave(mEventIndexArray);
}
synchronized void onDestroy() {
mEventIndexArray.clear();
mRecentEvents.clear();
mEventsProtoDiskReadWriter.deleteRecentEventsFile();
mEventIndexesProtoDiskReadWriter.deleteIndexesFile();
FileUtils.deleteContentsAndDir(mRootDir);
}
/** Deletes the events data that exceeds the retention period. */
synchronized void pruneOldEvents() {
long currentTime = mInjector.currentTimeMillis();
if (currentTime - mLastPruneTime > PRUNE_OLD_EVENTS_DELAY) {
mRecentEvents.removeOldEvents(currentTime - MAX_EVENTS_AGE);
mLastPruneTime = currentTime;
}
}
private synchronized void addEventInMemory(Event event) {
EventIndex eventIndex = mEventIndexArray.get(event.getType());
if (eventIndex == null) {
eventIndex = mInjector.createEventIndex();
mEventIndexArray.put(event.getType(), eventIndex);
}
eventIndex.addEvent(event.getTimestamp());
mRecentEvents.add(event);
}
@VisibleForTesting
static class Injector {
EventIndex createEventIndex() {
return new EventIndex();
}
long currentTimeMillis() {
return System.currentTimeMillis();
}
}
/** Reads and writes {@link Event}s on disk. */
private static class EventsProtoDiskReadWriter extends AbstractProtoDiskReadWriter<EventList> {
private static final String TAG = EventsProtoDiskReadWriter.class.getSimpleName();
private static final String RECENT_FILE = "recent";
EventsProtoDiskReadWriter(@NonNull File rootDir,
@NonNull ScheduledExecutorService scheduledExecutorService) {
super(rootDir, scheduledExecutorService);
rootDir.mkdirs();
}
@Override
ProtoStreamWriter<EventList> protoStreamWriter() {
return (protoOutputStream, data) -> {
for (Event event : data.getAllEvents()) {
long token = protoOutputStream.start(PeopleEventsProto.EVENTS);
event.writeToProto(protoOutputStream);
protoOutputStream.end(token);
}
};
}
@Override
ProtoStreamReader<EventList> protoStreamReader() {
return protoInputStream -> {
List<Event> results = Lists.newArrayList();
try {
while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
if (protoInputStream.getFieldNumber() != (int) PeopleEventsProto.EVENTS) {
continue;
}
long token = protoInputStream.start(PeopleEventsProto.EVENTS);
Event event = Event.readFromProto(protoInputStream);
protoInputStream.end(token);
results.add(event);
}
} catch (IOException e) {
Slog.e(TAG, "Failed to read protobuf input stream.", e);
}
EventList eventList = new EventList();
eventList.addAll(results);
return eventList;
};
}
@MainThread
void scheduleEventsSave(EventList recentEvents) {
scheduleSave(RECENT_FILE, recentEvents);
}
@MainThread
void saveEventsImmediately(EventList recentEvents) {
saveImmediately(RECENT_FILE, recentEvents);
}
/**
* Loads recent events from disk. This should be called when device is powered on.
*/
@WorkerThread
@Nullable
EventList loadRecentEventsFromDisk() {
return read(RECENT_FILE);
}
@WorkerThread
void deleteRecentEventsFile() {
delete(RECENT_FILE);
}
}
/** Reads and writes {@link EventIndex}s on disk. */
private static class EventIndexesProtoDiskReadWriter extends
AbstractProtoDiskReadWriter<SparseArray<EventIndex>> {
private static final String TAG = EventIndexesProtoDiskReadWriter.class.getSimpleName();
private static final String INDEXES_FILE = "index";
EventIndexesProtoDiskReadWriter(@NonNull File rootDir,
@NonNull ScheduledExecutorService scheduledExecutorService) {
super(rootDir, scheduledExecutorService);
rootDir.mkdirs();
}
@Override
ProtoStreamWriter<SparseArray<EventIndex>> protoStreamWriter() {
return (protoOutputStream, data) -> {
for (int i = 0; i < data.size(); i++) {
@Event.EventType int eventType = data.keyAt(i);
EventIndex index = data.valueAt(i);
long token = protoOutputStream.start(PeopleEventIndexesProto.TYPED_INDEXES);
protoOutputStream.write(TypedPeopleEventIndexProto.EVENT_TYPE, eventType);
long indexToken = protoOutputStream.start(TypedPeopleEventIndexProto.INDEX);
index.writeToProto(protoOutputStream);
protoOutputStream.end(indexToken);
protoOutputStream.end(token);
}
};
}
@Override
ProtoStreamReader<SparseArray<EventIndex>> protoStreamReader() {
return protoInputStream -> {
SparseArray<EventIndex> results = new SparseArray<>();
try {
while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
if (protoInputStream.getFieldNumber()
!= (int) PeopleEventIndexesProto.TYPED_INDEXES) {
continue;
}
long token = protoInputStream.start(PeopleEventIndexesProto.TYPED_INDEXES);
@Event.EventType int eventType = 0;
EventIndex index = EventIndex.EMPTY;
while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (protoInputStream.getFieldNumber()) {
case (int) TypedPeopleEventIndexProto.EVENT_TYPE:
eventType = protoInputStream.readInt(
TypedPeopleEventIndexProto.EVENT_TYPE);
break;
case (int) TypedPeopleEventIndexProto.INDEX:
long indexToken = protoInputStream.start(
TypedPeopleEventIndexProto.INDEX);
index = EventIndex.readFromProto(protoInputStream);
protoInputStream.end(indexToken);
break;
default:
Slog.w(TAG, "Could not read undefined field: "
+ protoInputStream.getFieldNumber());
}
}
results.append(eventType, index);
protoInputStream.end(token);
}
} catch (IOException e) {
Slog.e(TAG, "Failed to read protobuf input stream.", e);
}
return results;
};
}
@MainThread
void scheduleIndexesSave(SparseArray<EventIndex> indexes) {
scheduleSave(INDEXES_FILE, indexes);
}
@MainThread
void saveIndexesImmediately(SparseArray<EventIndex> indexes) {
saveImmediately(INDEXES_FILE, indexes);
}
@WorkerThread
@Nullable
SparseArray<EventIndex> loadIndexesFromDisk() {
return read(INDEXES_FILE);
}
@WorkerThread
void deleteIndexesFile() {
delete(INDEXES_FILE);
}
}
}