diff options
author | Trung Lam <lamtrung@google.com> | 2020-02-10 15:42:43 -0800 |
---|---|---|
committer | Trung Lam <lamtrung@google.com> | 2020-02-19 15:54:24 -0800 |
commit | 88961798e63a5d7c7f11200fcb1ff9c52b17d9a0 (patch) | |
tree | 3b8e2cf6c98a3a0baf30895abeabe21b0723f131 /services/people | |
parent | 9e5b4507635bbaab3acb307fef10a774c401c2c2 (diff) |
Add persistence of Event and EventIndex during device reboot.
Change-Id: Ic6c403a000a776eca2a3e9678c25862693a04c2e
Test: Built on device and unit tests.
Bug: 149356114
Diffstat (limited to 'services/people')
10 files changed, 608 insertions, 68 deletions
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 203e9804bfa3..78be00ff1630 100644 --- a/services/people/java/com/android/server/people/data/AbstractProtoDiskReadWriter.java +++ b/services/people/java/com/android/server/people/data/AbstractProtoDiskReadWriter.java @@ -51,11 +51,13 @@ import java.util.concurrent.TimeoutException; abstract class AbstractProtoDiskReadWriter<T> { private static final String TAG = AbstractProtoDiskReadWriter.class.getSimpleName(); + + // Common disk write delay that will be appropriate for most scenarios. + private static final long DEFAULT_DISK_WRITE_DELAY = 2L * DateUtils.MINUTE_IN_MILLIS; private static final long SHUTDOWN_DISK_WRITE_TIMEOUT = 5L * DateUtils.SECOND_IN_MILLIS; private final File mRootDir; private final ScheduledExecutorService mScheduledExecutorService; - private final long mWriteDelayMs; @GuardedBy("this") private ScheduledFuture<?> mScheduledFuture; @@ -75,10 +77,9 @@ abstract class AbstractProtoDiskReadWriter<T> { */ abstract ProtoStreamReader<T> protoStreamReader(); - AbstractProtoDiskReadWriter(@NonNull File rootDir, long writeDelayMs, + AbstractProtoDiskReadWriter(@NonNull File rootDir, @NonNull ScheduledExecutorService scheduledExecutorService) { mRootDir = rootDir; - mWriteDelayMs = writeDelayMs; mScheduledExecutorService = scheduledExecutorService; } @@ -174,7 +175,7 @@ abstract class AbstractProtoDiskReadWriter<T> { } mScheduledFuture = mScheduledExecutorService.schedule(this::flushScheduledData, - mWriteDelayMs, TimeUnit.MILLISECONDS); + DEFAULT_DISK_WRITE_DELAY, TimeUnit.MILLISECONDS); } /** @@ -183,7 +184,13 @@ abstract class AbstractProtoDiskReadWriter<T> { */ @MainThread synchronized void saveImmediately(@NonNull String fileName, @NonNull T data) { - if (mScheduledExecutorService.isShutdown()) { + mScheduledFileDataMap.put(fileName, data); + triggerScheduledFlushEarly(); + } + + @MainThread + private synchronized void triggerScheduledFlushEarly() { + if (mScheduledFileDataMap.isEmpty() || mScheduledExecutorService.isShutdown()) { return; } // Cancel existing future. @@ -194,7 +201,6 @@ abstract class AbstractProtoDiskReadWriter<T> { mScheduledFuture.cancel(true); } - mScheduledFileDataMap.put(fileName, data); // Submit flush and blocks until it completes. Blocking will prevent the device from // shutting down before flushing completes. Future<?> future = mScheduledExecutorService.submit(this::flushScheduledData); @@ -212,9 +218,10 @@ abstract class AbstractProtoDiskReadWriter<T> { return; } for (String fileName : mScheduledFileDataMap.keySet()) { - T data = mScheduledFileDataMap.remove(fileName); + T data = mScheduledFileDataMap.get(fileName); writeTo(fileName, data); } + mScheduledFileDataMap.clear(); mScheduledFuture = null; } 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 3afb209ae5cd..3e8f87a45fab 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.annotation.WorkerThread; import android.content.LocusId; import android.net.Uri; import android.text.TextUtils; -import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.Slog; import android.util.proto.ProtoInputStream; @@ -50,8 +49,6 @@ class ConversationStore { private static final String CONVERSATIONS_FILE_NAME = "conversations"; - private static final long DISK_WRITE_DELAY = 2L * DateUtils.MINUTE_IN_MILLIS; - // Shortcut ID -> Conversation Info @GuardedBy("this") private final Map<String, ConversationInfo> mConversationInfoMap = new ArrayMap<>(); @@ -92,7 +89,7 @@ class ConversationStore { */ @MainThread void loadConversationsFromDisk() { - mScheduledExecutorService.submit(() -> { + mScheduledExecutorService.execute(() -> { synchronized (this) { ConversationInfosProtoDiskReadWriter conversationInfosProtoDiskReadWriter = getConversationInfosProtoDiskReadWriter(); @@ -239,8 +236,7 @@ class ConversationStore { } if (mConversationInfosProtoDiskReadWriter == null) { mConversationInfosProtoDiskReadWriter = new ConversationInfosProtoDiskReadWriter( - mPackageDir, CONVERSATIONS_FILE_NAME, DISK_WRITE_DELAY, - mScheduledExecutorService); + mPackageDir, CONVERSATIONS_FILE_NAME, mScheduledExecutorService); } return mConversationInfosProtoDiskReadWriter; } @@ -264,16 +260,16 @@ class ConversationStore { return conversationInfo; } - /** Reads and writes {@link ConversationInfo} on disk. */ - static class ConversationInfosProtoDiskReadWriter extends + /** Reads and writes {@link ConversationInfo}s on disk. */ + private static class ConversationInfosProtoDiskReadWriter extends AbstractProtoDiskReadWriter<List<ConversationInfo>> { private final String mConversationInfoFileName; - ConversationInfosProtoDiskReadWriter(@NonNull File baseDir, + ConversationInfosProtoDiskReadWriter(@NonNull File rootDir, @NonNull String conversationInfoFileName, - long writeDelayMs, @NonNull ScheduledExecutorService scheduledExecutorService) { - super(baseDir, writeDelayMs, scheduledExecutorService); + @NonNull ScheduledExecutorService scheduledExecutorService) { + super(rootDir, scheduledExecutorService); mConversationInfoFileName = conversationInfoFileName; } 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 6b97c98b0029..7eb2176f2741 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 @@ public class DataManager { } pruneUninstalledPackageData(userData); - long currentTimeMillis = System.currentTimeMillis(); userData.forAllPackages(packageData -> { if (signal.isCanceled()) { return; } - packageData.getEventStore().pruneOldEvents(currentTimeMillis); + packageData.getEventStore().pruneOldEvents(); if (!packageData.isDefaultDialer()) { packageData.getEventStore().deleteEventHistories(EventStore.CATEGORY_CALL); } 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 81411c00db51..a929f6f68427 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.IntDef; import android.annotation.NonNull; import android.text.format.DateFormat; import android.util.ArraySet; +import android.util.Slog; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; +import com.android.server.people.PeopleEventProto; + +import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; @@ -29,6 +35,8 @@ import java.util.Set; /** An event representing the interaction with a specific conversation or app. */ public class Event { + private static final String TAG = Event.class.getSimpleName(); + public static final int TYPE_SHORTCUT_INVOCATION = 1; public static final int TYPE_NOTIFICATION_POSTED = 2; @@ -142,6 +150,36 @@ public class Event { return mDurationSeconds; } + /** Writes field members to {@link ProtoOutputStream}. */ + void writeToProto(@NonNull ProtoOutputStream protoOutputStream) { + protoOutputStream.write(PeopleEventProto.EVENT_TYPE, mType); + protoOutputStream.write(PeopleEventProto.TIME, mTimestamp); + protoOutputStream.write(PeopleEventProto.DURATION, mDurationSeconds); + } + + /** Reads from {@link ProtoInputStream} and constructs {@link Event}. */ + @NonNull + static Event readFromProto(@NonNull ProtoInputStream protoInputStream) throws IOException { + Event.Builder builder = new Event.Builder(); + while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (protoInputStream.getFieldNumber()) { + case (int) PeopleEventProto.EVENT_TYPE: + builder.setType(protoInputStream.readInt(PeopleEventProto.EVENT_TYPE)); + break; + case (int) PeopleEventProto.TIME: + builder.setTimestamp(protoInputStream.readLong(PeopleEventProto.TIME)); + break; + case (int) PeopleEventProto.DURATION: + builder.setDurationSeconds(protoInputStream.readInt(PeopleEventProto.DURATION)); + break; + default: + Slog.w(TAG, "Could not read undefined field: " + + protoInputStream.getFieldNumber()); + } + } + return builder.build(); + } + @Override public boolean equals(Object obj) { if (this == obj) { @@ -177,12 +215,14 @@ public class Event { /** Builder class for {@link Event} objects. */ static class Builder { - private final long mTimestamp; + private long mTimestamp; - private final int mType; + private int mType; private int mDurationSeconds; + private Builder() {} + Builder(long timestamp, @EventType int type) { mTimestamp = timestamp; mType = type; @@ -193,6 +233,16 @@ public class Event { return this; } + private Builder setTimestamp(long timestamp) { + mTimestamp = timestamp; + return this; + } + + private Builder setType(int type) { + mType = type; + return this; + } + Event build() { return new Event(this); } 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 6bef1db5582e..85661c622fc2 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,29 +171,42 @@ class EventHistoryImpl implements EventHistory { @Override @NonNull - public List<Event> queryEvents(Set<Integer> eventTypes, long startTime, long endTime) { + public synchronized List<Event> queryEvents(Set<Integer> eventTypes, long startTime, + long endTime) { return mRecentEvents.queryEvents(eventTypes, startTime, endTime); } - void addEvent(Event event) { - EventIndex eventIndex = mEventIndexArray.get(event.getType()); - if (eventIndex == null) { - eventIndex = mInjector.createEventIndex(); - mEventIndexArray.put(event.getType(), eventIndex); - } - eventIndex.addEvent(event.getTimestamp()); - mRecentEvents.add(event); + synchronized void addEvent(Event event) { + pruneOldEvents(); + addEventInMemory(event); + mEventsProtoDiskReadWriter.scheduleEventsSave(mRecentEvents); + mEventIndexesProtoDiskReadWriter.scheduleIndexesSave(mEventIndexArray); } - void onDestroy() { + synchronized void onDestroy() { mEventIndexArray.clear(); mRecentEvents.clear(); - // TODO: STOPSHIP: Delete the data files. + mEventsProtoDiskReadWriter.deleteRecentEventsFile(); + mEventIndexesProtoDiskReadWriter.deleteIndexesFile(); } /** Deletes the events data that exceeds the retention period. */ - void pruneOldEvents(long currentTimeMillis) { - // TODO: STOPSHIP: Delete the old events data files. + synchronized void pruneOldEvents() { + long currentTime = mInjector.currentTimeMillis(); + if (currentTime - mLastPruneTime > PRUNE_OLD_EVENTS_DELAY) { + mRecentEvents.removeOldEvents(currentTime - MAX_EVENTS_AGE); + mLastPruneTime = currentTime; + } + } + + private synchronized void addEventInMemory(Event event) { + EventIndex eventIndex = mEventIndexArray.get(event.getType()); + if (eventIndex == null) { + eventIndex = mInjector.createEventIndex(); + mEventIndexArray.put(event.getType(), eventIndex); + } + eventIndex.addEvent(event.getTimestamp()); + mRecentEvents.add(event); } @VisibleForTesting @@ -95,5 +215,174 @@ class EventHistoryImpl implements EventHistory { 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 069ec0e80ee4..47b620773180 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.NonNull; import android.annotation.Nullable; import android.text.format.DateFormat; import android.util.Range; +import android.util.Slog; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.people.PeopleEventIndexProto; +import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.time.Instant; @@ -34,6 +39,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.TimeZone; import java.util.function.Function; @@ -60,6 +66,7 @@ import java.util.function.Function; * </pre> */ public class EventIndex { + private static final String TAG = EventIndex.class.getSimpleName(); private static final int RETENTION_DAYS = 63; @@ -118,22 +125,23 @@ public class EventIndex { private final Injector mInjector; EventIndex() { - mInjector = new Injector(); - mEventBitmaps = new long[]{0L, 0L, 0L, 0L}; - mLastUpdatedTime = mInjector.currentTimeMillis(); + this(new Injector()); } - EventIndex(EventIndex from) { - mInjector = new Injector(); - mEventBitmaps = Arrays.copyOf(from.mEventBitmaps, TIME_SLOT_TYPES_COUNT); - mLastUpdatedTime = from.mLastUpdatedTime; + EventIndex(@NonNull EventIndex from) { + this(from.mInjector, Arrays.copyOf(from.mEventBitmaps, TIME_SLOT_TYPES_COUNT), + from.mLastUpdatedTime); } @VisibleForTesting - EventIndex(Injector injector) { + EventIndex(@NonNull Injector injector) { + this(injector, new long[]{0L, 0L, 0L, 0L}, injector.currentTimeMillis()); + } + + private EventIndex(@NonNull Injector injector, long[] eventBitmaps, long lastUpdatedTime) { mInjector = injector; - mEventBitmaps = new long[]{0L, 0L, 0L, 0L}; - mLastUpdatedTime = mInjector.currentTimeMillis(); + mEventBitmaps = eventBitmaps; + mLastUpdatedTime = lastUpdatedTime; } /** @@ -232,6 +240,31 @@ public class EventIndex { return sb.toString(); } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof EventIndex)) { + return false; + } + EventIndex other = (EventIndex) obj; + return mLastUpdatedTime == other.mLastUpdatedTime + && Arrays.equals(mEventBitmaps, other.mEventBitmaps); + } + + @Override + public int hashCode() { + return Objects.hash(mLastUpdatedTime, mEventBitmaps); + } + + synchronized void writeToProto(@NonNull ProtoOutputStream protoOutputStream) { + for (long bitmap : mEventBitmaps) { + protoOutputStream.write(PeopleEventIndexProto.EVENT_BITMAPS, bitmap); + } + protoOutputStream.write(PeopleEventIndexProto.LAST_UPDATED_TIME, mLastUpdatedTime); + } + /** Shifts the event bitmaps to make them up-to-date. */ private void updateEventBitmaps(long currentTimeMillis) { for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) { @@ -249,6 +282,28 @@ public class EventIndex { mLastUpdatedTime = currentTimeMillis; } + static EventIndex readFromProto(@NonNull ProtoInputStream protoInputStream) throws IOException { + int bitmapIndex = 0; + long[] eventBitmaps = new long[TIME_SLOT_TYPES_COUNT]; + long lastUpdated = 0L; + while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (protoInputStream.getFieldNumber()) { + case (int) PeopleEventIndexProto.EVENT_BITMAPS: + eventBitmaps[bitmapIndex++] = protoInputStream.readLong( + PeopleEventIndexProto.EVENT_BITMAPS); + break; + case (int) PeopleEventIndexProto.LAST_UPDATED_TIME: + lastUpdated = protoInputStream.readLong( + PeopleEventIndexProto.LAST_UPDATED_TIME); + break; + default: + Slog.e(TAG, "Could not read undefined field: " + + protoInputStream.getFieldNumber()); + } + } + return new EventIndex(new Injector(), eventBitmaps, lastUpdated); + } + private static LocalDateTime toLocalDateTime(long epochMilli) { return LocalDateTime.ofInstant( Instant.ofEpochMilli(epochMilli), TimeZone.getDefault().toZoneId()); 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 d770f912ea40..3788d6c92acf 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 @@ package com.android.server.people.data; import android.annotation.NonNull; +import com.android.internal.util.CollectionUtils; + import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -41,6 +43,16 @@ class EventList { mEvents.add(index, event); } + + /** + * Call #add on each event to keep the order. + */ + void addAll(@NonNull List<Event> events) { + for (Event event : events) { + add(event); + } + } + /** * Returns a {@link List} of {@link Event}s whose timestamps are between the specified {@code * fromTimestamp}, inclusive, and {@code toTimestamp} exclusive, and match the specified event @@ -73,6 +85,44 @@ class EventList { mEvents.clear(); } + /** + * Returns a copy of events. + */ + @NonNull + List<Event> getAllEvents() { + return CollectionUtils.copyOf(mEvents); + } + + /** + * Remove events that are older than the specified cut off threshold timestamp. + */ + void removeOldEvents(long cutOffThreshold) { + + // Everything before the cut off is considered old, and should be removed. + int cutOffIndex = firstIndexOnOrAfter(cutOffThreshold); + if (cutOffIndex == 0) { + return; + } + + // Clear entire list if the cut off is greater than the last element. + int eventsSize = mEvents.size(); + if (cutOffIndex == eventsSize) { + mEvents.clear(); + return; + } + + // Reorder the list starting from the cut off index. + int i = 0; + for (; cutOffIndex < eventsSize; i++, cutOffIndex++) { + mEvents.set(i, mEvents.get(cutOffIndex)); + } + + // Clear the list after reordering. + if (eventsSize > i) { + mEvents.subList(i, eventsSize).clear(); + } + } + /** Returns the first index whose timestamp is greater or equal to the provided timestamp. */ private int firstIndexOnOrAfter(long timestamp) { int result = mEvents.size(); 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 c8d44ac07106..00d4241fc5f7 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 @@ class EventStore { @Retention(RetentionPolicy.SOURCE) @interface EventCategory {} + @GuardedBy("this") private final List<Map<String, EventHistoryImpl>> mEventHistoryMaps = new ArrayList<>(); + private final List<File> mEventsCategoryDirs = new ArrayList<>(); + private final ScheduledExecutorService mScheduledExecutorService; - EventStore() { + EventStore(@NonNull File packageDir, + @NonNull ScheduledExecutorService scheduledExecutorService) { mEventHistoryMaps.add(CATEGORY_SHORTCUT_BASED, new ArrayMap<>()); mEventHistoryMaps.add(CATEGORY_LOCUS_ID_BASED, new ArrayMap<>()); mEventHistoryMaps.add(CATEGORY_CALL, new ArrayMap<>()); mEventHistoryMaps.add(CATEGORY_SMS, new ArrayMap<>()); mEventHistoryMaps.add(CATEGORY_CLASS_BASED, new ArrayMap<>()); + + File eventDir = new File(packageDir, "event"); + mEventsCategoryDirs.add(CATEGORY_SHORTCUT_BASED, new File(eventDir, "shortcut")); + mEventsCategoryDirs.add(CATEGORY_LOCUS_ID_BASED, new File(eventDir, "locus")); + mEventsCategoryDirs.add(CATEGORY_CALL, new File(eventDir, "call")); + mEventsCategoryDirs.add(CATEGORY_SMS, new File(eventDir, "sms")); + mEventsCategoryDirs.add(CATEGORY_CLASS_BASED, new File(eventDir, "class")); + + mScheduledExecutorService = scheduledExecutorService; + } + + /** + * Loads existing {@link EventHistoryImpl}s from disk. This should be called when device powers + * on and user is unlocked. + */ + @MainThread + void loadFromDisk() { + mScheduledExecutorService.execute(() -> { + synchronized (this) { + for (@EventCategory int category = 0; category < mEventsCategoryDirs.size(); + category++) { + File categoryDir = mEventsCategoryDirs.get(category); + Map<String, EventHistoryImpl> existingEventHistoriesImpl = + EventHistoryImpl.eventHistoriesImplFromDisk(categoryDir, + mScheduledExecutorService); + mEventHistoryMaps.get(category).putAll(existingEventHistoriesImpl); + } + } + }); + } + + /** + * Flushes all {@link EventHistoryImpl}s to disk. Should be called when device is shutting down. + */ + synchronized void saveToDisk() { + for (Map<String, EventHistoryImpl> map : mEventHistoryMaps) { + for (EventHistoryImpl eventHistory : map.values()) { + eventHistory.saveToDisk(); + } + } } /** @@ -74,7 +124,7 @@ class EventStore { * name. */ @Nullable - EventHistory getEventHistory(@EventCategory int category, String key) { + synchronized EventHistory getEventHistory(@EventCategory int category, String key) { return mEventHistoryMaps.get(category).get(key); } @@ -87,8 +137,11 @@ class EventStore { * name. */ @NonNull - EventHistoryImpl getOrCreateEventHistory(@EventCategory int category, String key) { - return mEventHistoryMaps.get(category).computeIfAbsent(key, k -> new EventHistoryImpl()); + synchronized EventHistoryImpl getOrCreateEventHistory(@EventCategory int category, String key) { + return mEventHistoryMaps.get(category).computeIfAbsent(key, + k -> new EventHistoryImpl( + new File(mEventsCategoryDirs.get(category), Uri.encode(key)), + mScheduledExecutorService)); } /** @@ -97,7 +150,7 @@ class EventStore { * @param key Category-specific key, it can be shortcut ID, locus ID, phone number, or class * name. */ - void deleteEventHistory(@EventCategory int category, String key) { + synchronized void deleteEventHistory(@EventCategory int category, String key) { EventHistoryImpl eventHistory = mEventHistoryMaps.get(category).remove(key); if (eventHistory != null) { eventHistory.onDestroy(); @@ -105,16 +158,18 @@ class EventStore { } /** Deletes all the events and index data for the specified category from disk. */ - void deleteEventHistories(@EventCategory int category) { + synchronized void deleteEventHistories(@EventCategory int category) { + for (EventHistoryImpl eventHistory : mEventHistoryMaps.get(category).values()) { + eventHistory.onDestroy(); + } mEventHistoryMaps.get(category).clear(); - // TODO: Implement this method to delete the data from disk. } /** Deletes the events data that exceeds the retention period. */ - void pruneOldEvents(long currentTimeMillis) { + synchronized void pruneOldEvents() { for (Map<String, EventHistoryImpl> map : mEventHistoryMaps) { for (EventHistoryImpl eventHistory : map.values()) { - eventHistory.pruneOldEvents(currentTimeMillis); + eventHistory.pruneOldEvents(); } } } @@ -125,7 +180,8 @@ class EventStore { * * @param keyChecker Check whether there exists a conversation contains this key. */ - void pruneOrphanEventHistories(@EventCategory int category, Predicate<String> keyChecker) { + synchronized void pruneOrphanEventHistories(@EventCategory int category, + Predicate<String> keyChecker) { Set<String> keys = mEventHistoryMaps.get(category).keySet(); List<String> keysToDelete = new ArrayList<>(); for (String key : keys) { @@ -141,4 +197,12 @@ class EventStore { } } } + + synchronized void onDestroy() { + for (Map<String, EventHistoryImpl> map : mEventHistoryMaps) { + for (EventHistoryImpl eventHistory : map.values()) { + eventHistory.onDestroy(); + } + } + } } 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 c55f97205bc5..e041a20cb965 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.Nullable; import android.annotation.UserIdInt; import android.content.LocusId; import android.text.TextUtils; +import android.util.ArrayMap; import java.io.File; +import java.util.Map; import java.util.concurrent.ScheduledExecutorService; import java.util.function.Consumer; import java.util.function.Predicate; @@ -63,22 +65,50 @@ public class PackageData { mUserId = userId; mPackageDataDir = new File(perUserPeopleDataDir, mPackageName); + mPackageDataDir.mkdirs(); + mConversationStore = new ConversationStore(mPackageDataDir, scheduledExecutorService, helper); - mEventStore = new EventStore(); + mEventStore = new EventStore(mPackageDataDir, scheduledExecutorService); mIsDefaultDialerPredicate = isDefaultDialerPredicate; mIsDefaultSmsAppPredicate = isDefaultSmsAppPredicate; } - /** Called when user is unlocked. */ - void loadFromDisk() { - mPackageDataDir.mkdirs(); + /** + * Returns a map of package directory names as keys and their associated {@link PackageData}. + * This should be called when device is powered on and unlocked. + */ + @NonNull + static Map<String, PackageData> packagesDataFromDisk(@UserIdInt int userId, + @NonNull Predicate<String> isDefaultDialerPredicate, + @NonNull Predicate<String> isDefaultSmsAppPredicate, + @NonNull ScheduledExecutorService scheduledExecutorService, + @NonNull File perUserPeopleDataDir, + @NonNull ContactsQueryHelper helper) { + Map<String, PackageData> results = new ArrayMap<>(); + File[] packageDirs = perUserPeopleDataDir.listFiles(File::isDirectory); + if (packageDirs == null) { + return results; + } + for (File packageDir : packageDirs) { + PackageData packageData = new PackageData(packageDir.getName(), userId, + isDefaultDialerPredicate, isDefaultSmsAppPredicate, scheduledExecutorService, + perUserPeopleDataDir, helper); + packageData.loadFromDisk(); + results.put(packageDir.getName(), packageData); + } + return results; + } + + private void loadFromDisk() { mConversationStore.loadConversationsFromDisk(); + mEventStore.loadFromDisk(); } /** Called when device is shutting down. */ void saveToDisk() { mConversationStore.saveConversationsToDisk(); + mEventStore.saveToDisk(); } @NonNull @@ -222,6 +252,7 @@ public class PackageData { } void onDestroy() { - // TODO: STOPSHIP: Implements this method for the case of package being uninstalled. + mEventStore.onDestroy(); + // TODO: STOPSHIP: Destroy conversation info for the case of package being uninstalled. } } 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 7ca4b6c76a36..d3cecceed884 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 @@ class UserData { // Ensures per user root directory for people data is present, and attempt to load // data from disk. mPerUserPeopleDataDir.mkdirs(); - for (PackageData packageData : mPackageDataMap.values()) { - packageData.loadFromDisk(); - } + mPackageDataMap.putAll(PackageData.packagesDataFromDisk(mUserId, this::isDefaultDialer, + this::isDefaultSmsApp, mScheduledExecutorService, mPerUserPeopleDataDir, mHelper)); } void setUserStopped() { |