Wait for Orbot to connect when proxying though Orbot before trying to load a URL.
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebView.java
index e5296732f487000d0e380aac68531241b9d3ec12..eb6392b2ad97d8fda4f0aac8ccbfeb8567cede16 100644 (file)
@@ -1,5 +1,7 @@
 /**
- * Copyright 2015-2016 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2015-2017 Soren Stoutner <soren@stoutner.com>.
+ *
+ * Download cookie code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
  *
  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
  *
@@ -22,10 +24,12 @@ package com.stoutner.privacybrowser.activities;
 import android.annotation.SuppressLint;
 import android.app.DialogFragment;
 import android.app.DownloadManager;
+import android.content.BroadcastReceiver;
 import android.content.ClipData;
 import android.content.ClipboardManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
@@ -70,6 +74,7 @@ import android.webkit.DownloadListener;
 import android.webkit.SslErrorHandler;
 import android.webkit.WebBackForwardList;
 import android.webkit.WebChromeClient;
+import android.webkit.WebResourceResponse;
 import android.webkit.WebStorage;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
@@ -93,19 +98,25 @@ import com.stoutner.privacybrowser.dialogs.SslCertificateError;
 import com.stoutner.privacybrowser.dialogs.UrlHistory;
 import com.stoutner.privacybrowser.dialogs.ViewSslCertificate;
 
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
 import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLEncoder;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+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 MainWebView extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, CreateHomeScreenShortcut.CreateHomeScreenSchortcutListener,
         SslCertificateError.SslCertificateErrorListener, DownloadFile.DownloadFileListener, DownloadImage.DownloadImageListener, UrlHistory.UrlHistoryListener {
 
     // `appBar` is public static so it can be accessed from `OrbotProxyHelper`.
-    // It is also used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`.
+    // It is also used in `onCreate()`, `onOptionsItemSelected()`, `closeFindOnPage()`, and `applySettings()`.
     public static ActionBar appBar;
 
     // `favoriteIcon` is public static so it can be accessed from `CreateHomeScreenShortcut`, `Bookmarks`, `CreateBookmark`, `CreateBookmarkFolder`, and `EditBookmark`.
@@ -119,6 +130,8 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
     // `sslCertificate` is public static so it can be accessed from `ViewSslCertificate`.  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()`.
+    public static String orbotStatus;
 
 
     // `drawerLayout` is used in `onCreate()`, `onNewIntent()`, and `onBackPressed()`.
@@ -130,16 +143,13 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
     // 'mainWebView' is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`, `findNextOnPage()`, `closeFindOnPage()`, and `loadUrlFromTextBox()`.
     private WebView mainWebView;
 
-    // `findOnPageEditText` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`.
-    private EditText findOnPageEditText;
-
     // `fullScreenVideoFrameLayout` is used in `onCreate()` and `onConfigurationChanged()`.
     private FrameLayout fullScreenVideoFrameLayout;
 
     // `swipeRefreshLayout` is used in `onCreate()`, `onPrepareOptionsMenu`, and `onRestart()`.
     private SwipeRefreshLayout swipeRefreshLayout;
 
-    // `cookieManager` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`, and `onRestart()`.
+    // `cookieManager` is used in `onCreate()`, `onOptionsItemSelected()`, and `onNavigationItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `onRestart()`.
     private CookieManager cookieManager;
 
     // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrlFromTextBox()`.
@@ -149,7 +159,7 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
     // It is `Boolean` instead of `boolean` because `applySettings()` needs to know if it is `null`.
     private Boolean javaScriptEnabled;
 
-    // `firstPartyCookiesEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applySettings()`.
+    // `firstPartyCookiesEnabled` is used in `onCreate()`, `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onDownloadImage()`, `onDownloadFile()`, and `applySettings()`.
     private boolean firstPartyCookiesEnabled;
 
     // `thirdPartyCookiesEnabled` used in `onCreate()`, `onCreateOptionsMenu()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, and `applySettings()`.
@@ -173,21 +183,45 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
     // `javaScriptEnabledSearchURL` is used in `loadURLFromTextBox()` and `applySettings()`.
     private String javaScriptEnabledSearchURL;
 
+    // `adBlockerEnabled` is used in `onCreate()` and `applySettings()`.
+    private boolean adBlockerEnabled;
+
     // `fullScreenBrowsingModeEnabled` is used in `onCreate()` and `applySettings()`.
     private boolean fullScreenBrowsingModeEnabled;
 
+    // `inFullScreenBrowsingMode` is used in `onCreate()` and `applySettings()`.
+    private boolean inFullScreenBrowsingMode;
+
     // `hideSystemBarsOnFullscreen` is used in `onCreate()` and `applySettings()`.
     private boolean hideSystemBarsOnFullscreen;
 
     // `translucentNavigationBarOnFullscreen` is used in `onCreate()` and `applySettings()`.
     private boolean translucentNavigationBarOnFullscreen;
 
+    // `proxyThroughOrbot` is used in `onCreate()` and `applySettings()`
+    private boolean proxyThroughOrbot;
+
+    // `pendingUrl` is used in `onCreate()` and `applySettings()`
+    private static String pendingUrl;
+
+    // `waitingForOrbotData` is used in `onCreate()` and `applySettings()`.
+    private String waitingForOrbotHTMLString;
+
+    // `findOnPageLinearLayout` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`.
+    private LinearLayout findOnPageLinearLayout;
+
+    // `findOnPageEditText` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`.
+    private EditText findOnPageEditText;
+
     // `mainMenu` is used in `onCreateOptionsMenu()` and `updatePrivacyIcons()`.
     private Menu mainMenu;
 
     // `drawerToggle` is used in `onCreate()`, `onPostCreate()`, `onConfigurationChanged()`, `onNewIntent()`, and `onNavigationItemSelected()`.
     private ActionBarDrawerToggle drawerToggle;
 
+    // `supportAppBar` is used in `onCreate()`, `onOptionsItemSelected()`, and `closeFindOnPage()`.
+    private Toolbar supportAppBar;
+
     // `urlTextBox` is used in `onCreate()`, `onOptionsItemSelected()`, and `loadUrlFromTextBox()`.
     private EditText urlTextBox;
 
@@ -200,6 +234,9 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
     // `inputMethodManager` is used in `onOptionsItemSelected()`, `loadUrlFromTextBox()`, and `closeFindOnPage()`.
     private InputMethodManager inputMethodManager;
 
+    // `mainWebViewRelativeLayout` is used in `onCreate()` and `onNavigationItemSelected()`.
+    private RelativeLayout mainWebViewRelativeLayout;
+
     @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")
@@ -211,7 +248,7 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
         inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
 
         // We need to use the `SupportActionBar` from `android.support.v7.app.ActionBar` until the minimum API is >= 21.
-        Toolbar supportAppBar = (Toolbar) findViewById(R.id.app_bar);
+        supportAppBar = (Toolbar) findViewById(R.id.app_bar);
         setSupportActionBar(supportAppBar);
         appBar = getSupportActionBar();
 
@@ -244,10 +281,51 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
             }
         });
 
+        // Set `waitingForOrbotHTMLString`.
+        waitingForOrbotHTMLString = "<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>";
+
+        // Initialize `pendingUrl`.
+        pendingUrl = "";
+
+        // Set the initial Orbot status.
+        orbotStatus = "unknown";
+
+        // Create an Orbot status `BroadcastReceiver`.
+        BroadcastReceiver orbotStatusBroadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                // 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 = "";
+
+                    // Load `formattedUrlString
+                    mainWebView.loadUrl(formattedUrlString, customHeaders);
+                }
+            }
+        };
+
+        // Register `orbotStatusBroadcastReceiver` on `this` context.
+        this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
+
         // Get handles for views that need to be accessed.
         drawerLayout = (DrawerLayout) findViewById(R.id.drawerlayout);
         rootCoordinatorLayout = (CoordinatorLayout) findViewById(R.id.root_coordinatorlayout);
+        mainWebViewRelativeLayout = (RelativeLayout) findViewById(R.id.main_webview_relativelayout);
         mainWebView = (WebView) findViewById(R.id.mainWebView);
+        findOnPageLinearLayout = (LinearLayout) findViewById(R.id.find_on_page_linearlayout);
         findOnPageEditText = (EditText) findViewById(R.id.find_on_page_edittext);
         fullScreenVideoFrameLayout = (FrameLayout) findViewById(R.id.full_screen_video_framelayout);
 
@@ -257,7 +335,10 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
             @Override
             public boolean onDoubleTap(MotionEvent event) {
                 if (fullScreenBrowsingModeEnabled) {  // Only process the double-tap if full screen browsing mode is enabled.
-                    if (appBar.isShowing()) {  // If `appBar` is visible, switch to full screen mode.
+                    // Toggle `inFullScreenBrowsingMode`.
+                    inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
+
+                    if (inFullScreenBrowsingMode) {  // Switch to full screen mode.
                         // Hide the `appBar`.
                         appBar.hide();
 
@@ -274,10 +355,10 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
                             // 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.
-                         */
+                            /* 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);
 
                             // Set `rootCoordinatorLayout` to fill the whole screen.
@@ -442,6 +523,28 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
         // drawerToggle creates the hamburger icon at the start of the AppBar.
         drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, supportAppBar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
 
+        // Initialize `adServerSet`.
+        final Set<String> adServersSet = new HashSet<>();
+
+        // Load the list of ad servers into memory.
+        try {
+            // Load `pgl.yoyo.org_adservers.txt` into a `BufferedReader`.
+            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(getAssets().open("pgl.yoyo.org_adservers.txt")));
+
+            // Create a string for storing each ad server.
+            String adServer;
+
+            // Populate `adServersSet`.
+            while ((adServer = bufferedReader.readLine()) != null) {
+                adServersSet.add(adServer);
+            }
+
+            // Close `bufferedReader`.
+            bufferedReader.close();
+        } catch (IOException ioException) {
+            // We're pretty sure the asset exists, so we don't need to worry about the `IOException` ever being thrown.
+        }
+
         mainWebView.setWebViewClient(new WebViewClient() {
             // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
             // We have to use the deprecated `shouldOverrideUrlLoading` until API >= 24.
@@ -468,28 +571,71 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
                 }
             }
 
+            // Block ads.  We have to use the deprecated `shouldInterceptRequest` until minimum API >= 21.
+            @SuppressWarnings("deprecation")
+            @Override
+            public WebResourceResponse shouldInterceptRequest(WebView view, String url){
+                if (adBlockerEnabled) {  // Block ads.
+                    // Extract the host from `url`.
+                    Uri requestUri = Uri.parse(url);
+                    String requestHost = requestUri.getHost();
+
+                    // Create a variable to track if this is an ad server.
+                    boolean requestHostIsAdServer = false;
+
+                    // Check all the subdomains of `requestHost` if it is not `null`.
+                    if (requestHost != null) {
+                        while (requestHost.contains(".")) {
+                            if (adServersSet.contains(requestHost)) {
+                                requestHostIsAdServer = true;
+                            }
+
+                            // Strip out the lowest subdomain of `requestHost`.
+                            requestHost = requestHost.substring(requestHost.indexOf(".") + 1);
+                        }
+                    }
+
+                    if (requestHostIsAdServer) {  // It is an ad server.
+                        // Return an empty `WebResourceResponse`.
+                        return new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
+                    } else {  // It is not an ad server.
+                        // `return null` loads the requested resource.
+                        return null;
+                    }
+                } else {  // Ad blocking is disabled.
+                    // `return null` loads the requested resource.
+                    return null;
+                }
+            }
+
             // Update the URL in urlTextBox when the page starts to load.
             @Override
             public void onPageStarted(WebView view, String url, Bitmap favicon) {
-                // 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;
+                // 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.
+                    // 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 loading URL is the URL text box.
+                    urlTextBox.setText(url);
+                }
             }
 
             // Update formattedUrlString and urlTextBox.  It is necessary to do this after the page finishes loading because the final URL can change during load.
             @Override
             public void onPageFinished(WebView view, String url) {
-                formattedUrlString = 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.
+                    formattedUrlString = url;
 
-                // Only update urlTextBox if the user is not typing in it.
-                if (!urlTextBox.hasFocus()) {
-                    urlTextBox.setText(formattedUrlString);
-                }
+                    // Only update urlTextBox if the user is not typing in it.
+                    if (!urlTextBox.hasFocus()) {
+                        urlTextBox.setText(formattedUrlString);
+                    }
 
-                // Store the SSL certificate so it can be accessed from `ViewSslCertificate`.
-                sslCertificate = mainWebView.getCertificate();
+                    // Store the SSL certificate so it can be accessed from `ViewSslCertificate`.
+                    sslCertificate = mainWebView.getCertificate();
+                }
             }
 
             // Handle SSL Certificate errors.
@@ -610,9 +756,6 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
         // Initialize the default preference values the first time the program is run.  `this` is the context.  `false` keeps this command from resetting any current preferences back to default.
         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
 
-        // Apply the settings from the shared preferences.
-        applySettings();
-
         // Get the intent information that started the app.
         final Intent intent = getIntent();
 
@@ -622,13 +765,13 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
             formattedUrlString = intentUriData.toString();
         }
 
-        // If formattedUrlString is null assign the homepage to it.
-        if (formattedUrlString == null) {
-            formattedUrlString = homepage;
-        }
+        // Apply the settings from the shared preferences.
+        applySettings();
 
-        // Load the initial website.
-        mainWebView.loadUrl(formattedUrlString, customHeaders);
+        // Load `formattedUrlString` if we are not proxying through Orbot and waiting for Orbot to connect.
+        if (!(proxyThroughOrbot & !orbotStatus.equals("ON"))) {
+            mainWebView.loadUrl(formattedUrlString, customHeaders);
+        }
 
         // If the favorite icon is null, load the default.
         if (favoriteIcon == null) {
@@ -638,6 +781,9 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
             favoriteIcon = favoriteIconBitmapDrawable.getBitmap();
         }
 
+        // Initialize `inFullScreenBrowsingMode`, which is always false at this point because Privacy Browser never starts in full screen browsing mode.
+        inFullScreenBrowsingMode = false;
+
         // Initialize AdView for the free flavor and request an ad.  If this is not the free flavor BannerAd.requestAd() does nothing.
         adView = findViewById(R.id.adView);
         BannerAd.requestAd(adView);
@@ -978,11 +1124,9 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
 
             case R.id.find_on_page:
                 // Hide the URL app bar.
-                Toolbar appBarToolbar = (Toolbar) findViewById(R.id.app_bar);
-                appBarToolbar.setVisibility(View.GONE);
+                supportAppBar.setVisibility(View.GONE);
 
                 // Show the Find on Page `RelativeLayout`.
-                LinearLayout findOnPageLinearLayout = (LinearLayout) findViewById(R.id.find_on_page_linearlayout);
                 findOnPageLinearLayout.setVisibility(View.VISIBLE);
 
                 // Display the keyboard.  We have to wait 200 ms before running the command to work around a bug in Android.
@@ -1092,6 +1236,14 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
                 startActivity(settingsIntent);
                 break;
 
+            /*
+            case R.id.domains:
+                // Launch `DomainsList`.
+                Intent domainsIntent = new Intent(this, DomainsList.class);
+                startActivity(domainsIntent);
+                break;
+            */
+
             case R.id.guide:
                 // Launch `Guide`.
                 Intent guideIntent = new Intent(this, Guide.class);
