blob: e7fe7a1fe158fd13905df3bc0462bafba43899ce [file] [log] [blame]
// 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.website;
import android.util.Pair;
import org.chromium.base.Callback;
import org.chromium.chrome.browser.ContentSettingsType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
/**
* Utility class that asynchronously fetches any Websites and the permissions
* that the user has set for them.
*/
public class WebsitePermissionsFetcher {
/**
* A callback to pass to WebsitePermissionsFetcher. This is run when the
* website permissions have been fetched.
*/
public interface WebsitePermissionsCallback {
void onWebsitePermissionsAvailable(Collection<Website> sites);
}
/**
* A specialization of Pair to hold an (origin, embedder) tuple. This overrides
* android.util.Pair#hashCode, which simply XORs the hashCodes of the pair of values together.
* Having origin == embedder (a fix for a crash in crbug.com/636330) results in pathological
* performance and causes Site Settings/All Sites to lag significantly on opening. See
* crbug.com/732907.
*/
public static class OriginAndEmbedder extends Pair<WebsiteAddress, WebsiteAddress> {
public OriginAndEmbedder(WebsiteAddress origin, WebsiteAddress embedder) {
super(origin, embedder);
}
public static OriginAndEmbedder create(WebsiteAddress origin, WebsiteAddress embedder) {
return new OriginAndEmbedder(origin, embedder);
}
private static boolean isEqual(Object o1, Object o2) {
// Returns true iff o1 == o2, handling nulls.
return (o1 == o2) || (o1 != null && o1.equals(o2));
}
@Override
public boolean equals(Object o) {
// Prior to KitKat, android.util.Pair would crash with a NullPointerException in this
// method. This override specialises the post-Kitkat implementation to this class, and
// correctly handles nulls.
if (!(o instanceof OriginAndEmbedder)) return false;
OriginAndEmbedder p = (OriginAndEmbedder) o;
return isEqual(p.first, first) && isEqual(p.second, second);
}
@Override
public int hashCode() {
// This is the calculation used by Arrays#hashCode().
int result = 31 + (first == null ? 0 : first.hashCode());
return 31 * result + (second == null ? 0 : second.hashCode());
}
}
// This map looks up Websites by their origin and embedder.
private final Map<OriginAndEmbedder, Website> mSites = new HashMap<>();
// The callback to run when the permissions have been fetched.
private final WebsitePermissionsCallback mCallback;
private final boolean mFetchSiteImportantInfo;
/**
* @param callback The callback to run when the fetch is complete.
*/
public WebsitePermissionsFetcher(WebsitePermissionsCallback callback) {
this(callback, false);
}
/**
* @param callback The callback to run when the fetch is complete.
* @param fetchSiteImportantInfo if the fetcher should query whether each site is 'important'.
*/
public WebsitePermissionsFetcher(
WebsitePermissionsCallback callback, boolean fetchSiteImportantInfo) {
mCallback = callback;
mFetchSiteImportantInfo = fetchSiteImportantInfo;
}
/**
* Fetches preferences for all sites that have them.
* TODO(mvanouwerkerk): Add an argument |url| to only fetch permissions for
* sites from the same origin as that of |url| - https://crbug.com/459222.
*/
public void fetchAllPreferences() {
TaskQueue queue = new TaskQueue();
// Populate features from more specific to less specific.
// Geolocation lookup permission is per-origin and per-embedder.
queue.add(new GeolocationInfoFetcher());
// Midi sysex access permission is per-origin and per-embedder.
queue.add(new MidiInfoFetcher());
// Cookies are stored per-host.
queue.add(new CookieExceptionInfoFetcher());
// Local storage info is per-origin.
queue.add(new LocalStorageInfoFetcher());
// Website storage is per-host.
queue.add(new WebStorageInfoFetcher());
// Popup exceptions are host-based patterns (unless we start
// synchronizing popup exceptions with desktop Chrome).
queue.add(new PopupExceptionInfoFetcher());
// Ads exceptions are host-based.
queue.add(new AdsExceptionInfoFetcher());
// JavaScript exceptions are host-based patterns.
queue.add(new JavaScriptExceptionInfoFetcher());
// Sound exceptions are host-based patterns.
queue.add(new SoundExceptionInfoFetcher());
// Protected media identifier permission is per-origin and per-embedder.
queue.add(new ProtectedMediaIdentifierInfoFetcher());
// Notification permission is per-origin.
queue.add(new NotificationInfoFetcher());
// Camera capture permission is per-origin and per-embedder.
queue.add(new CameraCaptureInfoFetcher());
// Micropohone capture permission is per-origin and per-embedder.
queue.add(new MicrophoneCaptureInfoFetcher());
// Background sync permission is per-origin.
queue.add(new BackgroundSyncExceptionInfoFetcher());
// Autoplay permission is per-origin.
queue.add(new AutoplayExceptionInfoFetcher());
// USB device permission is per-origin and per-embedder.
queue.add(new UsbInfoFetcher());
// Clipboard info is per-origin.
queue.add(new ClipboardInfoFetcher());
queue.add(new PermissionsAvailableCallbackRunner());
queue.next();
}
/**
* Fetches all preferences within a specific category.
*
* @param catgory A category to fetch.
*/
public void fetchPreferencesForCategory(SiteSettingsCategory category) {
if (category.showAllSites()) {
fetchAllPreferences();
return;
}
TaskQueue queue = new TaskQueue();
// Populate features from more specific to less specific.
if (category.showGeolocationSites()) {
// Geolocation lookup permission is per-origin and per-embedder.
queue.add(new GeolocationInfoFetcher());
} else if (category.showCookiesSites()) {
// Cookies exceptions are patterns.
queue.add(new CookieExceptionInfoFetcher());
} else if (category.showStorageSites()) {
// Local storage info is per-origin.
queue.add(new LocalStorageInfoFetcher());
// Website storage is per-host.
queue.add(new WebStorageInfoFetcher());
} else if (category.showCameraSites()) {
// Camera capture permission is per-origin and per-embedder.
queue.add(new CameraCaptureInfoFetcher());
} else if (category.showMicrophoneSites()) {
// Micropohone capture permission is per-origin and per-embedder.
queue.add(new MicrophoneCaptureInfoFetcher());
} else if (category.showPopupSites()) {
// Popup exceptions are host-based patterns (unless we start
// synchronizing popup exceptions with desktop Chrome.)
queue.add(new PopupExceptionInfoFetcher());
} else if (category.showAdsSites()) {
// Ads exceptions are host-based.
queue.add(new AdsExceptionInfoFetcher());
} else if (category.showJavaScriptSites()) {
// JavaScript exceptions are host-based patterns.
queue.add(new JavaScriptExceptionInfoFetcher());
} else if (category.showSoundSites()) {
// Sound exceptions are host-based patterns.
queue.add(new SoundExceptionInfoFetcher());
} else if (category.showNotificationsSites()) {
// Push notification permission is per-origin.
queue.add(new NotificationInfoFetcher());
} else if (category.showBackgroundSyncSites()) {
// Background sync info is per-origin.
queue.add(new BackgroundSyncExceptionInfoFetcher());
} else if (category.showProtectedMediaSites()) {
// Protected media identifier permission is per-origin and per-embedder.
queue.add(new ProtectedMediaIdentifierInfoFetcher());
} else if (category.showAutoplaySites()) {
// Autoplay permission is per-origin.
queue.add(new AutoplayExceptionInfoFetcher());
} else if (category.showUsbDevices()) {
// USB device permission is per-origin.
queue.add(new UsbInfoFetcher());
} else if (category.showClipboardSites()) {
// Clipboard permission is per-origin.
queue.add(new ClipboardInfoFetcher());
}
queue.add(new PermissionsAvailableCallbackRunner());
queue.next();
}
private Website findOrCreateSite(WebsiteAddress origin, WebsiteAddress embedder) {
OriginAndEmbedder key = OriginAndEmbedder.create(origin, embedder);
Website site = mSites.get(key);
if (site == null) {
site = new Website(origin, embedder);
mSites.put(key, site);
}
return site;
}
private void setException(int contentSettingsType) {
for (ContentSettingException exception :
WebsitePreferenceBridge.getContentSettingsExceptions(contentSettingsType)) {
// The pattern "*" represents the default setting, not a specific website.
if (exception.getPattern().equals("*")) continue;
WebsiteAddress address = WebsiteAddress.create(exception.getPattern());
if (address == null) continue;
Website site = findOrCreateSite(address, null);
switch (contentSettingsType) {
case ContentSettingsType.CONTENT_SETTINGS_TYPE_ADS:
site.setAdsException(exception);
break;
case ContentSettingsType.CONTENT_SETTINGS_TYPE_AUTOPLAY:
site.setAutoplayException(exception);
break;
case ContentSettingsType.CONTENT_SETTINGS_TYPE_BACKGROUND_SYNC:
site.setBackgroundSyncException(exception);
break;
case ContentSettingsType.CONTENT_SETTINGS_TYPE_COOKIES:
site.setCookieException(exception);
break;
case ContentSettingsType.CONTENT_SETTINGS_TYPE_JAVASCRIPT:
site.setJavaScriptException(exception);
break;
case ContentSettingsType.CONTENT_SETTINGS_TYPE_POPUPS:
site.setPopupException(exception);
break;
case ContentSettingsType.CONTENT_SETTINGS_TYPE_SOUND:
site.setSoundException(exception);
break;
default:
assert false : "Unexpected content setting type received: "
+ contentSettingsType;
break;
}
}
}
/**
* A single task in the WebsitePermissionsFetcher task queue. We need fetching of features to be
* serialized, as we need to have all the origins in place prior to populating the hosts.
*/
private abstract class Task {
/** Override this method to implement a synchronous task. */
void run() {}
/**
* Override this method to implement an asynchronous task. Call queue.next() once execution
* is complete.
*/
void runAsync(TaskQueue queue) {
run();
queue.next();
}
}
/**
* A queue used to store the sequence of tasks to run to fetch the website preferences. Each
* task is run sequentially, and some of the tasks may run asynchronously.
*/
private static class TaskQueue extends LinkedList<Task> {
void next() {
if (!isEmpty()) removeFirst().runAsync(this);
}
}
private class AutoplayExceptionInfoFetcher extends Task {
@Override
public void run() {
setException(ContentSettingsType.CONTENT_SETTINGS_TYPE_AUTOPLAY);
}
}
private class ClipboardInfoFetcher extends Task {
@Override
public void run() {
for (ClipboardInfo info : WebsitePreferenceBridge.getClipboardInfo()) {
WebsiteAddress origin = WebsiteAddress.create(info.getOrigin());
if (origin == null) continue;
WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder());
findOrCreateSite(origin, embedder).setClipboardInfo(info);
}
}
}
private class GeolocationInfoFetcher extends Task {
@Override
public void run() {
for (GeolocationInfo info : WebsitePreferenceBridge.getGeolocationInfo()) {
WebsiteAddress origin = WebsiteAddress.create(info.getOrigin());
if (origin == null) continue;
WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder());
findOrCreateSite(origin, embedder).setGeolocationInfo(info);
}
}
}
private class MidiInfoFetcher extends Task {
@Override
public void run() {
for (MidiInfo info : WebsitePreferenceBridge.getMidiInfo()) {
WebsiteAddress origin = WebsiteAddress.create(info.getOrigin());
if (origin == null) continue;
WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder());
findOrCreateSite(origin, embedder).setMidiInfo(info);
}
}
}
private class PopupExceptionInfoFetcher extends Task {
@Override
public void run() {
setException(ContentSettingsType.CONTENT_SETTINGS_TYPE_POPUPS);
}
}
private class AdsExceptionInfoFetcher extends Task {
@Override
public void run() {
setException(ContentSettingsType.CONTENT_SETTINGS_TYPE_ADS);
}
}
private class JavaScriptExceptionInfoFetcher extends Task {
@Override
public void run() {
setException(ContentSettingsType.CONTENT_SETTINGS_TYPE_JAVASCRIPT);
}
}
private class SoundExceptionInfoFetcher extends Task {
@Override
public void run() {
setException(ContentSettingsType.CONTENT_SETTINGS_TYPE_SOUND);
}
}
private class CookieExceptionInfoFetcher extends Task {
@Override
public void run() {
setException(ContentSettingsType.CONTENT_SETTINGS_TYPE_COOKIES);
}
}
private class LocalStorageInfoFetcher extends Task {
@Override
public void runAsync(final TaskQueue queue) {
WebsitePreferenceBridge.fetchLocalStorageInfo(new Callback<HashMap>() {
@Override
public void onResult(HashMap result) {
for (Object o : result.entrySet()) {
@SuppressWarnings("unchecked")
Map.Entry<String, LocalStorageInfo> entry =
(Map.Entry<String, LocalStorageInfo>) o;
WebsiteAddress address = WebsiteAddress.create(entry.getKey());
if (address == null) continue;
findOrCreateSite(address, null).setLocalStorageInfo(entry.getValue());
}
queue.next();
}
}, mFetchSiteImportantInfo);
}
}
private class WebStorageInfoFetcher extends Task {
@Override
public void runAsync(final TaskQueue queue) {
WebsitePreferenceBridge.fetchStorageInfo(new Callback<ArrayList>() {
@Override
public void onResult(ArrayList result) {
@SuppressWarnings("unchecked")
ArrayList<StorageInfo> infoArray = result;
for (StorageInfo info : infoArray) {
WebsiteAddress address = WebsiteAddress.create(info.getHost());
if (address == null) continue;
findOrCreateSite(address, null).addStorageInfo(info);
}
queue.next();
}
});
}
}
private class ProtectedMediaIdentifierInfoFetcher extends Task {
@Override
public void run() {
for (ProtectedMediaIdentifierInfo info :
WebsitePreferenceBridge.getProtectedMediaIdentifierInfo()) {
WebsiteAddress origin = WebsiteAddress.create(info.getOrigin());
if (origin == null) continue;
WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder());
findOrCreateSite(origin, embedder).setProtectedMediaIdentifierInfo(info);
}
}
}
private class NotificationInfoFetcher extends Task {
@Override
public void run() {
for (NotificationInfo info : WebsitePreferenceBridge.getNotificationInfo()) {
WebsiteAddress origin = WebsiteAddress.create(info.getOrigin());
if (origin == null) continue;
WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder());
findOrCreateSite(origin, embedder).setNotificationInfo(info);
}
}
}
private class CameraCaptureInfoFetcher extends Task {
@Override
public void run() {
for (CameraInfo info : WebsitePreferenceBridge.getCameraInfo()) {
WebsiteAddress origin = WebsiteAddress.create(info.getOrigin());
if (origin == null) continue;
WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder());
findOrCreateSite(origin, embedder).setCameraInfo(info);
}
}
}
private class MicrophoneCaptureInfoFetcher extends Task {
@Override
public void run() {
for (MicrophoneInfo info : WebsitePreferenceBridge.getMicrophoneInfo()) {
WebsiteAddress origin = WebsiteAddress.create(info.getOrigin());
if (origin == null) continue;
WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder());
findOrCreateSite(origin, embedder).setMicrophoneInfo(info);
}
}
}
private class BackgroundSyncExceptionInfoFetcher extends Task {
@Override
public void run() {
setException(ContentSettingsType.CONTENT_SETTINGS_TYPE_BACKGROUND_SYNC);
}
}
private class UsbInfoFetcher extends Task {
@Override
public void run() {
for (UsbInfo info : WebsitePreferenceBridge.getUsbInfo()) {
WebsiteAddress origin = WebsiteAddress.create(info.getOrigin());
if (origin == null) continue;
WebsiteAddress embedder = WebsiteAddress.create(info.getEmbedder());
findOrCreateSite(origin, embedder).addUsbInfo(info);
}
}
}
private class PermissionsAvailableCallbackRunner extends Task {
@Override
public void run() {
mCallback.onWebsitePermissionsAvailable(mSites.values());
}
}
}