1 // Copyright 2018 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.usage_stats; 6 7 import org.chromium.base.Function; 8 import org.chromium.base.Promise; 9 import org.chromium.chrome.browser.usage_stats.WebsiteEventProtos.Timestamp; 10 11 import java.util.ArrayList; 12 import java.util.Arrays; 13 import java.util.Iterator; 14 import java.util.List; 15 import java.util.concurrent.TimeUnit; 16 17 /** 18 * In-memory store of {@link org.chromium.chrome.browser.usage_stats.WebsiteEvent} objects. 19 * Allows for addition of events and querying for all events in a time interval. 20 */ 21 public class EventTracker { 22 private final UsageStatsBridge mBridge; 23 private Promise<List<WebsiteEvent>> mRootPromise; 24 EventTracker(UsageStatsBridge bridge)25 public EventTracker(UsageStatsBridge bridge) { 26 mBridge = bridge; 27 mRootPromise = new Promise<>(); 28 // We need to add a dummy exception handler so that Promise doesn't complain when we 29 // call variants of then() that don't take a single callback. These variants set an 30 // exception handler on the returned promise, so they expect there to be one on the root 31 // promise. 32 mRootPromise.except((e) -> {}); 33 mBridge.getAllEvents((result) -> { 34 List<WebsiteEvent> events = new ArrayList<>(result.size()); 35 for (WebsiteEventProtos.WebsiteEvent protoEvent : result) { 36 events.add(new WebsiteEvent(getJavaTimestamp(protoEvent.getTimestamp()), 37 protoEvent.getFqdn(), protoEvent.getType().getNumber())); 38 } 39 mRootPromise.fulfill(events); 40 }); 41 } 42 43 /** Query all events in the half-open range [start, end) */ queryWebsiteEvents(long start, long end)44 public Promise<List<WebsiteEvent>> queryWebsiteEvents(long start, long end) { 45 assert start < end; 46 return mRootPromise.then((Function<List<WebsiteEvent>, List<WebsiteEvent>>) (result) -> { 47 UsageStatsMetricsReporter.reportMetricsEvent(UsageStatsMetricsEvent.QUERY_EVENTS); 48 List<WebsiteEvent> sublist = sublistFromTimeRange(start, end, result); 49 List<WebsiteEvent> sublistCopy = new ArrayList<>(sublist.size()); 50 sublistCopy.addAll(sublist); 51 return sublistCopy; 52 }); 53 } 54 55 /** 56 * Adds an event to the end of the list of events. Adding an event whose timestamp precedes the 57 * last event in the list is illegal. The returned promise will be fulfilled once persistence 58 * succeeds, and rejected if persistence fails. 59 */ addWebsiteEvent(WebsiteEvent event)60 public Promise<Void> addWebsiteEvent(WebsiteEvent event) { 61 final Promise<Void> writePromise = new Promise<>(); 62 mRootPromise.then((result) -> { 63 assert result.size() == 0 64 || event.getTimestamp() >= result.get(result.size() - 1).getTimestamp(); 65 66 List<WebsiteEventProtos.WebsiteEvent> eventsList = Arrays.asList(getProtoEvent(event)); 67 mBridge.addEvents(eventsList, (didSucceed) -> { 68 if (didSucceed) { 69 result.add(event); 70 writePromise.fulfill(null); 71 } else { 72 writePromise.reject(); 73 } 74 }); 75 }, (e) -> {}); 76 77 return writePromise; 78 } 79 80 /** Remove every item in the list of events. */ clearAll()81 public Promise<Void> clearAll() { 82 final Promise<Void> writePromise = new Promise<>(); 83 mRootPromise.then((result) -> { 84 mBridge.deleteAllEvents((didSucceed) -> { 85 if (didSucceed) { 86 result.clear(); 87 writePromise.fulfill(null); 88 } else { 89 writePromise.reject(); 90 } 91 }); 92 }, (e) -> {}); 93 return writePromise; 94 } 95 96 /** Removes items in the list in the half-open range [startTimeMs, endTimeMs). */ clearRange(long startTimeMs, long endTimeMs)97 public Promise<Void> clearRange(long startTimeMs, long endTimeMs) { 98 final Promise<Void> writePromise = new Promise<>(); 99 mRootPromise.then((result) -> { 100 mBridge.deleteEventsInRange(startTimeMs, endTimeMs, (didSucceed) -> { 101 if (didSucceed) { 102 sublistFromTimeRange(startTimeMs, endTimeMs, result).clear(); 103 writePromise.fulfill(null); 104 } else { 105 writePromise.reject(); 106 } 107 }); 108 }, (e) -> {}); 109 return writePromise; 110 } 111 112 /** Clear any events that have a domain in fqdns. */ clearDomains(List<String> fqdns)113 public Promise<Void> clearDomains(List<String> fqdns) { 114 final Promise<Void> writePromise = new Promise<>(); 115 mRootPromise.then((result) -> { 116 mBridge.deleteEventsWithMatchingDomains( 117 fqdns.toArray(new String[fqdns.size()]), (didSucceed) -> { 118 if (didSucceed) { 119 filterMatchingDomains(fqdns, result); 120 writePromise.fulfill(null); 121 } else { 122 writePromise.reject(); 123 } 124 }); 125 }, (e) -> {}); 126 return writePromise; 127 } 128 getProtoEvent(WebsiteEvent event)129 private WebsiteEventProtos.WebsiteEvent getProtoEvent(WebsiteEvent event) { 130 return WebsiteEventProtos.WebsiteEvent.newBuilder() 131 .setFqdn(event.getFqdn()) 132 .setTimestamp(getProtoTimestamp(event.getTimestamp())) 133 .setType(getProtoEventType(event.getType())) 134 .build(); 135 } 136 getProtoTimestamp(long timestampMs)137 private Timestamp getProtoTimestamp(long timestampMs) { 138 return Timestamp.newBuilder() 139 .setSeconds(TimeUnit.MILLISECONDS.toSeconds(timestampMs)) 140 .setNanos((int) TimeUnit.MILLISECONDS.toNanos(timestampMs % 1000)) 141 .build(); 142 } 143 getProtoEventType( @ebsiteEvent.EventType int eventType)144 private WebsiteEventProtos.WebsiteEvent.EventType getProtoEventType( 145 @WebsiteEvent.EventType int eventType) { 146 switch (eventType) { 147 case WebsiteEvent.EventType.START: 148 return WebsiteEventProtos.WebsiteEvent.EventType.START_BROWSING; 149 case WebsiteEvent.EventType.STOP: 150 return WebsiteEventProtos.WebsiteEvent.EventType.STOP_BROWSING; 151 default: 152 return WebsiteEventProtos.WebsiteEvent.EventType.UNKNOWN; 153 } 154 } 155 getJavaTimestamp(Timestamp protoTimestamp)156 private long getJavaTimestamp(Timestamp protoTimestamp) { 157 return TimeUnit.SECONDS.toMillis(protoTimestamp.getSeconds()) 158 + TimeUnit.NANOSECONDS.toMillis(protoTimestamp.getNanos()); 159 } 160 sublistFromTimeRange( long start, long end, List<WebsiteEvent> websiteList)161 private static List<WebsiteEvent> sublistFromTimeRange( 162 long start, long end, List<WebsiteEvent> websiteList) { 163 return websiteList.subList(indexOf(start, websiteList), indexOf(end, websiteList)); 164 } 165 indexOf(long time, List<WebsiteEvent> websiteList)166 private static int indexOf(long time, List<WebsiteEvent> websiteList) { 167 for (int i = 0; i < websiteList.size(); i++) { 168 if (time <= websiteList.get(i).getTimestamp()) return i; 169 } 170 return websiteList.size(); 171 } 172 filterMatchingDomains(List<String> fqdns, List<WebsiteEvent> websiteList)173 private static void filterMatchingDomains(List<String> fqdns, List<WebsiteEvent> websiteList) { 174 Iterator<WebsiteEvent> eventsIterator = websiteList.iterator(); 175 while (eventsIterator.hasNext()) { 176 if (fqdns.contains(eventsIterator.next().getFqdn())) { 177 eventsIterator.remove(); 178 } 179 } 180 } 181 }