// 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.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.support.annotation.Nullable;
import android.widget.ListView;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.browsing_data.BrowsingDataType;
import org.chromium.chrome.browser.browsing_data.TimePeriod;
import org.chromium.chrome.browser.help.HelpAndFeedback;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.preferences.ButtonPreference;
import org.chromium.chrome.browser.preferences.ClearBrowsingDataCheckBoxPreference;
import org.chromium.chrome.browser.preferences.PrefServiceBridge;
import org.chromium.chrome.browser.preferences.SpinnerPreference;
import org.chromium.chrome.browser.preferences.TextMessageWithLinkAndIconPreference;
import org.chromium.chrome.browser.preferences.privacy.BrowsingDataCounterBridge.BrowsingDataCounterCallback;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tabmodel.TabModel.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.document.TabDelegate;
import org.chromium.components.signin.ChromeSigninController;
import java.util.Arrays;
import java.util.EnumSet;
/**
* Preference screen that allows the user to clear browsing data.
* The user can choose which types of data to clear (history, cookies, etc), and the time range
* from which to clear data.
*/
public class ClearBrowsingDataPreferences extends PreferenceFragment
implements PrefServiceBridge.ImportantSitesCallback,
PrefServiceBridge.OnClearBrowsingDataListener,
PrefServiceBridge.OtherFormsOfBrowsingHistoryListener,
Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
/**
* Represents a single item in the dialog.
*/
private static class Item implements BrowsingDataCounterCallback,
Preference.OnPreferenceClickListener {
private final ClearBrowsingDataPreferences mParent;
private final DialogOption mOption;
private final ClearBrowsingDataCheckBoxPreference mCheckbox;
private BrowsingDataCounterBridge mCounter;
private boolean mShouldAnnounceCounterResult;
public Item(ClearBrowsingDataPreferences parent,
DialogOption option,
ClearBrowsingDataCheckBoxPreference checkbox,
boolean selected,
boolean enabled) {
super();
mParent = parent;
mOption = option;
mCheckbox = checkbox;
mCounter = new BrowsingDataCounterBridge(this, mOption.getDataType());
mCheckbox.setOnPreferenceClickListener(this);
mCheckbox.setEnabled(enabled);
mCheckbox.setChecked(selected);
mCheckbox.setSummaryOff(""); // No summary when unchecked.
}
public void destroy() {
mCounter.destroy();
}
public DialogOption getOption() {
return mOption;
}
public boolean isSelected() {
return mCheckbox.isChecked();
}
@Override
public boolean onPreferenceClick(Preference preference) {
assert mCheckbox == preference;
mParent.updateButtonState();
mShouldAnnounceCounterResult = true;
PrefServiceBridge.getInstance().setBrowsingDataDeletionPreference(
mOption.getDataType(), mCheckbox.isChecked());
return true;
}
@Override
public void onCounterFinished(String result) {
mCheckbox.setSummaryOn(result);
if (mShouldAnnounceCounterResult) {
mCheckbox.announceForAccessibility(result);
}
}
/**
* Sets whether the BrowsingDataCounter result should be announced. This is when the counter
* recalculation was caused by a checkbox state change (as opposed to fragment
* initialization or time period change).
*/
public void setShouldAnnounceCounterResult(boolean value) {
mShouldAnnounceCounterResult = value;
}
}
private static final String PREF_HISTORY = "clear_history_checkbox";
private static final String PREF_COOKIES = "clear_cookies_checkbox";
private static final String PREF_CACHE = "clear_cache_checkbox";
private static final String PREF_PASSWORDS = "clear_passwords_checkbox";
private static final String PREF_FORM_DATA = "clear_form_data_checkbox";
@VisibleForTesting
public static final String PREF_GOOGLE_SUMMARY = "google_summary";
@VisibleForTesting
public static final String PREF_GENERAL_SUMMARY = "general_summary";
private static final String PREF_TIME_RANGE = "time_period_spinner";
/** The "Clear" button preference. */
@VisibleForTesting
public static final String PREF_CLEAR_BUTTON = "clear_button";
/** The tag used for logging. */
public static final String TAG = "ClearBrowsingDataPreferences";
/** The histogram for the dialog about other forms of browsing history. */
private static final String DIALOG_HISTOGRAM =
"History.ClearBrowsingData.ShownHistoryNoticeAfterClearing";
/** The web history URL. */
private static final String WEB_HISTORY_URL =
"https://history.google.com/history/?utm_source=chrome_cbd";
/**
* Used for the onActivityResult pattern. The value is arbitrary, just to distinguish from other
* activities that we might be using onActivityResult with as well.
*/
private static final int IMPORTANT_SITES_DIALOG_CODE = 1;
private static final int IMPORTANT_SITES_PERCENTAGE_BUCKET_COUNT = 20;
/**
* The various data types that can be cleared via this screen.
*/
public enum DialogOption {
CLEAR_HISTORY(BrowsingDataType.HISTORY, PREF_HISTORY),
CLEAR_COOKIES_AND_SITE_DATA(BrowsingDataType.COOKIES, PREF_COOKIES),
CLEAR_CACHE(BrowsingDataType.CACHE, PREF_CACHE),
CLEAR_PASSWORDS(BrowsingDataType.PASSWORDS, PREF_PASSWORDS),
CLEAR_FORM_DATA(BrowsingDataType.FORM_DATA, PREF_FORM_DATA);
private final int mDataType;
private final String mPreferenceKey;
private DialogOption(int dataType, String preferenceKey) {
mDataType = dataType;
mPreferenceKey = preferenceKey;
}
public int getDataType() {
return mDataType;
}
/**
* @return String The key of the corresponding preference.
*/
public String getPreferenceKey() {
return mPreferenceKey;
}
}
/**
* An option to be shown in the time period spiner.
*/
private static class TimePeriodSpinnerOption {
private int mTimePeriod;
private String mTitle;
/**
* Constructs this time period spinner option.
* @param timePeriod The time period represented as an int from the shared enum
* {@link TimePeriod}.
* @param title The text that will be used to represent this item in the spinner.
*/
public TimePeriodSpinnerOption(int timePeriod, String title) {
mTimePeriod = timePeriod;
mTitle = title;
}
/**
* @return The time period represented as an int from the shared enum {@link TimePeriod}
*/
public int getTimePeriod() {
return mTimePeriod;
}
@Override
public String toString() {
return mTitle;
}
}
private OtherFormsOfHistoryDialogFragment mDialogAboutOtherFormsOfBrowsingHistory;
private boolean mIsDialogAboutOtherFormsOfBrowsingHistoryEnabled;
private ProgressDialog mProgressDialog;
private Item[] mItems;
// This is a constant on the C++ side.
private int mMaxImportantSites;
// This is the sorted list of important registerable domains. If null, then we haven't finished
// fetching them yet.
private String[] mSortedImportantDomains;
// These are the reasons the above domains were chosen as important.
private int[] mSortedImportantDomainReasons;
// These are full url examples of the domains above. We use them for favicons.
private String[] mSortedExampleOrigins;
// This is the dialog we show to the user that lets them 'uncheck' (or exclude) the above
// important domains from being cleared.
private ConfirmImportantSitesDialogFragment mConfirmImportantSitesDialog;
private final EnumSet<DialogOption> getSelectedOptions() {
EnumSet<DialogOption> selected = EnumSet.noneOf(DialogOption.class);
for (Item item : mItems) {
if (item.isSelected()) selected.add(item.getOption());
}
return selected;
}
/**
* Requests the browsing data corresponding to the given dialog options to be deleted.
* @param options The dialog options whose corresponding data should be deleted.
*/
private final void clearBrowsingData(EnumSet<DialogOption> options,
@Nullable String[] blacklistedDomains, @Nullable int[] blacklistedDomainReasons,
@Nullable String[] ignoredDomains, @Nullable int[] ignoredDomainReasons) {
showProgressDialog();
int[] dataTypes = new int[options.size()];
int i = 0;
for (DialogOption option : options) {
dataTypes[i] = option.getDataType();
++i;
}
Object spinnerSelection =
((SpinnerPreference) findPreference(PREF_TIME_RANGE)).getSelectedOption();
int timePeriod = ((TimePeriodSpinnerOption) spinnerSelection).getTimePeriod();
if (blacklistedDomains != null && blacklistedDomains.length != 0) {
PrefServiceBridge.getInstance().clearBrowsingDataExcludingDomains(this, dataTypes,
timePeriod, blacklistedDomains, blacklistedDomainReasons, ignoredDomains,
ignoredDomainReasons);
} else {
PrefServiceBridge.getInstance().clearBrowsingData(this, dataTypes, timePeriod);
}
}
private void dismissProgressDialog() {
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
}
mProgressDialog = null;
}
/**
* Returns the Array of dialog options. Options are displayed in the same
* order as they appear in the array.
*/
private DialogOption[] getDialogOptions() {
return new DialogOption[] {
DialogOption.CLEAR_HISTORY,
DialogOption.CLEAR_COOKIES_AND_SITE_DATA,
DialogOption.CLEAR_CACHE,
DialogOption.CLEAR_PASSWORDS,
DialogOption.CLEAR_FORM_DATA
};
}
/**
* Returns the Array of time periods. Options are displayed in the same order as they appear
* in the array.
*/
private TimePeriodSpinnerOption[] getTimePeriodSpinnerOptions() {
Activity activity = getActivity();
TimePeriodSpinnerOption[] options = new TimePeriodSpinnerOption[] {
new TimePeriodSpinnerOption(TimePeriod.LAST_HOUR,
activity.getString(R.string.clear_browsing_data_period_hour)),
new TimePeriodSpinnerOption(TimePeriod.LAST_DAY,
activity.getString(R.string.clear_browsing_data_period_day)),
new TimePeriodSpinnerOption(TimePeriod.LAST_WEEK,
activity.getString(R.string.clear_browsing_data_period_week)),
new TimePeriodSpinnerOption(TimePeriod.FOUR_WEEKS,
activity.getString(R.string.clear_browsing_data_period_four_weeks)),
new TimePeriodSpinnerOption(TimePeriod.ALL_TIME,
activity.getString(R.string.clear_browsing_data_period_everything))};
return options;
}
/**
* Decides whether a given dialog option should be selected when the dialog is initialized.
* @param option The option in question.
* @return boolean Whether the given option should be preselected.
*/
private boolean isOptionSelectedByDefault(DialogOption option) {
return PrefServiceBridge.getInstance().getBrowsingDataDeletionPreference(
option.getDataType());
}
/**
* Called when clearing browsing data completes.
* Implements the ChromePreferences.OnClearBrowsingDataListener interface.
*/
@Override
public void onBrowsingDataCleared() {
if (getActivity() == null) return;
// If the user deleted their browsing history, the dialog about other forms of history
// is enabled, and it has never been shown before, show it. Note that opening a new
// DialogFragment is only possible if the Activity is visible.
//
// If conditions to show the dialog about other forms of history are not met, just close
// this preference screen.
if (MultiWindowUtils.isActivityVisible(getActivity())
&& getSelectedOptions().contains(DialogOption.CLEAR_HISTORY)
&& mIsDialogAboutOtherFormsOfBrowsingHistoryEnabled
&& !OtherFormsOfHistoryDialogFragment.wasDialogShown(getActivity())) {
mDialogAboutOtherFormsOfBrowsingHistory = new OtherFormsOfHistoryDialogFragment();
mDialogAboutOtherFormsOfBrowsingHistory.show(getActivity());
dismissProgressDialog();
RecordHistogram.recordBooleanHistogram(DIALOG_HISTOGRAM, true);
} else {
dismissProgressDialog();
getActivity().finish();
RecordHistogram.recordBooleanHistogram(DIALOG_HISTOGRAM, false);
}
}
/**
* Returns if we should show the important sites dialog. We check to see if
* <ol>
* <li>We've fetched the important sites,
* <li>there are important sites,
* <li>the feature is enabled, and
* <li>we have cache or cookies selected.
* </ol>
*/
private boolean shouldShowImportantSitesDialog() {
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.IMPORTANT_SITES_IN_CBD)) return false;
EnumSet<DialogOption> selectedOptions = getSelectedOptions();
if (!selectedOptions.contains(DialogOption.CLEAR_CACHE)
&& !selectedOptions.contains(DialogOption.CLEAR_COOKIES_AND_SITE_DATA)) {
return false;
}
boolean haveImportantSites =
mSortedImportantDomains != null && mSortedImportantDomains.length != 0;
RecordHistogram.recordBooleanHistogram(
"History.ClearBrowsingData.ImportantDialogShown", haveImportantSites);
return haveImportantSites;
}
@Override
public boolean onPreferenceClick(Preference preference) {
if (preference.getKey().equals(PREF_CLEAR_BUTTON)) {
if (shouldShowImportantSitesDialog()) {
showImportantDialogThenClear();
return true;
}
// If sites haven't been fetched, just clear the browsing data regularly rather than
// waiting to show the important sites dialog.
clearBrowsingData(getSelectedOptions(), null, null, null, null);
return true;
}
return false;
}
@Override
public boolean onPreferenceChange(Preference preference, Object value) {
if (preference.getKey().equals(PREF_TIME_RANGE)) {
// Inform the items that a recalculation is going to happen as a result of the time
// period change.
for (Item item : mItems) {
item.setShouldAnnounceCounterResult(false);
}
PrefServiceBridge.getInstance().setBrowsingDataDeletionTimePeriod(
((TimePeriodSpinnerOption) value).getTimePeriod());
return true;
}
return false;
}
/**
* Disable the "Clear" button if none of the options are selected. Otherwise, enable it.
*/
private void updateButtonState() {
ButtonPreference clearButton = (ButtonPreference) findPreference(PREF_CLEAR_BUTTON);
if (clearButton == null) return;
boolean isEnabled = !getSelectedOptions().isEmpty();
clearButton.setEnabled(isEnabled);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
RecordUserAction.record("ClearBrowsingData_DialogCreated");
mMaxImportantSites = PrefServiceBridge.getMaxImportantSites();
PrefServiceBridge.getInstance().requestInfoAboutOtherFormsOfBrowsingHistory(this);
getActivity().setTitle(R.string.clear_browsing_data_title);
addPreferencesFromResource(R.xml.clear_browsing_data_preferences);
DialogOption[] options = getDialogOptions();
mItems = new Item[options.length];
for (int i = 0; i < options.length; i++) {
boolean enabled = true;
// It is possible to disable the deletion of browsing history.
if (options[i] == DialogOption.CLEAR_HISTORY
&& !PrefServiceBridge.getInstance().canDeleteBrowsingHistory()) {
enabled = false;
PrefServiceBridge.getInstance().setBrowsingDataDeletionPreference(
DialogOption.CLEAR_HISTORY.getDataType(), false);
}
mItems[i] = new Item(
this,
options[i],
(ClearBrowsingDataCheckBoxPreference) findPreference(options[i].getPreferenceKey()),
isOptionSelectedByDefault(options[i]),
enabled);
}
// Not all checkboxes defined in the layout are necessarily handled by this class
// or a particular subclass. Hide those that are not.
EnumSet<DialogOption> unboundOptions = EnumSet.allOf(DialogOption.class);
unboundOptions.removeAll(Arrays.asList(getDialogOptions()));
for (DialogOption option : unboundOptions) {
getPreferenceScreen().removePreference(findPreference(option.getPreferenceKey()));
}
// The time range selection spinner.
SpinnerPreference spinner = (SpinnerPreference) findPreference(PREF_TIME_RANGE);
spinner.setOnPreferenceChangeListener(this);
TimePeriodSpinnerOption[] spinnerOptions = getTimePeriodSpinnerOptions();
int selectedTimePeriod =
PrefServiceBridge.getInstance().getBrowsingDataDeletionTimePeriod();
int spinnerOptionIndex = -1;
for (int i = 0; i < spinnerOptions.length; ++i) {
if (spinnerOptions[i].getTimePeriod() == selectedTimePeriod) {
spinnerOptionIndex = i;
break;
}
}
assert spinnerOptionIndex != -1;
spinner.setOptions(spinnerOptions, spinnerOptionIndex);
// The "Clear" button.
ButtonPreference clearButton = (ButtonPreference) findPreference(PREF_CLEAR_BUTTON);
clearButton.setOnPreferenceClickListener(this);
clearButton.setShouldDisableView(true);
// The general information footnote informs users about data that will not be deleted.
// If the user is signed in, it also informs users about the behavior of synced deletions.
// and we show an additional Google-specific footnote. This footnote informs users that they
// will not be signed out of their Google account, and if the web history service indicates
// that they have other forms of browsing history, then also about that.
TextMessageWithLinkAndIconPreference google_summary =
(TextMessageWithLinkAndIconPreference) findPreference(PREF_GOOGLE_SUMMARY);
TextMessageWithLinkAndIconPreference general_summary =
(TextMessageWithLinkAndIconPreference) findPreference(PREF_GENERAL_SUMMARY);
google_summary.setLinkClickDelegate(new Runnable() {
@Override
public void run() {
new TabDelegate(false /* incognito */).launchUrl(
WEB_HISTORY_URL, TabLaunchType.FROM_CHROME_UI);
}
});
general_summary.setLinkClickDelegate(new Runnable() {
@Override
public void run() {
HelpAndFeedback.getInstance(getActivity()).show(
getActivity(),
getResources().getString(R.string.help_context_clear_browsing_data),
Profile.getLastUsedProfile(),
null);
}
});
if (ChromeSigninController.get(getActivity()).isSignedIn()) {
general_summary.setSummary(
R.string.clear_browsing_data_footnote_sync_and_site_settings);
} else {
getPreferenceScreen().removePreference(google_summary);
general_summary.setSummary(R.string.clear_browsing_data_footnote_site_settings);
}
if (ChromeFeatureList.isEnabled(ChromeFeatureList.IMPORTANT_SITES_IN_CBD)) {
PrefServiceBridge.fetchImportantSites(this);
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Now that the dialog's view has been created, update the button state.
updateButtonState();
// Remove the dividers between checkboxes.
((ListView) getView().findViewById(android.R.id.list)).setDivider(null);
}
@Override
public void onDestroy() {
super.onDestroy();
dismissProgressDialog();
for (Item item : mItems) {
item.destroy();
}
}
// We either show the dialog, or modify the current one to display our messages. This avoids
// a dialog flash.
private final void showProgressDialog() {
if (getActivity() == null) return;
mProgressDialog = ProgressDialog.show(getActivity(),
getActivity().getString(R.string.clear_browsing_data_progress_title),
getActivity().getString(R.string.clear_browsing_data_progress_message), true,
false);
}
@VisibleForTesting
ProgressDialog getProgressDialog() {
return mProgressDialog;
}
@VisibleForTesting
ConfirmImportantSitesDialogFragment getImportantSitesDialogFragment() {
return mConfirmImportantSitesDialog;
}
/**
* This method shows the important sites dialog. After the dialog is shown, we correctly clear.
*/
private void showImportantDialogThenClear() {
mConfirmImportantSitesDialog = ConfirmImportantSitesDialogFragment.newInstance(
mSortedImportantDomains, mSortedImportantDomainReasons, mSortedExampleOrigins);
mConfirmImportantSitesDialog.setTargetFragment(this, IMPORTANT_SITES_DIALOG_CODE);
mConfirmImportantSitesDialog.show(
getFragmentManager(), ConfirmImportantSitesDialogFragment.FRAGMENT_TAG);
}
@Override
public void showNoticeAboutOtherFormsOfBrowsingHistory() {
if (getActivity() == null) return;
TextMessageWithLinkAndIconPreference google_summary =
(TextMessageWithLinkAndIconPreference) findPreference(PREF_GOOGLE_SUMMARY);
if (google_summary == null) return;
google_summary.setSummary(
R.string.clear_browsing_data_footnote_signed_and_other_forms_of_history);
}
@Override
public void enableDialogAboutOtherFormsOfBrowsingHistory() {
if (getActivity() == null) return;
mIsDialogAboutOtherFormsOfBrowsingHistoryEnabled = true;
}
/**
* Used only to access the dialog about other forms of browsing history from tests.
*/
@VisibleForTesting
OtherFormsOfHistoryDialogFragment getDialogAboutOtherFormsOfBrowsingHistory() {
return mDialogAboutOtherFormsOfBrowsingHistory;
}
@Override
public void onImportantRegisterableDomainsReady(
String[] domains, String[] exampleOrigins, int[] importantReasons) {
if (domains == null) return;
// mMaxImportantSites is a constant on the C++ side. While 0 is valid, use 1 as the minimum
// because histogram code assumes a min >= 1; the underflow bucket will record the 0s.
RecordHistogram.recordLinearCountHistogram("History.ClearBrowsingData.NumImportant",
domains.length, 1, mMaxImportantSites + 1, mMaxImportantSites + 1);
mSortedImportantDomains = Arrays.copyOf(domains, domains.length);
mSortedImportantDomainReasons = Arrays.copyOf(importantReasons, importantReasons.length);
mSortedExampleOrigins = Arrays.copyOf(exampleOrigins, exampleOrigins.length);
}
/**
* This is the callback for the important domain dialog. We should only clear if we get the
* positive button response.
*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == IMPORTANT_SITES_DIALOG_CODE && resultCode == Activity.RESULT_OK) {
// Deselected means that the user is excluding the domain from being cleared.
String[] deselectedDomains = data.getStringArrayExtra(
ConfirmImportantSitesDialogFragment.DESELECTED_DOMAINS_TAG);
int[] deselectedDomainReasons = data.getIntArrayExtra(
ConfirmImportantSitesDialogFragment.DESELECTED_DOMAIN_REASONS_TAG);
String[] ignoredDomains = data.getStringArrayExtra(
ConfirmImportantSitesDialogFragment.IGNORED_DOMAINS_TAG);
int[] ignoredDomainReasons = data.getIntArrayExtra(
ConfirmImportantSitesDialogFragment.IGNORED_DOMAIN_REASONS_TAG);
if (deselectedDomains != null && mSortedImportantDomains != null) {
// mMaxImportantSites is a constant on the C++ side.
RecordHistogram.recordCustomCountHistogram(
"History.ClearBrowsingData.ImportantDeselectedNum",
deselectedDomains.length, 0, mMaxImportantSites + 1,
mMaxImportantSites + 1);
RecordHistogram.recordCustomCountHistogram(
"History.ClearBrowsingData.ImportantIgnoredNum", ignoredDomains.length, 0,
mMaxImportantSites + 1, mMaxImportantSites + 1);
// We put our max at 20 instead of 100 to reduce the number of empty buckets (as
// our maximum denominator is 5).
RecordHistogram.recordEnumeratedHistogram(
"History.ClearBrowsingData.ImportantDeselectedPercent",
deselectedDomains.length * IMPORTANT_SITES_PERCENTAGE_BUCKET_COUNT
/ mSortedImportantDomains.length,
IMPORTANT_SITES_PERCENTAGE_BUCKET_COUNT + 1);
RecordHistogram.recordEnumeratedHistogram(
"History.ClearBrowsingData.ImportantIgnoredPercent",
ignoredDomains.length * IMPORTANT_SITES_PERCENTAGE_BUCKET_COUNT
/ mSortedImportantDomains.length,
IMPORTANT_SITES_PERCENTAGE_BUCKET_COUNT + 1);
}
clearBrowsingData(getSelectedOptions(), deselectedDomains, deselectedDomainReasons,
ignoredDomains, ignoredDomainReasons);
}
}
}