@@ -1136,12 +1288,20 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
                 customHeaders.clear();
 
                 // Detach all views from `mainWebViewRelativeLayout`.
-                RelativeLayout mainWebViewRelativeLayout = (RelativeLayout) findViewById(R.id.mainWebViewRelativeLayout);
                 mainWebViewRelativeLayout.removeAllViews();
 
                 // 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.
+                }
+
                 // Close Privacy Browser.  `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
                 if (Build.VERSION.SDK_INT >= 21) {
                     finishAndRemoveTask();
@@ -1391,66 +1551,95 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
 
     @Override
     public void onDownloadImage(AppCompatDialogFragment dialogFragment, String imageUrl) {
-        // Get a handle for the system `DOWNLOAD_SERVICE`.
-        DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
+        // Download the image if it has an HTTP or HTTPS URI.
+        if (imageUrl.startsWith("http")) {
+            // Get a handle for the system `DOWNLOAD_SERVICE`.
+            DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
 
-        // Parse `imageUrl`.
-        DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
+            // Parse `imageUrl`.
+            DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
 
-        // Get the file name from `dialogFragment`.
-        EditText downloadImageNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.download_image_name);
-        String imageName = downloadImageNameEditText.getText().toString();
+            // Pass cookies to download manager if cookies are enabled.  This is required to download images from websites that require a login.
+            if (firstPartyCookiesEnabled) {
+                // Get the cookies for `imageUrl`.
+                String cookies = cookieManager.getCookie(imageUrl);
 
-        // Once we have `WRITE_EXTERNAL_STORAGE` permissions we can use `setDestinationInExternalPublicDir`.
-        if (Build.VERSION.SDK_INT >= 23) { // If API >= 23, set the download save in the the `DIRECTORY_DOWNLOADS` using `imageName`.
-            downloadRequest.setDestinationInExternalFilesDir(this, "/", imageName);
-        } else { // Only set the title using `imageName`.
-            downloadRequest.setTitle(imageName);
-        }
+                // Add the cookies to `downloadRequest`.  In the HTTP request header, cookies are named `Cookie`.
+                downloadRequest.addRequestHeader("Cookie", cookies);
+            }
+
+            // Get the file name from `dialogFragment`.
+            EditText downloadImageNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.download_image_name);
+            String imageName = downloadImageNameEditText.getText().toString();
+
+            // Once we have `WRITE_EXTERNAL_STORAGE` permissions we can use `setDestinationInExternalPublicDir`.
+            if (Build.VERSION.SDK_INT >= 23) { // If API >= 23, set the download save in the the `DIRECTORY_DOWNLOADS` using `imageName`.
+                downloadRequest.setDestinationInExternalFilesDir(this, "/", imageName);
+            } else { // Only set the title using `imageName`.
+                downloadRequest.setTitle(imageName);
+            }
 
-        // Allow `MediaScanner` to index the download if it is a media file.
-        downloadRequest.allowScanningByMediaScanner();
+            // Allow `MediaScanner` to index the download if it is a media file.
+            downloadRequest.allowScanningByMediaScanner();
 
-        // Add the URL as the description for the download.
-        downloadRequest.setDescription(imageUrl);
+            // Add the URL as the description for the download.
+            downloadRequest.setDescription(imageUrl);
 
-        // Show the download notification after the download is completed.
-        downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+            // Show the download notification after the download is completed.
+            downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
 
-        // Initiate the download.
-        downloadManager.enqueue(downloadRequest);
+            // Initiate the download.
+            downloadManager.enqueue(downloadRequest);
+        } else {  // The image is not an HTTP or HTTPS URI.
+            Snackbar.make(mainWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
+        }
     }
 
     @Override
     public void onDownloadFile(AppCompatDialogFragment dialogFragment, String downloadUrl) {
-        // Get a handle for the system `DOWNLOAD_SERVICE`.
-        DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
+        // Download the file if it has an HTTP or HTTPS URI.
+        if (downloadUrl.startsWith("http")) {
 
-        // Parse `downloadUrl`.
-        DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
+            // Get a handle for the system `DOWNLOAD_SERVICE`.
+            DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
 
-        // Get the file name from `dialogFragment`.
-        EditText downloadFileNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.download_file_name);
-        String fileName = downloadFileNameEditText.getText().toString();
+            // Parse `downloadUrl`.
+            DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
 
-        // Once we have `WRITE_EXTERNAL_STORAGE` permissions we can use `setDestinationInExternalPublicDir`.
-        if (Build.VERSION.SDK_INT >= 23) { // If API >= 23, set the download save in the the `DIRECTORY_DOWNLOADS` using `fileName`.
-            downloadRequest.setDestinationInExternalFilesDir(this, "/", fileName);
-        } else { // Only set the title using `fileName`.
-            downloadRequest.setTitle(fileName);
-        }
+            // Pass cookies to download manager if cookies are enabled.  This is required to download files from websites that require a login.
+            if (firstPartyCookiesEnabled) {
+                // Get the cookies for `downloadUrl`.
+                String cookies = cookieManager.getCookie(downloadUrl);
+
+                // Add the cookies to `downloadRequest`.  In the HTTP request header, cookies are named `Cookie`.
+                downloadRequest.addRequestHeader("Cookie", cookies);
+            }
+
+            // Get the file name from `dialogFragment`.
+            EditText downloadFileNameEditText = (EditText) dialogFragment.getDialog().findViewById(R.id.download_file_name);
+            String fileName = downloadFileNameEditText.getText().toString();
+
+            // Once we have `WRITE_EXTERNAL_STORAGE` permissions we can use `setDestinationInExternalPublicDir`.
+            if (Build.VERSION.SDK_INT >= 23) { // If API >= 23, set the download location to `/sdcard/Android/data/com.stoutner.privacybrowser.standard/files` named `fileName`.
+                downloadRequest.setDestinationInExternalFilesDir(this, "/", fileName);
+            } else { // Only set the title using `fileName`.
+                downloadRequest.setTitle(fileName);
+            }
 
-        // Allow `MediaScanner` to index the download if it is a media file.
-        downloadRequest.allowScanningByMediaScanner();
+            // Allow `MediaScanner` to index the download if it is a media file.
+            downloadRequest.allowScanningByMediaScanner();
 
-        // Add the URL as the description for the download.
-        downloadRequest.setDescription(downloadUrl);
+            // Add the URL as the description for the download.
+            downloadRequest.setDescription(downloadUrl);
 
-        // Show the download notification after the download is completed.
-        downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+            // Show the download notification after the download is completed.
+            downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
 
-        // Initiate the download.
-        downloadManager.enqueue(downloadRequest);
+            // Initiate the download.
+            downloadManager.enqueue(downloadRequest);
+        } else {  // The download is not an HTTP or HTTPS URI.
+            Snackbar.make(mainWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
+        }
     }
 
     public void viewSslCertificate(View view) {
@@ -1608,12 +1797,10 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
         mainWebView.clearMatches();
 
         // Hide the Find on Page `RelativeLayout`.
-        LinearLayout findOnPageLinearLayout = (LinearLayout) findViewById(R.id.find_on_page_linearlayout);
         findOnPageLinearLayout.setVisibility(View.GONE);
 
         // Show the URL app bar.
-        Toolbar appBarToolbar = (Toolbar) findViewById(R.id.app_bar);
-        appBarToolbar.setVisibility(View.VISIBLE);
+        supportAppBar.setVisibility(View.VISIBLE);
 
         // Hide the keyboard so we can see the webpage.  `0` indicates no additional flags.
         inputMethodManager.hideSoftInputFromWindow(mainWebView.getWindowToken(), 0);
@@ -1627,14 +1814,20 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
         String userAgentString = sharedPreferences.getString("user_agent", "PrivacyBrowser/1.0");
         String customUserAgentString = sharedPreferences.getString("custom_user_agent", "PrivacyBrowser/1.0");
         String javaScriptDisabledSearchString = sharedPreferences.getString("javascript_disabled_search", "https://duckduckgo.com/html/?q=");
-        String javaScriptDisabledCustomSearchString = sharedPreferences.getString("javascript_disabled_search_custom_url", "");
+        String javaScriptDisabledSearchCustomURLString = sharedPreferences.getString("javascript_disabled_search_custom_url", "");
         String javaScriptEnabledSearchString = sharedPreferences.getString("javascript_enabled_search", "https://duckduckgo.com/?q=");
-        String javaScriptEnabledCustomSearchString = sharedPreferences.getString("javascript_enabled_search_custom_url", "");
+        String javaScriptEnabledSearchCustomURLString = sharedPreferences.getString("javascript_enabled_search_custom_url", "");
         String homepageString = sharedPreferences.getString("homepage", "https://www.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", "");
         String defaultFontSizeString = sharedPreferences.getString("default_font_size", "100");
         swipeToRefreshEnabled = sharedPreferences.getBoolean("swipe_to_refresh_enabled", false);
-        boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", true);
-        boolean proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", 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);
@@ -1661,8 +1854,75 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
             }
         }
 
