// Copyright 2015 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.physicalweb;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.components.location.LocationUtils;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.concurrent.TimeUnit;
import javax.annotation.concurrent.ThreadSafe;
/**
* Centralizes UMA data collection for the Physical Web feature.
*/
@ThreadSafe
public class PhysicalWebUma {
private static final String TAG = "PhysicalWeb";
private static final String HAS_DEFERRED_METRICS_KEY = "PhysicalWeb.HasDeferredMetrics";
private static final String OPT_IN_DECLINE_BUTTON_PRESS_COUNT =
"PhysicalWeb.OptIn.DeclineButtonPressed";
private static final String OPT_IN_ENABLE_BUTTON_PRESS_COUNT =
"PhysicalWeb.OptIn.EnableButtonPressed";
private static final String OPT_IN_HIGH_PRIORITY_NOTIFICATION_COUNT =
"PhysicalWeb.OptIn.HighPriorityNotificationShown";
private static final String OPT_IN_MIN_PRIORITY_NOTIFICATION_COUNT =
"PhysicalWeb.OptIn.MinPriorityNotificationShown";
private static final String OPT_IN_NOTIFICATION_PRESS_COUNT =
"PhysicalWeb.OptIn.NotificationPressed";
private static final String PREFS_FEATURE_DISABLED_COUNT = "PhysicalWeb.Prefs.FeatureDisabled";
private static final String PREFS_FEATURE_ENABLED_COUNT = "PhysicalWeb.Prefs.FeatureEnabled";
private static final String PREFS_LOCATION_DENIED_COUNT = "PhysicalWeb.Prefs.LocationDenied";
private static final String PREFS_LOCATION_GRANTED_COUNT = "PhysicalWeb.Prefs.LocationGranted";
private static final String PWS_BACKGROUND_RESOLVE_TIMES = "PhysicalWeb.ResolveTime.Background";
private static final String PWS_FOREGROUND_RESOLVE_TIMES = "PhysicalWeb.ResolveTime.Foreground";
private static final String PWS_REFRESH_RESOLVE_TIMES = "PhysicalWeb.ResolveTime.Refresh";
private static final String OPT_IN_NOTIFICATION_PRESS_DELAYS =
"PhysicalWeb.ReferralDelay.OptInNotification";
private static final String STANDARD_NOTIFICATION_PRESS_DELAYS =
"PhysicalWeb.ReferralDelay.StandardNotification";
private static final String URL_SELECTED_COUNT = "PhysicalWeb.UrlSelected";
private static final String TOTAL_URLS_INITIAL_COUNTS =
"PhysicalWeb.TotalUrls.OnInitialDisplay";
private static final String TOTAL_URLS_REFRESH_COUNTS =
"PhysicalWeb.TotalUrls.OnRefresh";
private static final String ACTIVITY_REFERRALS = "PhysicalWeb.ActivityReferral";
private static final String PHYSICAL_WEB_STATE = "PhysicalWeb.State";
private static final String LAUNCH_FROM_PREFERENCES = "LaunchFromPreferences";
private static final String LAUNCH_FROM_DIAGNOSTICS = "LaunchFromDiagnostics";
private static final String BLUETOOTH = "Bluetooth";
private static final String DATA_CONNECTION = "DataConnection";
private static final String LOCATION_PERMISSION = "LocationPermission";
private static final String LOCATION_SERVICES = "LocationServices";
private static final String PREFERENCE = "Preference";
private static final int BOOLEAN_BOUNDARY = 2;
private static final int TRISTATE_BOUNDARY = 3;
/**
* Records a URL selection.
*/
public static void onUrlSelected(Context context) {
handleAction(context, URL_SELECTED_COUNT);
}
/**
* Records a tap on the opt-in decline button.
*/
public static void onOptInDeclineButtonPressed(Context context) {
handleAction(context, OPT_IN_DECLINE_BUTTON_PRESS_COUNT);
}
/**
* Records a tap on the opt-in enable button.
*/
public static void onOptInEnableButtonPressed(Context context) {
handleAction(context, OPT_IN_ENABLE_BUTTON_PRESS_COUNT);
}
/**
* Records a display of a high priority opt-in notification.
*/
public static void onOptInHighPriorityNotificationShown(Context context) {
handleAction(context, OPT_IN_HIGH_PRIORITY_NOTIFICATION_COUNT);
}
/**
* Records a display of a min priority opt-in notification.
*/
public static void onOptInMinPriorityNotificationShown(Context context) {
handleAction(context, OPT_IN_MIN_PRIORITY_NOTIFICATION_COUNT);
}
/**
* Records a display of the opt-in activity.
*/
public static void onOptInNotificationPressed(Context context) {
handleAction(context, OPT_IN_NOTIFICATION_PRESS_COUNT);
}
/**
* Records when the user disables the Physical Web fetaure.
*/
public static void onPrefsFeatureDisabled(Context context) {
handleAction(context, PREFS_FEATURE_DISABLED_COUNT);
}
/**
* Records when the user enables the Physical Web fetaure.
*/
public static void onPrefsFeatureEnabled(Context context) {
handleAction(context, PREFS_FEATURE_ENABLED_COUNT);
}
/**
* Records when the user denies the location permission when enabling the Physical Web from the
* privacy settings menu.
*/
public static void onPrefsLocationDenied(Context context) {
handleAction(context, PREFS_LOCATION_DENIED_COUNT);
}
/**
* Records when the user grants the location permission when enabling the Physical Web from the
* privacy settings menu.
*/
public static void onPrefsLocationGranted(Context context) {
handleAction(context, PREFS_LOCATION_GRANTED_COUNT);
}
/**
* Records a response time from PWS for a resolution during a background scan.
* @param duration The length of time PWS took to respond.
*/
public static void onBackgroundPwsResolution(Context context, long duration) {
handleTime(context, PWS_BACKGROUND_RESOLVE_TIMES, duration, TimeUnit.MILLISECONDS);
}
/**
* Records a response time from PWS for a resolution during a foreground scan that is not
* explicitly user-initiated through a refresh.
* @param duration The length of time PWS took to respond.
*/
public static void onForegroundPwsResolution(Context context, long duration) {
handleTime(context, PWS_FOREGROUND_RESOLVE_TIMES, duration, TimeUnit.MILLISECONDS);
}
/**
* Records a response time from PWS for a resolution during a foreground scan that is explicitly
* user-initiated through a refresh.
* @param duration The length of time PWS took to respond.
*/
public static void onRefreshPwsResolution(Context context, long duration) {
handleTime(context, PWS_REFRESH_RESOLVE_TIMES, duration, TimeUnit.MILLISECONDS);
}
/**
* Records number of URLs displayed to a user when the URL list is first displayed.
* @param numUrls The number of URLs displayed to a user.
*/
public static void onUrlsDisplayed(Context context, int numUrls) {
if (LibraryLoader.isInitialized()) {
RecordHistogram.recordCountHistogram(TOTAL_URLS_INITIAL_COUNTS, numUrls);
} else {
storeValue(context, TOTAL_URLS_INITIAL_COUNTS, numUrls);
}
}
/**
* Records number of URLs displayed to a user when the user refreshes the URL list.
* @param numUrls The number of URLs displayed to a user.
*/
public static void onUrlsRefreshed(Context context, int numUrls) {
if (LibraryLoader.isInitialized()) {
RecordHistogram.recordCountHistogram(TOTAL_URLS_REFRESH_COUNTS, numUrls);
} else {
storeValue(context, TOTAL_URLS_REFRESH_COUNTS, numUrls);
}
}
/**
* Records a ListUrlActivity referral.
* @param refer The type of referral. This enum is listed as PhysicalWebActivityReferer in
* histograms.xml.
*/
public static void onActivityReferral(Context context, int referer) {
handleEnum(context, ACTIVITY_REFERRALS, referer, ListUrlsActivity.REFERER_BOUNDARY);
switch (referer) {
case ListUrlsActivity.NOTIFICATION_REFERER:
handleTime(context, STANDARD_NOTIFICATION_PRESS_DELAYS,
UrlManager.getInstance().getTimeSinceNotificationUpdate(),
TimeUnit.MILLISECONDS);
break;
case ListUrlsActivity.OPTIN_REFERER:
handleTime(context, OPT_IN_NOTIFICATION_PRESS_DELAYS,
UrlManager.getInstance().getTimeSinceNotificationUpdate(),
TimeUnit.MILLISECONDS);
break;
case ListUrlsActivity.PREFERENCE_REFERER:
recordPhysicalWebState(context, LAUNCH_FROM_PREFERENCES);
break;
case ListUrlsActivity.DIAGNOSTICS_REFERER:
recordPhysicalWebState(context, LAUNCH_FROM_DIAGNOSTICS);
break;
default:
break;
}
}
/**
* Calculate a Physical Web state.
* The Physical Web state includes:
* - The location provider
* - The location permission
* - The bluetooth status
* - The data connection status
* - The Physical Web preference status
*/
public static void recordPhysicalWebState(Context context, String actionName) {
LocationUtils locationUtils = LocationUtils.getInstance();
handleEnum(context, createStateString(LOCATION_SERVICES, actionName),
locationUtils.isSystemLocationSettingEnabled() ? 1 : 0, BOOLEAN_BOUNDARY);
handleEnum(context, createStateString(LOCATION_PERMISSION, actionName),
locationUtils.hasAndroidLocationPermission() ? 1 : 0, BOOLEAN_BOUNDARY);
handleEnum(context, createStateString(BLUETOOTH, actionName),
Utils.getBluetoothEnabledStatus(), TRISTATE_BOUNDARY);
handleEnum(context, createStateString(DATA_CONNECTION, actionName),
Utils.isDataConnectionActive() ? 1 : 0, BOOLEAN_BOUNDARY);
int preferenceState = 2;
if (!PhysicalWeb.isOnboarding()) {
preferenceState = PhysicalWeb.isPhysicalWebPreferenceEnabled() ? 1 : 0;
}
handleEnum(context, createStateString(PREFERENCE, actionName),
preferenceState, TRISTATE_BOUNDARY);
}
/**
* Uploads metrics that we have deferred for uploading.
*/
public static void uploadDeferredMetrics() {
// Read the metrics.
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
if (prefs.getBoolean(HAS_DEFERRED_METRICS_KEY, false)) {
AsyncTask.THREAD_POOL_EXECUTOR.execute(new UmaUploader(prefs));
}
}
private static String createStateString(String stateName, String actionName) {
return PHYSICAL_WEB_STATE + "." + stateName + "." + actionName;
}
private static void storeAction(Context context, String key) {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
int count = prefs.getInt(key, 0);
prefs.edit()
.putBoolean(HAS_DEFERRED_METRICS_KEY, true)
.putInt(key, count + 1)
.apply();
}
private static void storeValue(Context context, String key, Object value) {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
SharedPreferences.Editor prefsEditor = prefs.edit();
JSONArray values = null;
try {
values = new JSONArray(prefs.getString(key, "[]"));
values.put(value);
prefsEditor
.putBoolean(HAS_DEFERRED_METRICS_KEY, true)
.putString(key, values.toString())
.apply();
} catch (JSONException e) {
Log.e(TAG, "JSONException when storing " + key + " stats", e);
prefsEditor.remove(key).apply();
return;
}
prefsEditor.putString(key, values.toString()).apply();
}
private static void handleAction(Context context, String key) {
if (LibraryLoader.isInitialized()) {
RecordUserAction.record(key);
} else {
storeAction(context, key);
}
}
private static void handleTime(Context context, String key, long duration, TimeUnit tu) {
if (LibraryLoader.isInitialized()) {
RecordHistogram.recordTimesHistogram(key, duration, tu);
} else {
storeValue(context, key, duration);
}
}
private static void handleEnum(Context context, String key, int value, int boundary) {
if (LibraryLoader.isInitialized()) {
RecordHistogram.recordEnumeratedHistogram(key, value, boundary);
} else {
storeValue(context, key, value);
}
}
private static class UmaUploader implements Runnable {
SharedPreferences mPrefs;
UmaUploader(SharedPreferences prefs) {
mPrefs = prefs;
}
@Override
public void run() {
uploadActions(URL_SELECTED_COUNT);
uploadActions(OPT_IN_DECLINE_BUTTON_PRESS_COUNT);
uploadActions(OPT_IN_ENABLE_BUTTON_PRESS_COUNT);
uploadActions(OPT_IN_HIGH_PRIORITY_NOTIFICATION_COUNT);
uploadActions(OPT_IN_MIN_PRIORITY_NOTIFICATION_COUNT);
uploadActions(OPT_IN_NOTIFICATION_PRESS_COUNT);
uploadActions(PREFS_FEATURE_DISABLED_COUNT);
uploadActions(PREFS_FEATURE_ENABLED_COUNT);
uploadActions(PREFS_LOCATION_DENIED_COUNT);
uploadActions(PREFS_LOCATION_GRANTED_COUNT);
uploadTimes(PWS_BACKGROUND_RESOLVE_TIMES, TimeUnit.MILLISECONDS);
uploadTimes(PWS_FOREGROUND_RESOLVE_TIMES, TimeUnit.MILLISECONDS);
uploadTimes(PWS_REFRESH_RESOLVE_TIMES, TimeUnit.MILLISECONDS);
uploadTimes(STANDARD_NOTIFICATION_PRESS_DELAYS, TimeUnit.MILLISECONDS);
uploadTimes(OPT_IN_NOTIFICATION_PRESS_DELAYS, TimeUnit.MILLISECONDS);
uploadCounts(TOTAL_URLS_INITIAL_COUNTS);
uploadCounts(TOTAL_URLS_REFRESH_COUNTS);
uploadEnums(ACTIVITY_REFERRALS, ListUrlsActivity.REFERER_BOUNDARY);
uploadEnums(createStateString(LOCATION_SERVICES, LAUNCH_FROM_DIAGNOSTICS),
BOOLEAN_BOUNDARY);
uploadEnums(createStateString(LOCATION_PERMISSION, LAUNCH_FROM_DIAGNOSTICS),
BOOLEAN_BOUNDARY);
uploadEnums(createStateString(BLUETOOTH, LAUNCH_FROM_DIAGNOSTICS), TRISTATE_BOUNDARY);
uploadEnums(createStateString(DATA_CONNECTION, LAUNCH_FROM_DIAGNOSTICS),
BOOLEAN_BOUNDARY);
uploadEnums(createStateString(PREFERENCE, LAUNCH_FROM_DIAGNOSTICS), TRISTATE_BOUNDARY);
uploadEnums(createStateString(LOCATION_SERVICES, LAUNCH_FROM_PREFERENCES),
BOOLEAN_BOUNDARY);
uploadEnums(createStateString(LOCATION_PERMISSION, LAUNCH_FROM_PREFERENCES),
BOOLEAN_BOUNDARY);
uploadEnums(createStateString(BLUETOOTH, LAUNCH_FROM_PREFERENCES), TRISTATE_BOUNDARY);
uploadEnums(createStateString(DATA_CONNECTION, LAUNCH_FROM_PREFERENCES),
BOOLEAN_BOUNDARY);
uploadEnums(createStateString(PREFERENCE, LAUNCH_FROM_PREFERENCES), TRISTATE_BOUNDARY);
removePref(HAS_DEFERRED_METRICS_KEY);
}
private void removePref(String key) {
mPrefs.edit()
.remove(key)
.apply();
}
private static Number[] parseJsonNumberArray(String jsonArrayStr) {
try {
JSONArray values = new JSONArray(jsonArrayStr);
Number[] array = new Number[values.length()];
for (int i = 0; i < values.length(); i++) {
Object object = values.get(i);
if (!(object instanceof Number)) {
return null;
}
array[i] = (Number) object;
}
return array;
} catch (JSONException e) {
return null;
}
}
private static Long[] parseJsonLongArray(String jsonArrayStr) {
Number[] numbers = parseJsonNumberArray(jsonArrayStr);
if (numbers == null) {
return null;
}
Long[] array = new Long[numbers.length];
for (int i = 0; i < numbers.length; i++) {
array[i] = numbers[i].longValue();
}
return array;
}
private static Integer[] parseJsonIntegerArray(String jsonArrayStr) {
Number[] numbers = parseJsonNumberArray(jsonArrayStr);
if (numbers == null) {
return null;
}
Integer[] array = new Integer[numbers.length];
for (int i = 0; i < numbers.length; i++) {
array[i] = numbers[i].intValue();
}
return array;
}
private void uploadActions(String key) {
int count = mPrefs.getInt(key, 0);
removePref(key);
for (int i = 0; i < count; i++) {
RecordUserAction.record(key);
}
}
private void uploadTimes(final String key, final TimeUnit tu) {
String jsonTimesStr = mPrefs.getString(key, "[]");
removePref(key);
Long[] times = parseJsonLongArray(jsonTimesStr);
if (times == null) {
Log.e(TAG, "Error reporting " + key + " with values: " + jsonTimesStr);
return;
}
for (Long time : times) {
RecordHistogram.recordTimesHistogram(key, time, TimeUnit.MILLISECONDS);
}
}
private void uploadCounts(final String key) {
String jsonCountsStr = mPrefs.getString(key, "[]");
removePref(key);
Integer[] counts = parseJsonIntegerArray(jsonCountsStr);
if (counts == null) {
Log.e(TAG, "Error reporting " + key + " with values: " + jsonCountsStr);
return;
}
for (Integer count: counts) {
RecordHistogram.recordCountHistogram(key, count);
}
}
private void uploadEnums(final String key, int boundary) {
String jsonEnumsStr = mPrefs.getString(key, "[]");
removePref(key);
Integer[] values = parseJsonIntegerArray(jsonEnumsStr);
if (values == null) {
Log.e(TAG, "Error reporting " + key + " with values: " + jsonEnumsStr);
return;
}
for (Integer value: values) {
RecordHistogram.recordEnumeratedHistogram(key, value, boundary);
}
}
}
}