blob: c418a42df49d98fb9eeb9f98a01b97492286a49c [file] [log] [blame]
// Copyright 2016 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.ActivityManager;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.ListView;
import org.chromium.base.CollectionUtil;
import org.chromium.base.ContextUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.favicon.FaviconUtils;
import org.chromium.chrome.browser.favicon.IconType;
import org.chromium.chrome.browser.favicon.LargeIconBridge;
import org.chromium.chrome.browser.favicon.LargeIconBridge.LargeIconCallback;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.ui.widget.RoundedIconGenerator;
import org.chromium.chrome.browser.util.ConversionUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* Modal dialog that shows a list of important domains to the user which they can uncheck. Used to
* allow the user to exclude domains from being cleared by the clear browsing data function.
* We use proper bundle construction (through the {@link #newInstance(String[], int[], String[])}
* method) and onActivityResult return conventions.
*/
public class ConfirmImportantSitesDialogFragment extends DialogFragment {
private class ClearBrowsingDataAdapter extends ArrayAdapter<String>
implements AdapterView.OnItemClickListener {
private final String[] mDomains;
private final int mFaviconSize;
private RoundedIconGenerator mIconGenerator;
private ClearBrowsingDataAdapter(
String[] domains, String[] faviconURLs, Resources resources) {
super(getActivity(), R.layout.confirm_important_sites_list_row, domains);
mDomains = domains;
mFaviconURLs = faviconURLs;
mFaviconSize = resources.getDimensionPixelSize(R.dimen.default_favicon_size);
mIconGenerator = FaviconUtils.createRoundedRectangleIconGenerator(getResources());
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View childView = convertView;
if (childView == null) {
LayoutInflater inflater = LayoutInflater.from(getActivity());
childView =
inflater.inflate(R.layout.confirm_important_sites_list_row, parent, false);
ViewAndFaviconHolder viewHolder = new ViewAndFaviconHolder();
viewHolder.checkboxView = (CheckBox) childView.findViewById(R.id.icon_row_checkbox);
viewHolder.imageView = (ImageView) childView.findViewById(R.id.icon_row_image);
childView.setTag(viewHolder);
}
ViewAndFaviconHolder viewHolder = (ViewAndFaviconHolder) childView.getTag();
configureChildView(position, viewHolder);
return childView;
}
private void configureChildView(int position, ViewAndFaviconHolder viewHolder) {
String domain = mDomains[position];
viewHolder.checkboxView.setChecked(mCheckedState.get(domain));
viewHolder.checkboxView.setText(domain);
loadFavicon(viewHolder, mFaviconURLs[position]);
}
/**
* Called when a list item is clicked. We toggle the checkbox and update our selected
* domains list.
*/
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String domain = mDomains[position];
ViewAndFaviconHolder viewHolder = (ViewAndFaviconHolder) view.getTag();
boolean isChecked = mCheckedState.get(domain);
mCheckedState.put(domain, !isChecked);
viewHolder.checkboxView.setChecked(!isChecked);
}
private void loadFavicon(final ViewAndFaviconHolder viewHolder, final String url) {
viewHolder.imageCallback = new LargeIconCallback() {
@Override
public void onLargeIconAvailable(Bitmap icon, int fallbackColor,
boolean isFallbackColorDefault, @IconType int iconType) {
if (this != viewHolder.imageCallback) return;
Drawable image = getFaviconDrawable(icon, fallbackColor, url);
viewHolder.imageView.setImageDrawable(image);
}
};
mLargeIconBridge.getLargeIconForUrl(url, mFaviconSize, viewHolder.imageCallback);
}
private Drawable getFaviconDrawable(Bitmap icon, int fallbackColor, String url) {
if (icon == null) {
mIconGenerator.setBackgroundColor(fallbackColor);
icon = mIconGenerator.generateIconForUrl(url);
return new BitmapDrawable(getResources(), icon);
} else {
return FaviconUtils.createRoundedBitmapDrawable(getResources(),
Bitmap.createScaledBitmap(icon, mFaviconSize, mFaviconSize, false));
}
}
}
/**
* ViewHolder class optimizes looking up table row fields. findViewById is only called once
* per row view initialization, and the references are cached here. Also stores a reference to
* the favicon image callback so that we can make sure we load the correct favicon.
*/
private static class ViewAndFaviconHolder {
public CheckBox checkboxView;
public ImageView imageView;
public LargeIconCallback imageCallback;
}
/**
* Constructs a new instance of the important sites dialog fragment.
* @param importantDomains The list of important domains to display.
* @param importantDomainReasons The reasons for choosing each important domain.
* @param faviconURLs The list of favicon urls that correspond to each importantDomains.
* @return An instance of ConfirmImportantSitesDialogFragment with the bundle arguments set.
*/
public static ConfirmImportantSitesDialogFragment newInstance(
String[] importantDomains, int[] importantDomainReasons, String[] faviconURLs) {
ConfirmImportantSitesDialogFragment dialogFragment =
new ConfirmImportantSitesDialogFragment();
Bundle bundle = new Bundle();
bundle.putStringArray(IMPORTANT_DOMAINS_TAG, importantDomains);
bundle.putIntArray(IMPORTANT_DOMAIN_REASONS_TAG, importantDomainReasons);
bundle.putStringArray(FAVICON_URLS_TAG, faviconURLs);
dialogFragment.setArguments(bundle);
return dialogFragment;
}
private static final int FAVICON_MAX_CACHE_SIZE_BYTES =
100 * ConversionUtils.BYTES_PER_KILOBYTE; // 100KB
/** The tag used when showing the clear browsing fragment. */
public static final String FRAGMENT_TAG = "ConfirmImportantSitesDialogFragment";
/** The tag for the string array of deselected domains. These are meant to NOT be cleared. */
public static final String DESELECTED_DOMAINS_TAG = "DeselectedDomains";
/** The tag for the int array of reasons the deselected domains were important. */
public static final String DESELECTED_DOMAIN_REASONS_TAG = "DeselectedDomainReasons";
/** The tag for the string array of ignored domains, which whill be cleared. */
public static final String IGNORED_DOMAINS_TAG = "IgnoredDomains";
/** The tag for the int array of reasons the ignored domains were important. */
public static final String IGNORED_DOMAIN_REASONS_TAG = "IgnoredDomainReasons";
/** The tag used for logging. */
public static final String TAG = "ConfirmImportantSitesDialogFragment";
/** The tag used to store the important domains in the bundle. */
private static final String IMPORTANT_DOMAINS_TAG = "ImportantDomains";
/** The tag used to store the important domain reasons in the bundle. */
private static final String IMPORTANT_DOMAIN_REASONS_TAG = "ImportantDomainReasons";
/** The tag used to store the favicon urls corresponding to each important domain. */
private static final String FAVICON_URLS_TAG = "FaviconURLs";
/** Array of important registerable domains we're showing to the user. */
private String[] mImportantDomains;
/** Map of the reasons the above important domains were chosen. */
private Map<String, Integer> mImportantDomainsReasons;
/** Array of favicon urls to use for each important domain above. */
private String[] mFaviconURLs;
/** The map of domains to the checked state, where true is checked. */
private Map<String, Boolean> mCheckedState;
/** The alert dialog shown to the user. */
private AlertDialog mDialog;
/** Our adapter that we use with the list view in the dialog. */
private ClearBrowsingDataAdapter mAdapter;
private LargeIconBridge mLargeIconBridge;
private Profile mProfile;
/** We store the custom list view for testing */
private ListView mSitesListView;
public ConfirmImportantSitesDialogFragment() {
mImportantDomainsReasons = new HashMap<>();
mCheckedState = new HashMap<>();
}
@Override
public void setArguments(Bundle args) {
super.setArguments(args);
mImportantDomains = args.getStringArray(IMPORTANT_DOMAINS_TAG);
mFaviconURLs = args.getStringArray(FAVICON_URLS_TAG);
int[] importantDomainReasons = args.getIntArray(IMPORTANT_DOMAIN_REASONS_TAG);
for (int i = 0; i < mImportantDomains.length; ++i) {
mImportantDomainsReasons.put(mImportantDomains[i], importantDomainReasons[i]);
mCheckedState.put(mImportantDomains[i], true);
}
}
@VisibleForTesting
public Set<String> getDeselectedDomains() {
HashSet<String> deselected = new HashSet<>();
for (Entry<String, Boolean> entry : mCheckedState.entrySet()) {
if (!entry.getValue()) deselected.add(entry.getKey());
}
return deselected;
}
@VisibleForTesting
public ListView getSitesList() {
return mSitesListView;
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (mLargeIconBridge != null) {
mLargeIconBridge.destroy();
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// We check the domains and urls as well due to crbug.com/622879.
if (savedInstanceState != null) {
// The important domains and favicon URLs aren't currently saved, so if this dialog
// is recreated from a saved instance they will be null. This method must return a
// valid dialog, so these two array's are initialized, then the dialog is dismissed.
// TODO(dmurph): save mImportantDomains and mFaviconURLs so that they can be restored
// from a savedInstanceState and the dialog can be properly recreated rather than
// dismissed.
mImportantDomains = new String[0];
mFaviconURLs = new String[0];
dismiss();
}
mProfile = Profile.getLastUsedProfile().getOriginalProfile();
mLargeIconBridge = new LargeIconBridge(mProfile);
ActivityManager activityManager =
((ActivityManager) ContextUtils.getApplicationContext().getSystemService(
Context.ACTIVITY_SERVICE));
int maxSize = Math.min(
activityManager.getMemoryClass() / 16 * 25 * ConversionUtils.BYTES_PER_KILOBYTE,
FAVICON_MAX_CACHE_SIZE_BYTES);
mLargeIconBridge.createCache(maxSize);
mAdapter = new ClearBrowsingDataAdapter(mImportantDomains, mFaviconURLs, getResources());
DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == AlertDialog.BUTTON_POSITIVE) {
Intent data = new Intent();
List<String> deselectedDomains = new ArrayList<>();
List<Integer> deselectedDomainReasons = new ArrayList<>();
List<String> ignoredDomains = new ArrayList<>();
List<Integer> ignoredDomainReasons = new ArrayList<>();
for (Entry<String, Boolean> entry : mCheckedState.entrySet()) {
Integer reason = mImportantDomainsReasons.get(entry.getKey());
if (entry.getValue()) {
ignoredDomains.add(entry.getKey());
ignoredDomainReasons.add(reason);
} else {
deselectedDomains.add(entry.getKey());
deselectedDomainReasons.add(reason);
}
}
data.putExtra(DESELECTED_DOMAINS_TAG, deselectedDomains.toArray(new String[0]));
data.putExtra(DESELECTED_DOMAIN_REASONS_TAG,
CollectionUtil.integerListToIntArray(deselectedDomainReasons));
data.putExtra(IGNORED_DOMAINS_TAG, ignoredDomains.toArray(new String[0]));
data.putExtra(IGNORED_DOMAIN_REASONS_TAG,
CollectionUtil.integerListToIntArray(ignoredDomainReasons));
getTargetFragment().onActivityResult(
getTargetRequestCode(), Activity.RESULT_OK, data);
} else {
getTargetFragment().onActivityResult(getTargetRequestCode(),
Activity.RESULT_CANCELED, getActivity().getIntent());
}
}
};
// We create our own ListView, as AlertDialog doesn't let us set a message and a list
// adapter at the same time.
View messageAndListView = getActivity().getLayoutInflater().inflate(
R.layout.clear_browsing_important_dialog_listview, null);
mSitesListView = (ListView) messageAndListView.findViewById(R.id.select_dialog_listview);
mSitesListView.setAdapter(mAdapter);
mSitesListView.setOnItemClickListener(mAdapter);
final AlertDialog.Builder builder =
new AlertDialog.Builder(getActivity(), R.style.Theme_Chromium_AlertDialog)
.setTitle(R.string.storage_clear_site_storage_title)
.setPositiveButton(
R.string.clear_browsing_data_important_dialog_button, listener)
.setNegativeButton(R.string.cancel, listener)
.setView(messageAndListView);
mDialog = builder.create();
return mDialog;
}
}