chromium/chrome/android/java/src/org/chromium/chrome/browser/usage_stats/EventTracker.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.usage_stats;

import org.chromium.base.Promise;
import org.chromium.chrome.browser.usage_stats.WebsiteEventProtos.Timestamp;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * In-memory store of {@link org.chromium.chrome.browser.usage_stats.WebsiteEvent} objects.
 * Allows for addition of events and querying for all events in a time interval.
 */
public class EventTracker {
    private final UsageStatsBridge mBridge;
    private Promise<List<WebsiteEvent>> mRootPromise;

    public EventTracker(UsageStatsBridge bridge) {
        mBridge = bridge;
        mRootPromise = new Promise<>();
        // We need to add a placeholder exception handler so that Promise doesn't complain when we
        // call variants of then() that don't take a single callback. These variants set an
        // exception handler on the returned promise, so they expect there to be one on the root
        // promise.
        mRootPromise.except((e) -> {});
        mBridge.getAllEvents(
                (result) -> {
                    List<WebsiteEvent> events = new ArrayList<>(result.size());
                    for (WebsiteEventProtos.WebsiteEvent protoEvent : result) {
                        events.add(
                                new WebsiteEvent(
                                        getJavaTimestamp(protoEvent.getTimestamp()),
                                        protoEvent.getFqdn(),
                                        protoEvent.getType().getNumber()));
                    }
                    mRootPromise.fulfill(events);
                });
    }

    /** Query all events in the half-open range [start, end) */
    public Promise<List<WebsiteEvent>> queryWebsiteEvents(long start, long end) {
        assert start < end;
        return mRootPromise.then(
                (Function<List<WebsiteEvent>, List<WebsiteEvent>>)
                        (result) -> {
                            UsageStatsMetricsReporter.reportMetricsEvent(
                                    UsageStatsMetricsEvent.QUERY_EVENTS);
                            List<WebsiteEvent> sublist = sublistFromTimeRange(start, end, result);
                            List<WebsiteEvent> sublistCopy = new ArrayList<>(sublist.size());
                            sublistCopy.addAll(sublist);
                            return sublistCopy;
                        });
    }

    /**
     * Adds an event to the end of the list of events. Adding an event whose timestamp precedes the
     * last event in the list is illegal. The returned promise will be fulfilled once persistence
     * succeeds, and rejected if persistence fails.
     */
    public Promise<Void> addWebsiteEvent(WebsiteEvent event) {
        final Promise<Void> writePromise = new Promise<>();
        mRootPromise.then(
                (result) -> {
                    List<WebsiteEventProtos.WebsiteEvent> eventsList =
                            Arrays.asList(getProtoEvent(event));
                    mBridge.addEvents(
                            eventsList,
                            (didSucceed) -> {
                                if (didSucceed) {
                                    result.add(event);
                                    writePromise.fulfill(null);
                                } else {
                                    writePromise.reject();
                                }
                            });
                },
                (e) -> {});

        return writePromise;
    }

    /** Remove every item in the list of events. */
    public Promise<Void> clearAll() {
        final Promise<Void> writePromise = new Promise<>();
        mRootPromise.then(
                (result) -> {
                    mBridge.deleteAllEvents(
                            (didSucceed) -> {
                                if (didSucceed) {
                                    result.clear();
                                    writePromise.fulfill(null);
                                } else {
                                    writePromise.reject();
                                }
                            });
                },
                (e) -> {});
        return writePromise;
    }

    /** Removes items in the list in the half-open range [startTimeMs, endTimeMs). */
    public Promise<Void> clearRange(long startTimeMs, long endTimeMs) {
        final Promise<Void> writePromise = new Promise<>();
        mRootPromise.then(
                (result) -> {
                    mBridge.deleteEventsInRange(
                            startTimeMs,
                            endTimeMs,
                            (didSucceed) -> {
                                if (didSucceed) {
                                    sublistFromTimeRange(startTimeMs, endTimeMs, result).clear();
                                    writePromise.fulfill(null);
                                } else {
                                    writePromise.reject();
                                }
                            });
                },
                (e) -> {});
        return writePromise;
    }

    /** Clear any events that have a domain in fqdns. */
    public Promise<Void> clearDomains(List<String> fqdns) {
        final Promise<Void> writePromise = new Promise<>();
        mRootPromise.then(
                (result) -> {
                    mBridge.deleteEventsWithMatchingDomains(
                            fqdns.toArray(new String[fqdns.size()]),
                            (didSucceed) -> {
                                if (didSucceed) {
                                    filterMatchingDomains(fqdns, result);
                                    writePromise.fulfill(null);
                                } else {
                                    writePromise.reject();
                                }
                            });
                },
                (e) -> {});
        return writePromise;
    }

    private WebsiteEventProtos.WebsiteEvent getProtoEvent(WebsiteEvent event) {
        return WebsiteEventProtos.WebsiteEvent.newBuilder()
                .setFqdn(event.getFqdn())
                .setTimestamp(getProtoTimestamp(event.getTimestamp()))
                .setType(getProtoEventType(event.getType()))
                .build();
    }

    private Timestamp getProtoTimestamp(long timestampMs) {
        return Timestamp.newBuilder()
                .setSeconds(TimeUnit.MILLISECONDS.toSeconds(timestampMs))
                .setNanos((int) TimeUnit.MILLISECONDS.toNanos(timestampMs % 1000))
                .build();
    }

    private WebsiteEventProtos.WebsiteEvent.EventType getProtoEventType(
            @WebsiteEvent.EventType int eventType) {
        switch (eventType) {
            case WebsiteEvent.EventType.START:
                return WebsiteEventProtos.WebsiteEvent.EventType.START_BROWSING;
            case WebsiteEvent.EventType.STOP:
                return WebsiteEventProtos.WebsiteEvent.EventType.STOP_BROWSING;
            default:
                return WebsiteEventProtos.WebsiteEvent.EventType.UNKNOWN;
        }
    }

    private long getJavaTimestamp(Timestamp protoTimestamp) {
        return TimeUnit.SECONDS.toMillis(protoTimestamp.getSeconds())
                + TimeUnit.NANOSECONDS.toMillis(protoTimestamp.getNanos());
    }

    private static List<WebsiteEvent> sublistFromTimeRange(
            long start, long end, List<WebsiteEvent> websiteList) {
        return websiteList.subList(indexOf(start, websiteList), indexOf(end, websiteList));
    }

    private static int indexOf(long time, List<WebsiteEvent> websiteList) {
        for (int i = 0; i < websiteList.size(); i++) {
            if (time <= websiteList.get(i).getTimestamp()) return i;
        }
        return websiteList.size();
    }

    private static void filterMatchingDomains(List<String> fqdns, List<WebsiteEvent> websiteList) {
        Iterator<WebsiteEvent> eventsIterator = websiteList.iterator();
        while (eventsIterator.hasNext()) {
            if (fqdns.contains(eventsIterator.next().getFqdn())) {
                eventsIterator.remove();
            }
        }
    }
}