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 }