1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.people.data;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.text.format.DateFormat;
23 import android.util.Range;
24 import android.util.Slog;
25 import android.util.proto.ProtoInputStream;
26 import android.util.proto.ProtoOutputStream;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.server.people.PeopleEventIndexProto;
30 
31 import java.io.IOException;
32 import java.lang.annotation.Retention;
33 import java.lang.annotation.RetentionPolicy;
34 import java.time.Instant;
35 import java.time.LocalDateTime;
36 import java.time.ZoneId;
37 import java.time.temporal.ChronoUnit;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.Collections;
41 import java.util.List;
42 import java.util.Objects;
43 import java.util.TimeZone;
44 import java.util.function.Function;
45 
46 /**
47  * The index of {@link Event}s. It is used for quickly looking up the time distribution of
48  * {@link Event}s based on {@code Event#getTimestamp()}.
49  *
50  * <p>The 64-bits {code long} is used as the bitmap index. Each bit is to denote whether there are
51  * any events in a specified time slot. The least significant bit is for the most recent time slot.
52  * And the most significant bit is for the oldest time slot.
53  *
54  * <p>Multiple {code long}s are used to index the events in different time grains. For the recent
55  * events, the fine-grained bitmap index can provide the narrower time range. For the older events,
56  * the coarse-grained bitmap index can cover longer period but can only provide wider time range.
57  *
58  * <p>E.g. the below chart shows how the bitmap indexes index the events in the past 24 hours:
59  * <pre>
60  * 2020/1/3                                                             2020/1/4
61  *   0:00        4:00        8:00       12:00       16:00       20:00        0:00
62  *  --+-----------------------------------------------------------------------+-  1 day per bit
63  *  --+-----------+-----------+-----------+-----------+-----------+-----------+-  4 hours per bit
64  *  --+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+-  1 hour per bit
65  *                                                                     +++++++++  2 minutes per bit
66  *  </pre>
67  */
68 public class EventIndex {
69     private static final String TAG = EventIndex.class.getSimpleName();
70 
71     private static final int RETENTION_DAYS = 63;
72 
73     private static final int TIME_SLOT_ONE_DAY = 0;
74 
75     private static final int TIME_SLOT_FOUR_HOURS = 1;
76 
77     private static final int TIME_SLOT_ONE_HOUR = 2;
78 
79     private static final int TIME_SLOT_TWO_MINUTES = 3;
80 
81     @IntDef(prefix = {"TIME_SLOT_"}, value = {
82             TIME_SLOT_ONE_DAY,
83             TIME_SLOT_FOUR_HOURS,
84             TIME_SLOT_ONE_HOUR,
85             TIME_SLOT_TWO_MINUTES,
86     })
87     @Retention(RetentionPolicy.SOURCE)
88     private @interface TimeSlotType {
89     }
90 
91     private static final int TIME_SLOT_TYPES_COUNT = 4;
92 
93     static final EventIndex EMPTY = new EventIndex();
94 
95     private static final List<Function<Long, Range<Long>>> TIME_SLOT_FACTORIES =
96             Collections.unmodifiableList(
97                     Arrays.asList(
98                             EventIndex::createOneDayLongTimeSlot,
99                             EventIndex::createFourHoursLongTimeSlot,
100                             EventIndex::createOneHourLongTimeSlot,
101                             EventIndex::createTwoMinutesLongTimeSlot
102                     )
103             );
104 
105     /** Combines the two {@link EventIndex} objects and returns the combined result. */
combine(EventIndex lhs, EventIndex rhs)106     static EventIndex combine(EventIndex lhs, EventIndex rhs) {
107         EventIndex older = lhs.mLastUpdatedTime < rhs.mLastUpdatedTime ? lhs : rhs;
108         EventIndex younger = lhs.mLastUpdatedTime >= rhs.mLastUpdatedTime ? lhs : rhs;
109 
110         EventIndex combined = new EventIndex(older);
111         combined.updateEventBitmaps(younger.mLastUpdatedTime);
112 
113         for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) {
114             combined.mEventBitmaps[slotType] |= younger.mEventBitmaps[slotType];
115         }
116         return combined;
117     }
118 
119     private final long[] mEventBitmaps;
120 
121     private long mLastUpdatedTime;
122 
123     private final Object mLock = new Object();
124 
125     private final Injector mInjector;
126 
EventIndex()127     EventIndex() {
128         this(new Injector());
129     }
130 
EventIndex(@onNull EventIndex from)131     EventIndex(@NonNull EventIndex from) {
132         this(from.mInjector, from.mEventBitmaps, from.mLastUpdatedTime);
133     }
134 
135     @VisibleForTesting
EventIndex(@onNull Injector injector)136     EventIndex(@NonNull Injector injector) {
137         this(injector, new long[]{0L, 0L, 0L, 0L}, injector.currentTimeMillis());
138     }
139 
EventIndex(@onNull Injector injector, long[] eventBitmaps, long lastUpdatedTime)140     private EventIndex(@NonNull Injector injector, long[] eventBitmaps, long lastUpdatedTime) {
141         mInjector = injector;
142         mEventBitmaps = Arrays.copyOf(eventBitmaps, TIME_SLOT_TYPES_COUNT);
143         mLastUpdatedTime = lastUpdatedTime;
144     }
145 
146     /**
147      * Gets the most recent active time slot. A time slot is active if there is at least one event
148      * occurred in that time slot.
149      */
150     @Nullable
getMostRecentActiveTimeSlot()151     public Range<Long> getMostRecentActiveTimeSlot() {
152         synchronized (mLock) {
153             for (int slotType = TIME_SLOT_TYPES_COUNT - 1; slotType >= 0; slotType--) {
154                 if (mEventBitmaps[slotType] == 0L) {
155                     continue;
156                 }
157                 Range<Long> lastTimeSlot =
158                         TIME_SLOT_FACTORIES.get(slotType).apply(mLastUpdatedTime);
159                 int numberOfTrailingZeros = Long.numberOfTrailingZeros(mEventBitmaps[slotType]);
160                 long offset = getDuration(lastTimeSlot) * numberOfTrailingZeros;
161                 return Range.create(lastTimeSlot.getLower() - offset,
162                         lastTimeSlot.getUpper() - offset);
163             }
164         }
165         return null;
166     }
167 
168     /**
169      * Gets the active time slots. A time slot is active if there is at least one event occurred
170      * in that time slot.
171      *
172      * @return active time slots in chronological order.
173      */
174     @NonNull
getActiveTimeSlots()175     public List<Range<Long>> getActiveTimeSlots() {
176         List<Range<Long>> activeTimeSlots = new ArrayList<>();
177         synchronized (mLock) {
178             for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) {
179                 activeTimeSlots = combineTimeSlotLists(activeTimeSlots,
180                         getActiveTimeSlotsForType(slotType));
181             }
182         }
183         Collections.reverse(activeTimeSlots);
184         return activeTimeSlots;
185     }
186 
187     /** Returns whether this {@link EventIndex} instance is empty. */
isEmpty()188     public boolean isEmpty() {
189         synchronized (mLock) {
190             for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) {
191                 if (mEventBitmaps[slotType] != 0L) {
192                     return false;
193                 }
194             }
195         }
196         return true;
197     }
198 
199     /**
200      * Adds an event to this index with the given event time. Before the new event is recorded, the
201      * index is updated first with the current timestamp.
202      */
addEvent(long eventTime)203     void addEvent(long eventTime) {
204         if (EMPTY == this) {
205             throw new IllegalStateException("EMPTY instance is immutable");
206         }
207         synchronized (mLock) {
208             long currentTime = mInjector.currentTimeMillis();
209             updateEventBitmaps(currentTime);
210             for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) {
211                 int offset = diffTimeSlots(slotType, eventTime, currentTime);
212                 if (offset < Long.SIZE) {
213                     mEventBitmaps[slotType] |= (1L << offset);
214                 }
215             }
216         }
217     }
218 
219     /** Updates to make all bitmaps up to date. */
update()220     void update() {
221         updateEventBitmaps(mInjector.currentTimeMillis());
222     }
223 
224     @Override
toString()225     public String toString() {
226         StringBuilder sb = new StringBuilder();
227         sb.append("EventIndex {");
228         sb.append("perDayEventBitmap=0b");
229         sb.append(Long.toBinaryString(mEventBitmaps[TIME_SLOT_ONE_DAY]));
230         sb.append(", perFourHoursEventBitmap=0b");
231         sb.append(Long.toBinaryString(mEventBitmaps[TIME_SLOT_FOUR_HOURS]));
232         sb.append(", perHourEventBitmap=0b");
233         sb.append(Long.toBinaryString(mEventBitmaps[TIME_SLOT_ONE_HOUR]));
234         sb.append(", perTwoMinutesEventBitmap=0b");
235         sb.append(Long.toBinaryString(mEventBitmaps[TIME_SLOT_TWO_MINUTES]));
236         sb.append(", lastUpdatedTime=");
237         sb.append(DateFormat.format("yyyy-MM-dd HH:mm:ss", mLastUpdatedTime));
238         sb.append("}");
239         return sb.toString();
240     }
241 
242     @Override
equals(Object obj)243     public boolean equals(Object obj) {
244         if (this == obj) {
245             return true;
246         }
247         if (!(obj instanceof EventIndex)) {
248             return false;
249         }
250         EventIndex other = (EventIndex) obj;
251         return mLastUpdatedTime == other.mLastUpdatedTime
252                 && Arrays.equals(mEventBitmaps, other.mEventBitmaps);
253     }
254 
255     @Override
hashCode()256     public int hashCode() {
257         return Objects.hash(mLastUpdatedTime, mEventBitmaps);
258     }
259 
writeToProto(@onNull ProtoOutputStream protoOutputStream)260     synchronized void writeToProto(@NonNull ProtoOutputStream protoOutputStream) {
261         for (long bitmap : mEventBitmaps) {
262             protoOutputStream.write(PeopleEventIndexProto.EVENT_BITMAPS, bitmap);
263         }
264         protoOutputStream.write(PeopleEventIndexProto.LAST_UPDATED_TIME, mLastUpdatedTime);
265     }
266 
267     /** Shifts the event bitmaps to make them up-to-date. */
updateEventBitmaps(long currentTimeMillis)268     private void updateEventBitmaps(long currentTimeMillis) {
269         for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) {
270             int offset = diffTimeSlots(slotType, mLastUpdatedTime, currentTimeMillis);
271             if (offset < Long.SIZE) {
272                 mEventBitmaps[slotType] <<= offset;
273             } else {
274                 mEventBitmaps[slotType] = 0L;
275             }
276         }
277 
278         int bitsToClear = Long.SIZE - RETENTION_DAYS;
279         mEventBitmaps[TIME_SLOT_ONE_DAY] <<= bitsToClear;
280         mEventBitmaps[TIME_SLOT_ONE_DAY] >>>= bitsToClear;
281         mLastUpdatedTime = currentTimeMillis;
282     }
283 
readFromProto(@onNull ProtoInputStream protoInputStream)284     static EventIndex readFromProto(@NonNull ProtoInputStream protoInputStream) throws IOException {
285         int bitmapIndex = 0;
286         long[] eventBitmaps = new long[TIME_SLOT_TYPES_COUNT];
287         long lastUpdated = 0L;
288         while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
289             switch (protoInputStream.getFieldNumber()) {
290                 case (int) PeopleEventIndexProto.EVENT_BITMAPS:
291                     eventBitmaps[bitmapIndex++] = protoInputStream.readLong(
292                             PeopleEventIndexProto.EVENT_BITMAPS);
293                     break;
294                 case (int) PeopleEventIndexProto.LAST_UPDATED_TIME:
295                     lastUpdated = protoInputStream.readLong(
296                             PeopleEventIndexProto.LAST_UPDATED_TIME);
297                     break;
298                 default:
299                     Slog.e(TAG, "Could not read undefined field: "
300                             + protoInputStream.getFieldNumber());
301             }
302         }
303         return new EventIndex(new Injector(), eventBitmaps, lastUpdated);
304     }
305 
toLocalDateTime(long epochMilli)306     private static LocalDateTime toLocalDateTime(long epochMilli) {
307         return LocalDateTime.ofInstant(
308                 Instant.ofEpochMilli(epochMilli), TimeZone.getDefault().toZoneId());
309     }
310 
toEpochMilli(LocalDateTime localDateTime)311     private static long toEpochMilli(LocalDateTime localDateTime) {
312         return localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
313     }
314 
getDuration(Range<Long> timeSlot)315     private static long getDuration(Range<Long> timeSlot) {
316         return timeSlot.getUpper() - timeSlot.getLower();
317     }
318 
319     /**
320      * Finds the time slots for the given two timestamps and returns the distance (in the number
321      * of time slots) between these two time slots.
322      */
diffTimeSlots(@imeSlotType int timeSlotType, long fromTime, long toTime)323     private static int diffTimeSlots(@TimeSlotType int timeSlotType, long fromTime, long toTime) {
324         Function<Long, Range<Long>> timeSlotFactory = TIME_SLOT_FACTORIES.get(timeSlotType);
325         Range<Long> fromSlot = timeSlotFactory.apply(fromTime);
326         Range<Long> toSlot = timeSlotFactory.apply(toTime);
327         return (int) ((toSlot.getLower() - fromSlot.getLower()) / getDuration(fromSlot));
328     }
329 
330     /**
331      * Returns the active time slots for a specified type. The returned time slots are in
332      * reverse-chronological order.
333      */
getActiveTimeSlotsForType(@imeSlotType int timeSlotType)334     private List<Range<Long>> getActiveTimeSlotsForType(@TimeSlotType int timeSlotType) {
335         long eventBitmap = mEventBitmaps[timeSlotType];
336         Range<Long> latestTimeSlot = TIME_SLOT_FACTORIES.get(timeSlotType).apply(mLastUpdatedTime);
337         long startTime = latestTimeSlot.getLower();
338         final long duration = getDuration(latestTimeSlot);
339         List<Range<Long>> timeSlots = new ArrayList<>();
340         while (eventBitmap != 0) {
341             int trailingZeros = Long.numberOfTrailingZeros(eventBitmap);
342             if (trailingZeros > 0) {
343                 startTime -= duration * trailingZeros;
344                 eventBitmap >>>= trailingZeros;
345             }
346             if (eventBitmap != 0) {
347                 timeSlots.add(Range.create(startTime, startTime + duration));
348                 startTime -= duration;
349                 eventBitmap >>>= 1;
350             }
351         }
352         return timeSlots;
353     }
354 
355     /**
356      * Combines two lists of time slots into one. If one longer time slot covers one or multiple
357      * shorter time slots, the smaller time slot(s) will be added to the result and the longer one
358      * will be dropped. This ensures the returned list does not contain any overlapping time slots.
359      */
combineTimeSlotLists(List<Range<Long>> longerSlots, List<Range<Long>> shorterSlots)360     private static List<Range<Long>> combineTimeSlotLists(List<Range<Long>> longerSlots,
361             List<Range<Long>> shorterSlots) {
362         List<Range<Long>> result = new ArrayList<>();
363         int i = 0;
364         int j = 0;
365         while (i < longerSlots.size() && j < shorterSlots.size()) {
366             Range<Long> longerSlot = longerSlots.get(i);
367             Range<Long> shorterSlot = shorterSlots.get(j);
368             if (longerSlot.contains(shorterSlot)) {
369                 result.add(shorterSlot);
370                 i++;
371                 j++;
372             } else if (longerSlot.getLower() < shorterSlot.getLower()) {
373                 result.add(shorterSlot);
374                 j++;
375             } else {
376                 result.add(longerSlot);
377                 i++;
378             }
379         }
380         if (i < longerSlots.size()) {
381             result.addAll(longerSlots.subList(i, longerSlots.size()));
382         } else if (j < shorterSlots.size()) {
383             result.addAll(shorterSlots.subList(j, shorterSlots.size()));
384         }
385         return result;
386     }
387 
388     /**
389      * Finds and creates the time slot (duration = 1 day) that the given time falls into.
390      */
391     @NonNull
createOneDayLongTimeSlot(long time)392     private static Range<Long> createOneDayLongTimeSlot(long time) {
393         LocalDateTime beginTime = toLocalDateTime(time).truncatedTo(ChronoUnit.DAYS);
394         return Range.create(toEpochMilli(beginTime), toEpochMilli(beginTime.plusDays(1)));
395     }
396 
397     /**
398      * Finds and creates the time slot (duration = 4 hours) that the given time falls into.
399      */
400     @NonNull
createFourHoursLongTimeSlot(long time)401     private static Range<Long> createFourHoursLongTimeSlot(long time) {
402         int hourOfDay = toLocalDateTime(time).getHour();
403         LocalDateTime beginTime =
404                 toLocalDateTime(time).truncatedTo(ChronoUnit.HOURS).minusHours(hourOfDay % 4);
405         return Range.create(toEpochMilli(beginTime), toEpochMilli(beginTime.plusHours(4)));
406     }
407 
408     /**
409      * Finds and creates the time slot (duration = 1 hour) that the given time falls into.
410      */
411     @NonNull
createOneHourLongTimeSlot(long time)412     private static Range<Long> createOneHourLongTimeSlot(long time) {
413         LocalDateTime beginTime = toLocalDateTime(time).truncatedTo(ChronoUnit.HOURS);
414         return Range.create(toEpochMilli(beginTime), toEpochMilli(beginTime.plusHours(1)));
415     }
416 
417     /**
418      * Finds and creates the time slot (duration = 2 minutes) that the given time falls into.
419      */
420     @NonNull
createTwoMinutesLongTimeSlot(long time)421     private static Range<Long> createTwoMinutesLongTimeSlot(long time) {
422         int minuteOfHour = toLocalDateTime(time).getMinute();
423         LocalDateTime beginTime = toLocalDateTime(time).truncatedTo(
424                 ChronoUnit.MINUTES).minusMinutes(minuteOfHour % 2);
425         return Range.create(toEpochMilli(beginTime), toEpochMilli(beginTime.plusMinutes(2)));
426     }
427 
428     @VisibleForTesting
429     static class Injector {
430         /** This should be the only way to get the current timestamp in {@code EventIndex}. */
currentTimeMillis()431         long currentTimeMillis() {
432             return System.currentTimeMillis();
433         }
434     }
435 }
436