Commit 60e52e75 authored by Patrick Noland's avatar Patrick Noland Committed by Commit Bot

[Chromeshine] Implement basic UsageStats functionality

* Add UsageStatsService, the public interface for usage stats
* Add EventTracker, which allows addition and time range queries
of usage events
* Add TokenTracker and SuspensionTracker, which track token mappings
and suspensions, respectively

Bug:902574, 902499, 902496, 902494

Change-Id: I9831c03d78d90bbd301810bb2df36fd51ba2764e
Reviewed-on: https://chromium-review.googlesource.com/c/1354349Reviewed-by: default avatarTheresa <twellington@chromium.org>
Reviewed-by: default avatarFilip Gorski <fgorski@chromium.org>
Commit-Queue: Patrick Noland <pnoland@chromium.org>
Cr-Commit-Position: refs/heads/master@{#616088}
parent 45e9fe0c
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 java.util.ArrayList;
import java.util.List;
/**
* 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 List<WebsiteEvent> mWebsiteList;
public EventTracker() {
mWebsiteList = new ArrayList<>();
}
/** Query all events in the half-open range [start, end) */
public List<WebsiteEvent> queryWebsiteEvents(long start, long end) {
List<WebsiteEvent> sublist = sublistFromTimeRange(start, end);
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.
*/
public void addWebsiteEvent(WebsiteEvent event) {
if (mWebsiteList.size() > 0) {
assert event.getTimestamp() >= mWebsiteList.get(mWebsiteList.size() - 1).getTimestamp();
}
mWebsiteList.add(event);
}
private List<WebsiteEvent> sublistFromTimeRange(long start, long end) {
return mWebsiteList.subList(indexOf(start), indexOf(end));
}
private int indexOf(long time) {
for (int i = 0; i < mWebsiteList.size(); i++) {
boolean nextElementGreater =
(i + 1 < mWebsiteList.size()) && mWebsiteList.get(i + 1).getTimestamp() > time;
if (mWebsiteList.get(i).getTimestamp() == time || nextElementGreater) {
return i;
}
}
return mWebsiteList.size();
}
}
\ No newline at end of file
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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.chrome.browser.tabmodel.TabModelSelector;
/**
* Class that observes url and tab changes in order to track when browsing stops and starts for each
* visited fdqn.
*/
public class PageViewObserver {
private TabModelSelector mTabModelSelector;
private EventTracker mEventTracker;
public PageViewObserver(TabModelSelector tabModelSelector, EventTracker eventTracker) {
mTabModelSelector = tabModelSelector;
mEventTracker = eventTracker;
}
// TODO(pnoland): implement page view observation and push events to EventTracker.
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Class that tracks which sites are currently suspended.
*/
public class SuspensionTracker {
private Set<String> mSuspendedWebsites;
public SuspensionTracker() {
mSuspendedWebsites = new HashSet<String>();
}
public void setWebsitesSuspended(List<String> fqdns, boolean suspended) {
if (suspended) {
mSuspendedWebsites.addAll(fqdns);
} else {
mSuspendedWebsites.removeAll(fqdns);
}
}
public List<String> getAllSuspendedWebsites() {
List<String> result = new ArrayList<>();
result.addAll(mSuspendedWebsites);
return result;
}
public boolean isWebsiteSuspended(String fqdn) {
return mSuspendedWebsites.contains(fqdn);
}
}
\ No newline at end of file
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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;
/**
* Class that generates opaque names for use as tokens, which are themselves used as pseudonyms for
* a fully-qualified domain name (FQDN). These pseudonyms are used to identify the FQDN when
* reporting usage to the platform, which isn't trusted to know the actual FQDN.
*/
public class TokenGenerator {
private long mTokenCounter;
/**
* Default constructor for TokenGenerator.
*/
public TokenGenerator() {
this(1l);
}
/**
* Construct a TokenGenerator, starting the sequence of tokens at start.
*/
public TokenGenerator(long start) {
mTokenCounter = start;
}
/**
* Generate another token. The returned token is guaranteed to not duplicate any past tokens
* generated by this instance of TokenGenerator.
*/
public String nextToken() {
return Long.toString(mTokenCounter++);
}
}
\ No newline at end of file
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Class that tracks the mapping between tokens and fully-qualified domain names (FQDNs).
*/
public class TokenTracker {
private Map<String, String> mFqdnToTokenMap;
private Map<String, String> mTokenToFqdnMap;
private TokenGenerator mTokenGenerator;
public TokenTracker() {
mTokenToFqdnMap = new HashMap<>();
mFqdnToTokenMap = new HashMap<>();
mTokenGenerator = new TokenGenerator();
}
/**
* Associate a new token with FQDN, and return that token.
* If we're already tracking FQDN, return the corresponding token.
*/
public String startTrackingWebsite(String fqdn) {
if (isTrackingFqdn(fqdn)) {
return mFqdnToTokenMap.get(fqdn);
} else {
String token = mTokenGenerator.nextToken();
putMapping(token, fqdn);
return token;
}
}
/** Remove token and its associated FQDN, if we're already tracking token. */
public void stopTrackingToken(String token) {
if (isTrackingToken(token)) {
mFqdnToTokenMap.remove(getFqdnForToken(token));
mTokenToFqdnMap.remove(token);
}
}
/** Returns the token for a given FQDN, or null if we're not tracking that FQDN. */
public String getTokenForFqdn(String fqdn) {
return mFqdnToTokenMap.get(fqdn);
}
/** Get all the tokens we're tracking. */
public List<String> getAllTrackedTokens() {
List<String> result = new ArrayList<>();
result.addAll(mTokenToFqdnMap.keySet());
return result;
}
private String getFqdnForToken(String token) {
return mTokenToFqdnMap.get(token);
}
private void putMapping(String token, String fqdn) {
mTokenToFqdnMap.put(token, fqdn);
mFqdnToTokenMap.put(fqdn, token);
}
private boolean isTrackingFqdn(String fqdn) {
return mFqdnToTokenMap.containsKey(fqdn);
}
private boolean isTrackingToken(String token) {
return mTokenToFqdnMap.containsKey(token);
}
}
\ No newline at end of file
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 android.app.Activity;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import java.util.List;
/**
* Public interface for all usage stats related functionality. All calls to instances of
* UsageStatsService must be made on the UI thread.
*/
public class UsageStatsService {
private static UsageStatsService sInstance;
private EventTracker mEventTracker;
private SuspensionTracker mSuspensionTracker;
private TokenTracker mTokenTracker;
private boolean mOptInState;
/** Get the global instance of UsageStatsService */
public static UsageStatsService getInstance() {
if (sInstance == null) {
sInstance = new UsageStatsService();
}
return sInstance;
}
@VisibleForTesting
UsageStatsService() {
mEventTracker = new EventTracker();
mSuspensionTracker = new SuspensionTracker();
mTokenTracker = new TokenTracker();
}
/**
* Create a {@link PageViewObserver} for the given tab model selector and activity.
* @param tabModelSelector The tab model selector that should be used to get the current tab
* model.
* @param activity The activity in which page view events are occuring.
*/
public PageViewObserver createPageViewObserver(
TabModelSelector tabModelSelector, Activity activity) {
ThreadUtils.assertOnUiThread();
return new PageViewObserver(tabModelSelector, mEventTracker);
}
/** @return Whether the user has authorized DW to access usage stats data. */
public boolean getOptInState() {
ThreadUtils.assertOnUiThread();
// TODO(pnoland): return the value of the pref that controls opt in state.
return mOptInState;
}
/** Sets the user's opt in state. */
public void setOptInState(boolean state) {
ThreadUtils.assertOnUiThread();
// TODO(pnoland): set the value of the pref that controls opt in state.
mOptInState = state;
}
/** Query for all events that occurred in the half-open range [start, end) */
public List<WebsiteEvent> queryWebsiteEvents(long start, long end) {
ThreadUtils.assertOnUiThread();
return mEventTracker.queryWebsiteEvents(start, end);
}
/** Get all tokens that are currently being tracked. */
public List<String> getAllTrackedTokens() {
ThreadUtils.assertOnUiThread();
return mTokenTracker.getAllTrackedTokens();
}
/**
* Start tracking a full-qualified domain name(FQDN), returning the token used to identify it.
* If the FQDN is already tracked, this will return the existing token.
*/
public String startTrackingWebsite(String fqdn) {
ThreadUtils.assertOnUiThread();
return mTokenTracker.startTrackingWebsite(fqdn);
}
/**
* Stops tracking the site associated with the given token.
* If the token was not associated with a site, this does nothing.
*/
public void stopTrackingToken(String token) {
ThreadUtils.assertOnUiThread();
mTokenTracker.stopTrackingToken(token);
}
/**
* Suspend or unsuspend every site in FQDNs, depending on the truthiness of <c>suspended</c>.
*/
public void setWebsitesSuspended(List<String> fqdns, boolean suspended) {
ThreadUtils.assertOnUiThread();
mSuspensionTracker.setWebsitesSuspended(fqdns, suspended);
}
/** @return all the sites that are currently suspended. */
public List<String> getAllSuspendedWebsites() {
ThreadUtils.assertOnUiThread();
return mSuspensionTracker.getAllSuspendedWebsites();
}
/** @return whether the given site is suspended. */
public boolean isWebsiteSuspended(String fqdn) {
ThreadUtils.assertOnUiThread();
return mSuspensionTracker.isWebsiteSuspended(fqdn);
}
}
\ No newline at end of file
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 android.support.annotation.IntDef;
import android.support.annotation.NonNull;
/**
* A discrete point in time representing a single piece of browsing activity for a given
* fully-qualified domain name (FQDN).
*/
public class WebsiteEvent {
@IntDef({EventType.START, EventType.STOP})
public @interface EventType {
int START = 1;
int STOP = 2;
}
private final long mTimestamp;
private final String mFqdn;
private final @EventType int mEventType;
public WebsiteEvent(long timestamp, @NonNull String fqdn, @EventType int eventType) {
mTimestamp = timestamp;
assert fqdn != null;
mFqdn = fqdn;
mEventType = eventType;
}
public long getTimestamp() {
return mTimestamp;
}
public String getFqdn() {
return mFqdn;
}
public @EventType int getType() {
return mEventType;
}
}
\ No newline at end of file
......@@ -3,6 +3,8 @@
# found in the LICENSE file.
import("//chrome/android/feed/feed_java_sources.gni")
import(
"//chrome/android/java/src/org/chromium/chrome/browser/usage_stats/features.gni")
import("//components/feed/features.gni")
import("//components/offline_pages/buildflags/features.gni")
import("//device/vr/buildflags/buildflags.gni")
......@@ -1819,6 +1821,18 @@ if (enable_vr) {
]
}
if (enable_usage_stats) {
chrome_java_sources += [
"java/src/org/chromium/chrome/browser/usage_stats/EventTracker.java",
"java/src/org/chromium/chrome/browser/usage_stats/SuspensionTracker.java",
"java/src/org/chromium/chrome/browser/usage_stats/TokenTracker.java",
"java/src/org/chromium/chrome/browser/usage_stats/UsageStatsService.java",
"java/src/org/chromium/chrome/browser/usage_stats/WebsiteEvent.java",
"java/src/org/chromium/chrome/browser/usage_stats/TokenGenerator.java",
"java/src/org/chromium/chrome/browser/usage_stats/PageViewObserver.java",
]
}
chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/compositor/CompositorVisibilityTest.java",
"javatests/src/org/chromium/chrome/browser/ActivityTabProviderTest.java",
......@@ -2511,6 +2525,12 @@ chrome_junit_test_java_sources = [
"junit/src/org/chromium/chrome/browser/widget/selection/SelectionDelegateTest.java",
]
if (enable_usage_stats) {
chrome_junit_test_java_sources += [
"junit/src/org/chromium/chrome/browser/usage_stats/EventTrackerTest.java",
]
}
# Only used for testing, should not be shipped to end users.
if (enable_offline_pages_harness) {
chrome_java_sources += [ "java/src/org/chromium/chrome/browser/offlinepages/evaluation/OfflinePageEvaluationBridge.java" ]
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
import java.util.List;
/**
* Unit tests for EventTracker.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class EventTrackerTest {
private EventTracker mEventTracker;
@Before
public void setUp() {
mEventTracker = new EventTracker();
}
@Test
public void testRangeQueries() {
addEntries(100, 1l, 0l);
List<WebsiteEvent> result = mEventTracker.queryWebsiteEvents(0l, 50l);
assertEquals(result.size(), 50);
result = mEventTracker.queryWebsiteEvents(0l, 100l);
assertEquals(result.size(), 100);
}
private void addEntries(int quantity, long stepSize, long startTime) {
for (int i = 0; i < quantity; i++) {
mEventTracker.addWebsiteEvent(
new WebsiteEvent(startTime, "", WebsiteEvent.EventType.START));
startTime += stepSize;
}
}
}
fgorski@chromium.org
pnoland@chromium.org
# COMPONENT: UI>Browser>UsageStats
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment