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