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