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

src.com.android.server.people.data.EventIndex 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.IntDef;
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;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
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;

/**
 * The index of {@link Event}s. It is used for quickly looking up the time distribution of
 * {@link Event}s based on {@code Event#getTimestamp()}.
 *
 * 

The 64-bits {code long} is used as the bitmap index. Each bit is to denote whether there are * any events in a specified time slot. The least significant bit is for the most recent time slot. * And the most significant bit is for the oldest time slot. * *

Multiple {code long}s are used to index the events in different time grains. For the recent * events, the fine-grained bitmap index can provide the narrower time range. For the older events, * the coarse-grained bitmap index can cover longer period but can only provide wider time range. * *

E.g. the below chart shows how the bitmap indexes index the events in the past 24 hours: *

 * 2020/1/3                                                             2020/1/4
 *   0:00        4:00        8:00       12:00       16:00       20:00        0:00
 *  --+-----------------------------------------------------------------------+-  1 day per bit
 *  --+-----------+-----------+-----------+-----------+-----------+-----------+-  4 hours per bit
 *  --+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+-  1 hour per bit
 *                                                                     +++++++++  2 minutes per bit
 *  
*/ public class EventIndex { private static final String TAG = EventIndex.class.getSimpleName(); private static final int RETENTION_DAYS = 63; private static final int TIME_SLOT_ONE_DAY = 0; private static final int TIME_SLOT_FOUR_HOURS = 1; private static final int TIME_SLOT_ONE_HOUR = 2; private static final int TIME_SLOT_TWO_MINUTES = 3; @IntDef(prefix = {"TIME_SLOT_"}, value = { TIME_SLOT_ONE_DAY, TIME_SLOT_FOUR_HOURS, TIME_SLOT_ONE_HOUR, TIME_SLOT_TWO_MINUTES, }) @Retention(RetentionPolicy.SOURCE) private @interface TimeSlotType { } private static final int TIME_SLOT_TYPES_COUNT = 4; static final EventIndex EMPTY = new EventIndex(); private static final List>> TIME_SLOT_FACTORIES = Collections.unmodifiableList( Arrays.asList( EventIndex::createOneDayLongTimeSlot, EventIndex::createFourHoursLongTimeSlot, EventIndex::createOneHourLongTimeSlot, EventIndex::createTwoMinutesLongTimeSlot ) ); /** Combines the two {@link EventIndex} objects and returns the combined result. */ static EventIndex combine(EventIndex lhs, EventIndex rhs) { EventIndex older = lhs.mLastUpdatedTime < rhs.mLastUpdatedTime ? lhs : rhs; EventIndex younger = lhs.mLastUpdatedTime >= rhs.mLastUpdatedTime ? lhs : rhs; EventIndex combined = new EventIndex(older); combined.updateEventBitmaps(younger.mLastUpdatedTime); for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) { combined.mEventBitmaps[slotType] |= younger.mEventBitmaps[slotType]; } return combined; } private final long[] mEventBitmaps; private long mLastUpdatedTime; private final Object mLock = new Object(); private final Injector mInjector; EventIndex() { this(new Injector()); } EventIndex(@NonNull EventIndex from) { this(from.mInjector, from.mEventBitmaps, from.mLastUpdatedTime); } @VisibleForTesting 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 = Arrays.copyOf(eventBitmaps, TIME_SLOT_TYPES_COUNT); mLastUpdatedTime = lastUpdatedTime; } /** * Gets the most recent active time slot. A time slot is active if there is at least one event * occurred in that time slot. */ @Nullable public Range getMostRecentActiveTimeSlot() { synchronized (mLock) { for (int slotType = TIME_SLOT_TYPES_COUNT - 1; slotType >= 0; slotType--) { if (mEventBitmaps[slotType] == 0L) { continue; } Range lastTimeSlot = TIME_SLOT_FACTORIES.get(slotType).apply(mLastUpdatedTime); int numberOfTrailingZeros = Long.numberOfTrailingZeros(mEventBitmaps[slotType]); long offset = getDuration(lastTimeSlot) * numberOfTrailingZeros; return Range.create(lastTimeSlot.getLower() - offset, lastTimeSlot.getUpper() - offset); } } return null; } /** * Gets the active time slots. A time slot is active if there is at least one event occurred * in that time slot. * * @return active time slots in chronological order. */ @NonNull public List> getActiveTimeSlots() { List> activeTimeSlots = new ArrayList<>(); synchronized (mLock) { for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) { activeTimeSlots = combineTimeSlotLists(activeTimeSlots, getActiveTimeSlotsForType(slotType)); } } Collections.reverse(activeTimeSlots); return activeTimeSlots; } /** Returns whether this {@link EventIndex} instance is empty. */ public boolean isEmpty() { synchronized (mLock) { for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) { if (mEventBitmaps[slotType] != 0L) { return false; } } } return true; } /** * Adds an event to this index with the given event time. Before the new event is recorded, the * index is updated first with the current timestamp. */ void addEvent(long eventTime) { if (EMPTY == this) { throw new IllegalStateException("EMPTY instance is immutable"); } synchronized (mLock) { long currentTime = mInjector.currentTimeMillis(); updateEventBitmaps(currentTime); for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) { int offset = diffTimeSlots(slotType, eventTime, currentTime); if (offset < Long.SIZE) { mEventBitmaps[slotType] |= (1L << offset); } } } } /** Updates to make all bitmaps up to date. */ void update() { updateEventBitmaps(mInjector.currentTimeMillis()); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("EventIndex {"); sb.append("perDayEventBitmap=0b"); sb.append(Long.toBinaryString(mEventBitmaps[TIME_SLOT_ONE_DAY])); sb.append(", perFourHoursEventBitmap=0b"); sb.append(Long.toBinaryString(mEventBitmaps[TIME_SLOT_FOUR_HOURS])); sb.append(", perHourEventBitmap=0b"); sb.append(Long.toBinaryString(mEventBitmaps[TIME_SLOT_ONE_HOUR])); sb.append(", perTwoMinutesEventBitmap=0b"); sb.append(Long.toBinaryString(mEventBitmaps[TIME_SLOT_TWO_MINUTES])); sb.append(", lastUpdatedTime="); sb.append(DateFormat.format("yyyy-MM-dd HH:mm:ss", mLastUpdatedTime)); sb.append("}"); 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++) { int offset = diffTimeSlots(slotType, mLastUpdatedTime, currentTimeMillis); if (offset < Long.SIZE) { mEventBitmaps[slotType] <<= offset; } else { mEventBitmaps[slotType] = 0L; } } int bitsToClear = Long.SIZE - RETENTION_DAYS; mEventBitmaps[TIME_SLOT_ONE_DAY] <<= bitsToClear; mEventBitmaps[TIME_SLOT_ONE_DAY] >>>= bitsToClear; 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()); } private static long toEpochMilli(LocalDateTime localDateTime) { return localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); } private static long getDuration(Range timeSlot) { return timeSlot.getUpper() - timeSlot.getLower(); } /** * Finds the time slots for the given two timestamps and returns the distance (in the number * of time slots) between these two time slots. */ private static int diffTimeSlots(@TimeSlotType int timeSlotType, long fromTime, long toTime) { Function> timeSlotFactory = TIME_SLOT_FACTORIES.get(timeSlotType); Range fromSlot = timeSlotFactory.apply(fromTime); Range toSlot = timeSlotFactory.apply(toTime); return (int) ((toSlot.getLower() - fromSlot.getLower()) / getDuration(fromSlot)); } /** * Returns the active time slots for a specified type. The returned time slots are in * reverse-chronological order. */ private List> getActiveTimeSlotsForType(@TimeSlotType int timeSlotType) { long eventBitmap = mEventBitmaps[timeSlotType]; Range latestTimeSlot = TIME_SLOT_FACTORIES.get(timeSlotType).apply(mLastUpdatedTime); long startTime = latestTimeSlot.getLower(); final long duration = getDuration(latestTimeSlot); List> timeSlots = new ArrayList<>(); while (eventBitmap != 0) { int trailingZeros = Long.numberOfTrailingZeros(eventBitmap); if (trailingZeros > 0) { startTime -= duration * trailingZeros; eventBitmap >>>= trailingZeros; } if (eventBitmap != 0) { timeSlots.add(Range.create(startTime, startTime + duration)); startTime -= duration; eventBitmap >>>= 1; } } return timeSlots; } /** * Combines two lists of time slots into one. If one longer time slot covers one or multiple * shorter time slots, the smaller time slot(s) will be added to the result and the longer one * will be dropped. This ensures the returned list does not contain any overlapping time slots. */ private static List> combineTimeSlotLists(List> longerSlots, List> shorterSlots) { List> result = new ArrayList<>(); int i = 0; int j = 0; while (i < longerSlots.size() && j < shorterSlots.size()) { Range longerSlot = longerSlots.get(i); Range shorterSlot = shorterSlots.get(j); if (longerSlot.contains(shorterSlot)) { result.add(shorterSlot); i++; j++; } else if (longerSlot.getLower() < shorterSlot.getLower()) { result.add(shorterSlot); j++; } else { result.add(longerSlot); i++; } } if (i < longerSlots.size()) { result.addAll(longerSlots.subList(i, longerSlots.size())); } else if (j < shorterSlots.size()) { result.addAll(shorterSlots.subList(j, shorterSlots.size())); } return result; } /** * Finds and creates the time slot (duration = 1 day) that the given time falls into. */ @NonNull private static Range createOneDayLongTimeSlot(long time) { LocalDateTime beginTime = toLocalDateTime(time).truncatedTo(ChronoUnit.DAYS); return Range.create(toEpochMilli(beginTime), toEpochMilli(beginTime.plusDays(1))); } /** * Finds and creates the time slot (duration = 4 hours) that the given time falls into. */ @NonNull private static Range createFourHoursLongTimeSlot(long time) { int hourOfDay = toLocalDateTime(time).getHour(); LocalDateTime beginTime = toLocalDateTime(time).truncatedTo(ChronoUnit.HOURS).minusHours(hourOfDay % 4); return Range.create(toEpochMilli(beginTime), toEpochMilli(beginTime.plusHours(4))); } /** * Finds and creates the time slot (duration = 1 hour) that the given time falls into. */ @NonNull private static Range createOneHourLongTimeSlot(long time) { LocalDateTime beginTime = toLocalDateTime(time).truncatedTo(ChronoUnit.HOURS); return Range.create(toEpochMilli(beginTime), toEpochMilli(beginTime.plusHours(1))); } /** * Finds and creates the time slot (duration = 2 minutes) that the given time falls into. */ @NonNull private static Range createTwoMinutesLongTimeSlot(long time) { int minuteOfHour = toLocalDateTime(time).getMinute(); LocalDateTime beginTime = toLocalDateTime(time).truncatedTo( ChronoUnit.MINUTES).minusMinutes(minuteOfHour % 2); return Range.create(toEpochMilli(beginTime), toEpochMilli(beginTime.plusMinutes(2))); } @VisibleForTesting static class Injector { /** This should be the only way to get the current timestamp in {@code EventIndex}. */ long currentTimeMillis() { return System.currentTimeMillis(); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy