X-Git-Url: https://gitweb.stoutner.com/?p=PrivacyBrowserAndroid.git;a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fstoutner%2Fprivacybrowser%2Factivities%2FMainWebViewActivity.java;h=7549f5ca2e1a4b15c3cecbfde3d5fab96b1fb7e6;hp=5f82807c68417b1954bf960b009bb777b25b4011;hb=65531f439c8cb610d0e3e0f1e4127dc4d8a006d5;hpb=4b3880056172d007fa3cba98829e4323833c1875 diff --git a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java index 5f82807c..7549f5ca 100644 --- a/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java +++ b/app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 Soren Stoutner . + * Copyright © 2015-2017 Soren Stoutner . * * Download cookie code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner . * @@ -41,6 +41,7 @@ import android.net.http.SslCertificate; import android.net.http.SslError; import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.preference.PreferenceManager; import android.print.PrintDocumentAdapter; import android.print.PrintManager; @@ -59,7 +60,9 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatDialogFragment; import android.support.v7.widget.Toolbar; import android.text.Editable; +import android.text.Spanned; import android.text.TextWatcher; +import android.text.style.ForegroundColorSpan; import android.util.Patterns; import android.view.ContextMenu; import android.view.GestureDetector; @@ -72,7 +75,9 @@ import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.webkit.CookieManager; import android.webkit.DownloadListener; +import android.webkit.HttpAuthHandler; import android.webkit.SslErrorHandler; +import android.webkit.ValueCallback; import android.webkit.WebBackForwardList; import android.webkit.WebChromeClient; import android.webkit.WebResourceResponse; @@ -93,6 +98,8 @@ import com.stoutner.privacybrowser.BuildConfig; import com.stoutner.privacybrowser.R; import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog; import com.stoutner.privacybrowser.dialogs.DownloadImageDialog; +import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog; +import com.stoutner.privacybrowser.dialogs.PinnedSslCertificateMismatchDialog; import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog; import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog; import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper; @@ -102,6 +109,7 @@ import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog; import java.io.BufferedReader; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; @@ -109,6 +117,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -116,20 +125,23 @@ import java.util.Set; // We need to use AppCompatActivity from android.support.v7.app.AppCompatActivity to have access to the SupportActionBar until the minimum API is >= 21. public class MainWebViewActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, CreateHomeScreenShortcutDialog.CreateHomeScreenSchortcutListener, - SslCertificateErrorDialog.SslCertificateErrorListener, DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, UrlHistoryDialog.UrlHistoryListener { + HttpAuthenticationDialog.HttpAuthenticationListener, PinnedSslCertificateMismatchDialog.PinnedSslCertificateMismatchListener, SslCertificateErrorDialog.SslCertificateErrorListener, DownloadFileDialog.DownloadFileListener, + DownloadImageDialog.DownloadImageListener, UrlHistoryDialog.UrlHistoryListener { - // `appBar` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`, `onOptionsItemSelected()`, `closeFindOnPage()`, and `applyAppSettings()`. - public static ActionBar appBar; + // `darkTheme` is public static so it can be accessed from `AboutActivity`, `GuideActivity`, `AddDomainDialog`, `SettingsActivity`, `DomainsActivity`, `DomainsListFragment`, `BookmarksActivity`, `BookmarksDatabaseViewActivity`, + // `CreateBookmarkDialog`, `CreateBookmarkFolderDialog`, `DownloadFileDialog`, `DownloadImageDialog`, `EditBookmarkDialog`, `EditBookmarkFolderDialog`, `HttpAuthenticationDialog`, `MoveToFolderDialog`, `SslCertificateErrorDialog`, `UrlHistoryDialog`, + // `ViewSslCertificateDialog`, `CreateHomeScreenShortcutDialog`, and `OrbotProxyHelper`. It is also used in `onCreate()`, `applyAppSettings()`, `applyDomainSettings()`, and `updatePrivacyIcons()`. + public static boolean darkTheme; - // `favoriteIconBitmap` is public static so it can be accessed from `CreateHomeScreenShortcutDialog`, `BookmarksActivity`, `CreateBookmarkDialog`, `CreateBookmarkFolderDialog`, `EditBookmarkDialog`, `EditBookmarkFolderDialog`, `ViewSslCertificateDialog`. - // It is also used in `onCreate()`, `onCreateHomeScreenShortcutCreate()`, and `applyDomainSettings`. + // `favoriteIconBitmap` is public static so it can be accessed from `CreateHomeScreenShortcutDialog`, `BookmarksActivity`, `CreateBookmarkDialog`, `CreateBookmarkFolderDialog`, `EditBookmarkDialog`, `EditBookmarkFolderDialog`, + // and `ViewSslCertificateDialog`. It is also used in `onCreate()`, `onCreateHomeScreenShortcutCreate()`, and `applyDomainSettings`. public static Bitmap favoriteIconBitmap; // `formattedUrlString` is public static so it can be accessed from `BookmarksActivity`, `CreateBookmarkDialog`, and `AddDomainDialog`. // It is also used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onCreateHomeScreenShortcutCreate()`, and `loadUrlFromTextBox()`. public static String formattedUrlString; - // `sslCertificate` is public static so it can be accessed from `ViewSslCertificateDialog`. It is also used in `onCreate()`. + // `sslCertificate` is public static so it can be accessed from `DomainsActivity`, `DomainsListFragment`, `DomainSettingsFragment`, `PinnedSslCertificateMismatchDialog`, and `ViewSslCertificateDialog`. It is also used in `onCreate()`. public static SslCertificate sslCertificate; // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`. It is also used in `onCreate()`. @@ -138,8 +150,31 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // `webViewTitle` is public static so it can be accessed from `CreateBookmarkDialog` and `CreateHomeScreenShortcutDialog`. It is also used in `onCreate()`. public static String webViewTitle; + // `displayWebpageImagesBoolean` is public static so it can be accessed from `DomainSettingsFragment`. It is also used in `applyAppSettings()` and `applyDomainSettings()`. + public static boolean displayWebpageImagesBoolean; - // `navigatingHistory` is used in `onCreate()`, `onNavigationItemSelected()`, and `applyDomainSettings()`. + // `reloadOnRestart` is public static so it can be accessed from `SettingsFragment`. It is also used in `onRestart()` + public static boolean reloadOnRestart; + + // `reloadUrlOnRestart` is public static so it can be accessed from `SettingsFragment`. It is also used in `onRestart()`. + public static boolean loadUrlOnRestart; + + // The pinned domain SSL Certificate variables are public static so they can be accessed from `PinnedSslCertificateMismatchDialog`. They are also used in `onCreate()` and `applyDomainSettings()`. + public static int domainSettingsDatabaseId; + public static String pinnedDomainSslIssuedToCNameString; + public static String pinnedDomainSslIssuedToONameString; + public static String pinnedDomainSslIssuedToUNameString; + public static String pinnedDomainSslIssuedByCNameString; + public static String pinnedDomainSslIssuedByONameString; + public static String pinnedDomainSslIssuedByUNameString; + public static Date pinnedDomainSslStartDate; + public static Date pinnedDomainSslEndDate; + + + // `appBar` is used in `onCreate()`, `onOptionsItemSelected()`, `closeFindOnPage()`, and `applyAppSettings()`. + private ActionBar appBar; + + // `navigatingHistory` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchBack()`, and `applyDomainSettings()`. private boolean navigatingHistory; // `favoriteIconDefaultBitmap` is used in `onCreate()` and `applyDomainSettings`. @@ -151,7 +186,8 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // `rootCoordinatorLayout` is used in `onCreate()` and `applyAppSettings()`. private CoordinatorLayout rootCoordinatorLayout; - // 'mainWebView' is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`, `findNextOnPage()`, `closeFindOnPage()`, and `loadUrlFromTextBox()`. + // `mainWebView` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`, `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()` + // `onSslMismatchBack()`, and `setDisplayWebpageImages()`. private WebView mainWebView; // `fullScreenVideoFrameLayout` is used in `onCreate()` and `onConfigurationChanged()`. @@ -172,37 +208,42 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`. private final Map customHeaders = new HashMap<>(); - // `javaScriptEnabled` is also used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `applyAppSettings()`. - // It is `Boolean` instead of `boolean` because `applyAppSettings()` needs to know if it is `null`. - private Boolean javaScriptEnabled; + // `javaScriptEnabled` is also used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `applyDomainSettings()`, and `updatePrivacyIcons()`. + private boolean javaScriptEnabled; - // `firstPartyCookiesEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applyAppSettings()`. + // `firstPartyCookiesEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applyDomainSettings()`. private boolean firstPartyCookiesEnabled; - // `thirdPartyCookiesEnabled` used in `onCreate()`, `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`. + // `thirdPartyCookiesEnabled` used in `onCreate()`, `onPrepareOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. private boolean thirdPartyCookiesEnabled; - // `domStorageEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`. + // `domStorageEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. private boolean domStorageEnabled; - // `saveFormDataEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onOptionsItemSelected()`, and `applyAppSettings()`. + // `saveFormDataEnabled` is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applyDomainSettings()`. private boolean saveFormDataEnabled; + // `nightMode` is used in `onCreate()` and `applyDomainSettings()`. + private boolean nightMode; + // `swipeToRefreshEnabled` is used in `onPrepareOptionsMenu()` and `applyAppSettings()`. private boolean swipeToRefreshEnabled; // 'homepage' is used in `onCreate()`, `onNavigationItemSelected()`, and `applyAppSettings()`. private String homepage; - // `javaScriptDisabledSearchURL` is used in `loadURLFromTextBox()` and `applyAppSettings()`. - private String javaScriptDisabledSearchURL; - - // `javaScriptEnabledSearchURL` is used in `loadURLFromTextBox()` and `applyAppSettings()`. - private String javaScriptEnabledSearchURL; + // `searchURL` is used in `loadURLFromTextBox()` and `applyAppSettings()`. + private String searchURL; // `adBlockerEnabled` is used in `onCreate()` and `applyAppSettings()`. private boolean adBlockerEnabled; + // `privacyBrowserRuntime` is used in `onCreate()`, `onOptionsItemSelected()`, and `applyAppSettings()`. + private Runtime privacyBrowserRuntime; + + // `incognitoModeEnabled` is used in `onCreate()` and `applyAppSettings()`. + private boolean incognitoModeEnabled; + // `fullScreenBrowsingModeEnabled` is used in `onCreate()` and `applyAppSettings()`. private boolean fullScreenBrowsingModeEnabled; @@ -215,18 +256,30 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // `translucentNavigationBarOnFullscreen` is used in `onCreate()` and `applyAppSettings()`. private boolean translucentNavigationBarOnFullscreen; - // `proxyThroughOrbot` is used in `onCreate()` and `applyAppSettings()`. - private boolean proxyThroughOrbot; - - // `currentDomainName` is used in `onCreate(), `onNavigationItemSelected()`, and `applyDomainSettings()`. + // `currentDomainName` is used in `onCreate()`, `onNavigationItemSelected()`, `onSslMismatchProceed()`, and `applyDomainSettings()`. private String currentDomainName; - // `pendingUrl` is used in `onCreate()` and `applyAppSettings()`. - private static String pendingUrl; + // `ignorePinnedSslCertificateForDomain` is used in `onCreate()`, `onSslMismatchProceed()`, and `applyDomainSettings()`. + private boolean ignorePinnedSslCertificate; + + // `waitingForOrbot` is used in `onCreate()` and `applyAppSettings()`. + private boolean waitingForOrbot; + + // `domainSettingsApplied` is used in `applyDomainSettings()` and `setDisplayWebpageImages()`. + private boolean domainSettingsApplied; + + // `displayWebpageImagesInt` is used in `applyDomainSettings()` and `setDisplayWebpageImages()`. + private int displayWebpageImagesInt; + + // `onTheFlyDisplayImagesSet` is used in `applyDomainSettings()` and `setDisplayWebpageImages()`. + private boolean onTheFlyDisplayImagesSet; // `waitingForOrbotData` is used in `onCreate()` and `applyAppSettings()`. private String waitingForOrbotHTMLString; + // `privateDataDirectoryString` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`. + private String privateDataDirectoryString; + // `findOnPageLinearLayout` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`. private LinearLayout findOnPageLinearLayout; @@ -242,26 +295,63 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // `supportAppBar` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`. private Toolbar supportAppBar; - // `urlTextBox` is used in `onCreate()`, `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `loadUrl()`. + // `urlTextBox` is used in `onCreate()`, `onOptionsItemSelected()`, `loadUrlFromTextBox()`, `loadUrl()`, and `highlightUrlText()`. private EditText urlTextBox; + // `redColorSpan` is used in `onCreate()` and `highlightUrlText()`. + private ForegroundColorSpan redColorSpan; + + // `initialGrayColorSpan` is sued in `onCreate()` and `highlightUrlText()`. + private ForegroundColorSpan initialGrayColorSpan; + + // `finalGrayColorSpam` is used in `onCreate()` and `highlightUrlText()`. + private ForegroundColorSpan finalGrayColorSpan; + // `adView` is used in `onCreate()` and `onConfigurationChanged()`. private View adView; // `sslErrorHandler` is used in `onCreate()`, `onSslErrorCancel()`, and `onSslErrorProceed`. private SslErrorHandler sslErrorHandler; + // `httpAuthHandler` is used in `onCreate()`, `onHttpAuthenticationCancel()`, and `onHttpAuthenticationProceed()`. + private static HttpAuthHandler httpAuthHandler; + // `inputMethodManager` is used in `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `closeFindOnPage()`. private InputMethodManager inputMethodManager; // `mainWebViewRelativeLayout` is used in `onCreate()` and `onNavigationItemSelected()`. private RelativeLayout mainWebViewRelativeLayout; + // `urlIsLoading` is used in `onCreate()`, `loadUrl()`, and `applyDomainSettings()`. + private boolean urlIsLoading; + + // `pinnedDomainSslCertificate` is used in `onCreate()` and `applyDomainSettings()`. + private boolean pinnedDomainSslCertificate; + + @Override // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled. The whole premise of Privacy Browser is built around an understanding of these dangers. @SuppressLint("SetJavaScriptEnabled") + // Remove Android Studio's warning about deprecations. We have to use the deprecated `getColor()` until API >= 23. + @SuppressWarnings("deprecation") protected void onCreate(Bundle savedInstanceState) { + // Get a handle for `sharedPreferences`. `this` references the current context. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Get the theme preference. + darkTheme = sharedPreferences.getBoolean("dark_theme", false); + + // Set the activity theme. + if (darkTheme) { + setTheme(R.style.PrivacyBrowserDark); + } else { + setTheme(R.style.PrivacyBrowserLight); + } + + // Run the default commands. super.onCreate(savedInstanceState); + + // Set the content view. setContentView(R.layout.main_drawerlayout); // Get a handle for `inputMethodManager`. @@ -279,8 +369,31 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation appBar.setCustomView(R.layout.url_app_bar); appBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); - // Set the "go" button on the keyboard to load the URL in urlTextBox. + // Initialize the `ForegroundColorSpans` and `StyleSpan` for highlighting `urlTextBox`. We have to use the deprecated `getColor()` until API >= 23. + redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700)); + initialGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500)); + finalGrayColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.gray_500)); + + // Get a handle for `urlTextBox`. urlTextBox = (EditText) appBar.getCustomView().findViewById(R.id.url_edittext); + + // Remove the formatting from `urlTextBar` when the user is editing the text. + urlTextBox.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { // The user is editing `urlTextBox`. + // Remove the highlighting. + urlTextBox.getText().removeSpan(redColorSpan); + urlTextBox.getText().removeSpan(initialGrayColorSpan); + urlTextBox.getText().removeSpan(finalGrayColorSpan); + } else { // The user has stopped editing `urlTextBox`. + // Reapply the highlighting. + highlightUrlText(); + } + } + }); + + // Set the `Go` button on the keyboard to load the URL in `urlTextBox`. urlTextBox.setOnKeyListener(new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { @@ -304,10 +417,10 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Set `waitingForOrbotHTMLString`. waitingForOrbotHTMLString = "

