chromium/chrome/android/java/src/org/chromium/chrome/browser/browsing_data/ConfirmImportantSitesDialogFragment.java

// Copyright 2016 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 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.Drawable;
import android.os.Bundle;
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 android.widget.TextView;

import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;

import org.chromium.base.CollectionUtil;
import org.chromium.base.ContextUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.ui.favicon.FaviconUtils;
import org.chromium.chrome.browser.webapps.WebappRegistry;
import org.chromium.components.browser_ui.util.ConversionUtils;
import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
import org.chromium.components.favicon.IconType;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.components.favicon.LargeIconBridge.LargeIconCallback;

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(getContext());
        }

        @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 = childView.findViewById(R.id.icon_row_checkbox);
                viewHolder.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 =
                                    FaviconUtils.getIconDrawableWithoutFilter(
                                            icon,
                                            url,
                                            fallbackColor,
                                            mIconGenerator,
                                            getResources(),
                                            mFaviconSize);
                            viewHolder.imageView.setImageDrawable(image);
                        }
                    };
            mLargeIconBridge.getLargeIconForStringUrl(url, mFaviconSize, viewHolder.imageCallback);
        }
    }

    /**
     * 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 = ProfileManager.getLastUsedRegularProfile();
        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.integerCollectionToIntArray(
                                            deselectedDomainReasons));
                            data.putExtra(
                                    IGNORED_DOMAINS_TAG, ignoredDomains.toArray(new String[0]));
                            data.putExtra(
                                    IGNORED_DOMAIN_REASONS_TAG,
                                    CollectionUtil.integerCollectionToIntArray(
                                            ignoredDomainReasons));
                            getTargetFragment()
                                    .onActivityResult(
                                            getTargetRequestCode(), Activity.RESULT_OK, data);
                        } else {
                            getTargetFragment()
                                    .onActivityResult(
                                            getTargetRequestCode(),
                                            Activity.RESULT_CANCELED,
                                            getActivity().getIntent());
                        }
                    }
                };

        Set<String> originsWithApps = WebappRegistry.getInstance().getOriginsWithInstalledApp();
        boolean includesApp = false;
        for (String domain : mImportantDomains) {
            if (originsWithApps.contains(domain)) {
                includesApp = true;
                break;
            }
        }

        int titleResource =
                includesApp
                        ? R.string.important_sites_title_with_app
                        : R.string.important_sites_title;
        int messageResource =
                includesApp
                        ? R.string.clear_browsing_data_important_dialog_text_with_app
                        : R.string.clear_browsing_data_important_dialog_text;
        View messageAndListView =
                getActivity()
                        .getLayoutInflater()
                        .inflate(R.layout.clear_browsing_important_dialog_listview, null);
        mSitesListView = messageAndListView.findViewById(R.id.select_dialog_listview);
        mSitesListView.setAdapter(mAdapter);
        mSitesListView.setOnItemClickListener(mAdapter);
        TextView message = messageAndListView.findViewById(R.id.message);
        message.setText(messageResource);

        final AlertDialog.Builder builder =
                new AlertDialog.Builder(getActivity(), R.style.ThemeOverlay_BrowserUI_AlertDialog)
                        .setTitle(titleResource)
                        .setPositiveButton(
                                R.string.clear_browsing_data_important_dialog_button, listener)
                        .setNegativeButton(R.string.cancel, listener)
                        .setView(messageAndListView);
        mDialog = builder.create();

        return mDialog;
    }
}