// 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.preferences.privacy;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.device.DeviceClassManager;
import org.chromium.chrome.browser.physicalweb.PhysicalWeb;
import org.chromium.chrome.browser.preferences.PrefServiceBridge;
/**
* Reads, writes, and migrates preferences related to network usage and privacy.
*/
public class PrivacyPreferencesManager implements CrashReportingPermissionManager{
static final String DEPRECATED_PREF_CRASH_DUMP_UPLOAD = "crash_dump_upload";
static final String DEPRECATED_PREF_CRASH_DUMP_UPLOAD_NO_CELLULAR =
"crash_dump_upload_no_cellular";
private static final String DEPRECATED_PREF_CELLULAR_EXPERIMENT = "cellular_experiment";
public static final String PREF_METRICS_REPORTING = "metrics_reporting";
private static final String PREF_METRICS_IN_SAMPLE = "in_metrics_sample";
private static final String PREF_NETWORK_PREDICTIONS = "network_predictions";
private static final String PREF_BANDWIDTH_OLD = "prefetch_bandwidth";
private static final String PREF_BANDWIDTH_NO_CELLULAR_OLD = "prefetch_bandwidth_no_cellular";
private static final String ALLOW_PRERENDER_OLD = "allow_prefetch";
private static final String PREF_PHYSICAL_WEB = "physical_web";
private static final int PHYSICAL_WEB_OFF = 0;
private static final int PHYSICAL_WEB_ON = 1;
private static final int PHYSICAL_WEB_ONBOARDING = 2;
private static PrivacyPreferencesManager sInstance;
private final Context mContext;
private final SharedPreferences mSharedPreferences;
private boolean mCrashUploadingCommandLineDisabled;
@VisibleForTesting
PrivacyPreferencesManager(Context context) {
mContext = context;
mSharedPreferences = ContextUtils.getAppSharedPreferences();
// We default the command line flag to disable uploads unless altered on deferred startup
// to prevent unwanted uploads at startup. If the command line flag to enable uploading is
// turned on, the other conditions (e.g. user/network preferences) for when to upload apply.
// This currently applies to only crash reporting and is ignored for metrics reporting.
mCrashUploadingCommandLineDisabled = true;
migrateUsageAndCrashPreferences();
}
public static PrivacyPreferencesManager getInstance() {
if (sInstance == null) {
sInstance = new PrivacyPreferencesManager(ContextUtils.getApplicationContext());
}
return sInstance;
}
public void migrateUsageAndCrashPreferences() {
SharedPreferences.Editor editor = mSharedPreferences.edit();
if (mSharedPreferences.contains(DEPRECATED_PREF_CRASH_DUMP_UPLOAD)) {
String crashDumpNeverUpload = "crash_dump_never_upload";
setUsageAndCrashReporting(
!mSharedPreferences
.getString(DEPRECATED_PREF_CRASH_DUMP_UPLOAD, crashDumpNeverUpload)
.equals(crashDumpNeverUpload));
// Remove both this preference and the related one. If the related one is not removed
// now, later migrations could read from it and clobber the state.
editor.remove(DEPRECATED_PREF_CRASH_DUMP_UPLOAD);
if (mSharedPreferences.contains(DEPRECATED_PREF_CRASH_DUMP_UPLOAD_NO_CELLULAR)) {
editor.remove(DEPRECATED_PREF_CRASH_DUMP_UPLOAD_NO_CELLULAR);
}
} else if (mSharedPreferences.contains(DEPRECATED_PREF_CRASH_DUMP_UPLOAD_NO_CELLULAR)) {
setUsageAndCrashReporting(mSharedPreferences.getBoolean(
DEPRECATED_PREF_CRASH_DUMP_UPLOAD_NO_CELLULAR, false));
editor.remove(DEPRECATED_PREF_CRASH_DUMP_UPLOAD_NO_CELLULAR);
}
if (mSharedPreferences.contains(DEPRECATED_PREF_CELLULAR_EXPERIMENT)) {
editor.remove(DEPRECATED_PREF_CELLULAR_EXPERIMENT);
}
editor.apply();
}
/**
* Migrate and delete old preferences. Note that migration has to happen in Android-specific
* code because we need to access ALLOW_PRERENDER sharedPreference.
* TODO(bnc) https://crbug.com/394845. This change is planned for M38. After a year or so, it
* would be worth considering removing this migration code (also removing accessors in
* PrefServiceBridge and pref_service_bridge), and reverting to default for users
* who had set preferences but have not used Chrome for a year. This change would be subject to
* privacy review.
*/
public void migrateNetworkPredictionPreferences() {
PrefServiceBridge prefService = PrefServiceBridge.getInstance();
// See if PREF_NETWORK_PREDICTIONS is an old boolean value.
boolean predictionOptionIsBoolean = false;
try {
mSharedPreferences.getString(PREF_NETWORK_PREDICTIONS, "");
} catch (ClassCastException ex) {
predictionOptionIsBoolean = true;
}
// Nothing to do if the user or this migration code has already set the new
// preference.
if (!predictionOptionIsBoolean
&& prefService.obsoleteNetworkPredictionOptionsHasUserSetting()) {
return;
}
// Nothing to do if the old preferences are unset.
if (!predictionOptionIsBoolean
&& !mSharedPreferences.contains(PREF_BANDWIDTH_OLD)
&& !mSharedPreferences.contains(PREF_BANDWIDTH_NO_CELLULAR_OLD)) {
return;
}
// Migrate if the old preferences are at their default values.
// (Note that for PREF_BANDWIDTH*, if the setting is default, then there is no way to tell
// whether the user has set it.)
final String prefBandwidthDefault = BandwidthType.PRERENDER_ON_WIFI.title();
final String prefBandwidth =
mSharedPreferences.getString(PREF_BANDWIDTH_OLD, prefBandwidthDefault);
boolean prefBandwidthNoCellularDefault = true;
boolean prefBandwidthNoCellular = mSharedPreferences.getBoolean(
PREF_BANDWIDTH_NO_CELLULAR_OLD, prefBandwidthNoCellularDefault);
if (!(prefBandwidthDefault.equals(prefBandwidth))
|| (prefBandwidthNoCellular != prefBandwidthNoCellularDefault)) {
boolean newValue = true;
// Observe PREF_BANDWIDTH on mobile network capable devices.
if (isMobileNetworkCapable()) {
if (mSharedPreferences.contains(PREF_BANDWIDTH_OLD)) {
BandwidthType prefetchBandwidthTypePref = BandwidthType.getBandwidthFromTitle(
prefBandwidth);
if (BandwidthType.NEVER_PRERENDER.equals(prefetchBandwidthTypePref)) {
newValue = false;
} else if (BandwidthType.PRERENDER_ON_WIFI.equals(prefetchBandwidthTypePref)) {
newValue = true;
} else if (BandwidthType.ALWAYS_PRERENDER.equals(prefetchBandwidthTypePref)) {
newValue = true;
}
}
// Observe PREF_BANDWIDTH_NO_CELLULAR on devices without mobile network.
} else {
if (mSharedPreferences.contains(PREF_BANDWIDTH_NO_CELLULAR_OLD)) {
if (prefBandwidthNoCellular) {
newValue = true;
} else {
newValue = false;
}
}
}
// Save new value in Chrome PrefService.
prefService.setNetworkPredictionEnabled(newValue);
}
// Delete old sharedPreferences.
SharedPreferences.Editor sharedPreferencesEditor = mSharedPreferences.edit();
// Delete PREF_BANDWIDTH and PREF_BANDWIDTH_NO_CELLULAR: just migrated these options.
if (mSharedPreferences.contains(PREF_BANDWIDTH_OLD)) {
sharedPreferencesEditor.remove(PREF_BANDWIDTH_OLD);
}
if (mSharedPreferences.contains(PREF_BANDWIDTH_NO_CELLULAR_OLD)) {
sharedPreferencesEditor.remove(PREF_BANDWIDTH_NO_CELLULAR_OLD);
}
// Also delete ALLOW_PRERENDER, which was updated based on PREF_BANDWIDTH[_NO_CELLULAR] and
// network connectivity type, therefore does not carry additional information.
if (mSharedPreferences.contains(ALLOW_PRERENDER_OLD)) {
sharedPreferencesEditor.remove(ALLOW_PRERENDER_OLD);
}
// Delete bool PREF_NETWORK_PREDICTIONS so that string values can be stored. Note that this
// SharedPreference carries no information, because it used to be overwritten by
// kNetworkPredictionEnabled on startup, and now it is overwritten by
// kNetworkPredictionOptions on startup.
if (mSharedPreferences.contains(PREF_NETWORK_PREDICTIONS)) {
sharedPreferencesEditor.remove(PREF_NETWORK_PREDICTIONS);
}
sharedPreferencesEditor.apply();
}
private NetworkInfo getActiveNetworkInfo() {
ConnectivityManager connectivityManager =
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
return connectivityManager.getActiveNetworkInfo();
}
protected boolean isNetworkAvailable() {
NetworkInfo networkInfo = getActiveNetworkInfo();
return (networkInfo != null && networkInfo.isConnected());
}
protected boolean isWiFiOrEthernetNetwork() {
NetworkInfo networkInfo = getActiveNetworkInfo();
return networkInfo != null
&& (networkInfo.getType() == ConnectivityManager.TYPE_WIFI
|| networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET);
}
protected boolean isMobileNetworkCapable() {
ConnectivityManager connectivityManager =
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
// Android telephony team said it is OK to continue using getNetworkInfo() for our purposes.
// We cannot use ConnectivityManager#getAllNetworks() because that one only reports enabled
// networks. See crbug.com/532455.
@SuppressWarnings("deprecation")
NetworkInfo networkInfo = connectivityManager
.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
return networkInfo != null;
}
/**
* Checks whether prerender should be allowed and updates the preference if it is not set yet.
* @return Whether prerendering should be allowed.
*/
public boolean shouldPrerender() {
if (!DeviceClassManager.enablePrerendering()) return false;
migrateNetworkPredictionPreferences();
return PrefServiceBridge.getInstance().canPrefetchAndPrerender();
}
/**
* Check whether usage and crash reporting set to ON.
*
* @return boolean whether usage and crash reporting set to ON.
*/
public boolean isUsageAndCrashReportingEnabled() {
return mSharedPreferences.getBoolean(PREF_METRICS_REPORTING, false);
}
/**
* Sets the usage and crash reporting preference ON or OFF.
*
* @param enabled A boolean corresponding whether usage and crash reports uploads are allowed.
*/
public void setUsageAndCrashReporting(boolean enabled) {
mSharedPreferences.edit().putBoolean(PREF_METRICS_REPORTING, enabled).apply();
syncUsageAndCrashReportingPrefs();
}
/**
* Sets whether this client is in-sample. See
* {@link org.chromium.chrome.browser.metrics.UmaUtils#isClientInMetricsSample} for details.
*/
public void setClientInMetricsSample(boolean inSample) {
mSharedPreferences.edit().putBoolean(PREF_METRICS_IN_SAMPLE, inSample).apply();
}
/**
* Checks whether this client is in-sample. See
* {@link org.chromium.chrome.browser.metrics.UmaUtils#isClientInMetricsSample} for details.
*
* @returns boolean Whether client is in-sample.
*/
public boolean isClientInMetricsSample() {
// The default value is true to avoid sampling out crashes that occur before native code has
// been initialized on first run. We'd rather have some extra crashes than none from that
// time.
return mSharedPreferences.getBoolean(PREF_METRICS_IN_SAMPLE, true);
}
/**
* Provides a way to remove disabling crash uploading entirely.
* Enable crash uploading based on user's preference when an overriding flag
* does not exist in commandline.
* Used to differentiate from tests that trigger crashers intentionally, so these crashers are
* not uploaded.
*/
public void enablePotentialCrashUploading() {
mCrashUploadingCommandLineDisabled = false;
}
/**
* Check whether to allow uploading crash dump now.
* {@link #allowUploadCrashDump()} should return {@code true},
* and the network should be connected as well.
*
* This function should not result in a native call as it can be called in circumstances where
* natives are not guaranteed to be loaded.
*
* @return whether to allow uploading crash dump now.
*/
@Override
public boolean isUploadPermitted() {
return !mCrashUploadingCommandLineDisabled && isNetworkAvailable()
&& (isUsageAndCrashReportingEnabled() || isUploadEnabledForTests());
}
/**
* Check whether to allow UMA uploading.
*
* TODO(asvitkine): This is temporary split up from isUploadPermitted() above with
* the |mCrashUploadingCommandLineDisabled| check removed, in order to diagnose if
* that check is responsible for decreased UMA uploads in M49. http://crbug.com/602703
*
* This function should not result in a native call as it can be called in circumstances where
* natives are not guaranteed to be loaded.
*
* @return whether to allow UMA uploading.
*/
@Override
public boolean isUmaUploadPermitted() {
return isNetworkAvailable()
&& (isUsageAndCrashReportingEnabled() || isUploadEnabledForTests());
}
/**
* Check whether not to disable uploading crash dump by command line flag.
* If command line flag disables crash dump uploading, do not retry, but also do not delete.
* TODO(jchinlee): this is not quite a boolean. Depending on other refactoring, change to enum.
*
* @return whether experimental flag doesn't disable uploading crash dump.
*/
@Override
public boolean isUploadCommandLineDisabled() {
return mCrashUploadingCommandLineDisabled;
}
/**
* Check whether the user allows uploading.
* This doesn't take network condition or experimental state (i.e. disabling upload) into
* consideration.
* A crash dump may be retried if this check passes.
*
* @return whether user's preference allows uploading crash dump.
*/
@Override
public boolean isUploadUserPermitted() {
return isUsageAndCrashReportingEnabled();
}
/**
* Check whether uploading crash dump should be in constrained mode based on current connection
* type. This function shows whether in general uploads should be limited
* for this user and does not determine whether crash uploads are currently possible or not. Use
* |isUploadPermitted| function for that before calling |isUploadLimited|.
*
* @return whether uploading logic should be constrained.
*/
@Override
public boolean isUploadLimited() {
return !isWiFiOrEthernetNetwork();
}
/**
* Sets the Physical Web preference, which enables background scanning for bluetooth beacons
* and displays a notification when beacons are found.
*
* @param enabled A boolean indicating whether to notify on nearby beacons.
*/
public void setPhysicalWebEnabled(boolean enabled) {
int state = enabled ? PHYSICAL_WEB_ON : PHYSICAL_WEB_OFF;
boolean isOnboarding = isPhysicalWebOnboarding();
mSharedPreferences.edit().putInt(PREF_PHYSICAL_WEB, state).apply();
if (enabled) {
if (!isOnboarding) {
PhysicalWeb.startPhysicalWeb();
}
} else {
PhysicalWeb.stopPhysicalWeb();
}
}
/**
* Check whether the user is still in the Physical Web onboarding flow.
*
* @return boolean {@code true} if onboarding is not yet complete.
*/
public boolean isPhysicalWebOnboarding() {
int state = mSharedPreferences.getInt(PREF_PHYSICAL_WEB, PHYSICAL_WEB_ONBOARDING);
return (state == PHYSICAL_WEB_ONBOARDING);
}
/**
* Check whether Physical Web is configured to notify on nearby beacons.
*
* @return boolean {@code true} if the feature is enabled.
*/
public boolean isPhysicalWebEnabled() {
int state = mSharedPreferences.getInt(PREF_PHYSICAL_WEB, PHYSICAL_WEB_ONBOARDING);
return (state == PHYSICAL_WEB_ON);
}
/**
* Check whether the command line switch is used to force uploading if at all possible. Used by
* test devices to avoid UI manipulation.
*
* @return whether uploading should be enabled if at all possible.
*/
@Override
public boolean isUploadEnabledForTests() {
return CommandLine.getInstance().hasSwitch(ChromeSwitches.FORCE_CRASH_DUMP_UPLOAD);
}
/**
* Update usage and crash preferences based on Android preferences if possible in case they are
* out of sync.
*/
public void syncUsageAndCrashReportingPrefs() {
if (PrefServiceBridge.isInitialized()) {
PrefServiceBridge.getInstance().setMetricsReportingEnabled(
isUsageAndCrashReportingEnabled());
}
}
}