All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.server.people.data.EventHistoryImpl Maven / Gradle / Ivy

/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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.os.FileUtils;
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;
    private final File mRootDir;

    // Event Type -> Event Index
    @GuardedBy("this")
    private final SparseArray mEventIndexArray = new SparseArray<>();

    @GuardedBy("this")
    private final EventList mRecentEvents = new EventList();

    private long mLastPruneTime;

    EventHistoryImpl(@NonNull File rootDir,
            @NonNull ScheduledExecutorService scheduledExecutorService) {
        this(new Injector(), rootDir, scheduledExecutorService);
    }

    @VisibleForTesting
    EventHistoryImpl(@NonNull Injector injector, @NonNull File rootDir,
            @NonNull ScheduledExecutorService scheduledExecutorService) {
        mInjector = injector;
        mScheduledExecutorService = scheduledExecutorService;
        mLastPruneTime = injector.currentTimeMillis();

        mRootDir = rootDir;
        File eventsDir = new File(mRootDir, EVENTS_DIR);
        mEventsProtoDiskReadWriter = new EventsProtoDiskReadWriter(eventsDir,
                mScheduledExecutorService);
        File indexesDir = new File(mRootDir, INDEXES_DIR);
        mEventIndexesProtoDiskReadWriter = new EventIndexesProtoDiskReadWriter(indexesDir,
                scheduledExecutorService);
    }

    @WorkerThread
    @NonNull
    static Map eventHistoriesImplFromDisk(File categoryDir,
            ScheduledExecutorService scheduledExecutorService) {
        return eventHistoriesImplFromDisk(new Injector(), categoryDir, scheduledExecutorService);
    }

    @VisibleForTesting
    @NonNull
    static Map eventHistoriesImplFromDisk(Injector injector,
            File categoryDir, ScheduledExecutorService scheduledExecutorService) {
        Map 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 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 synchronized EventIndex getEventIndex(@Event.EventType int eventType) {
        EventIndex eventIndex = mEventIndexArray.get(eventType);
        return eventIndex != null ? new EventIndex(eventIndex) : mInjector.createEventIndex();
    }

    @Override
    @NonNull
    public synchronized EventIndex getEventIndex(Set eventTypes) {
        EventIndex combined = mInjector.createEventIndex();
        for (@Event.EventType int eventType : eventTypes) {
            EventIndex eventIndex = mEventIndexArray.get(eventType);
            if (eventIndex != null) {
                combined = EventIndex.combine(combined, eventIndex);
            }
        }
        return combined;
    }

    @Override
    @NonNull
    public synchronized List queryEvents(Set eventTypes, long startTime,
            long endTime) {
        return mRecentEvents.queryEvents(eventTypes, startTime, endTime);
    }

    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();
        FileUtils.deleteContentsAndDir(mRootDir);
    }

    /** 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();
            mEventIndexArray.put(event.getType(), eventIndex);
        }
        eventIndex.addEvent(event.getTimestamp());
        mRecentEvents.add(event);
    }

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

        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 protoStreamWriter() {
            return (protoOutputStream, data) -> {
                for (Event event : data.getAllEvents()) {
                    long token = protoOutputStream.start(PeopleEventsProto.EVENTS);
                    event.writeToProto(protoOutputStream);
                    protoOutputStream.end(token);
                }
            };
        }

        @Override
        ProtoStreamReader protoStreamReader() {
            return protoInputStream -> {
                List 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> {

        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> 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> protoStreamReader() {
            return protoInputStream -> {
                SparseArray 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 indexes) {
            scheduleSave(INDEXES_FILE, indexes);
        }

        @MainThread
        void saveIndexesImmediately(SparseArray indexes) {
            saveImmediately(INDEXES_FILE, indexes);
        }

        @WorkerThread
        @Nullable
        SparseArray loadIndexesFromDisk() {
            return read(INDEXES_FILE);
        }

        @WorkerThread
        void deleteIndexesFile() {
            delete(INDEXES_FILE);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy