Add persistence of Event and EventIndex during device reboot.
Change-Id: Ic6c403a000a776eca2a3e9678c25862693a04c2e Test: Built on device and unit tests. Bug: 149356114
This commit is contained in:
@@ -51,11 +51,13 @@ import java.util.concurrent.TimeoutException;
|
||||
abstract class AbstractProtoDiskReadWriter<T> {
|
||||
|
||||
private static final String TAG = AbstractProtoDiskReadWriter.class.getSimpleName();
|
||||
|
||||
// Common disk write delay that will be appropriate for most scenarios.
|
||||
private static final long DEFAULT_DISK_WRITE_DELAY = 2L * DateUtils.MINUTE_IN_MILLIS;
|
||||
private static final long SHUTDOWN_DISK_WRITE_TIMEOUT = 5L * DateUtils.SECOND_IN_MILLIS;
|
||||
|
||||
private final File mRootDir;
|
||||
private final ScheduledExecutorService mScheduledExecutorService;
|
||||
private final long mWriteDelayMs;
|
||||
|
||||
@GuardedBy("this")
|
||||
private ScheduledFuture<?> mScheduledFuture;
|
||||
@@ -75,10 +77,9 @@ abstract class AbstractProtoDiskReadWriter<T> {
|
||||
*/
|
||||
abstract ProtoStreamReader<T> protoStreamReader();
|
||||
|
||||
AbstractProtoDiskReadWriter(@NonNull File rootDir, long writeDelayMs,
|
||||
AbstractProtoDiskReadWriter(@NonNull File rootDir,
|
||||
@NonNull ScheduledExecutorService scheduledExecutorService) {
|
||||
mRootDir = rootDir;
|
||||
mWriteDelayMs = writeDelayMs;
|
||||
mScheduledExecutorService = scheduledExecutorService;
|
||||
}
|
||||
|
||||
@@ -174,7 +175,7 @@ abstract class AbstractProtoDiskReadWriter<T> {
|
||||
}
|
||||
|
||||
mScheduledFuture = mScheduledExecutorService.schedule(this::flushScheduledData,
|
||||
mWriteDelayMs, TimeUnit.MILLISECONDS);
|
||||
DEFAULT_DISK_WRITE_DELAY, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,7 +184,13 @@ abstract class AbstractProtoDiskReadWriter<T> {
|
||||
*/
|
||||
@MainThread
|
||||
synchronized void saveImmediately(@NonNull String fileName, @NonNull T data) {
|
||||
if (mScheduledExecutorService.isShutdown()) {
|
||||
mScheduledFileDataMap.put(fileName, data);
|
||||
triggerScheduledFlushEarly();
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private synchronized void triggerScheduledFlushEarly() {
|
||||
if (mScheduledFileDataMap.isEmpty() || mScheduledExecutorService.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
// Cancel existing future.
|
||||
@@ -194,7 +201,6 @@ abstract class AbstractProtoDiskReadWriter<T> {
|
||||
mScheduledFuture.cancel(true);
|
||||
}
|
||||
|
||||
mScheduledFileDataMap.put(fileName, data);
|
||||
// Submit flush and blocks until it completes. Blocking will prevent the device from
|
||||
// shutting down before flushing completes.
|
||||
Future<?> future = mScheduledExecutorService.submit(this::flushScheduledData);
|
||||
@@ -212,9 +218,10 @@ abstract class AbstractProtoDiskReadWriter<T> {
|
||||
return;
|
||||
}
|
||||
for (String fileName : mScheduledFileDataMap.keySet()) {
|
||||
T data = mScheduledFileDataMap.remove(fileName);
|
||||
T data = mScheduledFileDataMap.get(fileName);
|
||||
writeTo(fileName, data);
|
||||
}
|
||||
mScheduledFileDataMap.clear();
|
||||
mScheduledFuture = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import android.annotation.WorkerThread;
|
||||
import android.content.LocusId;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Slog;
|
||||
import android.util.proto.ProtoInputStream;
|
||||
@@ -50,8 +49,6 @@ class ConversationStore {
|
||||
|
||||
private static final String CONVERSATIONS_FILE_NAME = "conversations";
|
||||
|
||||
private static final long DISK_WRITE_DELAY = 2L * DateUtils.MINUTE_IN_MILLIS;
|
||||
|
||||
// Shortcut ID -> Conversation Info
|
||||
@GuardedBy("this")
|
||||
private final Map<String, ConversationInfo> mConversationInfoMap = new ArrayMap<>();
|
||||
@@ -92,7 +89,7 @@ class ConversationStore {
|
||||
*/
|
||||
@MainThread
|
||||
void loadConversationsFromDisk() {
|
||||
mScheduledExecutorService.submit(() -> {
|
||||
mScheduledExecutorService.execute(() -> {
|
||||
synchronized (this) {
|
||||
ConversationInfosProtoDiskReadWriter conversationInfosProtoDiskReadWriter =
|
||||
getConversationInfosProtoDiskReadWriter();
|
||||
@@ -239,8 +236,7 @@ class ConversationStore {
|
||||
}
|
||||
if (mConversationInfosProtoDiskReadWriter == null) {
|
||||
mConversationInfosProtoDiskReadWriter = new ConversationInfosProtoDiskReadWriter(
|
||||
mPackageDir, CONVERSATIONS_FILE_NAME, DISK_WRITE_DELAY,
|
||||
mScheduledExecutorService);
|
||||
mPackageDir, CONVERSATIONS_FILE_NAME, mScheduledExecutorService);
|
||||
}
|
||||
return mConversationInfosProtoDiskReadWriter;
|
||||
}
|
||||
@@ -264,16 +260,16 @@ class ConversationStore {
|
||||
return conversationInfo;
|
||||
}
|
||||
|
||||
/** Reads and writes {@link ConversationInfo} on disk. */
|
||||
static class ConversationInfosProtoDiskReadWriter extends
|
||||
/** Reads and writes {@link ConversationInfo}s on disk. */
|
||||
private static class ConversationInfosProtoDiskReadWriter extends
|
||||
AbstractProtoDiskReadWriter<List<ConversationInfo>> {
|
||||
|
||||
private final String mConversationInfoFileName;
|
||||
|
||||
ConversationInfosProtoDiskReadWriter(@NonNull File baseDir,
|
||||
ConversationInfosProtoDiskReadWriter(@NonNull File rootDir,
|
||||
@NonNull String conversationInfoFileName,
|
||||
long writeDelayMs, @NonNull ScheduledExecutorService scheduledExecutorService) {
|
||||
super(baseDir, writeDelayMs, scheduledExecutorService);
|
||||
@NonNull ScheduledExecutorService scheduledExecutorService) {
|
||||
super(rootDir, scheduledExecutorService);
|
||||
mConversationInfoFileName = conversationInfoFileName;
|
||||
}
|
||||
|
||||
|
||||
@@ -319,12 +319,11 @@ public class DataManager {
|
||||
}
|
||||
pruneUninstalledPackageData(userData);
|
||||
|
||||
long currentTimeMillis = System.currentTimeMillis();
|
||||
userData.forAllPackages(packageData -> {
|
||||
if (signal.isCanceled()) {
|
||||
return;
|
||||
}
|
||||
packageData.getEventStore().pruneOldEvents(currentTimeMillis);
|
||||
packageData.getEventStore().pruneOldEvents();
|
||||
if (!packageData.isDefaultDialer()) {
|
||||
packageData.getEventStore().deleteEventHistories(EventStore.CATEGORY_CALL);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,13 @@ import android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.text.format.DateFormat;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Slog;
|
||||
import android.util.proto.ProtoInputStream;
|
||||
import android.util.proto.ProtoOutputStream;
|
||||
|
||||
import com.android.server.people.PeopleEventProto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Objects;
|
||||
@@ -29,6 +35,8 @@ import java.util.Set;
|
||||
/** An event representing the interaction with a specific conversation or app. */
|
||||
public class Event {
|
||||
|
||||
private static final String TAG = Event.class.getSimpleName();
|
||||
|
||||
public static final int TYPE_SHORTCUT_INVOCATION = 1;
|
||||
|
||||
public static final int TYPE_NOTIFICATION_POSTED = 2;
|
||||
@@ -142,6 +150,36 @@ public class Event {
|
||||
return mDurationSeconds;
|
||||
}
|
||||
|
||||
/** Writes field members to {@link ProtoOutputStream}. */
|
||||
void writeToProto(@NonNull ProtoOutputStream protoOutputStream) {
|
||||
protoOutputStream.write(PeopleEventProto.EVENT_TYPE, mType);
|
||||
protoOutputStream.write(PeopleEventProto.TIME, mTimestamp);
|
||||
protoOutputStream.write(PeopleEventProto.DURATION, mDurationSeconds);
|
||||
}
|
||||
|
||||
/** Reads from {@link ProtoInputStream} and constructs {@link Event}. */
|
||||
@NonNull
|
||||
static Event readFromProto(@NonNull ProtoInputStream protoInputStream) throws IOException {
|
||||
Event.Builder builder = new Event.Builder();
|
||||
while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
|
||||
switch (protoInputStream.getFieldNumber()) {
|
||||
case (int) PeopleEventProto.EVENT_TYPE:
|
||||
builder.setType(protoInputStream.readInt(PeopleEventProto.EVENT_TYPE));
|
||||
break;
|
||||
case (int) PeopleEventProto.TIME:
|
||||
builder.setTimestamp(protoInputStream.readLong(PeopleEventProto.TIME));
|
||||
break;
|
||||
case (int) PeopleEventProto.DURATION:
|
||||
builder.setDurationSeconds(protoInputStream.readInt(PeopleEventProto.DURATION));
|
||||
break;
|
||||
default:
|
||||
Slog.w(TAG, "Could not read undefined field: "
|
||||
+ protoInputStream.getFieldNumber());
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
@@ -177,12 +215,14 @@ public class Event {
|
||||
/** Builder class for {@link Event} objects. */
|
||||
static class Builder {
|
||||
|
||||
private final long mTimestamp;
|
||||
private long mTimestamp;
|
||||
|
||||
private final int mType;
|
||||
private int mType;
|
||||
|
||||
private int mDurationSeconds;
|
||||
|
||||
private Builder() {}
|
||||
|
||||
Builder(long timestamp, @EventType int type) {
|
||||
mTimestamp = timestamp;
|
||||
mType = type;
|
||||
@@ -193,6 +233,16 @@ public class Event {
|
||||
return this;
|
||||
}
|
||||
|
||||
private Builder setTimestamp(long timestamp) {
|
||||
mTimestamp = timestamp;
|
||||
return this;
|
||||
}
|
||||
|
||||
private Builder setType(int type) {
|
||||
mType = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
Event build() {
|
||||
return new Event(this);
|
||||
}
|
||||
|
||||
@@ -16,42 +16,149 @@
|
||||
|
||||
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.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;
|
||||
|
||||
// Event Type -> Event Index
|
||||
@GuardedBy("this")
|
||||
private final SparseArray<EventIndex> mEventIndexArray = new SparseArray<>();
|
||||
|
||||
@GuardedBy("this")
|
||||
private final EventList mRecentEvents = new EventList();
|
||||
|
||||
EventHistoryImpl() {
|
||||
mInjector = new Injector();
|
||||
private long mLastPruneTime;
|
||||
|
||||
EventHistoryImpl(@NonNull File rootDir,
|
||||
@NonNull ScheduledExecutorService scheduledExecutorService) {
|
||||
this(new Injector(), rootDir, scheduledExecutorService);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
EventHistoryImpl(Injector injector) {
|
||||
EventHistoryImpl(@NonNull Injector injector, @NonNull File rootDir,
|
||||
@NonNull ScheduledExecutorService scheduledExecutorService) {
|
||||
mInjector = injector;
|
||||
mScheduledExecutorService = scheduledExecutorService;
|
||||
mLastPruneTime = injector.currentTimeMillis();
|
||||
|
||||
File eventsDir = new File(rootDir, EVENTS_DIR);
|
||||
mEventsProtoDiskReadWriter = new EventsProtoDiskReadWriter(eventsDir,
|
||||
mScheduledExecutorService);
|
||||
File indexesDir = new File(rootDir, 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 EventIndex getEventIndex(@Event.EventType int eventType) {
|
||||
public synchronized EventIndex getEventIndex(@Event.EventType int eventType) {
|
||||
EventIndex eventIndex = mEventIndexArray.get(eventType);
|
||||
return eventIndex != null ? new EventIndex(eventIndex) : mInjector.createEventIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public EventIndex getEventIndex(Set<Integer> eventTypes) {
|
||||
public synchronized EventIndex getEventIndex(Set<Integer> eventTypes) {
|
||||
EventIndex combined = mInjector.createEventIndex();
|
||||
for (@Event.EventType int eventType : eventTypes) {
|
||||
EventIndex eventIndex = mEventIndexArray.get(eventType);
|
||||
@@ -64,11 +171,35 @@ class EventHistoryImpl implements EventHistory {
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<Event> queryEvents(Set<Integer> eventTypes, long startTime, long endTime) {
|
||||
public synchronized List<Event> queryEvents(Set<Integer> eventTypes, long startTime,
|
||||
long endTime) {
|
||||
return mRecentEvents.queryEvents(eventTypes, startTime, endTime);
|
||||
}
|
||||
|
||||
void addEvent(Event event) {
|
||||
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();
|
||||
}
|
||||
|
||||
/** 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();
|
||||
@@ -78,22 +209,180 @@ class EventHistoryImpl implements EventHistory {
|
||||
mRecentEvents.add(event);
|
||||
}
|
||||
|
||||
void onDestroy() {
|
||||
mEventIndexArray.clear();
|
||||
mRecentEvents.clear();
|
||||
// TODO: STOPSHIP: Delete the data files.
|
||||
}
|
||||
|
||||
/** Deletes the events data that exceeds the retention period. */
|
||||
void pruneOldEvents(long currentTimeMillis) {
|
||||
// TODO: STOPSHIP: Delete the old events data files.
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,14 @@ import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.text.format.DateFormat;
|
||||
import android.util.Range;
|
||||
import android.util.Slog;
|
||||
import android.util.proto.ProtoInputStream;
|
||||
import android.util.proto.ProtoOutputStream;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.server.people.PeopleEventIndexProto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.time.Instant;
|
||||
@@ -34,6 +39,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.TimeZone;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -60,6 +66,7 @@ import java.util.function.Function;
|
||||
* </pre>
|
||||
*/
|
||||
public class EventIndex {
|
||||
private static final String TAG = EventIndex.class.getSimpleName();
|
||||
|
||||
private static final int RETENTION_DAYS = 63;
|
||||
|
||||
@@ -118,22 +125,23 @@ public class EventIndex {
|
||||
private final Injector mInjector;
|
||||
|
||||
EventIndex() {
|
||||
mInjector = new Injector();
|
||||
mEventBitmaps = new long[]{0L, 0L, 0L, 0L};
|
||||
mLastUpdatedTime = mInjector.currentTimeMillis();
|
||||
this(new Injector());
|
||||
}
|
||||
|
||||
EventIndex(EventIndex from) {
|
||||
mInjector = new Injector();
|
||||
mEventBitmaps = Arrays.copyOf(from.mEventBitmaps, TIME_SLOT_TYPES_COUNT);
|
||||
mLastUpdatedTime = from.mLastUpdatedTime;
|
||||
EventIndex(@NonNull EventIndex from) {
|
||||
this(from.mInjector, Arrays.copyOf(from.mEventBitmaps, TIME_SLOT_TYPES_COUNT),
|
||||
from.mLastUpdatedTime);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
EventIndex(Injector injector) {
|
||||
EventIndex(@NonNull Injector injector) {
|
||||
this(injector, new long[]{0L, 0L, 0L, 0L}, injector.currentTimeMillis());
|
||||
}
|
||||
|
||||
private EventIndex(@NonNull Injector injector, long[] eventBitmaps, long lastUpdatedTime) {
|
||||
mInjector = injector;
|
||||
mEventBitmaps = new long[]{0L, 0L, 0L, 0L};
|
||||
mLastUpdatedTime = mInjector.currentTimeMillis();
|
||||
mEventBitmaps = eventBitmaps;
|
||||
mLastUpdatedTime = lastUpdatedTime;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,6 +240,31 @@ public class EventIndex {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof EventIndex)) {
|
||||
return false;
|
||||
}
|
||||
EventIndex other = (EventIndex) obj;
|
||||
return mLastUpdatedTime == other.mLastUpdatedTime
|
||||
&& Arrays.equals(mEventBitmaps, other.mEventBitmaps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(mLastUpdatedTime, mEventBitmaps);
|
||||
}
|
||||
|
||||
synchronized void writeToProto(@NonNull ProtoOutputStream protoOutputStream) {
|
||||
for (long bitmap : mEventBitmaps) {
|
||||
protoOutputStream.write(PeopleEventIndexProto.EVENT_BITMAPS, bitmap);
|
||||
}
|
||||
protoOutputStream.write(PeopleEventIndexProto.LAST_UPDATED_TIME, mLastUpdatedTime);
|
||||
}
|
||||
|
||||
/** Shifts the event bitmaps to make them up-to-date. */
|
||||
private void updateEventBitmaps(long currentTimeMillis) {
|
||||
for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) {
|
||||
@@ -249,6 +282,28 @@ public class EventIndex {
|
||||
mLastUpdatedTime = currentTimeMillis;
|
||||
}
|
||||
|
||||
static EventIndex readFromProto(@NonNull ProtoInputStream protoInputStream) throws IOException {
|
||||
int bitmapIndex = 0;
|
||||
long[] eventBitmaps = new long[TIME_SLOT_TYPES_COUNT];
|
||||
long lastUpdated = 0L;
|
||||
while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
|
||||
switch (protoInputStream.getFieldNumber()) {
|
||||
case (int) PeopleEventIndexProto.EVENT_BITMAPS:
|
||||
eventBitmaps[bitmapIndex++] = protoInputStream.readLong(
|
||||
PeopleEventIndexProto.EVENT_BITMAPS);
|
||||
break;
|
||||
case (int) PeopleEventIndexProto.LAST_UPDATED_TIME:
|
||||
lastUpdated = protoInputStream.readLong(
|
||||
PeopleEventIndexProto.LAST_UPDATED_TIME);
|
||||
break;
|
||||
default:
|
||||
Slog.e(TAG, "Could not read undefined field: "
|
||||
+ protoInputStream.getFieldNumber());
|
||||
}
|
||||
}
|
||||
return new EventIndex(new Injector(), eventBitmaps, lastUpdated);
|
||||
}
|
||||
|
||||
private static LocalDateTime toLocalDateTime(long epochMilli) {
|
||||
return LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(epochMilli), TimeZone.getDefault().toZoneId());
|
||||
|
||||
@@ -18,6 +18,8 @@ package com.android.server.people.data;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
|
||||
import com.android.internal.util.CollectionUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -41,6 +43,16 @@ class EventList {
|
||||
mEvents.add(index, event);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Call #add on each event to keep the order.
|
||||
*/
|
||||
void addAll(@NonNull List<Event> events) {
|
||||
for (Event event : events) {
|
||||
add(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link List} of {@link Event}s whose timestamps are between the specified {@code
|
||||
* fromTimestamp}, inclusive, and {@code toTimestamp} exclusive, and match the specified event
|
||||
@@ -73,6 +85,44 @@ class EventList {
|
||||
mEvents.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of events.
|
||||
*/
|
||||
@NonNull
|
||||
List<Event> getAllEvents() {
|
||||
return CollectionUtils.copyOf(mEvents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove events that are older than the specified cut off threshold timestamp.
|
||||
*/
|
||||
void removeOldEvents(long cutOffThreshold) {
|
||||
|
||||
// Everything before the cut off is considered old, and should be removed.
|
||||
int cutOffIndex = firstIndexOnOrAfter(cutOffThreshold);
|
||||
if (cutOffIndex == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear entire list if the cut off is greater than the last element.
|
||||
int eventsSize = mEvents.size();
|
||||
if (cutOffIndex == eventsSize) {
|
||||
mEvents.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Reorder the list starting from the cut off index.
|
||||
int i = 0;
|
||||
for (; cutOffIndex < eventsSize; i++, cutOffIndex++) {
|
||||
mEvents.set(i, mEvents.get(cutOffIndex));
|
||||
}
|
||||
|
||||
// Clear the list after reordering.
|
||||
if (eventsSize > i) {
|
||||
mEvents.subList(i, eventsSize).clear();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the first index whose timestamp is greater or equal to the provided timestamp. */
|
||||
private int firstIndexOnOrAfter(long timestamp) {
|
||||
int result = mEvents.size();
|
||||
|
||||
@@ -17,16 +17,22 @@
|
||||
package com.android.server.people.data;
|
||||
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.MainThread;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.net.Uri;
|
||||
import android.util.ArrayMap;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/** The store that stores and accesses the events data for a package. */
|
||||
@@ -57,14 +63,58 @@ class EventStore {
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface EventCategory {}
|
||||
|
||||
@GuardedBy("this")
|
||||
private final List<Map<String, EventHistoryImpl>> mEventHistoryMaps = new ArrayList<>();
|
||||
private final List<File> mEventsCategoryDirs = new ArrayList<>();
|
||||
private final ScheduledExecutorService mScheduledExecutorService;
|
||||
|
||||
EventStore() {
|
||||
EventStore(@NonNull File packageDir,
|
||||
@NonNull ScheduledExecutorService scheduledExecutorService) {
|
||||
mEventHistoryMaps.add(CATEGORY_SHORTCUT_BASED, new ArrayMap<>());
|
||||
mEventHistoryMaps.add(CATEGORY_LOCUS_ID_BASED, new ArrayMap<>());
|
||||
mEventHistoryMaps.add(CATEGORY_CALL, new ArrayMap<>());
|
||||
mEventHistoryMaps.add(CATEGORY_SMS, new ArrayMap<>());
|
||||
mEventHistoryMaps.add(CATEGORY_CLASS_BASED, new ArrayMap<>());
|
||||
|
||||
File eventDir = new File(packageDir, "event");
|
||||
mEventsCategoryDirs.add(CATEGORY_SHORTCUT_BASED, new File(eventDir, "shortcut"));
|
||||
mEventsCategoryDirs.add(CATEGORY_LOCUS_ID_BASED, new File(eventDir, "locus"));
|
||||
mEventsCategoryDirs.add(CATEGORY_CALL, new File(eventDir, "call"));
|
||||
mEventsCategoryDirs.add(CATEGORY_SMS, new File(eventDir, "sms"));
|
||||
mEventsCategoryDirs.add(CATEGORY_CLASS_BASED, new File(eventDir, "class"));
|
||||
|
||||
mScheduledExecutorService = scheduledExecutorService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads existing {@link EventHistoryImpl}s from disk. This should be called when device powers
|
||||
* on and user is unlocked.
|
||||
*/
|
||||
@MainThread
|
||||
void loadFromDisk() {
|
||||
mScheduledExecutorService.execute(() -> {
|
||||
synchronized (this) {
|
||||
for (@EventCategory int category = 0; category < mEventsCategoryDirs.size();
|
||||
category++) {
|
||||
File categoryDir = mEventsCategoryDirs.get(category);
|
||||
Map<String, EventHistoryImpl> existingEventHistoriesImpl =
|
||||
EventHistoryImpl.eventHistoriesImplFromDisk(categoryDir,
|
||||
mScheduledExecutorService);
|
||||
mEventHistoryMaps.get(category).putAll(existingEventHistoriesImpl);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes all {@link EventHistoryImpl}s to disk. Should be called when device is shutting down.
|
||||
*/
|
||||
synchronized void saveToDisk() {
|
||||
for (Map<String, EventHistoryImpl> map : mEventHistoryMaps) {
|
||||
for (EventHistoryImpl eventHistory : map.values()) {
|
||||
eventHistory.saveToDisk();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +124,7 @@ class EventStore {
|
||||
* name.
|
||||
*/
|
||||
@Nullable
|
||||
EventHistory getEventHistory(@EventCategory int category, String key) {
|
||||
synchronized EventHistory getEventHistory(@EventCategory int category, String key) {
|
||||
return mEventHistoryMaps.get(category).get(key);
|
||||
}
|
||||
|
||||
@@ -87,8 +137,11 @@ class EventStore {
|
||||
* name.
|
||||
*/
|
||||
@NonNull
|
||||
EventHistoryImpl getOrCreateEventHistory(@EventCategory int category, String key) {
|
||||
return mEventHistoryMaps.get(category).computeIfAbsent(key, k -> new EventHistoryImpl());
|
||||
synchronized EventHistoryImpl getOrCreateEventHistory(@EventCategory int category, String key) {
|
||||
return mEventHistoryMaps.get(category).computeIfAbsent(key,
|
||||
k -> new EventHistoryImpl(
|
||||
new File(mEventsCategoryDirs.get(category), Uri.encode(key)),
|
||||
mScheduledExecutorService));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +150,7 @@ class EventStore {
|
||||
* @param key Category-specific key, it can be shortcut ID, locus ID, phone number, or class
|
||||
* name.
|
||||
*/
|
||||
void deleteEventHistory(@EventCategory int category, String key) {
|
||||
synchronized void deleteEventHistory(@EventCategory int category, String key) {
|
||||
EventHistoryImpl eventHistory = mEventHistoryMaps.get(category).remove(key);
|
||||
if (eventHistory != null) {
|
||||
eventHistory.onDestroy();
|
||||
@@ -105,16 +158,18 @@ class EventStore {
|
||||
}
|
||||
|
||||
/** Deletes all the events and index data for the specified category from disk. */
|
||||
void deleteEventHistories(@EventCategory int category) {
|
||||
synchronized void deleteEventHistories(@EventCategory int category) {
|
||||
for (EventHistoryImpl eventHistory : mEventHistoryMaps.get(category).values()) {
|
||||
eventHistory.onDestroy();
|
||||
}
|
||||
mEventHistoryMaps.get(category).clear();
|
||||
// TODO: Implement this method to delete the data from disk.
|
||||
}
|
||||
|
||||
/** Deletes the events data that exceeds the retention period. */
|
||||
void pruneOldEvents(long currentTimeMillis) {
|
||||
synchronized void pruneOldEvents() {
|
||||
for (Map<String, EventHistoryImpl> map : mEventHistoryMaps) {
|
||||
for (EventHistoryImpl eventHistory : map.values()) {
|
||||
eventHistory.pruneOldEvents(currentTimeMillis);
|
||||
eventHistory.pruneOldEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +180,8 @@ class EventStore {
|
||||
*
|
||||
* @param keyChecker Check whether there exists a conversation contains this key.
|
||||
*/
|
||||
void pruneOrphanEventHistories(@EventCategory int category, Predicate<String> keyChecker) {
|
||||
synchronized void pruneOrphanEventHistories(@EventCategory int category,
|
||||
Predicate<String> keyChecker) {
|
||||
Set<String> keys = mEventHistoryMaps.get(category).keySet();
|
||||
List<String> keysToDelete = new ArrayList<>();
|
||||
for (String key : keys) {
|
||||
@@ -141,4 +197,12 @@ class EventStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void onDestroy() {
|
||||
for (Map<String, EventHistoryImpl> map : mEventHistoryMaps) {
|
||||
for (EventHistoryImpl eventHistory : map.values()) {
|
||||
eventHistory.onDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,10 @@ import android.annotation.Nullable;
|
||||
import android.annotation.UserIdInt;
|
||||
import android.content.LocusId;
|
||||
import android.text.TextUtils;
|
||||
import android.util.ArrayMap;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
@@ -63,22 +65,50 @@ public class PackageData {
|
||||
mUserId = userId;
|
||||
|
||||
mPackageDataDir = new File(perUserPeopleDataDir, mPackageName);
|
||||
mPackageDataDir.mkdirs();
|
||||
|
||||
mConversationStore = new ConversationStore(mPackageDataDir, scheduledExecutorService,
|
||||
helper);
|
||||
mEventStore = new EventStore();
|
||||
mEventStore = new EventStore(mPackageDataDir, scheduledExecutorService);
|
||||
mIsDefaultDialerPredicate = isDefaultDialerPredicate;
|
||||
mIsDefaultSmsAppPredicate = isDefaultSmsAppPredicate;
|
||||
}
|
||||
|
||||
/** Called when user is unlocked. */
|
||||
void loadFromDisk() {
|
||||
mPackageDataDir.mkdirs();
|
||||
/**
|
||||
* Returns a map of package directory names as keys and their associated {@link PackageData}.
|
||||
* This should be called when device is powered on and unlocked.
|
||||
*/
|
||||
@NonNull
|
||||
static Map<String, PackageData> packagesDataFromDisk(@UserIdInt int userId,
|
||||
@NonNull Predicate<String> isDefaultDialerPredicate,
|
||||
@NonNull Predicate<String> isDefaultSmsAppPredicate,
|
||||
@NonNull ScheduledExecutorService scheduledExecutorService,
|
||||
@NonNull File perUserPeopleDataDir,
|
||||
@NonNull ContactsQueryHelper helper) {
|
||||
Map<String, PackageData> results = new ArrayMap<>();
|
||||
File[] packageDirs = perUserPeopleDataDir.listFiles(File::isDirectory);
|
||||
if (packageDirs == null) {
|
||||
return results;
|
||||
}
|
||||
for (File packageDir : packageDirs) {
|
||||
PackageData packageData = new PackageData(packageDir.getName(), userId,
|
||||
isDefaultDialerPredicate, isDefaultSmsAppPredicate, scheduledExecutorService,
|
||||
perUserPeopleDataDir, helper);
|
||||
packageData.loadFromDisk();
|
||||
results.put(packageDir.getName(), packageData);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private void loadFromDisk() {
|
||||
mConversationStore.loadConversationsFromDisk();
|
||||
mEventStore.loadFromDisk();
|
||||
}
|
||||
|
||||
/** Called when device is shutting down. */
|
||||
void saveToDisk() {
|
||||
mConversationStore.saveConversationsToDisk();
|
||||
mEventStore.saveToDisk();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -222,6 +252,7 @@ public class PackageData {
|
||||
}
|
||||
|
||||
void onDestroy() {
|
||||
// TODO: STOPSHIP: Implements this method for the case of package being uninstalled.
|
||||
mEventStore.onDestroy();
|
||||
// TODO: STOPSHIP: Destroy conversation info for the case of package being uninstalled.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +73,8 @@ class UserData {
|
||||
// Ensures per user root directory for people data is present, and attempt to load
|
||||
// data from disk.
|
||||
mPerUserPeopleDataDir.mkdirs();
|
||||
for (PackageData packageData : mPackageDataMap.values()) {
|
||||
packageData.loadFromDisk();
|
||||
}
|
||||
mPackageDataMap.putAll(PackageData.packagesDataFromDisk(mUserId, this::isDefaultDialer,
|
||||
this::isDefaultSmsApp, mScheduledExecutorService, mPerUserPeopleDataDir, mHelper));
|
||||
}
|
||||
|
||||
void setUserStopped() {
|
||||
|
||||
@@ -21,11 +21,16 @@ import static com.android.server.people.data.TestUtils.timestamp;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
@@ -60,11 +65,16 @@ public final class AggregateEventHistoryImplTest {
|
||||
|
||||
EventHistoryImpl.Injector injector = new EventHistoryImplInjector();
|
||||
|
||||
mEventHistory1 = new EventHistoryImpl(injector);
|
||||
Context ctx = InstrumentationRegistry.getContext();
|
||||
File testDir = new File(ctx.getCacheDir(), "testdir");
|
||||
MockScheduledExecutorService mockScheduledExecutorService =
|
||||
new MockScheduledExecutorService();
|
||||
|
||||
mEventHistory1 = new EventHistoryImpl(injector, testDir, mockScheduledExecutorService);
|
||||
mEventHistory1.addEvent(E1);
|
||||
mEventHistory1.addEvent(E2);
|
||||
|
||||
mEventHistory2 = new EventHistoryImpl(injector);
|
||||
mEventHistory2 = new EventHistoryImpl(injector, testDir, mockScheduledExecutorService);
|
||||
mEventHistory2.addEvent(E3);
|
||||
mEventHistory2.addEvent(E4);
|
||||
}
|
||||
|
||||
@@ -21,18 +21,27 @@ import static com.android.server.people.data.TestUtils.timestamp;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.FileUtils;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
|
||||
import com.google.android.collect.Lists;
|
||||
import com.google.android.collect.Sets;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public final class EventHistoryImplTest {
|
||||
|
||||
private static final long CURRENT_TIMESTAMP = timestamp("01-30 18:50");
|
||||
|
||||
private static final Event E1 = new Event(timestamp("01-06 05:26"),
|
||||
@@ -41,26 +50,48 @@ public final class EventHistoryImplTest {
|
||||
Event.TYPE_NOTIFICATION_OPENED);
|
||||
private static final Event E3 = new Event(timestamp("01-30 03:06"),
|
||||
Event.TYPE_SHARE_IMAGE);
|
||||
private static final Event E4 = new Event(timestamp("01-30 18:14"),
|
||||
private static final Event E4 = new Event(timestamp("01-30 16:14"),
|
||||
Event.TYPE_SMS_INCOMING);
|
||||
private static final Event E5 = new Event(timestamp("01-30 18:30"),
|
||||
Event.TYPE_SMS_INCOMING);
|
||||
|
||||
private static final EventIndex.Injector EVENT_INDEX_INJECTOR = new EventIndex.Injector() {
|
||||
@Override
|
||||
long currentTimeMillis() {
|
||||
return CURRENT_TIMESTAMP;
|
||||
}
|
||||
};
|
||||
private static final EventHistoryImpl.Injector EVENT_HISTORY_INJECTOR =
|
||||
new EventHistoryImpl.Injector() {
|
||||
@Override
|
||||
EventIndex createEventIndex() {
|
||||
return new EventIndex(EVENT_INDEX_INJECTOR);
|
||||
}
|
||||
|
||||
@Override
|
||||
long currentTimeMillis() {
|
||||
return CURRENT_TIMESTAMP;
|
||||
}
|
||||
};
|
||||
|
||||
private EventHistoryImpl mEventHistory;
|
||||
private File mCacheDir;
|
||||
private File mFile;
|
||||
private MockScheduledExecutorService mMockScheduledExecutorService;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
EventIndex.Injector eventIndexInjector = new EventIndex.Injector() {
|
||||
@Override
|
||||
long currentTimeMillis() {
|
||||
return CURRENT_TIMESTAMP;
|
||||
}
|
||||
};
|
||||
EventHistoryImpl.Injector eventHistoryInjector = new EventHistoryImpl.Injector() {
|
||||
@Override
|
||||
EventIndex createEventIndex() {
|
||||
return new EventIndex(eventIndexInjector);
|
||||
}
|
||||
};
|
||||
mEventHistory = new EventHistoryImpl(eventHistoryInjector);
|
||||
Context ctx = InstrumentationRegistry.getContext();
|
||||
mCacheDir = ctx.getCacheDir();
|
||||
mFile = new File(mCacheDir, "testdir");
|
||||
mMockScheduledExecutorService = new MockScheduledExecutorService();
|
||||
mEventHistory = new EventHistoryImpl(EVENT_HISTORY_INJECTOR, mFile,
|
||||
mMockScheduledExecutorService);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
FileUtils.deleteContentsAndDir(mFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -115,4 +146,105 @@ public final class EventHistoryImplTest {
|
||||
Sets.newArraySet(Event.TYPE_SHARE_IMAGE), 0L, Long.MAX_VALUE);
|
||||
assertEquals(1, events.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPersistenceAndRestoration() {
|
||||
mEventHistory.addEvent(E1);
|
||||
mEventHistory.addEvent(E2);
|
||||
mEventHistory.addEvent(E3);
|
||||
mEventHistory.addEvent(E4);
|
||||
mEventHistory.addEvent(E5);
|
||||
|
||||
// futures of events and event index flush.
|
||||
long futuresExecuted = mMockScheduledExecutorService.fastForwardTime(
|
||||
3L * DateUtils.MINUTE_IN_MILLIS);
|
||||
assertEquals(2, futuresExecuted);
|
||||
|
||||
EventIndex indexBeforePowerOff = mEventHistory.getEventIndex(Event.ALL_EVENT_TYPES);
|
||||
|
||||
resetAndLoadEventHistory();
|
||||
|
||||
List<Event> events = mEventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
|
||||
assertEquals(2, events.size());
|
||||
assertTrue(events.containsAll(Lists.newArrayList(E4, E5)));
|
||||
|
||||
EventIndex indexAfterPowerOff = mEventHistory.getEventIndex(Event.ALL_EVENT_TYPES);
|
||||
assertEquals(indexBeforePowerOff, indexAfterPowerOff);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMimicDevicePowerOff() {
|
||||
mEventHistory.addEvent(E1);
|
||||
mEventHistory.addEvent(E2);
|
||||
mEventHistory.addEvent(E3);
|
||||
mEventHistory.addEvent(E4);
|
||||
mEventHistory.addEvent(E5);
|
||||
mEventHistory.saveToDisk();
|
||||
|
||||
EventIndex indexBeforePowerOff = mEventHistory.getEventIndex(Event.ALL_EVENT_TYPES);
|
||||
|
||||
// Ensure that futures were cancelled and the immediate flush occurred.
|
||||
assertEquals(0, mMockScheduledExecutorService.getFutures().size());
|
||||
|
||||
// Expect to see 2 executes from #saveToDisk, one for events and another for index.
|
||||
assertEquals(2, mMockScheduledExecutorService.getExecutes().size());
|
||||
|
||||
resetAndLoadEventHistory();
|
||||
|
||||
List<Event> events = mEventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
|
||||
assertEquals(2, events.size());
|
||||
assertTrue(events.containsAll(Lists.newArrayList(E4, E5)));
|
||||
|
||||
EventIndex indexAfterPowerOff = mEventHistory.getEventIndex(Event.ALL_EVENT_TYPES);
|
||||
assertEquals(indexBeforePowerOff, indexAfterPowerOff);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnDestroy() {
|
||||
mEventHistory.addEvent(E1);
|
||||
mEventHistory.addEvent(E2);
|
||||
mEventHistory.addEvent(E3);
|
||||
mEventHistory.addEvent(E4);
|
||||
mEventHistory.addEvent(E5);
|
||||
mEventHistory.saveToDisk();
|
||||
|
||||
mEventHistory.onDestroy();
|
||||
|
||||
List<Event> events = mEventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
|
||||
assertTrue(events.isEmpty());
|
||||
|
||||
EventIndex index = mEventHistory.getEventIndex(Event.ALL_EVENT_TYPES);
|
||||
assertTrue(index.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEventHistoriesImplFromDisk() {
|
||||
mEventHistory.addEvent(E1);
|
||||
mEventHistory.addEvent(E2);
|
||||
mEventHistory.addEvent(E3);
|
||||
mEventHistory.addEvent(E4);
|
||||
mEventHistory.addEvent(E5);
|
||||
mEventHistory.saveToDisk();
|
||||
|
||||
EventIndex indexBefore = mEventHistory.getEventIndex(Event.ALL_EVENT_TYPES);
|
||||
|
||||
Map<String, EventHistoryImpl> map = EventHistoryImpl.eventHistoriesImplFromDisk(
|
||||
EVENT_HISTORY_INJECTOR, mCacheDir, mMockScheduledExecutorService);
|
||||
assertEquals(1, map.size());
|
||||
assertTrue(map.containsKey("testdir"));
|
||||
|
||||
List<Event> events = map.get("testdir").queryEvents(Event.ALL_EVENT_TYPES, 0L,
|
||||
Long.MAX_VALUE);
|
||||
assertEquals(2, events.size());
|
||||
assertTrue(events.containsAll(Lists.newArrayList(E4, E5)));
|
||||
|
||||
EventIndex indexAfter = map.get("testdir").getEventIndex(Event.ALL_EVENT_TYPES);
|
||||
assertEquals(indexBefore, indexAfter);
|
||||
}
|
||||
|
||||
private void resetAndLoadEventHistory() {
|
||||
mEventHistory = new EventHistoryImpl(EVENT_HISTORY_INJECTOR, mFile,
|
||||
mMockScheduledExecutorService);
|
||||
mEventHistory.loadFromDisk();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ class MockScheduledExecutorService implements ScheduledExecutorService {
|
||||
long totalExecuted = 0;
|
||||
for (MockScheduledFuture<?> future : futuresCopy) {
|
||||
if (future.getDelay() < mTimeElapsedMillis) {
|
||||
future.getCommand().run();
|
||||
mExecutes.add(future.getCommand());
|
||||
future.getRunnable().run();
|
||||
mExecutes.add(future.getRunnable());
|
||||
totalExecuted += 1;
|
||||
} else {
|
||||
mFutures.add(future);
|
||||
@@ -96,7 +96,8 @@ class MockScheduledExecutorService implements ScheduledExecutorService {
|
||||
@Override
|
||||
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period,
|
||||
TimeUnit unit) {
|
||||
throw new UnsupportedOperationException();
|
||||
Preconditions.checkState(unit == TimeUnit.MILLISECONDS);
|
||||
return new MockScheduledFuture<>(command, period, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -132,7 +133,13 @@ class MockScheduledExecutorService implements ScheduledExecutorService {
|
||||
|
||||
@Override
|
||||
public <T> Future<T> submit(Callable<T> task) {
|
||||
throw new UnsupportedOperationException();
|
||||
MockScheduledFuture<T> future = new MockScheduledFuture<>(task, 0, TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
future.getCallable().call();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -141,11 +148,11 @@ class MockScheduledExecutorService implements ScheduledExecutorService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<?> submit(Runnable command) {
|
||||
mExecutes.add(command);
|
||||
MockScheduledFuture<?> future = new MockScheduledFuture<>(command, 0,
|
||||
public Future<?> submit(Runnable runnable) {
|
||||
mExecutes.add(runnable);
|
||||
MockScheduledFuture<?> future = new MockScheduledFuture<>(runnable, 0,
|
||||
TimeUnit.MILLISECONDS);
|
||||
future.getCommand().run();
|
||||
future.getRunnable().run();
|
||||
return future;
|
||||
}
|
||||
|
||||
@@ -181,12 +188,22 @@ class MockScheduledExecutorService implements ScheduledExecutorService {
|
||||
|
||||
class MockScheduledFuture<V> implements ScheduledFuture<V> {
|
||||
|
||||
private final Runnable mCommand;
|
||||
private final Runnable mRunnable;
|
||||
private final Callable<V> mCallable;
|
||||
private final long mDelay;
|
||||
private boolean mCancelled = false;
|
||||
|
||||
MockScheduledFuture(Runnable command, long delay, TimeUnit timeUnit) {
|
||||
mCommand = command;
|
||||
MockScheduledFuture(Runnable runnable, long delay, TimeUnit timeUnit) {
|
||||
this(runnable, null, delay);
|
||||
}
|
||||
|
||||
MockScheduledFuture(Callable<V> callable, long delay, TimeUnit timeUnit) {
|
||||
this(null, callable, delay);
|
||||
}
|
||||
|
||||
private MockScheduledFuture(Runnable runnable, Callable<V> callable, long delay) {
|
||||
mCallable = callable;
|
||||
mRunnable = runnable;
|
||||
mDelay = delay;
|
||||
}
|
||||
|
||||
@@ -194,8 +211,12 @@ class MockScheduledExecutorService implements ScheduledExecutorService {
|
||||
return mDelay;
|
||||
}
|
||||
|
||||
public Runnable getCommand() {
|
||||
return mCommand;
|
||||
public Runnable getRunnable() {
|
||||
return mRunnable;
|
||||
}
|
||||
|
||||
public Callable<V> getCallable() {
|
||||
return mCallable;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package com.android.server.people.data;
|
||||
|
||||
import static com.android.server.people.data.TestUtils.timestamp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -62,7 +64,8 @@ public final class UsageStatsQueryHelperTest {
|
||||
private static final LocusId LOCUS_ID_1 = new LocusId("locus_1");
|
||||
private static final LocusId LOCUS_ID_2 = new LocusId("locus_2");
|
||||
|
||||
@Mock private UsageStatsManagerInternal mUsageStatsManagerInternal;
|
||||
@Mock
|
||||
private UsageStatsManagerInternal mUsageStatsManagerInternal;
|
||||
|
||||
private TestPackageData mPackageData;
|
||||
private UsageStatsQueryHelper mHelper;
|
||||
@@ -233,7 +236,7 @@ public final class UsageStatsQueryHelperTest {
|
||||
private static class TestPackageData extends PackageData {
|
||||
|
||||
private final TestConversationStore mConversationStore;
|
||||
private final TestEventStore mEventStore = new TestEventStore();
|
||||
private final TestEventStore mEventStore;
|
||||
|
||||
TestPackageData(@NonNull String packageName, @UserIdInt int userId,
|
||||
@NonNull Predicate<String> isDefaultDialerPredicate,
|
||||
@@ -244,6 +247,7 @@ public final class UsageStatsQueryHelperTest {
|
||||
scheduledExecutorService, rootDir, helper);
|
||||
mConversationStore = new TestConversationStore(rootDir, scheduledExecutorService,
|
||||
helper);
|
||||
mEventStore = new TestEventStore(rootDir, scheduledExecutorService);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -261,8 +265,31 @@ public final class UsageStatsQueryHelperTest {
|
||||
|
||||
private static class TestEventStore extends EventStore {
|
||||
|
||||
private final EventHistoryImpl mShortcutEventHistory = new TestEventHistoryImpl();
|
||||
private final EventHistoryImpl mLocusEventHistory = new TestEventHistoryImpl();
|
||||
private static final long CURRENT_TIMESTAMP = timestamp("01-30 18:50");
|
||||
private static final EventIndex.Injector EVENT_INDEX_INJECTOR = new EventIndex.Injector() {
|
||||
@Override
|
||||
long currentTimeMillis() {
|
||||
return CURRENT_TIMESTAMP;
|
||||
}
|
||||
};
|
||||
private static final EventHistoryImpl.Injector EVENT_HISTORY_INJECTOR =
|
||||
new EventHistoryImpl.Injector() {
|
||||
@Override
|
||||
EventIndex createEventIndex() {
|
||||
return new EventIndex(EVENT_INDEX_INJECTOR);
|
||||
}
|
||||
};
|
||||
|
||||
private final EventHistoryImpl mShortcutEventHistory;
|
||||
private final EventHistoryImpl mLocusEventHistory;
|
||||
|
||||
TestEventStore(File rootDir, ScheduledExecutorService scheduledExecutorService) {
|
||||
super(rootDir, scheduledExecutorService);
|
||||
mShortcutEventHistory = new TestEventHistoryImpl(EVENT_HISTORY_INJECTOR, rootDir,
|
||||
scheduledExecutorService);
|
||||
mLocusEventHistory = new TestEventHistoryImpl(EVENT_HISTORY_INJECTOR, rootDir,
|
||||
scheduledExecutorService);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
@@ -280,6 +307,11 @@ public final class UsageStatsQueryHelperTest {
|
||||
|
||||
private final List<Event> mEvents = new ArrayList<>();
|
||||
|
||||
TestEventHistoryImpl(Injector injector, File rootDir,
|
||||
ScheduledExecutorService scheduledExecutorService) {
|
||||
super(injector, rootDir, scheduledExecutorService);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<Event> queryEvents(Set<Integer> eventTypes, long startTime, long endTime) {
|
||||
|
||||
Reference in New Issue
Block a user