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:
Trung Lam
2020-02-10 15:42:43 -08:00
parent 9e5b450763
commit 88961798e6
14 changed files with 837 additions and 102 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View 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());

View File

@@ -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();

View File

@@ -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();
}
}
}
}

View File

@@ -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.
}
}

View File

@@ -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() {

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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) {