Merge changes I8ad6ec29,Ic6c403a0
* changes:
Implement onDestroy for ConversationStore for when an app gets uninstalled.
Add persistence of Event and EventIndex during device reboot.
diff --git a/services/people/java/com/android/server/people/data/AbstractProtoDiskReadWriter.java b/services/people/java/com/android/server/people/data/AbstractProtoDiskReadWriter.java
index 203e980..7672cd0 100644
--- a/services/people/java/com/android/server/people/data/AbstractProtoDiskReadWriter.java
+++ b/services/people/java/com/android/server/people/data/AbstractProtoDiskReadWriter.java
@@ -51,15 +51,18 @@
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;
+ // File name -> data class
@GuardedBy("this")
private Map<String, T> mScheduledFileDataMap = new ArrayMap<>();
@@ -75,15 +78,15 @@
*/
abstract ProtoStreamReader<T> protoStreamReader();
- AbstractProtoDiskReadWriter(@NonNull File rootDir, long writeDelayMs,
+ AbstractProtoDiskReadWriter(@NonNull File rootDir,
@NonNull ScheduledExecutorService scheduledExecutorService) {
mRootDir = rootDir;
- mWriteDelayMs = writeDelayMs;
mScheduledExecutorService = scheduledExecutorService;
}
@WorkerThread
- void delete(@NonNull String fileName) {
+ synchronized void delete(@NonNull String fileName) {
+ mScheduledFileDataMap.remove(fileName);
final File file = getFile(fileName);
if (!file.exists()) {
return;
@@ -174,7 +177,7 @@
}
mScheduledFuture = mScheduledExecutorService.schedule(this::flushScheduledData,
- mWriteDelayMs, TimeUnit.MILLISECONDS);
+ DEFAULT_DISK_WRITE_DELAY, TimeUnit.MILLISECONDS);
}
/**
@@ -183,7 +186,13 @@
*/
@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 +203,6 @@
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 +220,10 @@
return;
}
for (String fileName : mScheduledFileDataMap.keySet()) {
- T data = mScheduledFileDataMap.remove(fileName);
+ T data = mScheduledFileDataMap.get(fileName);
writeTo(fileName, data);
}
+ mScheduledFileDataMap.clear();
mScheduledFuture = null;
}
diff --git a/services/people/java/com/android/server/people/data/ConversationStore.java b/services/people/java/com/android/server/people/data/ConversationStore.java
index 3afb209..62e9da8 100644
--- a/services/people/java/com/android/server/people/data/ConversationStore.java
+++ b/services/people/java/com/android/server/people/data/ConversationStore.java
@@ -23,7 +23,6 @@
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 @@
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 @@
*/
@MainThread
void loadConversationsFromDisk() {
- mScheduledExecutorService.submit(() -> {
+ mScheduledExecutorService.execute(() -> {
synchronized (this) {
ConversationInfosProtoDiskReadWriter conversationInfosProtoDiskReadWriter =
getConversationInfosProtoDiskReadWriter();
@@ -194,6 +191,15 @@
return getConversation(mNotifChannelIdToShortcutIdMap.get(notifChannelId));
}
+ synchronized void onDestroy() {
+ mConversationInfoMap.clear();
+ mContactUriToShortcutIdMap.clear();
+ mLocusIdToShortcutIdMap.clear();
+ mNotifChannelIdToShortcutIdMap.clear();
+ mPhoneNumberToShortcutIdMap.clear();
+ mConversationInfosProtoDiskReadWriter.deleteConversationsFile();
+ }
+
@MainThread
private synchronized void updateConversationsInMemory(
@NonNull ConversationInfo conversationInfo) {
@@ -239,8 +245,7 @@
}
if (mConversationInfosProtoDiskReadWriter == null) {
mConversationInfosProtoDiskReadWriter = new ConversationInfosProtoDiskReadWriter(
- mPackageDir, CONVERSATIONS_FILE_NAME, DISK_WRITE_DELAY,
- mScheduledExecutorService);
+ mPackageDir, CONVERSATIONS_FILE_NAME, mScheduledExecutorService);
}
return mConversationInfosProtoDiskReadWriter;
}
@@ -264,16 +269,16 @@
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;
}
@@ -328,5 +333,10 @@
void saveConversationsImmediately(@NonNull List<ConversationInfo> conversationInfos) {
saveImmediately(mConversationInfoFileName, conversationInfos);
}
+
+ @WorkerThread
+ void deleteConversationsFile() {
+ delete(mConversationInfoFileName);
+ }
}
}
diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java
index 6b97c98..7eb2176 100644
--- a/services/people/java/com/android/server/people/data/DataManager.java
+++ b/services/people/java/com/android/server/people/data/DataManager.java
@@ -319,12 +319,11 @@
}
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);
}
diff --git a/services/people/java/com/android/server/people/data/Event.java b/services/people/java/com/android/server/people/data/Event.java
index 81411c0..a929f6f 100644
--- a/services/people/java/com/android/server/people/data/Event.java
+++ b/services/people/java/com/android/server/people/data/Event.java
@@ -20,7 +20,13 @@
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 @@
/** 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 @@
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 @@
/** 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 @@
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);
}
diff --git a/services/people/java/com/android/server/people/data/EventHistoryImpl.java b/services/people/java/com/android/server/people/data/EventHistoryImpl.java
index 6bef1db..85661c6 100644
--- a/services/people/java/com/android/server/people/data/EventHistoryImpl.java
+++ b/services/people/java/com/android/server/people/data/EventHistoryImpl.java
@@ -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 @@
@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 @@
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);
+ }
}
}
diff --git a/services/people/java/com/android/server/people/data/EventIndex.java b/services/people/java/com/android/server/people/data/EventIndex.java
index 069ec0e..47b6207 100644
--- a/services/people/java/com/android/server/people/data/EventIndex.java
+++ b/services/people/java/com/android/server/people/data/EventIndex.java
@@ -21,9 +21,14 @@
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.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 @@
* </pre>
*/
public class EventIndex {
+ private static final String TAG = EventIndex.class.getSimpleName();
private static final int RETENTION_DAYS = 63;
@@ -118,22 +125,23 @@
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 @@
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 @@
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());
diff --git a/services/people/java/com/android/server/people/data/EventList.java b/services/people/java/com/android/server/people/data/EventList.java
index d770f91..3788d6c 100644
--- a/services/people/java/com/android/server/people/data/EventList.java
+++ b/services/people/java/com/android/server/people/data/EventList.java
@@ -18,6 +18,8 @@
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 @@
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 @@
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();
diff --git a/services/people/java/com/android/server/people/data/EventStore.java b/services/people/java/com/android/server/people/data/EventStore.java
index c8d44ac..00d4241 100644
--- a/services/people/java/com/android/server/people/data/EventStore.java
+++ b/services/people/java/com/android/server/people/data/EventStore.java
@@ -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 @@
@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 @@
* 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 @@
* 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 @@
* @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 @@
}
/** 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 @@
*
* @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 @@
}
}
}
+
+ synchronized void onDestroy() {
+ for (Map<String, EventHistoryImpl> map : mEventHistoryMaps) {
+ for (EventHistoryImpl eventHistory : map.values()) {
+ eventHistory.onDestroy();
+ }
+ }
+ }
}
diff --git a/services/people/java/com/android/server/people/data/PackageData.java b/services/people/java/com/android/server/people/data/PackageData.java
index c55f972..d47e2cc 100644
--- a/services/people/java/com/android/server/people/data/PackageData.java
+++ b/services/people/java/com/android/server/people/data/PackageData.java
@@ -27,8 +27,10 @@
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 @@
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 @@
}
void onDestroy() {
- // TODO: STOPSHIP: Implements this method for the case of package being uninstalled.
+ mEventStore.onDestroy();
+ mConversationStore.onDestroy();
}
}
diff --git a/services/people/java/com/android/server/people/data/UserData.java b/services/people/java/com/android/server/people/data/UserData.java
index 7ca4b6c..d3cecce 100644
--- a/services/people/java/com/android/server/people/data/UserData.java
+++ b/services/people/java/com/android/server/people/data/UserData.java
@@ -73,9 +73,8 @@
// 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() {
diff --git a/services/tests/servicestests/src/com/android/server/people/data/AggregateEventHistoryImplTest.java b/services/tests/servicestests/src/com/android/server/people/data/AggregateEventHistoryImplTest.java
index b614a4f..443718d 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/AggregateEventHistoryImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/AggregateEventHistoryImplTest.java
@@ -21,11 +21,16 @@
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 @@
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);
}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/EventHistoryImplTest.java b/services/tests/servicestests/src/com/android/server/people/data/EventHistoryImplTest.java
index 43e1001..825ca10 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/EventHistoryImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/EventHistoryImplTest.java
@@ -21,18 +21,27 @@
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 @@
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 @@
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();
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/MockScheduledExecutorService.java b/services/tests/servicestests/src/com/android/server/people/data/MockScheduledExecutorService.java
index 8b8ba12..aecbc8d 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/MockScheduledExecutorService.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/MockScheduledExecutorService.java
@@ -54,8 +54,8 @@
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 @@
@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 @@
@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 @@
}
@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 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 @@
return mDelay;
}
- public Runnable getCommand() {
- return mCommand;
+ public Runnable getRunnable() {
+ return mRunnable;
+ }
+
+ public Callable<V> getCallable() {
+ return mCallable;
}
@Override
diff --git a/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java
index d444466..418067f 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java
@@ -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 @@
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 @@
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 @@
scheduledExecutorService, rootDir, helper);
mConversationStore = new TestConversationStore(rootDir, scheduledExecutorService,
helper);
+ mEventStore = new TestEventStore(rootDir, scheduledExecutorService);
}
@Override
@@ -261,8 +265,31 @@
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 @@
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) {