/*
- * Copyright © 2019 Soren Stoutner <soren@stoutner.com>.
+ * Copyright © 2019-2020 Soren Stoutner <soren@stoutner.com>.
*
* This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
*
package com.stoutner.privacybrowser.views;
import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
import android.util.AttributeSet;
import android.view.MotionEvent;
+import android.webkit.HttpAuthHandler;
+import android.webkit.SslErrorHandler;
import android.webkit.WebView;
import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
import androidx.core.view.NestedScrollingChild2;
import androidx.core.view.NestedScrollingChildHelper;
import androidx.core.view.ViewCompat;
+import com.stoutner.privacybrowser.R;
+
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Date;
+import java.util.List;
// NestedScrollWebView extends WebView to handle nested scrolls (scrolling the app bar off the screen).
public class NestedScrollWebView extends WebView implements NestedScrollingChild2 {
- // These constants identify the blocklists.
+ // Define the blocklists constants.
public final static int BLOCKED_REQUESTS = 0;
- public final static int EASY_LIST = 1;
- public final static int EASY_PRIVACY = 2;
+ public final static int EASYLIST = 1;
+ public final static int EASYPRIVACY = 2;
public final static int FANBOYS_ANNOYANCE_LIST = 3;
public final static int FANBOYS_SOCIAL_BLOCKING_LIST = 4;
- public final static int ULTRA_PRIVACY = 5;
- public final static int THIRD_PARTY_REQUESTS = 6;
+ public final static int ULTRALIST = 5;
+ public final static int ULTRAPRIVACY = 6;
+ public final static int THIRD_PARTY_REQUESTS = 7;
+
+ // Define the saved state constants.
+ private final String DOMAIN_SETTINGS_APPLIED = "domain_settings_applied";
+ private final String DOMAIN_SETTINGS_DATABASE_ID = "domain_settings_database_id";
+ private final String CURRENT_URl = "current_url";
+ private final String CURRENT_DOMAIN_NAME = "current_domain_name";
+ private final String ACCEPT_FIRST_PARTY_COOKIES = "accept_first_party_cookies";
+ private final String EASYLIST_ENABLED = "easylist_enabled";
+ private final String EASYPRIVACY_ENABLED = "easyprivacy_enabled";
+ private final String FANBOYS_ANNOYANCE_LIST_ENABLED = "fanboys_annoyance_list_enabled";
+ private final String FANBOYS_SOCIAL_BLOCKING_LIST_ENABLED = "fanboys_social_blocking_list_enabled";
+ private final String ULTRALIST_ENABLED = "ultralist_enabled";
+ private final String ULTRAPRIVACY_ENABLED = "ultraprivacy_enabled";
+ private final String BLOCK_ALL_THIRD_PARTY_REQUESTS = "block_all_third_party_requests";
+ private final String HAS_PINNED_SSL_CERTIFICATE = "has_pinned_ssl_certificate";
+ private final String PINNED_SSL_ISSUED_TO_CNAME = "pinned_ssl_issued_to_cname";
+ private final String PINNED_SSL_ISSUED_TO_ONAME = "pinned_ssl_issued_to_oname";
+ private final String PINNED_SSL_ISSUED_TO_UNAME = "pinned_ssl_issued_to_uname";
+ private final String PINNED_SSL_ISSUED_BY_CNAME = "pinned_ssl_issued_by_cname";
+ private final String PINNED_SSL_ISSUED_BY_ONAME = "pinned_ssl_issued_by_oname";
+ private final String PINNED_SSL_ISSUED_BY_UNAME = "pinned_ssl_issued_by_uname";
+ private final String PINNED_SSL_START_DATE = "pinned_ssl_start_date";
+ private final String PINNED_SSL_END_DATE = "pinned_ssl_end_date";
+ private final String HAS_PINNED_IP_ADDRESSES = "has_pinned_ip_addresses";
+ private final String PINNED_IP_ADDRESSES = "pinned_ip_addresses";
+ private final String IGNORE_PINNED_DOMAIN_INFORMATION = "ignore_pinned_domain_information";
+ private final String SWIPE_TO_REFRESH = "swipe_to_refresh";
+ private final String JAVASCRIPT_ENABLED = "javascript_enabled";
+ private final String DOM_STORAGE_ENABLED = "dom_storage_enabled";
+ private final String USER_AGENT = "user_agent";
+ private final String WIDE_VIEWPORT = "wide_viewport";
+ private final String FONT_SIZE = "font_size";
// Keep a copy of the WebView fragment ID.
private long webViewFragmentId;
+ // Store the handlers.
+ private SslErrorHandler sslErrorHandler;
+ private HttpAuthHandler httpAuthHandler;
+
// Track if domain settings are applied to this nested scroll WebView and, if so, the database ID.
private boolean domainSettingsApplied;
private int domainSettingsDatabaseId;
+ // Keep track of the current URL. This is used to not block resource requests to the main URL.
+ private String currentUrl;
+
// Keep track of when the domain name changes so that domain settings can be reapplied. This should never be null.
private String currentDomainName = "";
+ // Track the status of first-party cookies. This is necessary because first-party cookie status is app wide instead of WebView specific.
+ private boolean acceptFirstPartyCookies;
+
// Track the resource requests.
- private ArrayList<String[]> resourceRequests = new ArrayList<>();
+ private List<String[]> resourceRequests = Collections.synchronizedList(new ArrayList<>()); // Using a synchronized list makes adding resource requests thread safe.
private boolean easyListEnabled;
private boolean easyPrivacyEnabled;
private boolean fanboysAnnoyanceListEnabled;
private boolean fanboysSocialBlockingListEnabled;
+ private boolean ultraListEnabled;
private boolean ultraPrivacyEnabled;
private boolean blockAllThirdPartyRequests;
private int blockedRequests;
private int easyPrivacyBlockedRequests;
private int fanboysAnnoyanceListBlockedRequests;
private int fanboysSocialBlockingListBlockedRequests;
+ private int ultraListBlockedRequests;
private int ultraPrivacyBlockedRequests;
private int thirdPartyBlockedRequests;
// The ignore pinned domain information tracker. This is set when a user proceeds past a pinned mismatch dialog to prevent the dialog from showing again until after the domain changes.
private boolean ignorePinnedDomainInformation;
+ // The default or favorite icon.
+ private Bitmap favoriteOrDefaultIcon;
+
+ // Track swipe to refresh.
+ private boolean swipeToRefresh;
+
+ // Track a URL waiting for a proxy.
+ private String waitingForProxyUrlString = "";
+
// The nested scrolling child helper is used throughout the class.
private NestedScrollingChildHelper nestedScrollingChildHelper;
}
+ // SSL error handler.
+ public void setSslErrorHandler(SslErrorHandler sslErrorHandler) {
+ // Store the current SSL error handler.
+ this.sslErrorHandler = sslErrorHandler;
+ }
+
+ public SslErrorHandler getSslErrorHandler() {
+ // Return the current SSL error handler.
+ return sslErrorHandler;
+ }
+
+ public void resetSslErrorHandler() {
+ // Reset the current SSL error handler.
+ sslErrorHandler = null;
+ }
+
+
+ // HTTP authentication handler.
+ public void setHttpAuthHandler(HttpAuthHandler httpAuthHandler) {
+ // Store the current HTTP authentication handler.
+ this.httpAuthHandler = httpAuthHandler;
+ }
+
+ public HttpAuthHandler getHttpAuthHandler() {
+ // Return the current HTTP authentication handler.
+ return httpAuthHandler;
+ }
+
+ public void resetHttpAuthHandler() {
+ // Reset the current HTTP authentication handler.
+ httpAuthHandler = null;
+ }
+
+
// Domain settings.
public void setDomainSettingsApplied(boolean applied) {
// Store the domain settings applied status.
}
+ // Current URL.
+ public void setCurrentUrl(String url) {
+ // Store the current URL.
+ currentUrl = url;
+ }
+
+ public String getCurrentUrl() {
+ // Return the current URL.
+ return currentUrl;
+ }
+
+
// Current domain name. To function well when called, the domain name should never be allowed to be null.
public void setCurrentDomainName(@NonNull String domainName) {
// Store the current domain name.
}
+ // First-party cookies.
+ public void setAcceptFirstPartyCookies(boolean status) {
+ // Store the accept first-party cookies status.
+ acceptFirstPartyCookies = status;
+ }
+
+ public boolean getAcceptFirstPartyCookies() {
+ // Return the accept first-party cookies status.
+ return acceptFirstPartyCookies;
+ }
+
+
// Resource requests.
public void addResourceRequest(String[] resourceRequest) {
// Add the resource request to the list.
resourceRequests.add(resourceRequest);
}
- public ArrayList<String[]> getResourceRequests() {
- // Return the list of resource requests.
+ public List<String[]> getResourceRequests() {
+ // Return the list of resource requests as an array list.
return resourceRequests;
}
public void enableBlocklist(int blocklist, boolean status) {
// Update the status of the indicated blocklist.
switch (blocklist) {
- case EASY_LIST:
+ case EASYLIST:
// Update the status of the blocklist.
easyListEnabled = status;
break;
- case EASY_PRIVACY:
+ case EASYPRIVACY:
// Update the status of the blocklist.
easyPrivacyEnabled = status;
break;
fanboysSocialBlockingListEnabled = status;
break;
- case ULTRA_PRIVACY:
+ case ULTRALIST:
+ // Update the status of the blocklist.
+ ultraListEnabled = status;
+ break;
+
+ case ULTRAPRIVACY:
// Update the status of the blocklist.
ultraPrivacyEnabled = status;
break;
public boolean isBlocklistEnabled(int blocklist) {
// Get the status of the indicated blocklist.
switch (blocklist) {
- case EASY_LIST:
+ case EASYLIST:
// Return the status of the blocklist.
return easyListEnabled;
- case EASY_PRIVACY:
+ case EASYPRIVACY:
// Return the status of the blocklist.
return easyPrivacyEnabled;
// Return the status of the blocklist.
return fanboysSocialBlockingListEnabled;
- case ULTRA_PRIVACY:
+ case ULTRALIST:
+ // Return the status of the blocklist.
+ return ultraListEnabled;
+
+ case ULTRAPRIVACY:
// Return the status of the blocklist.
return ultraPrivacyEnabled;
easyPrivacyBlockedRequests = 0;
fanboysAnnoyanceListBlockedRequests = 0;
fanboysSocialBlockingListBlockedRequests = 0;
+ ultraListBlockedRequests = 0;
ultraPrivacyBlockedRequests = 0;
thirdPartyBlockedRequests = 0;
}
blockedRequests++;
break;
- case EASY_LIST:
+ case EASYLIST:
// Increment the EasyList blocked requests count.
easyListBlockedRequests++;
break;
- case EASY_PRIVACY:
+ case EASYPRIVACY:
// Increment the EasyPrivacy blocked requests count.
easyPrivacyBlockedRequests++;
break;
fanboysSocialBlockingListBlockedRequests++;
break;
- case ULTRA_PRIVACY:
+ case ULTRALIST:
+ // Increment the UltraList blocked requests count.
+ ultraListBlockedRequests++;
+ break;
+
+ case ULTRAPRIVACY:
// Increment the UltraPrivacy blocked requests count.
ultraPrivacyBlockedRequests++;
break;
// Return the blocked requests count.
return blockedRequests;
- case EASY_LIST:
+ case EASYLIST:
// Return the EasyList blocked requests count.
return easyListBlockedRequests;
- case EASY_PRIVACY:
+ case EASYPRIVACY:
// Return the EasyPrivacy blocked requests count.
return easyPrivacyBlockedRequests;
// Return the Fanboy's Social Blocking List blocked requests count.
return fanboysSocialBlockingListBlockedRequests;
- case ULTRA_PRIVACY:
+ case ULTRALIST:
+ // Return the UltraList blocked requests count.
+ return ultraListBlockedRequests;
+
+ case ULTRAPRIVACY:
// Return the UltraPrivacy blocked requests count.
return ultraPrivacyBlockedRequests;
}
- // Ignore pinned information. The syntax looks better as written, even if it is always inverted.
- @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ // Ignore pinned information.
+ public void setIgnorePinnedDomainInformation(boolean status) {
+ // Set the status of the ignore pinned domain information tracker.
+ ignorePinnedDomainInformation = status;
+ }
+
public boolean ignorePinnedDomainInformation() {
// Return the status of the ignore pinned domain information tracker.
return ignorePinnedDomainInformation;
}
- public void setIgnorePinnedDomainInformation(boolean status) {
- // Set the status of the ignore pinned domain information tracker.
- ignorePinnedDomainInformation = status;
+
+ // Favorite or default icon.
+ public void initializeFavoriteIcon() {
+ // Get the default favorite icon drawable. `ContextCompat` must be used until API >= 21.
+ Drawable favoriteIconDrawable = ContextCompat.getDrawable(getContext(), R.drawable.world);
+
+ // Cast the favorite icon drawable to a bitmap drawable.
+ BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable;
+
+ // Remove the incorrect warning below that the favorite icon bitmap drawable might be null.
+ assert favoriteIconBitmapDrawable != null;
+
+ // Store the default icon bitmap.
+ favoriteOrDefaultIcon = favoriteIconBitmapDrawable.getBitmap();
+ }
+
+ public void setFavoriteOrDefaultIcon(Bitmap icon) {
+ // Scale the favorite icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
+ if ((icon.getHeight() > 256) || (icon.getWidth() > 256)) {
+ favoriteOrDefaultIcon = Bitmap.createScaledBitmap(icon, 256, 256, true);
+ } else {
+ // Store the icon as presented.
+ favoriteOrDefaultIcon = icon;
+ }
+ }
+
+ public Bitmap getFavoriteOrDefaultIcon() {
+ // Return the favorite or default icon.
+ return favoriteOrDefaultIcon;
+ }
+
+
+ // Swipe to refresh.
+ public void setSwipeToRefresh(boolean status) {
+ // Store the swipe to refresh status.
+ swipeToRefresh = status;
+ }
+
+ public boolean getSwipeToRefresh() {
+ // Return the swipe to refresh status.
+ return swipeToRefresh;
+ }
+
+
+ // Waiting for proxy.
+ public void setWaitingForProxyUrlString(String urlString) {
+ // Store the waiting for proxy URL string.
+ waitingForProxyUrlString = urlString;
+ }
+
+ public String getWaitingForProxyUrlString() {
+ // Return the waiting for proxy URL string.
+ return waitingForProxyUrlString;
+ }
+
+ public void resetWaitingForProxyUrlString() {
+ // Clear the waiting for proxy URL string.
+ waitingForProxyUrlString = "";
+ }
+
+ // Scroll range.
+ public int getHorizontalScrollRange() {
+ // Return the horizontal scroll range.
+ return computeHorizontalScrollRange();
+ }
+
+ public int getVerticalScrollRange() {
+ // Return the vertical scroll range.
+ return computeVerticalScrollRange();
}
// Dispatch the nested pre-school. This scrolls the app bar if it needs it. `offsetInWindow` will be returned with an updated value.
if (dispatchNestedPreScroll(0, preScrollDeltaY, consumedScroll, offsetInWindow)) {
// Update the scroll delta Y if some of it was consumed.
+ // There is currently a bug in Android where if scrolling up at a certain slow speed the input can lock the pre scroll and continue to consume it after the app bar is fully displayed.
scrollDeltaY = preScrollDeltaY - consumedScroll[1];
}
// Check to see if the WebView is at the top and and the scroll action is downward.
if ((webViewYPosition == 0) && (scrollDeltaY < 0)) { // Swipe to refresh is being engaged.
- // Stop the nested scroll so that swipe to refresh has complete control.
+ // Stop the nested scroll so that swipe to refresh has complete control. This way releasing the scroll to refresh circle doesn't scroll the WebView at the same time.
stopNestedScroll();
} else { // Swipe to refresh is not being engaged.
// Start the nested scroll so that the app bar can scroll off the screen.
return motionEventHandled;
}
+ public Bundle saveNestedScrollWebViewState() {
+ // Create a saved state bundle.
+ Bundle savedState = new Bundle();
+
+ // Initialize the long date variables.
+ long pinnedSslStartDateLong = 0;
+ long pinnedSslEndDateLong = 0;
+
+ // Convert the dates to longs.
+ if (pinnedSslStartDate != null) {
+ pinnedSslStartDateLong = pinnedSslStartDate.getTime();
+ }
+
+ if (pinnedSslEndDate != null) {
+ pinnedSslEndDateLong = pinnedSslEndDate.getTime();
+ }
+
+ // Populate the saved state bundle.
+ savedState.putBoolean(DOMAIN_SETTINGS_APPLIED, domainSettingsApplied);
+ savedState.putInt(DOMAIN_SETTINGS_DATABASE_ID, domainSettingsDatabaseId);
+ savedState.putString(CURRENT_URl, currentUrl);
+ savedState.putString(CURRENT_DOMAIN_NAME, currentDomainName);
+ savedState.putBoolean(ACCEPT_FIRST_PARTY_COOKIES, acceptFirstPartyCookies);
+ savedState.putBoolean(EASYLIST_ENABLED, easyListEnabled);
+ savedState.putBoolean(EASYPRIVACY_ENABLED, easyPrivacyEnabled);
+ savedState.putBoolean(FANBOYS_ANNOYANCE_LIST_ENABLED, fanboysAnnoyanceListEnabled);
+ savedState.putBoolean(FANBOYS_SOCIAL_BLOCKING_LIST_ENABLED, fanboysSocialBlockingListEnabled);
+ savedState.putBoolean(ULTRALIST_ENABLED, ultraListEnabled);
+ savedState.putBoolean(ULTRAPRIVACY_ENABLED, ultraPrivacyEnabled);
+ savedState.putBoolean(BLOCK_ALL_THIRD_PARTY_REQUESTS, blockAllThirdPartyRequests);
+ savedState.putBoolean(HAS_PINNED_SSL_CERTIFICATE, hasPinnedSslCertificate);
+ savedState.putString(PINNED_SSL_ISSUED_TO_CNAME, pinnedSslIssuedToCName);
+ savedState.putString(PINNED_SSL_ISSUED_TO_ONAME, pinnedSslIssuedToOName);
+ savedState.putString(PINNED_SSL_ISSUED_TO_UNAME, pinnedSslIssuedToUName);
+ savedState.putString(PINNED_SSL_ISSUED_BY_CNAME, pinnedSslIssuedByCName);
+ savedState.putString(PINNED_SSL_ISSUED_BY_ONAME, pinnedSslIssuedByOName);
+ savedState.putString(PINNED_SSL_ISSUED_BY_UNAME, pinnedSslIssuedByUName);
+ savedState.putLong(PINNED_SSL_START_DATE, pinnedSslStartDateLong);
+ savedState.putLong(PINNED_SSL_END_DATE, pinnedSslEndDateLong);
+ savedState.putBoolean(HAS_PINNED_IP_ADDRESSES, hasPinnedIpAddresses);
+ savedState.putString(PINNED_IP_ADDRESSES, pinnedIpAddresses);
+ savedState.putBoolean(IGNORE_PINNED_DOMAIN_INFORMATION, ignorePinnedDomainInformation);
+ savedState.putBoolean(SWIPE_TO_REFRESH, swipeToRefresh);
+ savedState.putBoolean(JAVASCRIPT_ENABLED, this.getSettings().getJavaScriptEnabled());
+ savedState.putBoolean(DOM_STORAGE_ENABLED, this.getSettings().getDomStorageEnabled());
+ savedState.putString(USER_AGENT, this.getSettings().getUserAgentString());
+ savedState.putBoolean(WIDE_VIEWPORT, this.getSettings().getUseWideViewPort());
+ savedState.putInt(FONT_SIZE, this.getSettings().getTextZoom());
+
+ // Return the saved state bundle.
+ return savedState;
+ }
+
+ public void restoreNestedScrollWebViewState(Bundle savedState) {
+ // Restore the class variables.
+ domainSettingsApplied = savedState.getBoolean(DOMAIN_SETTINGS_APPLIED);
+ domainSettingsDatabaseId = savedState.getInt(DOMAIN_SETTINGS_DATABASE_ID);
+ currentUrl = savedState.getString(CURRENT_URl);
+ currentDomainName = savedState.getString(CURRENT_DOMAIN_NAME);
+ acceptFirstPartyCookies = savedState.getBoolean(ACCEPT_FIRST_PARTY_COOKIES);
+ easyListEnabled = savedState.getBoolean(EASYLIST_ENABLED);
+ easyPrivacyEnabled = savedState.getBoolean(EASYPRIVACY_ENABLED);
+ fanboysAnnoyanceListEnabled = savedState.getBoolean(FANBOYS_ANNOYANCE_LIST_ENABLED);
+ fanboysSocialBlockingListEnabled = savedState.getBoolean(FANBOYS_SOCIAL_BLOCKING_LIST_ENABLED);
+ ultraListEnabled = savedState.getBoolean(ULTRALIST_ENABLED);
+ ultraPrivacyEnabled = savedState.getBoolean(ULTRAPRIVACY_ENABLED);
+ blockAllThirdPartyRequests = savedState.getBoolean(BLOCK_ALL_THIRD_PARTY_REQUESTS);
+ hasPinnedSslCertificate = savedState.getBoolean(HAS_PINNED_SSL_CERTIFICATE);
+ pinnedSslIssuedToCName = savedState.getString(PINNED_SSL_ISSUED_TO_CNAME);
+ pinnedSslIssuedToOName = savedState.getString(PINNED_SSL_ISSUED_TO_ONAME);
+ pinnedSslIssuedToUName = savedState.getString(PINNED_SSL_ISSUED_TO_UNAME);
+ pinnedSslIssuedByCName = savedState.getString(PINNED_SSL_ISSUED_BY_CNAME);
+ pinnedSslIssuedByOName = savedState.getString(PINNED_SSL_ISSUED_BY_ONAME);
+ pinnedSslIssuedByUName = savedState.getString(PINNED_SSL_ISSUED_BY_UNAME);
+ hasPinnedIpAddresses = savedState.getBoolean(HAS_PINNED_IP_ADDRESSES);
+ pinnedIpAddresses = savedState.getString(PINNED_IP_ADDRESSES);
+ ignorePinnedDomainInformation = savedState.getBoolean(IGNORE_PINNED_DOMAIN_INFORMATION);
+ swipeToRefresh = savedState.getBoolean(SWIPE_TO_REFRESH);
+ this.getSettings().setJavaScriptEnabled(savedState.getBoolean(JAVASCRIPT_ENABLED));
+ this.getSettings().setDomStorageEnabled(savedState.getBoolean(DOM_STORAGE_ENABLED));
+ this.getSettings().setUserAgentString(savedState.getString(USER_AGENT));
+ this.getSettings().setUseWideViewPort(savedState.getBoolean(WIDE_VIEWPORT));
+ this.getSettings().setTextZoom(savedState.getInt(FONT_SIZE));
+
+ // Get the date longs.
+ long pinnedSslStartDateLong = savedState.getLong(PINNED_SSL_START_DATE);
+ long pinnedSslEndDateLong = savedState.getLong(PINNED_SSL_END_DATE);
+
+ // Set the pinned SSL start date to `null` if the saved date long is 0 because creating a new date results in an error if the input is 0.
+ if (pinnedSslStartDateLong == 0) {
+ pinnedSslStartDate = null;
+ } else {
+ pinnedSslStartDate = new Date(pinnedSslStartDateLong);
+ }
+
+ // Set the Pinned SSL end date to `null` if the saved date long is 0 because creating a new date results in an error if the input is 0.
+ if (pinnedSslEndDateLong == 0) {
+ pinnedSslEndDate = null;
+ } else {
+ pinnedSslEndDate = new Date(pinnedSslEndDateLong);
+ }
+ }
+
// The Android accessibility guidelines require overriding `performClick()` and calling it from `onTouchEvent()`.
@Override
public boolean performClick() {