Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 333c8e1e authored by Trung Lam's avatar Trung Lam Committed by Android (Google) Code Review
Browse files

Merge changes I8ad6ec29,Ic6c403a0

* changes:
  Implement onDestroy for ConversationStore for when an app gets uninstalled.
  Add persistence of Event and EventIndex during device reboot.
parents bc67dc3f e66d38a6
Loading
Loading
Loading
Loading
+17 −8
Original line number Diff line number Diff line
@@ -51,15 +51,18 @@ 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;

    // File name -> data class
    @GuardedBy("this")
    private Map<String, T> mScheduledFileDataMap = new ArrayMap<>();

@@ -75,15 +78,15 @@ 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;
    }

    @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 @@ abstract class AbstractProtoDiskReadWriter<T> {
        }

        mScheduledFuture = mScheduledExecutorService.schedule(this::flushScheduledData,
                mWriteDelayMs, TimeUnit.MILLISECONDS);
                DEFAULT_DISK_WRITE_DELAY, TimeUnit.MILLISECONDS);
    }

    /**
@@ -183,7 +186,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 +203,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 +220,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;
    }

+21 −11
Original line number Diff line number Diff line
@@ -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();
@@ -194,6 +191,15 @@ class ConversationStore {
        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 @@ 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 +269,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;
        }

@@ -328,5 +333,10 @@ class ConversationStore {
        void saveConversationsImmediately(@NonNull List<ConversationInfo> conversationInfos) {
            saveImmediately(mConversationInfoFileName, conversationInfos);
        }

        @WorkerThread
        void deleteConversationsFile() {
            delete(mConversationInfoFileName);
        }
    }
}
+1 −2
Original line number Diff line number Diff line
@@ -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);
            }
+52 −2
Original line number Diff line number Diff line
@@ -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);
        }
+307 −18
Original line number Diff line number Diff line
@@ -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);
        }
    }
}
Loading