-        // Apply the other settings from `sharedPreferences`.
-        homepage = homepageString;
+        // 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 the user agent initial status.
@@ -1683,20 +1943,6 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
                 break;
         }
 
-        // Set JavaScript disabled search.
-        if (javaScriptDisabledSearchString.equals("Custom URL")) {  // Get the custom URL string.
-            javaScriptDisabledSearchURL = javaScriptDisabledCustomSearchString;
-        } 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 = javaScriptEnabledCustomSearchString;
-        } else {  // Use the string from the pre-built list.
-            javaScriptEnabledSearchURL = javaScriptEnabledSearchString;
-        }
-
         // Set Do Not Track status.
         if (doNotTrackEnabled) {
             customHeaders.put("DNT", "1");
@@ -1704,16 +1950,8 @@ public class MainWebView extends AppCompatActivity implements NavigationView.OnN
             customHeaders.remove("DNT");
         }
 
-        // Set Orbot proxy status.
-        if (proxyThroughOrbot) {
-            // Set the proxy.  `this` refers to the current activity where an `AlertDialog` might be displayed.
-            OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
-        } else {  // Reset the proxy to default.  The host is `""` and the port is `"0"`.
-            OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
-        }
-
-        // If we are in full screen mode update the `SYSTEM_UI` flags.
-        if (!appBar.isShowing()) {
+        // Update the `SYSTEM_UI` flags if we are in full screen mode.
+        if (inFullScreenBrowsingMode) {
             if (hideSystemBarsOnFullscreen) {  // Hide everything.
                 // Remove the translucent navigation setting if it is currently flagged.
                 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);