// Copyright 2015 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.browsing_data;
import static org.chromium.chrome.browser.browsing_data.TimePeriodUtils.getTimePeriodSpinnerOptions;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.text.SpannableString;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.collection.ArraySet;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.Callback;
import org.chromium.base.CollectionUtil;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browsing_data.BrowsingDataCounterBridge.BrowsingDataCounterCallback;
import org.chromium.chrome.browser.browsing_data.TimePeriodUtils.TimePeriodSpinnerOption;
import org.chromium.chrome.browser.feedback.HelpAndFeedbackLauncherFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.quick_delete.QuickDeleteController;
import org.chromium.chrome.browser.settings.ProfileDependentSetting;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.signin.services.SigninManager;
import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.ui.signin.SignOutCoordinator;
import org.chromium.components.browser_ui.settings.ClickableSpansTextMessagePreference;
import org.chromium.components.browser_ui.settings.CustomDividerFragment;
import org.chromium.components.browser_ui.settings.SettingsPage;
import org.chromium.components.browser_ui.settings.SettingsUtils;
import org.chromium.components.browser_ui.settings.SpinnerPreference;
import org.chromium.components.browser_ui.util.TraceEventVectorDrawableCompat;
import org.chromium.components.browsing_data.DeleteBrowsingDataAction;
import org.chromium.components.signin.metrics.SignoutReason;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.ui.modaldialog.ModalDialogManagerHolder;
import org.chromium.ui.text.NoUnderlineClickableSpan;
import org.chromium.ui.text.SpanApplier;
import org.chromium.ui.text.SpanApplier.SpanInfo;
import org.chromium.ui.widget.ButtonCompat;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Set;
/**
* Settings 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 abstract class ClearBrowsingDataFragment extends PreferenceFragmentCompat
implements BrowsingDataBridge.OnClearBrowsingDataListener,
Preference.OnPreferenceClickListener,
Preference.OnPreferenceChangeListener,
SigninManager.SignInStateObserver,
CustomDividerFragment,
SettingsPage,
ProfileDependentSetting {
static final String FETCHER_SUPPLIED_FROM_OUTSIDE =
"ClearBrowsingDataFetcherSuppliedFromOutside";
static final String CLEAR_BROWSING_DATA_REFERRER = "ClearBrowsingDataReferrer";
/** Represents a single item in the dialog. */
private static class Item
implements BrowsingDataCounterCallback, Preference.OnPreferenceClickListener {
private static final int MIN_DP_FOR_ICON = 360;
private final ClearBrowsingDataFragment mParent;
private final @DialogOption int mOption;
private final ClearBrowsingDataCheckBoxPreference mCheckbox;
private BrowsingDataCounterBridge mCounter;
private boolean mShouldAnnounceCounterResult;
public Item(
Context context,
ClearBrowsingDataFragment parent,
@DialogOption int option,
ClearBrowsingDataCheckBoxPreference checkbox,
boolean selected,
boolean enabled) {
super();
mParent = parent;
mOption = option;
mCheckbox = checkbox;
if (option == DialogOption.CLEAR_TABS && !enabled) {
mCheckbox.setSummary(R.string.clear_tabs_disabled_summary);
} else {
mCounter =
new BrowsingDataCounterBridge(
parent.getProfile(),
this,
ClearBrowsingDataFragment.getDataType(mOption),
mParent.getClearBrowsingDataTabType());
}
mCheckbox.setOnPreferenceClickListener(this);
mCheckbox.setEnabled(enabled);
mCheckbox.setChecked(selected);
int dp = mParent.getResources().getConfiguration().smallestScreenWidthDp;
if (dp >= MIN_DP_FOR_ICON) {
@ColorRes
int colorId =
enabled
? R.color.default_icon_color_tint_list
: R.color.default_icon_color_disabled;
mCheckbox.setIcon(
SettingsUtils.getTintedIcon(
context, ClearBrowsingDataFragment.getIcon(option), colorId));
}
}
public void destroy() {
if (mCounter != null) mCounter.destroy();
}
public @DialogOption int getOption() {
return mOption;
}
public boolean isSelected() {
return mCheckbox.isChecked();
}
@Override
public boolean onPreferenceClick(Preference preference) {
assert mCheckbox == preference;
mParent.updateButtonState();
mShouldAnnounceCounterResult = true;
BrowsingDataBridge.getForProfile(mParent.getProfile())
.setBrowsingDataDeletionPreference(
ClearBrowsingDataFragment.getDataType(mOption),
mParent.getClearBrowsingDataTabType(),
mCheckbox.isChecked());
return true;
}
@Override
public void onCounterFinished(String result) {
mCheckbox.setSummary(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;
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static final String PREF_TIME_RANGE = "time_period_spinner";
static final String PREF_GOOGLE_DATA_TEXT = "clear_google_data_text";
static final String PREF_SEARCH_HISTORY_NON_GOOGLE_TEXT =
"clear_search_history_non_google_text";
static final String PREF_SIGN_OUT_OF_CHROME_TEXT = "sign_out_of_chrome_text";
/** The "Clear" button preference. */
@VisibleForTesting public static final String PREF_CLEAR_BUTTON = "clear_button";
/** The tag used for logging. */
public static final String TAG = "ClearBrowsingDataFragment";
/** The histogram for the dialog about other forms of browsing history. */
private static final String DIALOG_HISTOGRAM =
"History.ClearBrowsingData.ShownHistoryNoticeAfterClearing";
/**
* 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. */
@IntDef({
DialogOption.CLEAR_HISTORY,
DialogOption.CLEAR_COOKIES_AND_SITE_DATA,
DialogOption.CLEAR_CACHE,
DialogOption.CLEAR_TABS,
DialogOption.CLEAR_PASSWORDS,
DialogOption.CLEAR_FORM_DATA,
DialogOption.CLEAR_SITE_SETTINGS
})
@Retention(RetentionPolicy.SOURCE)
public @interface DialogOption {
// Values used in "for" loop below - should start from 0 and can't have gaps, lowest value
// is additionally used for starting loop.
// All updates here must also be reflected in {@link #getDataType(int) getDataType}, {@link
// #getPreferenceKey(int) getPreferenceKey} and {@link #getIcon(int) getIcon}.
int CLEAR_HISTORY = 0;
int CLEAR_COOKIES_AND_SITE_DATA = 1;
int CLEAR_CACHE = 2;
int CLEAR_TABS = 3;
int CLEAR_PASSWORDS = 4;
int CLEAR_FORM_DATA = 5;
int CLEAR_SITE_SETTINGS = 6;
int NUM_ENTRIES = 7;
}
public static final String CLEAR_BROWSING_DATA_FETCHER = "clearBrowsingDataFetcher";
private OtherFormsOfHistoryDialogFragment mDialogAboutOtherFormsOfBrowsingHistory;
private Profile mProfile;
private SigninManager mSigninManager;
private ProgressDialog mProgressDialog;
private Item[] mItems;
private ClearBrowsingDataFetcher mFetcher;
// 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 @TimePeriod int mLastSelectedTimePeriod;
private boolean mShouldShowPostDeleteFeedback;
private final ObservableSupplierImpl<String> mPageTitle = new ObservableSupplierImpl<>();
/**
* @return All available {@link DialogOption} entries.
*/
protected static final Set<Integer> getAllOptions() {
assert DialogOption.CLEAR_HISTORY == 0;
Set<Integer> all = new ArraySet<>();
for (@DialogOption int i = DialogOption.CLEAR_HISTORY; i < DialogOption.NUM_ENTRIES; i++) {
all.add(i);
}
return all;
}
static @BrowsingDataType int getDataType(@DialogOption int type) {
switch (type) {
case DialogOption.CLEAR_CACHE:
return BrowsingDataType.CACHE;
case DialogOption.CLEAR_COOKIES_AND_SITE_DATA:
return BrowsingDataType.SITE_DATA;
case DialogOption.CLEAR_FORM_DATA:
return BrowsingDataType.FORM_DATA;
case DialogOption.CLEAR_HISTORY:
return BrowsingDataType.HISTORY;
case DialogOption.CLEAR_PASSWORDS:
return BrowsingDataType.PASSWORDS;
case DialogOption.CLEAR_SITE_SETTINGS:
return BrowsingDataType.SITE_SETTINGS;
case DialogOption.CLEAR_TABS:
return BrowsingDataType.TABS;
default:
throw new IllegalArgumentException();
}
}
static String getPreferenceKey(@DialogOption int type) {
switch (type) {
case DialogOption.CLEAR_CACHE:
return "clear_cache_checkbox";
case DialogOption.CLEAR_COOKIES_AND_SITE_DATA:
return "clear_cookies_checkbox";
case DialogOption.CLEAR_FORM_DATA:
return "clear_form_data_checkbox";
case DialogOption.CLEAR_HISTORY:
return "clear_history_checkbox";
case DialogOption.CLEAR_PASSWORDS:
return "clear_passwords_checkbox";
case DialogOption.CLEAR_SITE_SETTINGS:
return "clear_site_settings_checkbox";
case DialogOption.CLEAR_TABS:
return "clear_tabs_checkbox";
default:
throw new IllegalArgumentException();
}
}
static @DrawableRes int getIcon(@DialogOption int type) {
switch (type) {
case DialogOption.CLEAR_CACHE:
return R.drawable.ic_collections_grey;
case DialogOption.CLEAR_COOKIES_AND_SITE_DATA:
return R.drawable.permission_cookie;
case DialogOption.CLEAR_FORM_DATA:
return R.drawable.ic_edit_24dp;
case DialogOption.CLEAR_HISTORY:
return R.drawable.ic_watch_later_24dp;
case DialogOption.CLEAR_PASSWORDS:
return R.drawable.ic_password_manager_key;
case DialogOption.CLEAR_SITE_SETTINGS:
return R.drawable.ic_tv_options_input_settings_rotated_grey;
case DialogOption.CLEAR_TABS:
return R.drawable.ic_tab_icon_24dp;
default:
throw new IllegalArgumentException();
}
}
/**
* A method to create the {@link ClearBrowsingDataFragment} arguments.
*
* @param referrer The name of the referrer activity.
* @param isFetcherSuppliedFromOutside A boolean indicating whether the {@link
* ClearBrowsingDataFetcher} would be supplied later or it needs to be re-created.
*/
public static Bundle createFragmentArgs(String referrer, boolean isFetcherSuppliedFromOutside) {
Bundle bundle = new Bundle();
bundle.putBoolean(
ClearBrowsingDataFragment.FETCHER_SUPPLIED_FROM_OUTSIDE,
isFetcherSuppliedFromOutside);
bundle.putString(ClearBrowsingDataFragment.CLEAR_BROWSING_DATA_REFERRER, referrer);
return bundle;
}
/**
* @return The currently selected {@link DialogOption} entries.
*/
protected final Set<Integer> getSelectedOptions() {
Set<Integer> selected = new ArraySet<>();
for (Item item : mItems) {
if (item.isSelected()) selected.add(item.getOption());
}
return selected;
}
@Override
public void setProfile(Profile profile) {
mProfile = profile;
}
/** @return The Profile associated with the displayed Settings. */
protected Profile getProfile() {
return mProfile;
}
/**
* @param fetcher A ClearBrowsingDataFetcher.
*/
public void setClearBrowsingDataFetcher(ClearBrowsingDataFetcher fetcher) {
assert mFetcher == null : "Fetcher previously set already.";
mFetcher = fetcher;
}
@VisibleForTesting
public ClearBrowsingDataFetcher getClearBrowsingDataFetcher() {
return mFetcher;
}
/** Notifies subclasses that browsing data is about to be cleared. */
protected void onClearBrowsingData() {}
/**
* 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 void clearBrowsingData(
Set<Integer> options,
@Nullable String[] excludedDomains,
@Nullable int[] excludedDomainReasons,
@Nullable String[] ignoredDomains,
@Nullable int[] ignoredDomainReasons) {
onClearBrowsingData();
showProgressDialog();
Set<Integer> dataTypes = new ArraySet<>();
for (@DialogOption Integer option : options) {
dataTypes.add(getDataType(option));
}
final @CookieOrCacheDeletionChoice int choice;
if (dataTypes.contains(BrowsingDataType.SITE_DATA)) {
choice =
dataTypes.contains(BrowsingDataType.CACHE)
? CookieOrCacheDeletionChoice.BOTH_COOKIES_AND_CACHE
: CookieOrCacheDeletionChoice.ONLY_COOKIES;
} else {
choice =
dataTypes.contains(BrowsingDataType.CACHE)
? CookieOrCacheDeletionChoice.ONLY_CACHE
: CookieOrCacheDeletionChoice.NEITHER_COOKIES_NOR_CACHE;
}
RecordHistogram.recordEnumeratedHistogram(
"History.ClearBrowsingData.UserDeletedCookieOrCacheFromDialog",
choice,
CookieOrCacheDeletionChoice.MAX_CHOICE_VALUE);
RecordHistogram.recordEnumeratedHistogram(
"Privacy.DeleteBrowsingData.Action",
DeleteBrowsingDataAction.CLEAR_BROWSING_DATA_DIALOG,
DeleteBrowsingDataAction.MAX_VALUE);
Object spinnerSelection =
((SpinnerPreference) findPreference(PREF_TIME_RANGE)).getSelectedOption();
mLastSelectedTimePeriod = ((TimePeriodSpinnerOption) spinnerSelection).getTimePeriod();
int[] dataTypesArray = CollectionUtil.integerCollectionToIntArray(dataTypes);
if (excludedDomains != null && excludedDomains.length != 0) {
BrowsingDataBridge.getForProfile(mProfile)
.clearBrowsingDataExcludingDomains(
this,
dataTypesArray,
mLastSelectedTimePeriod,
excludedDomains,
excludedDomainReasons,
ignoredDomains,
ignoredDomainReasons);
} else {
BrowsingDataBridge.getForProfile(mProfile)
.clearBrowsingData(this, dataTypesArray, mLastSelectedTimePeriod);
}
}
private void dismissProgressDialog() {
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
}
mProgressDialog = null;
}
/** Returns the list of supported {@link DialogOption}. */
protected abstract List<Integer> getDialogOptions(Bundle fragmentArgs);
/** Returns whether is a basic or advanced Clear Browsing Data tab. */
protected abstract @ClearBrowsingDataTab int getClearBrowsingDataTabType();
/**
* 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 int option) {
return BrowsingDataBridge.getForProfile(mProfile)
.getBrowsingDataDeletionPreference(
getDataType(option), getClearBrowsingDataTabType());
}
/**
* Called when clearing browsing data completes.
* Implements the BrowsingDataBridge.OnClearBrowsingDataListener interface.
*/
@Override
public void onBrowsingDataCleared() {
if (getActivity() == null) return;
mShouldShowPostDeleteFeedback = QuickDeleteController.isQuickDeleteFollowupEnabled();
// 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)
&& mFetcher.isDialogAboutOtherFormsOfBrowsingHistoryEnabled()
&& !OtherFormsOfHistoryDialogFragment.wasDialogShown()) {
mDialogAboutOtherFormsOfBrowsingHistory = new OtherFormsOfHistoryDialogFragment();
FragmentActivity fragmentActivity = (FragmentActivity) getActivity();
mDialogAboutOtherFormsOfBrowsingHistory.show(fragmentActivity);
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() {
Set<Integer> selectedOptions = getSelectedOptions();
if (!selectedOptions.contains(DialogOption.CLEAR_CACHE)
&& !selectedOptions.contains(DialogOption.CLEAR_COOKIES_AND_SITE_DATA)) {
return false;
}
boolean haveImportantSites =
mFetcher.getSortedImportantDomains() != null
&& mFetcher.getSortedImportantDomains().length != 0;
RecordHistogram.recordBooleanHistogram(
"History.ClearBrowsingData.ImportantDialogShown", haveImportantSites);
return haveImportantSites;
}
@Override
public boolean onPreferenceClick(Preference preference) {
if (preference.getKey().equals(PREF_CLEAR_BUTTON)) {
onClearButtonClicked();
return true;
}
return false;
}
/**
* Either shows the important sites dialog or clears browsing data according to the selected
* options.
*/
private void onClearButtonClicked() {
if (shouldShowImportantSitesDialog()) {
showImportantDialogThenClear();
return;
}
// 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);
}
@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);
}
BrowsingDataBridge.getForProfile(mProfile)
.setBrowsingDataDeletionTimePeriod(
getClearBrowsingDataTabType(),
((TimePeriodSpinnerOption) value).getTimePeriod());
return true;
}
return false;
}
/** Disable the "Clear" button if none of the options are selected. Otherwise, enable it. */
private void updateButtonState() {
Button clearButton = (Button) getView().findViewById(R.id.clear_button);
boolean isEnabled = !getSelectedOptions().isEmpty();
clearButton.setEnabled(isEnabled);
}
private int getSpinnerIndex(
@TimePeriod int timePeriod, TimePeriodSpinnerOption[] spinnerOptions) {
int spinnerOptionIndex = -1;
for (int i = 0; i < spinnerOptions.length; ++i) {
if (spinnerOptions[i].getTimePeriod() == timePeriod) {
spinnerOptionIndex = i;
break;
}
}
return spinnerOptionIndex;
}
private void setUpClearBrowsingDataFetcher(Bundle savedInstanceState, Bundle fragmentArgs) {
if (savedInstanceState != null) {
mFetcher = savedInstanceState.getParcelable(CLEAR_BROWSING_DATA_FETCHER);
return;
}
boolean isSuppliedFromOutside =
fragmentArgs.getBoolean(
ClearBrowsingDataFragment.FETCHER_SUPPLIED_FROM_OUTSIDE, false);
if (!isSuppliedFromOutside) {
assert mFetcher == null : "Fetcher previously re-assigned";
mFetcher = new ClearBrowsingDataFetcher();
mFetcher.fetchImportantSites(mProfile);
mFetcher.requestInfoAboutOtherFormsOfBrowsingHistory(mProfile);
}
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Bundle fragmentArgs = getArguments();
assert fragmentArgs != null : "A valid fragment argument is required.";
setUpClearBrowsingDataFetcher(savedInstanceState, fragmentArgs);
mPageTitle.set(getString(R.string.clear_browsing_data_title));
SettingsUtils.addPreferencesFromResource(this, R.xml.clear_browsing_data_preferences_tab);
mSigninManager = IdentityServicesProvider.get().getSigninManager(mProfile);
List<Integer> options = getDialogOptions(fragmentArgs);
mItems = new Item[options.size()];
BrowsingDataBridge browsingDataBridge = BrowsingDataBridge.getForProfile(mProfile);
for (int i = 0; i < options.size(); i++) {
@DialogOption int option = options.get(i);
boolean enabled = true;
// It is possible to disable the deletion of browsing history.
if (option == DialogOption.CLEAR_HISTORY
&& !UserPrefs.get(mProfile).getBoolean(Pref.ALLOW_DELETING_BROWSER_HISTORY)) {
enabled = false;
browsingDataBridge.setBrowsingDataDeletionPreference(
getDataType(DialogOption.CLEAR_HISTORY), ClearBrowsingDataTab.BASIC, false);
browsingDataBridge.setBrowsingDataDeletionPreference(
getDataType(DialogOption.CLEAR_HISTORY),
ClearBrowsingDataTab.ADVANCED,
false);
}
// Disable tabs closure if the user is in multi-window mode.
// TODO(b/333036591): Remove this check once tab closure works properly across
// multi-instances.
if (option == DialogOption.CLEAR_TABS) {
enabled = !isInMultiWindowMode();
RecordHistogram.recordBooleanHistogram(
"Privacy.ClearBrowsingData.TabsEnabled", enabled);
}
mItems[i] =
new Item(
getActivity(),
this,
option,
(ClearBrowsingDataCheckBoxPreference)
findPreference(getPreferenceKey(option)),
enabled && isOptionSelectedByDefault(option),
enabled);
}
// Not all checkboxes defined in the layout are necessarily handled by this class
// or a particular subclass. Hide those that are not.
Set<Integer> unboundOptions = getAllOptions();
unboundOptions.removeAll(options);
for (@DialogOption Integer option : unboundOptions) {
getPreferenceScreen().removePreference(findPreference(getPreferenceKey(option)));
}
// The time range selection spinner.
SpinnerPreference spinner = (SpinnerPreference) findPreference(PREF_TIME_RANGE);
TimePeriodSpinnerOption[] spinnerOptions = getTimePeriodSpinnerOptions(getActivity());
@TimePeriod
int selectedTimePeriod =
browsingDataBridge.getBrowsingDataDeletionTimePeriod(getClearBrowsingDataTabType());
int spinnerOptionIndex = getSpinnerIndex(selectedTimePeriod, spinnerOptions);
// If there is no previously-selected value, use last hour as the default.
if (spinnerOptionIndex == -1) {
spinnerOptionIndex = getSpinnerIndex(TimePeriod.LAST_HOUR, spinnerOptions);
}
assert spinnerOptionIndex != -1;
spinner.setOptions(spinnerOptions, spinnerOptionIndex);
spinner.setOnPreferenceChangeListener(this);
// Text for sign-out option.
updateSignOutOfChromeText();
mSigninManager.addSignInStateObserver(this);
setHasOptionsMenu(true);
}
@Override
public ObservableSupplier<String> getPageTitle() {
return mPageTitle;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// mFetcher acts as a cache for important sites and history data. If the activity gets
// suspended, we can save the cached data and reuse it when we are activated again.
outState.putParcelable(CLEAR_BROWSING_DATA_FETCHER, mFetcher);
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
LinearLayout view =
(LinearLayout) super.onCreateView(inflater, container, savedInstanceState);
// Add button to bottom of the preferences view.
ButtonCompat clearButton =
(ButtonCompat) inflater.inflate(R.layout.clear_browsing_data_button, view, false);
clearButton.setOnClickListener((View v) -> onClearButtonClicked());
view.addView(clearButton);
// Disable animations of preference changes.
getListView().setItemAnimator(null);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Now that the dialog's view has been created, update the button state.
updateButtonState();
}
@Override
public boolean hasDivider() {
// Remove the dividers between checkboxes.
return false;
}
@Override
public void onDestroy() {
super.onDestroy();
dismissProgressDialog();
for (Item item : mItems) {
item.destroy();
}
mSigninManager.removeSignInStateObserver(this);
if (mShouldShowPostDeleteFeedback) {
triggerHapticFeedback();
showSnackbar();
}
}
// We either show the dialog, or modify the current one to display our messages. This avoids
// a dialog flash.
private 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;
}
private void updateSignOutOfChromeText() {
ClickableSpansTextMessagePreference signOutOfChromeTextPref =
findPreference(ClearBrowsingDataFragment.PREF_SIGN_OUT_OF_CHROME_TEXT);
if (mSigninManager.isSignOutAllowed()) {
signOutOfChromeTextPref.setSummary(buildSignOutOfChromeText());
signOutOfChromeTextPref.setVisible(true);
} else {
signOutOfChromeTextPref.setVisible(false);
}
}
@VisibleForTesting
SpannableString buildSignOutOfChromeText() {
int signOutOfChromeStringId =
getClearBrowsingDataTabType() == ClearBrowsingDataTab.ADVANCED
&& ChromeFeatureList.isEnabled(
ChromeFeatureList.QUICK_DELETE_FOR_ANDROID)
? R.string.sign_out_of_chrome_link_advanced
: R.string.sign_out_of_chrome_link;
return SpanApplier.applySpans(
getContext().getString(signOutOfChromeStringId),
new SpanInfo(
"<link1>",
"</link1>",
new NoUnderlineClickableSpan(
requireContext(), createSignOutOfChromeCallback())));
}
private Callback<View> createSignOutOfChromeCallback() {
return view ->
SignOutCoordinator.startSignOutFlow(
requireContext(),
mProfile,
getFragmentManager(),
((ModalDialogManagerHolder) getActivity()).getModalDialogManager(),
((SnackbarManager.SnackbarManageable) getActivity()).getSnackbarManager(),
SignoutReason.USER_CLICKED_SIGNOUT_FROM_CLEAR_BROWSING_DATA_PAGE,
/* showConfirmDialog= */ true,
() -> {});
}
/**
* This method shows the important sites dialog. After the dialog is shown, we correctly clear.
*/
private void showImportantDialogThenClear() {
mConfirmImportantSitesDialog =
ConfirmImportantSitesDialogFragment.newInstance(
mFetcher.getSortedImportantDomains(),
mFetcher.getSortedImportantDomainReasons(),
mFetcher.getSortedExampleOrigins());
mConfirmImportantSitesDialog.setTargetFragment(this, IMPORTANT_SITES_DIALOG_CODE);
mConfirmImportantSitesDialog.show(
getFragmentManager(), ConfirmImportantSitesDialogFragment.FRAGMENT_TAG);
}
/** Used only to access the dialog about other forms of browsing history from tests. */
@VisibleForTesting
OtherFormsOfHistoryDialogFragment getDialogAboutOtherFormsOfBrowsingHistory() {
return mDialogAboutOtherFormsOfBrowsingHistory;
}
/**
* 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 && mFetcher.getSortedImportantDomains() != null) {
// mMaxImportantSites is a constant on the C++ side.
RecordHistogram.recordCustomCountHistogram(
"History.ClearBrowsingData.ImportantDeselectedNum",
deselectedDomains.length,
1,
mFetcher.getMaxImportantSites() + 1,
mFetcher.getMaxImportantSites() + 1);
RecordHistogram.recordCustomCountHistogram(
"History.ClearBrowsingData.ImportantIgnoredNum",
ignoredDomains.length,
1,
mFetcher.getMaxImportantSites() + 1,
mFetcher.getMaxImportantSites() + 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
/ mFetcher.getSortedImportantDomains().length,
IMPORTANT_SITES_PERCENTAGE_BUCKET_COUNT + 1);
RecordHistogram.recordEnumeratedHistogram(
"History.ClearBrowsingData.ImportantIgnoredPercent",
ignoredDomains.length
* IMPORTANT_SITES_PERCENTAGE_BUCKET_COUNT
/ mFetcher.getSortedImportantDomains().length,
IMPORTANT_SITES_PERCENTAGE_BUCKET_COUNT + 1);
}
clearBrowsingData(
getSelectedOptions(),
deselectedDomains,
deselectedDomainReasons,
ignoredDomains,
ignoredDomainReasons);
}
}
/** {@link SigninManager.SignInStateObserver} implementation. */
@Override
public void onSignOutAllowedChanged() {
updateSignOutOfChromeText();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
menu.clear();
MenuItem help =
menu.add(Menu.NONE, R.id.menu_id_targeted_help, Menu.NONE, R.string.menu_help);
help.setIcon(
TraceEventVectorDrawableCompat.create(
getResources(), R.drawable.ic_help_and_feedback, getActivity().getTheme()));
help.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menu_id_targeted_help) {
HelpAndFeedbackLauncherFactory.getForProfile(mProfile)
.show(
getActivity(),
getString(R.string.help_context_clear_browsing_data),
null);
return true;
}
return super.onOptionsItemSelected(item);
}
/** Get the last focused activity that has not been destroyed. */
private Activity getLastFocusedActivity() {
if (ApplicationStatus.hasVisibleActivities()) {
return ApplicationStatus.getLastTrackedFocusedActivity();
} else {
return null;
}
}
/** A method to show the post-delete snack-bar confirmation. */
private void showSnackbar() {
SnackbarManager snackbarManager = null;
Activity activity = getLastFocusedActivity();
if (activity instanceof SnackbarManager.SnackbarManageable) {
snackbarManager = ((SnackbarManager.SnackbarManageable) activity).getSnackbarManager();
}
if (snackbarManager == null) return;
String snackbarMessage;
if (mLastSelectedTimePeriod == TimePeriod.ALL_TIME) {
snackbarMessage =
getActivity().getString(R.string.quick_delete_snackbar_all_time_message);
} else {
snackbarMessage =
getActivity()
.getString(
R.string.quick_delete_snackbar_message,
TimePeriodUtils.getTimePeriodString(
getActivity(), mLastSelectedTimePeriod));
}
Snackbar snackbar =
Snackbar.make(
snackbarMessage,
/* controller= */ null,
Snackbar.TYPE_NOTIFICATION,
Snackbar.UMA_CLEAR_BROWSING_DATA);
snackbarManager.showSnackbar(snackbar);
}
private void triggerHapticFeedback() {
Activity activity = getLastFocusedActivity();
if (activity == null) return;
Vibrator v = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
final long duration = 50;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
v.vibrate(VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE));
} else {
// Deprecated in API 26.
v.vibrate(duration);
}
}
private boolean isInMultiWindowMode() {
return MultiWindowUtils.getInstanceCount() > 1;
}
}