" + getString(R.string.waiting_for_orbot) + "

"; - // Initialize `currentDomainName`, `pendingUrl`, and `orbotStatus`. + // Initialize `currentDomainName`, `orbotStatus`, and `waitingForOrbot`. currentDomainName = ""; - pendingUrl = ""; orbotStatus = "unknown"; + waitingForOrbot = false; // Create an Orbot status `BroadcastReceiver`. BroadcastReceiver orbotStatusBroadcastReceiver = new BroadcastReceiver() { @@ -316,19 +429,10 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Store the content of the status message in `orbotStatus`. orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS"); - // If we are waiting on `pendingUrl`, load it now that Orbot is connected. - if (orbotStatus.equals("ON") && !pendingUrl.isEmpty()) { - - // Wait 500 milliseconds, because Orbot isn't really ready yet. - try { - Thread.sleep(500); - } catch (InterruptedException exception) { - // Do nothing. - } - - // Copy `pendingUrl` to `formattedUrlString` and reset `pendingUrl` to be empty. - formattedUrlString = pendingUrl; - pendingUrl = ""; + // If we are waiting on Orbot, load the website now that Orbot is connected. + if (orbotStatus.equals("ON") && waitingForOrbot) { + // Reset `waitingForOrbot`. + waitingForOrbot = false; // Load `formattedUrlString loadUrl(formattedUrlString); @@ -537,8 +641,11 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation navigationForwardMenuItem.setEnabled(mainWebView.canGoForward()); navigationHistoryMenuItem.setEnabled((mainWebView.canGoBack() || mainWebView.canGoForward())); - // Hide the keyboard so we can see the navigation menu. `0` indicates no additional flags. + // Hide the keyboard (if displayed) so we can see the navigation menu. `0` indicates no additional flags. inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); + + // Clear the focus from `urlTextBox` if it has it. + urlTextBox.clearFocus(); } } }); @@ -574,8 +681,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation @SuppressWarnings("deprecation") @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - // Use an external email program if the link begins with `mailto:`. - if (url.startsWith("mailto:")) { + if (url.startsWith("mailto:")) { // Load the URL in an external email program because it begins with `mailto:`. // We use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched. Intent emailIntent = new Intent(Intent.ACTION_SENDTO); @@ -587,10 +693,15 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Make it so. startActivity(emailIntent); + + // Returning `true` indicates the application is handling the URL. return true; } else { // Load the URL in Privacy Browser. - loadUrl(url); - return true; + // Apply the domain settings for the new URL. + applyDomainSettings(url); + + // Returning `false` causes the current `WebView` to handle the URL and prevents it from adding redirects to the history list. + return false; } } @@ -631,38 +742,79 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation } } + // Handle HTTP authentication requests. + @Override + public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { + // Store `handler` so it can be accessed from `onHttpAuthenticationCancel()` and `onHttpAuthenticationProceed()`. + httpAuthHandler = handler; + + // Display the HTTP authentication dialog. + AppCompatDialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm); + httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication)); + } + // Update the URL in urlTextBox when the page starts to load. @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { - // Reset `webViewTitle` - webViewTitle = getString(R.string.no_title); + // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied. + if (nightMode) { + mainWebView.setVisibility(View.INVISIBLE); + } // Check to see if we are waiting on Orbot. - if (pendingUrl.isEmpty()) { // We are not waiting on Orbot, so we need to process the URL. + if (!waitingForOrbot) { // We are not waiting on Orbot, so we need to process the URL. // We need to update `formattedUrlString` at the beginning of the load, so that if the user toggles JavaScript during the load the new website is reloaded. formattedUrlString = url; - // Display the loading URL is the URL text box. - urlTextBox.setText(url); + // Display the formatted URL text. + urlTextBox.setText(formattedUrlString); + + // Apply text highlighting to `urlTextBox`. + highlightUrlText(); // Apply any custom domain settings if the URL was loaded by navigating history. if (navigatingHistory) { applyDomainSettings(url); } + + // Set `urlIsLoading` to `true`, so that redirects while loading do not trigger changes in the user agent, which forces another reload of the existing page. + urlIsLoading = true; } } - // Update formattedUrlString and urlTextBox. It is necessary to do this after the page finishes loading because the final URL can change during load. + // It is necessary to update `formattedUrlString` and `urlTextBox` after the page finishes loading because the final URL can change during load. @Override public void onPageFinished(WebView view, String url) { - // Check to see if we are waiting on Orbot. - if (pendingUrl.isEmpty()) { // we are not waiting on Orbot, so we need to process the URL. + // Reset `urlIsLoading`, which is used to prevent reloads on redirect if the user agent changes. + urlIsLoading = false; + + // Clear the cache and history if Incognito Mode is enabled. + if (incognitoModeEnabled) { + // Clear the cache. `true` includes disk files. + mainWebView.clearCache(true); + + // Clear the back/forward history. + mainWebView.clearHistory(); + + // Manually delete cache folders. + try { + // Delete the main `cache` folder. + privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache"); + + // Delete the `app_webview` folder, which contains an additional `WebView` cache. See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`. + privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview"); + } catch (IOException e) { + // Do nothing if an error is thrown. + } + } + + // Update `urlTextBox` and apply domain settings if not waiting on Orbot. + if (!waitingForOrbot) { // Check to see if `WebView` has set `url` to be `about:blank`. if (url.equals("about:blank")) { // `WebView` is blank, so `formattedUrlString` should be `""` and `urlTextBox` should display a hint. // Set `formattedUrlString` to `""`. formattedUrlString = ""; - // Update `urlTextBox`. urlTextBox.setText(formattedUrlString); // Request focus for `urlTextBox`. @@ -671,7 +823,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Display the keyboard. inputMethodManager.showSoftInput(urlTextBox, 0); - // Apply the domain settings. + // Apply the domain settings. This clears any settings from the previous domain. applyDomainSettings(formattedUrlString); } else { // `WebView` has loaded a webpage. // Set `formattedUrlString`. @@ -679,24 +831,108 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Only update `urlTextBox` if the user is not typing in it. if (!urlTextBox.hasFocus()) { + // Display the formatted URL text. urlTextBox.setText(formattedUrlString); + + // Apply text highlighting to `urlTextBox`. + highlightUrlText(); } } - // Store the SSL certificate so it can be accessed from `ViewSslCertificateDialog`. + // Store the SSL certificate so it can be accessed from `ViewSslCertificateDialog` and `PinnedSslCertificateMismatchDialog`. sslCertificate = mainWebView.getCertificate(); + + // Check the current website SSL certificate against the pinned SSL certificate if there is a pinned SSL certificate the user has not chosen to ignore it for this session. + if (pinnedDomainSslCertificate && !ignorePinnedSslCertificate) { + // Initialize the current SSL certificate variables. + String currentWebsiteIssuedToCName = ""; + String currentWebsiteIssuedToOName = ""; + String currentWebsiteIssuedToUName = ""; + String currentWebsiteIssuedByCName = ""; + String currentWebsiteIssuedByOName = ""; + String currentWebsiteIssuedByUName = ""; + Date currentWebsiteSslStartDate = null; + Date currentWebsiteSslEndDate = null; + + + // Extract the individual pieces of information from the current website SSL certificate if it is not null. + if (sslCertificate != null) { + currentWebsiteIssuedToCName = sslCertificate.getIssuedTo().getCName(); + currentWebsiteIssuedToOName = sslCertificate.getIssuedTo().getOName(); + currentWebsiteIssuedToUName = sslCertificate.getIssuedTo().getUName(); + currentWebsiteIssuedByCName = sslCertificate.getIssuedBy().getCName(); + currentWebsiteIssuedByOName = sslCertificate.getIssuedBy().getOName(); + currentWebsiteIssuedByUName = sslCertificate.getIssuedBy().getUName(); + currentWebsiteSslStartDate = sslCertificate.getValidNotBeforeDate(); + currentWebsiteSslEndDate = sslCertificate.getValidNotAfterDate(); + } + + // Initialize `String` variables to store the SSL certificate dates. `Strings` are needed to compare the values below, which doesn't work with `Dates` if they are `null`. + String currentWebsiteSslStartDateString = ""; + String currentWebsiteSslEndDateString = ""; + String pinnedDomainSslStartDateString = ""; + String pinnedDomainSslEndDateString = ""; + + // Convert the `Dates` to `Strings` if they are not `null`. + if (currentWebsiteSslStartDate != null) { + currentWebsiteSslStartDateString = currentWebsiteSslStartDate.toString(); + } + + if (currentWebsiteSslEndDate != null) { + currentWebsiteSslEndDateString = currentWebsiteSslEndDate.toString(); + } + + if (pinnedDomainSslStartDate != null) { + pinnedDomainSslStartDateString = pinnedDomainSslStartDate.toString(); + } + + if (pinnedDomainSslEndDate != null) { + pinnedDomainSslEndDateString = pinnedDomainSslEndDate.toString(); + } + + // Check to see if the pinned SSL certificate matches the current website certificate. + if (!currentWebsiteIssuedToCName.equals(pinnedDomainSslIssuedToCNameString) || !currentWebsiteIssuedToOName.equals(pinnedDomainSslIssuedToONameString) || !currentWebsiteIssuedToUName.equals(pinnedDomainSslIssuedToUNameString) || + !currentWebsiteIssuedByCName.equals(pinnedDomainSslIssuedByCNameString) || !currentWebsiteIssuedByOName.equals(pinnedDomainSslIssuedByONameString) || !currentWebsiteIssuedByUName.equals(pinnedDomainSslIssuedByUNameString) || + !currentWebsiteSslStartDateString.equals(pinnedDomainSslStartDateString) || !currentWebsiteSslEndDateString.equals(pinnedDomainSslEndDateString)) { // The pinned SSL certificate doesn't match the current domain certificate. + //Display the pinned SSL certificate mismatch `AlertDialog`. + AppCompatDialogFragment pinnedSslCertificateMismatchDialogFragment = new PinnedSslCertificateMismatchDialog(); + pinnedSslCertificateMismatchDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_mismatch)); + } + } } } // Handle SSL Certificate errors. @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - // Store `handler` so it can be accesses from `onSslErrorCancel()` and `onSslErrorProceed()`. - sslErrorHandler = handler; - - // Display the SSL error `AlertDialog`. - AppCompatDialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error); - sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.ssl_certificate_error)); + // Get the current website SSL certificate. + SslCertificate currentWebsiteSslCertificate = error.getCertificate(); + + // Extract the individual pieces of information from the current website SSL certificate. + String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName(); + String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName(); + String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName(); + String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName(); + String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName(); + String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName(); + Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate(); + Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate(); + + // Proceed to the website if the current SSL website certificate matches the pinned domain certificate. + if (pinnedDomainSslCertificate && + currentWebsiteIssuedToCName.equals(pinnedDomainSslIssuedToCNameString) && currentWebsiteIssuedToOName.equals(pinnedDomainSslIssuedToONameString) && currentWebsiteIssuedToUName.equals(pinnedDomainSslIssuedToUNameString) && + currentWebsiteIssuedByCName.equals(pinnedDomainSslIssuedByCNameString) && currentWebsiteIssuedByOName.equals(pinnedDomainSslIssuedByONameString) && currentWebsiteIssuedByUName.equals(pinnedDomainSslIssuedByUNameString) && + currentWebsiteSslStartDate.equals(pinnedDomainSslStartDate) && currentWebsiteSslEndDate.equals(pinnedDomainSslEndDate)) { // An SSL certificate is pinned and matches the current domain certificate. + // Proceed to the website without displaying an error. + handler.proceed(); + } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate. + // Store `handler` so it can be accesses from `onSslErrorCancel()` and `onSslErrorProceed()`. + sslErrorHandler = handler; + + // Display the SSL error `AlertDialog`. + AppCompatDialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error); + sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error)); + } } }); @@ -709,10 +945,42 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation public void onProgressChanged(WebView view, int progress) { progressBar.setProgress(progress); if (progress < 100) { + // Show the progress bar. progressBar.setVisibility(View.VISIBLE); } else { + // Hide the progress bar. progressBar.setVisibility(View.GONE); + // Inject the night mode CSS if night mode is enabled. + if (nightMode) { // Night mode is enabled. + // `background-color: #212121` sets the background to be dark gray. `color: #BDBDBD` sets the text color to be light gray. `box-shadow: none` removes a lower underline on links used by WordPress. + // `text-decoration: none` removes all text underlines. `text-shadow: none` removes text shadows, which usually have a hard coded color. `border: none` removes all borders, which can also be used to underline text. + // `a {color: #1565C0}` sets links to be a dark blue. `!important` takes precedent over any existing sub-settings. + mainWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = '" + + "* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important; text-shadow: none !important; border: none !important;}" + + "a {color: #1565C0 !important;}" + + "'; parent.appendChild(style)})()", new ValueCallback() { + @Override + public void onReceiveValue(String value) { + // Initialize a `Handler` to display `mainWebView`. + Handler displayWebViewHandler = new Handler(); + + // Setup a `Runnable` to display `mainWebView` after a delay to allow the CSS to be applied. + Runnable displayWebViewRunnable = new Runnable() { + public void run() { + mainWebView.setVisibility(View.VISIBLE); + } + }; + + // Use `displayWebViewHandler` to delay the displaying of `mainWebView` for 500 milliseconds. + displayWebViewHandler.postDelayed(displayWebViewRunnable, 500); + } + }); + } else { // Night mode is disabled. + // Display `mainWebView` in case it was hidden before loading domain settings. + mainWebView.setVisibility(View.VISIBLE); + } + //Stop the `SwipeToRefresh` indicator if it is running swipeRefreshLayout.setRefreshing(false); } @@ -798,7 +1066,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { // Show the `DownloadFileDialog` `AlertDialog` and name this instance `@string/download`. AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength); - downloadFileDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.download)); + downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); } }); @@ -808,6 +1076,15 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Hide zoom controls. mainWebView.getSettings().setDisplayZoomControls(false); + // Set `mainWebView` to use a wide viewport. Otherwise, some web pages will be scrunched and some content will render outside the screen. + mainWebView.getSettings().setUseWideViewPort(true); + + // Set `mainWebView` to load in overview mode (zoomed out to the maximum width). + mainWebView.getSettings().setLoadWithOverviewMode(true); + + // Explicitly disable geolocation. + mainWebView.getSettings().setGeolocationEnabled(false); + // Initialize cookieManager. cookieManager = CookieManager.getInstance(); @@ -828,6 +1105,12 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation formattedUrlString = launchingIntentUriData.toString(); } + // Get a handle for the `Runtime`. + privacyBrowserRuntime = Runtime.getRuntime(); + + // Store the application's private data directory. + privateDataDirectoryString = getApplicationInfo().dataDir; // `dataDir` will vary, but will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`. + // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode. inFullScreenBrowsingMode = false; @@ -840,13 +1123,11 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation thirdPartyCookiesEnabled = false; domStorageEnabled = false; saveFormDataEnabled = false; + nightMode = false; // Initialize `webViewTitle`. webViewTitle = getString(R.string.no_title); - // Apply the app settings from the shared preferences. - applyAppSettings(); - // Initialize `favoriteIconBitmap`. We have to use `ContextCompat` until API >= 21. Drawable favoriteIconDrawable = ContextCompat.getDrawable(getApplicationContext(), R.drawable.world); BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable; @@ -855,15 +1136,17 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // If the favorite icon is null, load the default. if (favoriteIconBitmap == null) { favoriteIconBitmap = favoriteIconDefaultBitmap; + } + + // Apply the app settings from the shared preferences. + applyAppSettings(); - // Load `formattedUrlString` if we are not proxying through Orbot and waiting for Orbot to connect. - if (!(proxyThroughOrbot && !orbotStatus.equals("ON"))) { + // Load `formattedUrlString` if we are not waiting for Orbot to connect. + if (!waitingForOrbot) { loadUrl(formattedUrlString); } - } } - @Override protected void onNewIntent(Intent intent) { // Sets the new intent as the activity intent, so that any future `getIntent()`s pick up this one instead of creating a new activity. @@ -887,6 +1170,71 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation mainWebView.requestFocus(); } + @Override + public void onRestart() { + super.onRestart(); + + // Apply the app settings, which may have been changed in `SettingsActivity`. + applyAppSettings(); + + // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. + updatePrivacyIcons(true); + + // Set the display webpage images mode. + setDisplayWebpageImages(); + + // Reload the webpage if displaying of images has been disabled in `SettingsFragment`. + if (reloadOnRestart) { + // Reload `mainWebView`. + mainWebView.reload(); + + // Reset `reloadOnRestartBoolean`. + reloadOnRestart = false; + } + + // Load the URL on restart to apply changes to night mode. + if (loadUrlOnRestart) { + // Load the current `formattedUrlString`. + loadUrl(formattedUrlString); + + // Reset `loadUrlOnRestart. + loadUrlOnRestart = false; + } + } + + // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`. + @Override + public void onResume() { + super.onResume(); + + // Resume JavaScript (if enabled). + mainWebView.resumeTimers(); + + // Resume `mainWebView`. + mainWebView.onResume(); + + // Resume the adView for the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + BannerAd.resumeAd(adView); + } + } + + @Override + public void onPause() { + // Pause `mainWebView`. + mainWebView.onPause(); + + // Stop all JavaScript. + mainWebView.pauseTimers(); + + // Pause the adView or it will continue to consume resources in the background on the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + BannerAd.pauseAd(adView); + } + + super.onPause(); + } + @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. @@ -899,26 +1247,26 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation updatePrivacyIcons(false); // Get handles for the menu items. - MenuItem toggleFirstPartyCookies = menu.findItem(R.id.toggleFirstPartyCookies); - MenuItem toggleThirdPartyCookies = menu.findItem(R.id.toggleThirdPartyCookies); - MenuItem toggleDomStorage = menu.findItem(R.id.toggleDomStorage); - MenuItem toggleSaveFormData = menu.findItem(R.id.toggleSaveFormData); + MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies); + MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies); + MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage); + MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); // Only display third-party cookies if SDK >= 21 - toggleThirdPartyCookies.setVisible(Build.VERSION.SDK_INT >= 21); + toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21); // Get the shared preference values. `this` references the current context. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); // Set the status of the additional app bar icons. The default is `false`. if (sharedPreferences.getBoolean("display_additional_app_bar_icons", false)) { - toggleFirstPartyCookies.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - toggleDomStorage.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - toggleSaveFormData.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + toggleSaveFormDataMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); } else { //Do not display the additional icons. - toggleFirstPartyCookies.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - toggleDomStorage.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - toggleSaveFormData.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + toggleSaveFormDataMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); } return true; @@ -927,35 +1275,53 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation @Override public boolean onPrepareOptionsMenu(Menu menu) { // Get handles for the menu items. - MenuItem toggleFirstPartyCookies = menu.findItem(R.id.toggleFirstPartyCookies); - MenuItem toggleThirdPartyCookies = menu.findItem(R.id.toggleThirdPartyCookies); - MenuItem toggleDomStorage = menu.findItem(R.id.toggleDomStorage); - MenuItem toggleSaveFormData = menu.findItem(R.id.toggleSaveFormData); - MenuItem clearCookies = menu.findItem(R.id.clearCookies); - MenuItem clearFormData = menu.findItem(R.id.clearFormData); + MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies); + MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies); + MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage); + MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data); + MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies); + MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage); + MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data); + MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size); + MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images); MenuItem refreshMenuItem = menu.findItem(R.id.refresh); // Set the status of the menu item checkboxes. - toggleFirstPartyCookies.setChecked(firstPartyCookiesEnabled); - toggleThirdPartyCookies.setChecked(thirdPartyCookiesEnabled); - toggleDomStorage.setChecked(domStorageEnabled); - toggleSaveFormData.setChecked(saveFormDataEnabled); + toggleFirstPartyCookiesMenuItem.setChecked(firstPartyCookiesEnabled); + toggleThirdPartyCookiesMenuItem.setChecked(thirdPartyCookiesEnabled); + toggleDomStorageMenuItem.setChecked(domStorageEnabled); + toggleSaveFormDataMenuItem.setChecked(saveFormDataEnabled); + displayImagesMenuItem.setChecked(mainWebView.getSettings().getLoadsImagesAutomatically()); // Enable third-party cookies if first-party cookies are enabled. - toggleThirdPartyCookies.setEnabled(firstPartyCookiesEnabled); + toggleThirdPartyCookiesMenuItem.setEnabled(firstPartyCookiesEnabled); - // Enable DOM Storage if JavaScript is enabled. - toggleDomStorage.setEnabled(javaScriptEnabled); + // Enable `DOM Storage` if JavaScript is enabled. + toggleDomStorageMenuItem.setEnabled(javaScriptEnabled); - // Enable Clear Cookies if there are any. - clearCookies.setEnabled(cookieManager.hasCookies()); + // Enable `Clear Cookies` if there are any. + clearCookiesMenuItem.setEnabled(cookieManager.hasCookies()); - // Enable Clear Form Data is there is any. - WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this); - clearFormData.setEnabled(mainWebViewDatabase.hasFormData()); + // Get a count of the number of files in the `Local Storage` directory. + File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/"); + int localStorageDirectoryNumberOfFiles = 0; + if (localStorageDirectory.exists()) { + localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length; + } - // Only show `Refresh` if `swipeToRefresh` is disabled. - refreshMenuItem.setVisible(!swipeToRefreshEnabled); + // Get a count of the number of files in the `IndexedDB` directory. + File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB"); + int indexedDBDirectoryNumberOfFiles = 0; + if (indexedDBDirectory.exists()) { + indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length; + } + + // Enable `Clear DOM Storage` if there is any. + clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0); + + // Enable `Clear Form Data` is there is any. + WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this); + clearFormDataMenuItem.setEnabled(mainWebViewDatabase.hasFormData()); // Initialize font size variables. int fontSize = mainWebView.getSettings().getTextZoom(); @@ -964,52 +1330,59 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Prepare the font size title and current size menu item. switch (fontSize) { + case 25: + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent); + break; + case 50: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.fifty_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeFiftyPercent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent); break; case 75: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.seventy_five_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeSeventyFivePercent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent); break; case 100: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeOneHundredPercent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent); break; case 125: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_twenty_five_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeOneHundredTwentyFivePercent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent); break; case 150: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_fifty_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeOneHundredFiftyPercent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent); break; case 175: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_seventy_five_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeOneHundredSeventyFivePercent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent); break; case 200: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.two_hundred_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeTwoHundredPercent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent); break; default: - fontSizeTitle = getResources().getString(R.string.font_size) + " - " + getResources().getString(R.string.one_hundred_percent); - selectedFontSizeMenuItem = menu.findItem(R.id.fontSizeOneHundredPercent); + fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent); + selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent); break; } // Set the font size title and select the current size menu item. - MenuItem fontSizeMenuItem = menu.findItem(R.id.fontSize); fontSizeMenuItem.setTitle(fontSizeTitle); selectedFontSizeMenuItem.setChecked(true); + // Only show `Refresh` if `swipeToRefresh` is disabled. + refreshMenuItem.setVisible(!swipeToRefreshEnabled); + // Run all the other default commands. super.onPrepareOptionsMenu(menu); @@ -1027,7 +1400,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Set the commands that relate to the menu entries. switch (menuItemId) { - case R.id.toggleJavaScript: + case R.id.toggle_javascript: // Switch the status of javaScriptEnabled. javaScriptEnabled = !javaScriptEnabled; @@ -1050,7 +1423,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation mainWebView.reload(); return true; - case R.id.toggleFirstPartyCookies: + case R.id.toggle_first_party_cookies: // Switch the status of firstPartyCookiesEnabled. firstPartyCookiesEnabled = !firstPartyCookiesEnabled; @@ -1076,7 +1449,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation mainWebView.reload(); return true; - case R.id.toggleThirdPartyCookies: + case R.id.toggle_third_party_cookies: if (Build.VERSION.SDK_INT >= 21) { // Switch the status of thirdPartyCookiesEnabled. thirdPartyCookiesEnabled = !thirdPartyCookiesEnabled; @@ -1099,7 +1472,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation } // Else do nothing because SDK < 21. return true; - case R.id.toggleDomStorage: + case R.id.toggle_dom_storage: // Switch the status of domStorageEnabled. domStorageEnabled = !domStorageEnabled; @@ -1123,7 +1496,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation mainWebView.reload(); return true; - case R.id.toggleSaveFormData: + case R.id.toggle_save_form_data: // Switch the status of saveFormDataEnabled. saveFormDataEnabled = !saveFormDataEnabled; @@ -1147,60 +1520,156 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation mainWebView.reload(); return true; - case R.id.clearCookies: - if (Build.VERSION.SDK_INT < 21) { - cookieManager.removeAllCookie(); - } else { - cookieManager.removeAllCookies(null); - } - Snackbar.make(findViewById(R.id.main_webview), R.string.cookies_deleted, Snackbar.LENGTH_SHORT).show(); + case R.id.clear_cookies: + Snackbar.make(findViewById(R.id.main_webview), R.string.cookies_deleted, Snackbar.LENGTH_LONG) + .setAction(R.string.undo, new View.OnClickListener() { + @Override + public void onClick(View v) { + // Do nothing because everything will be handled by `onDismissed()` below. + } + }) + .addCallback(new Snackbar.Callback() { + @Override + public void onDismissed(Snackbar snackbar, int event) { + switch (event) { + // The user pushed the `Undo` button. + case Snackbar.Callback.DISMISS_EVENT_ACTION: + // Do nothing. + break; + + // The `Snackbar` was dismissed without the `Undo` button being pushed. + default: + // `cookieManager.removeAllCookie()` varies by SDK. + if (Build.VERSION.SDK_INT < 21) { + cookieManager.removeAllCookie(); + } else { + // `null` indicates no callback. + cookieManager.removeAllCookies(null); + } + } + } + }) + .show(); + return true; + + case R.id.clear_dom_storage: + Snackbar.make(findViewById(R.id.main_webview), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG) + .setAction(R.string.undo, new View.OnClickListener() { + @Override + public void onClick(View v) { + // Do nothing because everything will be handled by `onDismissed()` below. + } + }) + .addCallback(new Snackbar.Callback() { + @Override + public void onDismissed(Snackbar snackbar, int event) { + switch (event) { + // The user pushed the `Undo` button. + case Snackbar.Callback.DISMISS_EVENT_ACTION: + // Do nothing. + break; + + // The `Snackbar` was dismissed without the `Undo` button being pushed. + default: + // Delete the DOM Storage. + WebStorage webStorage = WebStorage.getInstance(); + webStorage.deleteAllData(); + + // Manually remove `IndexedDB` if it exists. + try { + privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); + } catch (IOException e) { + // Do nothing if an error is thrown. + } + } + } + }) + .show(); return true; - case R.id.clearDomStorage: - WebStorage webStorage = WebStorage.getInstance(); - webStorage.deleteAllData(); - Snackbar.make(findViewById(R.id.main_webview), R.string.dom_storage_deleted, Snackbar.LENGTH_SHORT).show(); + case R.id.clear_form_data: + Snackbar.make(findViewById(R.id.main_webview), R.string.form_data_deleted, Snackbar.LENGTH_LONG) + .setAction(R.string.undo, new View.OnClickListener() { + @Override + public void onClick(View v) { + // Do nothing because everything will be handled by `onDismissed()` below. + } + }) + .addCallback(new Snackbar.Callback() { + @Override + public void onDismissed(Snackbar snackbar, int event) { + switch (event) { + // The user pushed the `Undo` button. + case Snackbar.Callback.DISMISS_EVENT_ACTION: + // Do nothing. + break; + + // The `Snackbar` was dismissed without the `Undo` button being pushed. + default: + // Delete the form data. + WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext()); + mainWebViewDatabase.clearFormData(); + } + } + }) + .show(); return true; - case R.id.clearFormData: - WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(this); - mainWebViewDatabase.clearFormData(); - Snackbar.make(findViewById(R.id.main_webview), R.string.form_data_deleted, Snackbar.LENGTH_SHORT).show(); + case R.id.font_size_twenty_five_percent: + mainWebView.getSettings().setTextZoom(25); return true; - case R.id.fontSizeFiftyPercent: + case R.id.font_size_fifty_percent: mainWebView.getSettings().setTextZoom(50); return true; - case R.id.fontSizeSeventyFivePercent: + case R.id.font_size_seventy_five_percent: mainWebView.getSettings().setTextZoom(75); return true; - case R.id.fontSizeOneHundredPercent: + case R.id.font_size_one_hundred_percent: mainWebView.getSettings().setTextZoom(100); return true; - case R.id.fontSizeOneHundredTwentyFivePercent: + case R.id.font_size_one_hundred_twenty_five_percent: mainWebView.getSettings().setTextZoom(125); return true; - case R.id.fontSizeOneHundredFiftyPercent: + case R.id.font_size_one_hundred_fifty_percent: mainWebView.getSettings().setTextZoom(150); return true; - case R.id.fontSizeOneHundredSeventyFivePercent: + case R.id.font_size_one_hundred_seventy_five_percent: mainWebView.getSettings().setTextZoom(175); return true; - case R.id.fontSizeTwoHundredPercent: + case R.id.font_size_two_hundred_percent: mainWebView.getSettings().setTextZoom(200); return true; + case R.id.display_images: + if (mainWebView.getSettings().getLoadsImagesAutomatically()) { // Images are currently loaded automatically. + mainWebView.getSettings().setLoadsImagesAutomatically(false); + mainWebView.reload(); + } else { // Images are not currently loaded automatically. + mainWebView.getSettings().setLoadsImagesAutomatically(true); + } + + // Set `onTheFlyDisplayImagesSet`. + onTheFlyDisplayImagesSet = true; + return true; + case R.id.share: + // Setup the share string. + String shareString = webViewTitle + " – " + urlTextBox.getText().toString(); + + // Create the share intent. Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_TEXT, urlTextBox.getText().toString()); + shareIntent.putExtra(Intent.EXTRA_TEXT, shareString); shareIntent.setType("text/plain"); + + // Make it so. startActivity(Intent.createChooser(shareIntent, "Share URL")); return true; @@ -1211,26 +1680,20 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Show the Find on Page `RelativeLayout`. findOnPageLinearLayout.setVisibility(View.VISIBLE); - // Display the keyboard. We have to wait 200 ms before running the command to work around a bug in Android. - // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working - findOnPageEditText.postDelayed(new Runnable() - { + // Display the keyboard. We have to wait 200 ms before running the command to work around a bug in Android. http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working + findOnPageEditText.postDelayed(new Runnable() { @Override public void run() { // Set the focus on `findOnPageEditText`. findOnPageEditText.requestFocus(); - // Display the keyboard. + // Display the keyboard. `0` sets no input flags. inputMethodManager.showSoftInput(findOnPageEditText, 0); } }, 200); return true; - case R.id.refresh: - mainWebView.reload(); - return true; - case R.id.print: // Get a `PrintManager` instance. PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); @@ -1239,17 +1702,21 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation PrintDocumentAdapter printDocumentAdapter = mainWebView.createPrintDocumentAdapter(); // Print the document. The print attributes are `null`. - printManager.print(getResources().getString(R.string.privacy_browser_web_page), printDocumentAdapter, null); + printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null); return true; - case R.id.addToHomescreen: + case R.id.add_to_homescreen: // Show the `CreateHomeScreenShortcutDialog` `AlertDialog` and name this instance `R.string.create_shortcut`. AppCompatDialogFragment createHomeScreenShortcutDialogFragment = new CreateHomeScreenShortcutDialog(); - createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.create_shortcut)); + createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut)); //Everything else will be handled by `CreateHomeScreenShortcutDialog` and the associated listener below. return true; + case R.id.refresh: + mainWebView.reload(); + return true; + default: // Don't consume the event. return super.onOptionsItemSelected(menuItem); @@ -1293,7 +1760,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Show the `UrlHistoryDialog` `AlertDialog` and name this instance `R.string.history`. `this` is the `Context`. AppCompatDialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(this, webBackForwardList); - urlHistoryDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.history)); + urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history)); break; case R.id.bookmarks: @@ -1343,30 +1810,89 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation break; case R.id.clearAndExit: - // Clear cookies. The commands changed slightly in API 21. - if (Build.VERSION.SDK_INT >= 21) { - cookieManager.removeAllCookies(null); - } else { - cookieManager.removeAllCookie(); + // Get a handle for `sharedPreferences`. `this` references the current context. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true); + + // Clear cookies. + if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) { + // The command to remove cookies changed slightly in API 21. + if (Build.VERSION.SDK_INT >= 21) { + cookieManager.removeAllCookies(null); + } else { + cookieManager.removeAllCookie(); + } + + // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run. + try { + // We have to use two commands because `Runtime.exec()` does not like `*`. + privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies"); + privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal"); + } catch (IOException e) { + // Do nothing if an error is thrown. + } } // Clear DOM storage. - WebStorage domStorage = WebStorage.getInstance(); - domStorage.deleteAllData(); + if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) { + // Ask `WebStorage` to clear the DOM storage. + WebStorage webStorage = WebStorage.getInstance(); + webStorage.deleteAllData(); + + // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run. + try { + // We have to use a `String[]` because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise. + privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"}); + + // We have to use multiple commands because `Runtime.exec()` does not like `*`. + privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB"); + privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager"); + privacyBrowserRuntime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal"); + privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases"); + } catch (IOException e) { + // Do nothing if an error is thrown. + } + } // Clear form data. - WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this); - webViewDatabase.clearFormData(); + if (clearEverything || sharedPreferences.getBoolean("clear_form_data", true)) { + WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this); + webViewDatabase.clearFormData(); + + // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run. + try { + // We have to use a `String[]` because the database contains a space and `Runtime.exec` will not escape the string correctly otherwise. + privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"}); + privacyBrowserRuntime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"}); + } catch (IOException e) { + // Do nothing if an error is thrown. + } + } - // Clear cache. The argument of "true" includes disk files. - mainWebView.clearCache(true); + // Clear the cache. + if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) { + // `true` includes disk files. + mainWebView.clearCache(true); - // Clear the back/forward history. - mainWebView.clearHistory(); + // Manually delete the cache directories. + try { + // Delete the main cache directory. + privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/cache"); + + // Delete the secondary `Service Worker` cache directory. We have to use a `String[]` because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise. + privacyBrowserRuntime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"}); + } catch (IOException e) { + // Do nothing if an error is thrown. + } + } - // Clear any SSL certificate preferences. + // Clear SSL certificate preferences. mainWebView.clearSslPreferences(); + // Clear the back/forward history. + mainWebView.clearHistory(); + // Clear `formattedUrlString`. formattedUrlString = null; @@ -1379,13 +1905,14 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Destroy the internal state of `mainWebView`. mainWebView.destroy(); - // Manually delete the `app_webview` folder, which contains an additional `WebView` cache. See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`. - Runtime runtime = Runtime.getRuntime(); - String dataDirString = getApplicationInfo().dataDir; // `dataDir` will vary, but will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`. - try { - runtime.exec("rm -rf " + dataDirString + "/app_webview"); - } catch (IOException e) { - // Do nothing if the files do not exist. + // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache. + // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`. + if (clearEverything) { + try { + privacyBrowserRuntime.exec("rm -rf " + privateDataDirectoryString + "/app_webview"); + } catch (IOException e) { + // Do nothing if an error is thrown. + } } // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list. @@ -1398,9 +1925,6 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Remove the terminated program from RAM. The status code is `0`. System.exit(0); break; - - default: - break; } // Close the navigation drawer. @@ -1468,7 +1992,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation @Override public boolean onMenuItemClick(MenuItem item) { // Save the link URL in a `ClipData`. - ClipData srcAnchorTypeClipData = ClipData.newPlainText(getResources().getString(R.string.url), linkUrl); + ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl); // Set the `ClipData` as the clipboard's primary clip. clipboardManager.setPrimaryClip(srcAnchorTypeClipData); @@ -1511,7 +2035,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation @Override public boolean onMenuItemClick(MenuItem item) { // Save the email address in a `ClipData`. - ClipData srcEmailTypeClipData = ClipData.newPlainText(getResources().getString(R.string.email_address), linkUrl); + ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl); // Set the `ClipData` as the clipboard's primary clip. clipboardManager.setPrimaryClip(srcEmailTypeClipData); @@ -1546,7 +2070,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation public boolean onMenuItemClick(MenuItem item) { // Show the `DownloadImageDialog` `AlertDialog` and name this instance `@string/download`. AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); - downloadImageDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.download)); + downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); return false; } }); @@ -1556,7 +2080,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation @Override public boolean onMenuItemClick(MenuItem item) { // Save the image URL in a `ClipData`. - ClipData srcImageTypeClipData = ClipData.newPlainText(getResources().getString(R.string.url), imageUrl); + ClipData srcImageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl); // Set the `ClipData` as the clipboard's primary clip. clipboardManager.setPrimaryClip(srcImageTypeClipData); @@ -1592,7 +2116,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation public boolean onMenuItemClick(MenuItem item) { // Show the `DownloadImageDialog` `AlertDialog` and name this instance `@string/download`. AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl); - downloadImageDialogFragment.show(getSupportFragmentManager(), getResources().getString(R.string.download)); + downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)); return false; } }); @@ -1602,7 +2126,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation @Override public boolean onMenuItemClick(MenuItem item) { // Save the image URL in a `ClipData`. - ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getResources().getString(R.string.url), imageUrl); + ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl); // Set the `ClipData` as the clipboard's primary clip. clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData); @@ -1646,6 +2170,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl)); // Pass cookies to download manager if cookies are enabled. This is required to download images from websites that require a login. + // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner . if (firstPartyCookiesEnabled) { // Get the cookies for `imageUrl`. String cookies = cookieManager.getCookie(imageUrl); @@ -1693,6 +2218,7 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl)); // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login. + // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner . if (firstPartyCookiesEnabled) { // Get the cookies for `downloadUrl`. String cookies = cookieManager.getCookie(downloadUrl); @@ -1728,10 +2254,26 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation } } + @Override + public void onHttpAuthenticationCancel() { + // Cancel the `HttpAuthHandler`. + httpAuthHandler.cancel(); + } + + @Override + public void onHttpAuthenticationProceed(AppCompatDialogFragment dialogFragment) { + // Get handles for the `EditTexts`. + EditText usernameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.http_authentication_username); + EditText passwordEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.http_authentication_password); + + // Proceed with the HTTP authentication. + httpAuthHandler.proceed(usernameEditText.getText().toString(), passwordEditText.getText().toString()); + } + public void viewSslCertificate(View view) { // Show the `ViewSslCertificateDialog` `AlertDialog` and name this instance `@string/view_ssl_certificate`. DialogFragment viewSslCertificateDialogFragment = new ViewSslCertificateDialog(); - viewSslCertificateDialogFragment.show(getFragmentManager(), getResources().getString(R.string.view_ssl_certificate)); + viewSslCertificateDialogFragment.show(getFragmentManager(), getString(R.string.view_ssl_certificate)); } @Override @@ -1744,6 +2286,26 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation sslErrorHandler.proceed(); } + @Override + public void onSslMismatchBack() { + if (mainWebView.canGoBack()) { // There is a back page in the history. + // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. + navigatingHistory = true; + + // Go back. + mainWebView.goBack(); + } else { // There are no pages to go back to. + // Load a blank page + loadUrl(""); + } + } + + @Override + public void onSslMismatchProceed() { + // Do not check the pinned SSL certificate for this domain again until the domain changes. + ignorePinnedSslCertificate = true; + } + @Override public void onUrlHistoryEntrySelected(int moveBackOrForwardSteps) { // Set `navigatingHistory` so that the domain settings are applied when the new URL is loaded. @@ -1780,63 +2342,19 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation } } - @Override - public void onPause() { - // Pause `mainWebView`. - mainWebView.onPause(); - - // Stop all JavaScript. - mainWebView.pauseTimers(); + private void loadUrlFromTextBox() throws UnsupportedEncodingException { + // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string. + String unformattedUrlString = urlTextBox.getText().toString().trim(); - // Pause the adView or it will continue to consume resources in the background on the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - BannerAd.pauseAd(adView); - } + // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search. + if ((Patterns.WEB_URL.matcher(unformattedUrlString).matches()) || (unformattedUrlString.startsWith("http://")) || (unformattedUrlString.startsWith("https://"))) { + // Add `http://` at the beginning if it is missing. Otherwise the app will segfault. + if (!unformattedUrlString.startsWith("http")) { + unformattedUrlString = "http://" + unformattedUrlString; + } - super.onPause(); - } - - @Override - public void onResume() { - super.onResume(); - - // Resume JavaScript (if enabled). - mainWebView.resumeTimers(); - - // Resume `mainWebView`. - mainWebView.onResume(); - - // Resume the adView for the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - BannerAd.resumeAd(adView); - } - } - - @Override - public void onRestart() { - super.onRestart(); - - // Apply the settings from shared preferences, which might have been changed in `SettingsActivity`. - applyAppSettings(); - - // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. - updatePrivacyIcons(true); - - } - - private void loadUrlFromTextBox() throws UnsupportedEncodingException { - // Get the text from urlTextBox and convert it to a string. trim() removes white spaces from the beginning and end of the string. - String unformattedUrlString = urlTextBox.getText().toString().trim(); - - // Check to see if `unformattedUrlString` is a valid URL. Otherwise, convert it into a search. - if ((Patterns.WEB_URL.matcher(unformattedUrlString).matches()) || (unformattedUrlString.contains("localhost"))) { - // Add `http://` at the beginning if it is missing. Otherwise the app will segfault. - if (!unformattedUrlString.startsWith("http")) { - unformattedUrlString = "http://" + unformattedUrlString; - } - - // Initialize `unformattedUrl`. - URL unformattedUrl = null; + // Initialize `unformattedUrl`. + URL unformattedUrl = null; // Convert `unformattedUrlString` to a `URL`, then to a `URI`, and then back to a `String`, which sanitizes the input and adds in any missing components. try { @@ -1862,12 +2380,8 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Sanitize the search input and convert it to a search. final String encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8"); - // Use the correct search URL. - if (javaScriptEnabled) { // JavaScript is enabled. - formattedUrlString = javaScriptEnabledSearchURL + encodedUrlString; - } else { // JavaScript is disabled. - formattedUrlString = javaScriptDisabledSearchURL + encodedUrlString; - } + // Add the base search URL. + formattedUrlString = searchURL + encodedUrlString; } loadUrl(formattedUrlString); @@ -1883,6 +2397,193 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Load the URL. mainWebView.loadUrl(url, customHeaders); + + // Set `urlIsLoading` to prevent changes in the user agent on websites with redirects from reloading the current website. + urlIsLoading = true; + } + + public void findPreviousOnPage(View view) { + // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards. + mainWebView.findNext(false); + } + + public void findNextOnPage(View view) { + // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards. + mainWebView.findNext(true); + } + + public void closeFindOnPage(View view) { + // Delete the contents of `find_on_page_edittext`. + findOnPageEditText.setText(null); + + // Clear the highlighted phrases. + mainWebView.clearMatches(); + + // Hide the Find on Page `RelativeLayout`. + findOnPageLinearLayout.setVisibility(View.GONE); + + // Show the URL app bar. + supportAppBar.setVisibility(View.VISIBLE); + + // Hide the keyboard so we can see the webpage. `0` indicates no additional flags. + inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); + } + + private void applyAppSettings() { + // Get a handle for `sharedPreferences`. `this` references the current context. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Store the values from `sharedPreferences` in variables. + String homepageString = sharedPreferences.getString("homepage", "https://start.duckduckgo.com"); + String torHomepageString = sharedPreferences.getString("tor_homepage", "https://3g2upl4pq6kufc4m.onion"); + String torSearchString = sharedPreferences.getString("tor_search", "https://3g2upl4pq6kufc4m.onion/html/?q="); + String torSearchCustomURLString = sharedPreferences.getString("tor_search_custom_url", ""); + String searchString = sharedPreferences.getString("search", "https://duckduckgo.com/html/?q="); + String searchCustomURLString = sharedPreferences.getString("search_custom_url", ""); + adBlockerEnabled = sharedPreferences.getBoolean("block_ads", true); + incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false); + boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false); + boolean proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false); + fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("full_screen_browsing_mode", false); + hideSystemBarsOnFullscreen = sharedPreferences.getBoolean("hide_system_bars", false); + translucentNavigationBarOnFullscreen = sharedPreferences.getBoolean("translucent_navigation_bar", true); + swipeToRefreshEnabled = sharedPreferences.getBoolean("swipe_to_refresh", false); + displayWebpageImagesBoolean = sharedPreferences.getBoolean("display_webpage_images", true); + + // Set the homepage, search, and proxy options. + if (proxyThroughOrbot) { // Set the Tor options. + // Set `torHomepageString` as `homepage`. + homepage = torHomepageString; + + // If formattedUrlString is null assign the homepage to it. + if (formattedUrlString == null) { + formattedUrlString = homepage; + } + + // Set the search URL. + if (torSearchString.equals("Custom URL")) { // Get the custom URL string. + searchURL = torSearchCustomURLString; + } else { // Use the string from the pre-built list. + searchURL = torSearchString; + } + + // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed. + OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118"); + + // Set the `appBar` background to indicate proxying through Orbot is enabled. `this` refers to the context. + if (darkTheme) { + appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.dark_blue_30)); + } else { + appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.blue_50)); + } + + // Display a message to the user if we are waiting on Orbot. + if (!orbotStatus.equals("ON")) { + // Set `waitingForOrbot`. + waitingForOrbot = true; + + // Load a waiting page. `null` specifies no encoding, which defaults to ASCII. + mainWebView.loadData(waitingForOrbotHTMLString, "text/html", null); + } + } else { // Set the non-Tor options. + // Set `homepageString` as `homepage`. + homepage = homepageString; + + // If formattedUrlString is null assign the homepage to it. + if (formattedUrlString == null) { + formattedUrlString = homepage; + } + + // Set the search URL. + if (searchString.equals("Custom URL")) { // Get the custom URL string. + searchURL = searchCustomURLString; + } else { // Use the string from the pre-built list. + searchURL = searchString; + } + + // Reset the proxy to default. The host is `""` and the port is `"0"`. + OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0"); + + // Set the default `appBar` background. `this` refers to the context. + if (darkTheme) { + appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_900)); + } else { + appBar.setBackgroundDrawable(ContextCompat.getDrawable(this, R.color.gray_100)); + } + + // Reset `waitingForOrbot. + waitingForOrbot = false; + } + + // Set swipe to refresh. + swipeRefreshLayout.setEnabled(swipeToRefreshEnabled); + + // Set Do Not Track status. + if (doNotTrackEnabled) { + customHeaders.put("DNT", "1"); + } else { + customHeaders.remove("DNT"); + } + + // Apply the appropriate full screen mode the `SYSTEM_UI` flags. + if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { + if (hideSystemBarsOnFullscreen) { // Hide everything. + // Remove the translucent navigation setting if it is currently flagged. + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + + // Remove the translucent status bar overlay. + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + + // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command. + drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + + /* SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. + * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. + * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically rehides them after they are shown. + */ + rootCoordinatorLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } else { // Hide everything except the status and navigation bars. + // Add the translucent status flag if it is unset. + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + + if (translucentNavigationBarOnFullscreen) { + // Set the navigation bar to be translucent. + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + } else { + // Set the navigation bar to be black. + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + } + } + } else { // Switch to normal viewing mode. + // Reset `inFullScreenBrowsingMode` to `false`. + inFullScreenBrowsingMode = false; + + // Show the `appBar` if `findOnPageLinearLayout` is not visible. + if (findOnPageLinearLayout.getVisibility() == View.GONE) { + appBar.show(); + } + + // Show the `BannerAd` in the free flavor. + if (BuildConfig.FLAVOR.contentEquals("free")) { + // Reload the ad. Because the screen may have rotated, we need to use `reloadAfterRotate`. + BannerAd.reloadAfterRotate(adView, getApplicationContext(), getString(R.string.ad_id)); + + // Reinitialize the `adView` variable, as the `View` will have been removed and re-added by `BannerAd.reloadAfterRotate()`. + adView = findViewById(R.id.adview); + } + + // Remove the translucent navigation bar flag if it is set. + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + + // Add the translucent status flag if it is unset. This also resets `drawerLayout's` `View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN`. + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + + // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`. + rootCoordinatorLayout.setSystemUiVisibility(0); + + // Constrain `rootCoordinatorLayout` inside the status and navigation bars. + rootCoordinatorLayout.setFitsSystemWindows(true); + } } // We have to use the deprecated `.getDrawable()` until the minimum API >= 21. @@ -1914,6 +2615,9 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Set the new `hostname` as the `currentDomainName`. currentDomainName = hostName; + // Reset `ignorePinnedSslCertificate`. + ignorePinnedSslCertificate = false; + // Reset `favoriteIconBitmap` and display it in the `appbar`. favoriteIconBitmap = favoriteIconDefaultBitmap; favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true)); @@ -1943,21 +2647,21 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation // Close `domainNameCursor. domainNameCursor.close(); - // Initialize variables to track if this domain has stored domain settings, and if so, under which name. - boolean hostHasDomainSettings = false; + // Initialize variables to track if domain settings will be applied and, if so, under which name. + domainSettingsApplied = false; String domainNameInDatabase = null; // Check the hostname. if (domainSettingsSet.contains(hostName)) { - hostHasDomainSettings = true; + domainSettingsApplied = true; domainNameInDatabase = hostName; } // If `hostName` is not `null`, check all the subdomains of `hostName` against wildcard domains in `domainCursor`. if (hostName != null) { - while (hostName.contains(".") && !hostHasDomainSettings) { // Stop checking if we run out of `.` or if we already know that `hostHasDomainSettings` is `true`. + while (hostName.contains(".") && !domainSettingsApplied) { // Stop checking if we run out of `.` or if we already know that `domainSettingsApplied` is `true`. if (domainSettingsSet.contains("*." + hostName)) { // Check the host name prepended by `*.`. - hostHasDomainSettings = true; + domainSettingsApplied = true; domainNameInDatabase = "*." + hostName; } @@ -1966,19 +2670,68 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation } } - if (hostHasDomainSettings) { // The url we are loading has custom domain settings. + // Get a handle for the shared preference. `this` references the current context. + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Store the general preference information. + String defaultFontSizeString = sharedPreferences.getString("default_font_size", "100"); + String defaultUserAgentString = sharedPreferences.getString("user_agent", "PrivacyBrowser/1.0"); + String defaultCustomUserAgentString = sharedPreferences.getString("custom_user_agent", "PrivacyBrowser/1.0"); + nightMode = sharedPreferences.getBoolean("night_mode", false); + + if (domainSettingsApplied) { // The url we are loading has custom domain settings. // Get a cursor for the current host and move it to the first position. Cursor currentHostDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase); currentHostDomainSettingsCursor.moveToFirst(); // Get the settings from the cursor. + domainSettingsDatabaseId = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID))); javaScriptEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1); firstPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1); thirdPartyCookiesEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1); domStorageEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1); saveFormDataEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1); - String userAgentString = (currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT))); - int fontSize = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE))); + String userAgentString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT)); + int fontSize = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE)); + displayWebpageImagesInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES)); + int nightModeInt = currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE)); + pinnedDomainSslCertificate = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1); + pinnedDomainSslIssuedToCNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME)); + pinnedDomainSslIssuedToONameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION)); + pinnedDomainSslIssuedToUNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT)); + pinnedDomainSslIssuedByCNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME)); + pinnedDomainSslIssuedByONameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION)); + pinnedDomainSslIssuedByUNameString = currentHostDomainSettingsCursor.getString(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT)); + + // Set `nightMode` according to `nightModeInt`. If `nightModeInt` is `DomainsDatabaseHelper.NIGHT_MODE_SYSTEM_DEFAULT` the current setting from `sharedPreferences` will be used. + switch (nightModeInt) { + case DomainsDatabaseHelper.NIGHT_MODE_ENABLED: + nightMode = true; + break; + + case DomainsDatabaseHelper.NIGHT_MODE_DISABLED: + nightMode = false; + break; + } + + // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`. + if (nightMode) { + javaScriptEnabled = true; + } + + // Set the pinned SSL certificate start date to `null` if the saved date `long` is 0. + if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) { + pinnedDomainSslStartDate = null; + } else { + pinnedDomainSslStartDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE))); + } + + // Set the pinned SSL certificate end date to `null` if the saved date `long` is 0. + if (currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) { + pinnedDomainSslEndDate = null; + } else { + pinnedDomainSslEndDate = new Date(currentHostDomainSettingsCursor.getLong(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE))); + } // Close `currentHostDomainSettingsCursor`. currentHostDomainSettingsCursor.close(); @@ -1988,37 +2741,70 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation cookieManager.setAcceptCookie(firstPartyCookiesEnabled); mainWebView.getSettings().setDomStorageEnabled(domStorageEnabled); mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); - mainWebView.getSettings().setTextZoom(fontSize); + + // Apply the font size. + if (fontSize == 0) { // Apply the default font size. + mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString)); + } else { // Apply the specified font size. + mainWebView.getSettings().setTextZoom(fontSize); + } // Set third-party cookies status if API >= 21. if (Build.VERSION.SDK_INT >= 21) { cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled); } - // Set the user agent. - if (userAgentString.equals("WebView default user agent")) { - // Set the user agent to `""`, which uses the default value. - mainWebView.getSettings().setUserAgentString(""); - } else { - // Use the selected user agent. - mainWebView.getSettings().setUserAgentString(userAgentString); + // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload. + if (!urlIsLoading) { + switch (userAgentString) { + case "System default user agent": + // Set the user agent according to the system default. + switch (defaultUserAgentString) { + case "WebView default user agent": + // Set the user agent to `""`, which uses the default value. + mainWebView.getSettings().setUserAgentString(""); + break; + + case "Custom user agent": + // Set the custom user agent. + mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString); + break; + + default: + // Use the selected user agent. + mainWebView.getSettings().setUserAgentString(defaultUserAgentString); + } + break; + + case "WebView default user agent": + // Set the user agent to `""`, which uses the default value. + mainWebView.getSettings().setUserAgentString(""); + break; + + default: + // Use the selected user agent. + mainWebView.getSettings().setUserAgentString(userAgentString); + } } // Set a green background on `urlTextBox` to indicate that custom domain settings are being used. We have to use the deprecated `.getDrawable()` until the minimum API >= 21. - urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_green)); + if (darkTheme) { + urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue)); + } else { + urlAppBarRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green)); + } } else { // The URL we are loading does not have custom domain settings. Load the defaults. - // Get the shared preference values. `this` references the current context. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - // Store the values from `sharedPreferences` in variables. javaScriptEnabled = sharedPreferences.getBoolean("javascript_enabled", false); firstPartyCookiesEnabled = sharedPreferences.getBoolean("first_party_cookies_enabled", false); thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies_enabled", false); domStorageEnabled = sharedPreferences.getBoolean("dom_storage_enabled", false); saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data_enabled", false); - String userAgentString = sharedPreferences.getString("user_agent", "PrivacyBrowser/1.0"); - String customUserAgentString = sharedPreferences.getString("custom_user_agent", "PrivacyBrowser/1.0"); - String defaultFontSizeString = sharedPreferences.getString("default_font_size", "100"); + + // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`. + if (nightMode) { + javaScriptEnabled = true; + } // Apply the default settings. mainWebView.getSettings().setJavaScriptEnabled(javaScriptEnabled); @@ -2027,36 +2813,53 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation mainWebView.getSettings().setSaveFormData(saveFormDataEnabled); mainWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString)); + // Reset the pinned SSL certificate information. + domainSettingsDatabaseId = -1; + pinnedDomainSslCertificate = false; + pinnedDomainSslIssuedToCNameString = ""; + pinnedDomainSslIssuedToONameString = ""; + pinnedDomainSslIssuedToUNameString = ""; + pinnedDomainSslIssuedByCNameString = ""; + pinnedDomainSslIssuedByONameString = ""; + pinnedDomainSslIssuedByUNameString = ""; + pinnedDomainSslStartDate = null; + pinnedDomainSslEndDate = null; + // Set third-party cookies status if API >= 21. if (Build.VERSION.SDK_INT >= 21) { cookieManager.setAcceptThirdPartyCookies(mainWebView, thirdPartyCookiesEnabled); } - // Set the default user agent. - switch (userAgentString) { - case "WebView default user agent": - // Set the user agent to `""`, which uses the default value. - mainWebView.getSettings().setUserAgentString(""); - break; - - case "Custom user agent": - // Set the custom user agent. - mainWebView.getSettings().setUserAgentString(customUserAgentString); - break; - - default: - // Use the selected user agent. - mainWebView.getSettings().setUserAgentString(userAgentString); - break; + // Only set the user agent if the webpage is not currently loading. Otherwise, changing the user agent on redirects can cause the original website to reload. + if (!urlIsLoading) { + switch (defaultUserAgentString) { + case "WebView default user agent": + // Set the user agent to `""`, which uses the default value. + mainWebView.getSettings().setUserAgentString(""); + break; + + case "Custom user agent": + // Set the custom user agent. + mainWebView.getSettings().setUserAgentString(defaultCustomUserAgentString); + break; + + default: + // Use the selected user agent. + mainWebView.getSettings().setUserAgentString(defaultUserAgentString); + } } // Set a transparent background on `urlTextBox`. We have to use the deprecated `.getDrawable()` until the minimum API >= 21. - urlAppBarRelativeLayout.setBackgroundDrawable(getResources().getDrawable(R.drawable.url_bar_background_transparent)); + urlAppBarRelativeLayout.setBackgroundDrawable(getResources().getDrawable(R.color.transparent)); } // Close `domainsDatabaseHelper`. domainsDatabaseHelper.close(); + // Remove the `onTheFlyDisplayImagesSet` flag and set the display webpage images mode. `true` indicates that custom domain settings are applied. + onTheFlyDisplayImagesSet = false; + setDisplayWebpageImages(); + // Update the privacy icons, but only if `mainMenu` has already been populated. if (mainMenu != null) { updatePrivacyIcons(true); @@ -2064,230 +2867,81 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation } } - public void findPreviousOnPage(View view) { - // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards. - mainWebView.findNext(false); - } - - public void findNextOnPage(View view) { - // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards. - mainWebView.findNext(true); - } - - public void closeFindOnPage(View view) { - // Delete the contents of `find_on_page_edittext`. - findOnPageEditText.setText(null); - - // Clear the highlighted phrases. - mainWebView.clearMatches(); - - // Hide the Find on Page `RelativeLayout`. - findOnPageLinearLayout.setVisibility(View.GONE); - - // Show the URL app bar. - supportAppBar.setVisibility(View.VISIBLE); - - // Hide the keyboard so we can see the webpage. `0` indicates no additional flags. - inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0); - } - - private void applyAppSettings() { - // Get the shared preference values. `this` references the current context. - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - - // Store the values from `sharedPreferences` in variables. - String javaScriptDisabledSearchString = sharedPreferences.getString("javascript_disabled_search", "https://duckduckgo.com/html/?q="); - String javaScriptDisabledSearchCustomURLString = sharedPreferences.getString("javascript_disabled_search_custom_url", ""); - String javaScriptEnabledSearchString = sharedPreferences.getString("javascript_enabled_search", "https://duckduckgo.com/?q="); - String javaScriptEnabledSearchCustomURLString = sharedPreferences.getString("javascript_enabled_search_custom_url", ""); - String homepageString = sharedPreferences.getString("homepage", "https://duckduckgo.com"); - String torHomepageString = sharedPreferences.getString("tor_homepage", "https://3g2upl4pq6kufc4m.onion"); - String torJavaScriptDisabledSearchString = sharedPreferences.getString("tor_javascript_disabled_search", "https://3g2upl4pq6kufc4m.onion/html/?q="); - String torJavaScriptDisabledSearchCustomURLString = sharedPreferences.getString("tor_javascript_disabled_search_custom_url", ""); - String torJavaScriptEnabledSearchString = sharedPreferences.getString("tor_javascript_enabled_search", "https://3g2upl4pq6kufc4m.onion/?q="); - String torJavaScriptEnabledSearchCustomURLString = sharedPreferences.getString("tor_javascript_enabled_search_custom_url", ""); - swipeToRefreshEnabled = sharedPreferences.getBoolean("swipe_to_refresh_enabled", false); - adBlockerEnabled = sharedPreferences.getBoolean("block_ads", true); - boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false); - proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false); - fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean("enable_full_screen_browsing_mode", false); - hideSystemBarsOnFullscreen = sharedPreferences.getBoolean("hide_system_bars", false); - translucentNavigationBarOnFullscreen = sharedPreferences.getBoolean("translucent_navigation_bar", true); - - // Set the homepage, search, and proxy options. - if (proxyThroughOrbot) { // Set the Tor options. - // Set `torHomepageString` as `homepage`. - homepage = torHomepageString; - - // If formattedUrlString is null assign the homepage to it. - if (formattedUrlString == null) { - formattedUrlString = homepage; - } - - // Set JavaScript disabled search. - if (torJavaScriptDisabledSearchString.equals("Custom URL")) { // Get the custom URL string. - javaScriptDisabledSearchURL = torJavaScriptDisabledSearchCustomURLString; - } else { // Use the string from the pre-built list. - javaScriptDisabledSearchURL = torJavaScriptDisabledSearchString; - } - - // Set JavaScript enabled search. - if (torJavaScriptEnabledSearchString.equals("Custom URL")) { // Get the custom URL string. - javaScriptEnabledSearchURL = torJavaScriptEnabledSearchCustomURLString; - } else { // Use the string from the pre-built list. - javaScriptEnabledSearchURL = torJavaScriptEnabledSearchString; - } - - // Set the proxy. `this` refers to the current activity where an `AlertDialog` might be displayed. - OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118"); - - // Display a message to the user if we are waiting on Orbot. - if (!orbotStatus.equals("ON")) { - // Save `formattedUrlString` in `pendingUrl`. - pendingUrl = formattedUrlString; - - // Load a waiting page. `null` specifies no encoding, which defaults to ASCII. - mainWebView.loadData(waitingForOrbotHTMLString, "text/html", null); - } - } else { // Set the non-Tor options. - // Set `homepageString` as `homepage`. - homepage = homepageString; - - // If formattedUrlString is null assign the homepage to it. - if (formattedUrlString == null) { - formattedUrlString = homepage; - } - - // Set JavaScript disabled search. - if (javaScriptDisabledSearchString.equals("Custom URL")) { // Get the custom URL string. - javaScriptDisabledSearchURL = javaScriptDisabledSearchCustomURLString; - } else { // Use the string from the pre-built list. - javaScriptDisabledSearchURL = javaScriptDisabledSearchString; - } - - // Set JavaScript enabled search. - if (javaScriptEnabledSearchString.equals("Custom URL")) { // Get the custom URL string. - javaScriptEnabledSearchURL = javaScriptEnabledSearchCustomURLString; - } else { // Use the string from the pre-built list. - javaScriptEnabledSearchURL = javaScriptEnabledSearchString; - } - - // Reset the proxy to default. The host is `""` and the port is `"0"`. - OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0"); - - // Reset `pendingUrl` if we are currently waiting for Orbot to connect. - if (!pendingUrl.isEmpty()) { - formattedUrlString = pendingUrl; - pendingUrl = ""; - } - } - - // Set swipe to refresh. - swipeRefreshLayout.setEnabled(swipeToRefreshEnabled); - - // Set Do Not Track status. - if (doNotTrackEnabled) { - customHeaders.put("DNT", "1"); - } else { - customHeaders.remove("DNT"); - } - - // Apply the appropriate full screen mode the `SYSTEM_UI` flags. - if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { - if (hideSystemBarsOnFullscreen) { // Hide everything. - // Remove the translucent navigation setting if it is currently flagged. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); - - // Remove the translucent status bar overlay. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - - // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command. - drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + private void setDisplayWebpageImages() { + if (!onTheFlyDisplayImagesSet) { + if (domainSettingsApplied) { // Custom domain settings are applied. + switch (displayWebpageImagesInt) { + case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_SYSTEM_DEFAULT: + mainWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImagesBoolean); + break; - /* SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen. - * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen. - * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically rehides them after they are shown. - */ - rootCoordinatorLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - } else { // Hide everything except the status and navigation bars. - // Add the translucent status flag if it is unset. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_ENABLED: + mainWebView.getSettings().setLoadsImagesAutomatically(true); + break; - if (translucentNavigationBarOnFullscreen) { - // Set the navigation bar to be translucent. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); - } else { - // Set the navigation bar to be black. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + case DomainsDatabaseHelper.DISPLAY_WEBPAGE_IMAGES_DISABLED: + mainWebView.getSettings().setLoadsImagesAutomatically(false); + break; } + } else { // Default settings are applied. + mainWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImagesBoolean); } - } else { // Switch to normal viewing mode. - // Reset `inFullScreenBrowsingMode` to `false`. - inFullScreenBrowsingMode = false; - - // Show the `appBar`. - appBar.show(); - - // Show the `BannerAd` in the free flavor. - if (BuildConfig.FLAVOR.contentEquals("free")) { - // Reload the ad. Because the screen may have rotated, we need to use `reloadAfterRotate`. - BannerAd.reloadAfterRotate(adView, getApplicationContext(), getString(R.string.ad_id)); - - // Reinitialize the `adView` variable, as the `View` will have been removed and re-added by `BannerAd.reloadAfterRotate()`. - adView = findViewById(R.id.adview); - } - - // Remove the translucent navigation bar flag if it is set. - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); - - // Add the translucent status flag if it is unset. This also resets `drawerLayout's` `View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN`. - getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - - // Remove any `SYSTEM_UI` flags from `rootCoordinatorLayout`. - rootCoordinatorLayout.setSystemUiVisibility(0); - - // Constrain `rootCoordinatorLayout` inside the status and navigation bars. - rootCoordinatorLayout.setFitsSystemWindows(true); } } private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) { // Get handles for the icons. - MenuItem privacyIcon = mainMenu.findItem(R.id.toggleJavaScript); - MenuItem firstPartyCookiesIcon = mainMenu.findItem(R.id.toggleFirstPartyCookies); - MenuItem domStorageIcon = mainMenu.findItem(R.id.toggleDomStorage); - MenuItem formDataIcon = mainMenu.findItem(R.id.toggleSaveFormData); + MenuItem privacyIconMenuItem = mainMenu.findItem(R.id.toggle_javascript); + MenuItem firstPartyCookiesIconMenuItem = mainMenu.findItem(R.id.toggle_first_party_cookies); + MenuItem domStorageIconMenuItem = mainMenu.findItem(R.id.toggle_dom_storage); + MenuItem formDataIconMenuItem = mainMenu.findItem(R.id.toggle_save_form_data); // Update `privacyIcon`. if (javaScriptEnabled) { // JavaScript is enabled. - privacyIcon.setIcon(R.drawable.javascript_enabled); + privacyIconMenuItem.setIcon(R.drawable.javascript_enabled); } else if (firstPartyCookiesEnabled) { // JavaScript is disabled but cookies are enabled. - privacyIcon.setIcon(R.drawable.warning); + privacyIconMenuItem.setIcon(R.drawable.warning); } else { // All the dangerous features are disabled. - privacyIcon.setIcon(R.drawable.privacy_mode); + privacyIconMenuItem.setIcon(R.drawable.privacy_mode); } // Update `firstPartyCookiesIcon`. if (firstPartyCookiesEnabled) { // First-party cookies are enabled. - firstPartyCookiesIcon.setIcon(R.drawable.cookies_enabled); + firstPartyCookiesIconMenuItem.setIcon(R.drawable.cookies_enabled); } else { // First-party cookies are disabled. - firstPartyCookiesIcon.setIcon(R.drawable.cookies_disabled); + if (darkTheme) { + firstPartyCookiesIconMenuItem.setIcon(R.drawable.cookies_disabled_dark); + } else { + firstPartyCookiesIconMenuItem.setIcon(R.drawable.cookies_disabled_light); + } } // Update `domStorageIcon`. if (javaScriptEnabled && domStorageEnabled) { // Both JavaScript and DOM storage are enabled. - domStorageIcon.setIcon(R.drawable.dom_storage_enabled); + domStorageIconMenuItem.setIcon(R.drawable.dom_storage_enabled); } else if (javaScriptEnabled) { // JavaScript is enabled but DOM storage is disabled. - domStorageIcon.setIcon(R.drawable.dom_storage_disabled); + if (darkTheme) { + domStorageIconMenuItem.setIcon(R.drawable.dom_storage_disabled_dark); + } else { + domStorageIconMenuItem.setIcon(R.drawable.dom_storage_disabled_light); + } } else { // JavaScript is disabled, so DOM storage is ghosted. - domStorageIcon.setIcon(R.drawable.dom_storage_ghosted); + if (darkTheme) { + domStorageIconMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark); + } else { + domStorageIconMenuItem.setIcon(R.drawable.dom_storage_ghosted_light); + } } // Update `formDataIcon`. if (saveFormDataEnabled) { // Form data is enabled. - formDataIcon.setIcon(R.drawable.form_data_enabled); + formDataIconMenuItem.setIcon(R.drawable.form_data_enabled); } else { // Form data is disabled. - formDataIcon.setIcon(R.drawable.form_data_disabled); + if (darkTheme) { + formDataIconMenuItem.setIcon(R.drawable.form_data_disabled_dark); + } else { + formDataIconMenuItem.setIcon(R.drawable.form_data_disabled_light); + } } // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`. `this` references the current activity. @@ -2295,4 +2949,22 @@ public class MainWebViewActivity extends AppCompatActivity implements Navigation ActivityCompat.invalidateOptionsMenu(this); } } + + private void highlightUrlText() { + String urlString = urlTextBox.getText().toString(); + + if (urlString.startsWith("http://")) { // Highlight connections that are not encrypted. + urlTextBox.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } else if (urlString.startsWith("https://")) { // Highlight connections that are encrypted. + urlTextBox.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + + // Get the index of the `/` immediately after the domain name. + int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2)); + + // De-emphasize the text after the domain name. + if (endOfDomainName > 0) { + urlTextBox.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + } }