chromium/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/ChosenObjectSettings.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.components.browser_ui.site_settings;

import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.EditorInfo;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.core.view.MenuItemCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;

import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.build.BuildConfig;
import org.chromium.components.browser_ui.settings.ChromeImageViewPreference;
import org.chromium.components.browser_ui.settings.CustomDividerFragment;
import org.chromium.components.browser_ui.settings.ManagedPreferencesUtils;
import org.chromium.components.browser_ui.settings.SettingsPage;
import org.chromium.components.browser_ui.util.TraceEventVectorDrawableCompat;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Locale;

/**
 * Shows a particular chosen object (e.g. a USB device) and the list of sites that have been granted
 * access to it by the user.
 */
public class ChosenObjectSettings extends BaseSiteSettingsFragment
        implements SettingsPage, CustomDividerFragment {
    public static final String EXTRA_OBJECT_INFOS = "org.chromium.chrome.preferences.object_infos";
    public static final String EXTRA_SITES = "org.chromium.chrome.preferences.site_set";
    public static final String EXTRA_CATEGORY =
            "org.chromium.chrome.preferences.content_settings_type";

    // The site settings category we are showing.
    private SiteSettingsCategory mCategory;
    // The set of object permissions being examined.
    private ArrayList<ChosenObjectInfo> mObjectInfos;
    // The set of sites to display.
    private ArrayList<Website> mSites;
    // The view for searching the list of items.
    private SearchView mSearchView;
    // If not blank, represents a substring to use to search for site names.
    private String mSearch = "";

    private final ObservableSupplierImpl<String> mPageTitle = new ObservableSupplierImpl<>();

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        // Set empty preferences screen.
        PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(getStyledContext());
        setPreferenceScreen(screen);
    }

    @Override
    @SuppressWarnings("unchecked")
    public void onActivityCreated(Bundle savedInstanceState) {
        int contentSettingsType = getArguments().getInt(EXTRA_CATEGORY);
        mCategory =
                SiteSettingsCategory.createFromContentSettingsType(
                        getSiteSettingsDelegate().getBrowserContextHandle(), contentSettingsType);
        mObjectInfos =
                (ArrayList<ChosenObjectInfo>) getArguments().getSerializable(EXTRA_OBJECT_INFOS);
        checkObjectConsistency();
        mSites = (ArrayList<Website>) getArguments().getSerializable(EXTRA_SITES);
        String title = getArguments().getString(SingleCategorySettings.EXTRA_TITLE);
        if (title != null) mPageTitle.set(title);

        setHasOptionsMenu(true);

        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public ObservableSupplier<String> getPageTitle() {
        return mPageTitle;
    }

    @Override
    public boolean hasDivider() {
        return false;
    }

    /**
     * Checks the consistency of |mObjectInfos|.
     *
     * This method asserts that for all the entries in |mObjectInfos| the getObject() method
     * returns the same value. This must be true because this activity is displaying permissions
     * for a single object. Each instance varies only in which site it represents.
     */
    private void checkObjectConsistency() {
        if (BuildConfig.ENABLE_ASSERTS) {
            String exampleObject = mObjectInfos.get(0).getObject();
            for (ChosenObjectInfo info : mObjectInfos) {
                assert info.getObject().equals(exampleObject);
            }
        }
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        menu.clear();
        inflater.inflate(R.menu.website_preferences_menu, menu);

        MenuItem searchItem = menu.findItem(R.id.search);
        mSearchView = (SearchView) MenuItemCompat.getActionView(searchItem);
        mSearchView.setImeOptions(EditorInfo.IME_FLAG_NO_FULLSCREEN);
        SearchView.OnQueryTextListener queryTextListener =
                new SearchView.OnQueryTextListener() {
                    @Override
                    public boolean onQueryTextSubmit(String query) {
                        return true;
                    }

                    @Override
                    public boolean onQueryTextChange(String query) {
                        // Make search case-insensitive.
                        query = query.toLowerCase(Locale.getDefault());

                        if (query.equals(mSearch)) return true;

                        mSearch = query;
                        getInfo();
                        return true;
                    }
                };
        mSearchView.setOnQueryTextListener(queryTextListener);

        if (getSiteSettingsDelegate().isHelpAndFeedbackEnabled()) {
            MenuItem help =
                    menu.add(
                            Menu.NONE,
                            R.id.menu_id_site_settings_help,
                            Menu.NONE,
                            R.string.menu_help);
            help.setIcon(
                    TraceEventVectorDrawableCompat.create(
                            getResources(),
                            R.drawable.ic_help_and_feedback,
                            getContext().getTheme()));
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.menu_id_site_settings_help) {
            getSiteSettingsDelegate().launchSettingsHelpAndFeedbackActivity(getActivity());
            return true;
        }
        return false;
    }

    @Override
    public void onResume() {
        super.onResume();

        if (mSites == null) {
            getInfo();
        } else {
            resetList();
        }
    }

    /**
     * Iterates through |mObjectInfos| to revoke the object permissions. If the list of objects
     * contains one that is managed, a toast will display to explain that the managed objects cannot
     * be reset, otherwise all of the objects are reset so the activity is finished.
     */
    public void revokeObjectPermissions() {
        boolean hasManagedObject = false;
        for (ChosenObjectInfo info : mObjectInfos) {
            if (info.isManaged()) {
                hasManagedObject = true;
            } else {
                info.revoke(getSiteSettingsDelegate().getBrowserContextHandle());
            }
        }

        // Managed objects cannot be revoked, so finish the activity only if the list did not
        // contain managed objects.
        if (hasManagedObject) {
            ManagedPreferencesUtils.showManagedSettingsCannotBeResetToast(getContext());
        } else {
            getActivity().finish();
        }
    }

    private class ResultsPopulator implements WebsitePermissionsFetcher.WebsitePermissionsCallback {
        @Override
        public void onWebsitePermissionsAvailable(Collection<Website> sites) {
            // This method may be called after the activity has been destroyed.
            // In that case, bail out.
            if (getActivity() == null) return;

            // Remember the object being examined in this view so that we can filter the results
            // to only include sites with permission for this particular object.
            String exampleObject = mObjectInfos.get(0).getObject();

            mObjectInfos.clear();
            mSites = new ArrayList<Website>();
            for (Website site : sites) {
                for (ChosenObjectInfo info : site.getChosenObjectInfo()) {
                    if (info.getObject().equals(exampleObject)) {
                        mObjectInfos.add(info);
                        if (mSearch.isEmpty()
                                || site.getTitle()
                                        .toLowerCase(Locale.getDefault())
                                        .contains(mSearch)) {
                            mSites.add(site);
                        }
                    }
                }
            }

            // After revoking a site's permission to access an object the user may end up back at
            // this activity. It is awkward to display this empty list because there's no action
            // that can be taken from it. In this case we dismiss this activity as well, taking
            // them back to SingleCategorySettings which will now no longer offer the option to
            // examine the permissions for this object.
            if (mObjectInfos.isEmpty()) {
                getActivity().finish();
            } else {
                resetList();
            }
        }
    }

    /**
     * Refreshes the list of sites with access to the object being examined.
     *
     * <p>resetList() is called to refresh the view when the data is ready.
     */
    private void getInfo() {
        WebsitePermissionsFetcher fetcher =
                new WebsitePermissionsFetcher(getSiteSettingsDelegate());
        fetcher.fetchPreferencesForCategory(mCategory, new ResultsPopulator());
    }

    /**
     * Configures two Preferences that make up the header portion of this fragment. The first
     * Preference displays the chosen object name and provides a button to reset all of the site
     * permissions for the object. The second Preference is a horizontal divider line.
     */
    private void createHeader() {
        PreferenceScreen preferenceScreen = getPreferenceScreen();
        ChromeImageViewPreference header = new ChromeImageViewPreference(getStyledContext());
        String titleText = mObjectInfos.get(0).getName();
        String dialogMsg =
                getView()
                        .getContext()
                        .getString(
                                R.string.chosen_object_website_reset_confirmation_for, titleText);

        header.setTitle(titleText);
        header.setImageView(
                R.drawable.ic_delete_white_24dp,
                R.string.website_settings_revoke_all_permissions_for_device,
                (View view) -> {
                    new AlertDialog.Builder(
                                    getContext(), R.style.ThemeOverlay_BrowserUI_AlertDialog)
                            .setTitle(R.string.reset)
                            .setMessage(dialogMsg)
                            .setPositiveButton(
                                    R.string.reset,
                                    (DialogInterface dialog, int which) -> {
                                        revokeObjectPermissions();
                                        getInfo();
                                    })
                            .setNegativeButton(R.string.cancel, null)
                            .show();
                });
        preferenceScreen.addPreference(header);

        // TODO(chouinard): Handle this header and divider in a cleaner way. May need to migrate
        // WebsitePreference to extend ChromeBasePreference to more easily set dividers
        // programmatically.
        Preference divider = new Preference(getStyledContext());
        divider.setLayoutResource(R.layout.horizontal_divider);
        preferenceScreen.addPreference(divider);
    }

    /**
     * Refreshes the preference list by recreating the heading and the sites allowed to access the
     * chosen object preference for this fragment.
     */
    private void resetList() {
        PreferenceScreen preferenceScreen = getPreferenceScreen();
        preferenceScreen.removeAll();
        createHeader();

        // Each item |i| in |mSites| and |mObjectInfos| correspond to each other.
        // See SingleCategorySettings.addChosenObjects().
        for (int i = 0; i < mSites.size() && i < mObjectInfos.size(); ++i) {
            Website site = mSites.get(i);
            ChosenObjectInfo info = mObjectInfos.get(i);
            WebsitePreference preference =
                    new WebsitePreference(
                            getStyledContext(), getSiteSettingsDelegate(), site, mCategory);

            preference.getExtras().putSerializable(SingleWebsiteSettings.EXTRA_SITE, site);
            preference.setFragment(SingleWebsiteSettings.class.getCanonicalName());
            preference.setImageView(
                    R.drawable.ic_delete_white_24dp,
                    R.string.website_settings_revoke_device_permission,
                    (View view) -> {
                        info.revoke(getSiteSettingsDelegate().getBrowserContextHandle());
                        getInfo();
                    });

            preference.setManagedPreferenceDelegate(
                    new ForwardingManagedPreferenceDelegate(
                            getSiteSettingsDelegate().getManagedPreferenceDelegate()) {
                        @Override
                        public boolean isPreferenceControlledByPolicy(Preference preference) {
                            return info.isManaged();
                        }

                        @Override
                        public boolean isPreferenceClickDisabled(Preference preference) {
                            return false;
                        }
                    });

            preferenceScreen.addPreference(preference);
        }

        // Force this list to be reloaded if the activity is resumed.
        mSites = null;
    }

    private Context getStyledContext() {
        return getPreferenceManager().getContext();
    }
}