]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blobdiff - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Remove the add domain dialog when creating a domain from the options menu.
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.java
index 032aacb07109d24d164e906a9a6b8731448addb9..a758fedebb87bc0b883af47524c6efb283404bb0 100644 (file)
 
 package com.stoutner.privacybrowser.activities;
 
+import android.Manifest;
 import android.annotation.SuppressLint;
 import android.app.DialogFragment;
 import android.app.DownloadManager;
+import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ClipData;
 import android.content.ClipboardManager;
@@ -31,6 +33,7 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.database.Cursor;
 import android.graphics.Bitmap;
@@ -43,6 +46,7 @@ import android.net.http.SslCertificate;
 import android.net.http.SslError;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Environment;
 import android.os.Handler;
 import android.preference.PreferenceManager;
 import android.print.PrintDocumentAdapter;
@@ -52,6 +56,7 @@ import android.support.design.widget.CoordinatorLayout;
 import android.support.design.widget.FloatingActionButton;
 import android.support.design.widget.NavigationView;
 import android.support.design.widget.Snackbar;
+import android.support.v4.app.ActivityCompat;
 import android.support.v4.content.ContextCompat;
 // `ShortcutInfoCompat`, `ShortcutManagerCompat`, and `IconCompat` can be switched to the non-compat version once API >= 26.
 import android.support.v4.content.pm.ShortcutInfoCompat;
@@ -104,11 +109,11 @@ import android.widget.TextView;
 import com.stoutner.privacybrowser.BannerAd;
 import com.stoutner.privacybrowser.BuildConfig;
 import com.stoutner.privacybrowser.R;
-import com.stoutner.privacybrowser.dialogs.AddDomainDialog;
 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
 import com.stoutner.privacybrowser.dialogs.DownloadImageDialog;
+import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog;
 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
@@ -139,12 +144,12 @@ import java.util.List;
 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 MainWebViewActivity extends AppCompatActivity implements AddDomainDialog.AddDomainListener, CreateBookmarkDialog.CreateBookmarkListener,
-        CreateBookmarkFolderDialog.CreateBookmarkFolderListener, CreateHomeScreenShortcutDialog.CreateHomeScreenSchortcutListener, DownloadFileDialog.DownloadFileListener,
-        DownloadImageDialog.DownloadImageListener, EditBookmarkDialog.EditBookmarkListener, EditBookmarkFolderDialog.EditBookmarkFolderListener, HttpAuthenticationDialog.HttpAuthenticationListener,
-        NavigationView.OnNavigationItemSelectedListener, PinnedSslCertificateMismatchDialog.PinnedSslCertificateMismatchListener, SslCertificateErrorDialog.SslCertificateErrorListener,
-        UrlHistoryDialog.UrlHistoryListener {
+// AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21.
+public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
+        CreateHomeScreenShortcutDialog.CreateHomeScreenSchortcutListener, DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener,
+        DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener, EditBookmarkFolderDialog.EditBookmarkFolderListener,
+        HttpAuthenticationDialog.HttpAuthenticationListener, NavigationView.OnNavigationItemSelectedListener, PinnedSslCertificateMismatchDialog.PinnedSslCertificateMismatchListener,
+        SslCertificateErrorDialog.SslCertificateErrorListener, UrlHistoryDialog.UrlHistoryListener {
 
     // `darkTheme` is public static so it can be accessed from `AboutActivity`, `GuideActivity`, `AddDomainDialog`, `SettingsActivity`, `DomainsActivity`, `DomainsListFragment`, `BookmarksActivity`,
     // `BookmarksDatabaseViewActivity`, `CreateBookmarkDialog`, `CreateBookmarkFolderDialog`, `DownloadFileDialog`, `DownloadImageDialog`, `EditBookmarkDialog`, `EditBookmarkFolderDialog`,
@@ -277,12 +282,15 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
     // The block list variables are used in `onCreate()` and `applyAppSettings()`.
     private boolean easyListEnabled;
     private boolean easyPrivacyEnabled;
-    private boolean fanboyAnnoyanceListEnabled;
-    private boolean fanboySocialBlockingListEnabled;
+    private boolean fanboysAnnoyanceListEnabled;
+    private boolean fanboysSocialBlockingListEnabled;
 
     // `privacyBrowserRuntime` is used in `onCreate()`, `onOptionsItemSelected()`, and `applyAppSettings()`.
     private Runtime privacyBrowserRuntime;
 
+    // `proxyThroughOrbot` is used in `onRestart()` and `applyAppSettings()`.
+    boolean proxyThroughOrbot;
+
     // `incognitoModeEnabled` is used in `onCreate()` and `applyAppSettings()`.
     private boolean incognitoModeEnabled;
 
@@ -301,8 +309,8 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
     // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
     private boolean reapplyDomainSettingsOnRestart;
 
-    // `returnFromSettings` is used in `onNavigationItemSelected()` and `onRestart()`.
-    private boolean returnFromSettings;
+    // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
+    private boolean reapplyAppSettingsOnRestart;
 
     // `currentDomainName` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onAddDomain()`, and `applyDomainSettings()`.
     private String currentDomainName;
@@ -390,9 +398,22 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
     // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
     private String oldFolderNameString;
 
+    // The download strings are used in `onCreate()` and `onRequestPermissionResult()`.
+    private String downloadUrl;
+    private String downloadContentDisposition;
+    private long downloadContentLength;
+
+    // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
+    private String downloadImageUrl;
+
+    // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, and `onRequestPermissionResult()`.
+    private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
+    private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
+
     @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"})
+    // Also, remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
+    @SuppressLint({"SetJavaScriptEnabled", "ClickableViewAccessibility"})
     // Remove Android Studio's warning about deprecations.  We have to use the deprecated `getColor()` until API >= 23.
     @SuppressWarnings("deprecation")
     protected void onCreate(Bundle savedInstanceState) {
@@ -941,9 +962,33 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
 
         // Allow the downloading of files.
         mainWebView.setDownloadListener((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(), getString(R.string.download));
+            // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
+            if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
+                // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
+
+                // Store the variables for future use by `onRequestPermissionsResult()`.
+                downloadUrl = url;
+                downloadContentDisposition = contentDisposition;
+                downloadContentLength = contentLength;
+
+                // Show a dialog if the user has previously denied the permission.
+                if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
+                    // Get a handle for the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
+                    DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
+
+                    // Show the download location permission alert dialog.  The permission will be requested when the the dialog is closed.
+                    downloadLocationPermissionDialogFragment.show(getFragmentManager(), getString(R.string.download_location));
+                } else {  // Show the permission request directly.
+                    // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
+                    ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
+                }
+            } else {  // The WRITE_EXTERNAL_STORAGE permission has already been granted.
+                // Get a handle for the download file alert dialog.
+                AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(url, contentDisposition, contentLength);
+
+                // Show the download file alert dialog.
+                downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
+            }
         });
 
         // Allow pinch to zoom.
@@ -1036,46 +1081,68 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
 
         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.
+            // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
             @SuppressWarnings("deprecation")
             @Override
             public boolean shouldOverrideUrlLoading(WebView view, String url) {
-                if (url.startsWith("mailto:")) {  // Load the email address in an external email program.
+                if (url.startsWith("http")) {  // Load the URL in Privacy Browser.
+                    // Apply the domain settings for the new URL.
+                    applyDomainSettings(url, true);
+
+                    // Returning false causes the current `WebView` to handle the URL and prevents it from adding redirects to the history list.
+                    return false;
+                } else if (url.startsWith("mailto:")) {  // Load the email address in an external email program.
                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
 
-                    // Parse the url and set it as the data for the `Intent`.
+                    // Parse the url and set it as the data for the intent.
                     emailIntent.setData(Uri.parse(url));
 
-                    // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
+                    // Open the email program in a new task instead of as part of Privacy Browser.
                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 
                     // Make it so.
                     startActivity(emailIntent);
 
-                    // Returning `true` indicates the application is handling the URL.
+                    // Returning true indicates Privacy Browser is handling the URL by creating an intent.
                     return true;
                 } else if (url.startsWith("tel:")) {  // Load the phone number in the dialer.
-                    // `ACTION_DIAL` open the dialer and loads the phone number, but waits for the user to place the call.
+                    // Open the dialer and load the phone number, but wait for the user to place the call.
                     Intent dialIntent = new Intent(Intent.ACTION_DIAL);
 
                     // Add the phone number to the intent.
                     dialIntent.setData(Uri.parse(url));
 
-                    // `FLAG_ACTIVITY_NEW_TASK` opens the dialer in a new task instead as part of Privacy Browser.
+                    // Open the dialer in a new task instead of as part of Privacy Browser.
                     dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 
                     // Make it so.
                     startActivity(dialIntent);
 
-                    // Returning `true` indicates the application is handling the URL.
+                    // Returning true indicates Privacy Browser is handling the URL by creating an intent.
                     return true;
-                } else {  // Load the URL in Privacy Browser.
-                    // Apply the domain settings for the new URL.
-                    applyDomainSettings(url);
+                } else {  // Load a system chooser to select an app that can handle the URL.
+                    // Open an app that can handle the URL.
+                    Intent genericIntent = new Intent(Intent.ACTION_VIEW);
 
-                    // Returning `false` causes the current `WebView` to handle the URL and prevents it from adding redirects to the history list.
-                    return false;
+                    // Add the URL to the intent.
+                    genericIntent.setData(Uri.parse(url));
+
+                    // List all apps that can handle the URL instead of just opening the first one.
+                    genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
+
+                    // Open the app in a new task instead of as part of Privacy Browser.
+                    genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+                    // Start the app or display a snackbar if no app is available to handle the URL.
+                    try {
+                        startActivity(genericIntent);
+                    } catch (ActivityNotFoundException exception) {
+                        Snackbar.make(mainWebView, getString(R.string.unrecognized_url) + "  " + url, Snackbar.LENGTH_SHORT).show();
+                    }
+
+                    // Returning true indicates Privacy Browser is handling the URL by creating an intent.
+                    return true;
                 }
             }
 
@@ -1103,12 +1170,12 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
                 }
 
                 // Check Fanboy’s Annoyance List if it is enabled.
-                if (fanboyAnnoyanceListEnabled) {
+                if (fanboysAnnoyanceListEnabled) {
                     if (blockListHelper.isBlocked(formattedUrlString, url, fanboyAnnoyance)) {
                         // The resource request was blocked.  Return an empty web resource response.
                         return emptyWebResourceResponse;
                     }
-                } else if (fanboySocialBlockingListEnabled){  // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
+                } else if (fanboysSocialBlockingListEnabled){  // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
                     if (blockListHelper.isBlocked(formattedUrlString, url, fanboySocial)) {
                         // The resource request was blocked.  Return an empty web resource response.
                         return emptyWebResourceResponse;
@@ -1153,7 +1220,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
 
                     // Apply any custom domain settings if the URL was loaded by navigating history.
                     if (navigatingHistory) {
-                        applyDomainSettings(url);
+                        applyDomainSettings(url, true);
                     }
 
                     // 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.
@@ -1182,11 +1249,12 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
 
                     // Manually delete cache folders.
                     try {
-                        // Delete the main `cache` folder.
+                        // Delete the main cache directory.
                         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");
+                        // Delete the secondary `Service Worker` cache directory.
+                        // A `String[]` must be used 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.
                     }
@@ -1208,7 +1276,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
                         inputMethodManager.showSoftInput(urlTextBox, 0);
 
                         // Apply the domain settings.  This clears any settings from the previous domain.
-                        applyDomainSettings(formattedUrlString);
+                        applyDomainSettings(formattedUrlString, true);
                     } else {  // `WebView` has loaded a webpage.
                         // Set `formattedUrlString`.
                         formattedUrlString = url;
@@ -1349,6 +1417,11 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
                 drawerLayout.closeDrawer(GravityCompat.START);
             }
 
+            // Close the bookmarks drawer if it is open.
+            if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
+                drawerLayout.closeDrawer(GravityCompat.END);
+            }
+
             // Clear the keyboard if displayed and remove the focus on the urlTextBar if it has it.
             mainWebView.requestFocus();
         }
@@ -1359,34 +1432,43 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
         // Run the default commands.
         super.onRestart();
 
-        // Apply the app settings if returning from the Settings activity..
-        if (returnFromSettings) {
-            // Reset the return from settings flag.
-            returnFromSettings = false;
+        // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
+        if (proxyThroughOrbot) {
+            // Request Orbot to start.  If Orbot is already running no hard will be caused by this request.
+            Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
 
+            // Send the intent to the Orbot package.
+            orbotIntent.setPackage("org.torproject.android");
+
+            // Make it so.
+            sendBroadcast(orbotIntent);
+        }
+
+        // Apply the app settings if returning from the Settings activity..
+        if (reapplyAppSettingsOnRestart) {
             // Apply the app settings.
             applyAppSettings();
 
-            // Set the display webpage images mode.
-            setDisplayWebpageImages();
-        }
+            // Reload the webpage if displaying of images has been disabled in the Settings activity.
+            if (reloadOnRestart) {
+                // Reload `mainWebView`.
+                mainWebView.reload();
 
-        // Reload the webpage if displaying of images has been disabled in the Settings activity.
-        if (reloadOnRestart) {
-            // Reload `mainWebView`.
-            mainWebView.reload();
+                // Reset `reloadOnRestartBoolean`.
+                reloadOnRestart = false;
+            }
 
-            // Reset `reloadOnRestartBoolean`.
-            reloadOnRestart = false;
+            // Reset the return from settings flag.
+            reapplyAppSettingsOnRestart = false;
         }
 
         // Apply the domain settings if returning from the Domains activity.
         if (reapplyDomainSettingsOnRestart) {
+            // Reapply the domain settings.
+            applyDomainSettings(formattedUrlString, false);
+
             // Reset `reapplyDomainSettingsOnRestart`.
             reapplyDomainSettingsOnRestart = false;
-
-            // Reapply the domain settings.
-            applyDomainSettings(formattedUrlString);
         }
 
         // Load the URL on restart to apply changes to night mode.
@@ -1410,7 +1492,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
             restartFromBookmarksActivity = false;
         }
 
-        // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
+        // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.  This can be important if the screen was rotated.
         updatePrivacyIcons(true);
     }
 
@@ -1641,9 +1723,28 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
                     // Make it so.
                     startActivity(domainsIntent);
                 } else {  // Add a new domain.
-                    // Show the add domain `AlertDialog`.
-                    AppCompatDialogFragment addDomainDialog = new AddDomainDialog();
-                    addDomainDialog.show(getSupportFragmentManager(), getResources().getString(R.string.add_domain));
+                    // Apply the new domain settings on returning to `MainWebViewActivity`.
+                    reapplyDomainSettingsOnRestart = true;
+                    currentDomainName = "";
+
+                    // Get the current domain
+                    Uri currentUri = Uri.parse(formattedUrlString);
+                    String currentDomain = currentUri.getHost();
+
+                    // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
+                    DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
+
+                    // Create the domain and store the database ID.
+                    int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
+
+                    // Create an intent to launch the domains activity.
+                    Intent domainsIntent = new Intent(this, DomainsActivity.class);
+
+                    // Put extra information instructing the domains activity to directly load the new domain.
+                    domainsIntent.putExtra("LoadDomain", newDomainDatabaseId);
+
+                    // Make it so.
+                    startActivity(domainsIntent);
                 }
                 return true;
 
@@ -2025,7 +2126,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
                 break;
 
             case R.id.domains:
-                // Reapply the domain settings on returning to `MainWebViewActivity`.
+                // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
                 reapplyDomainSettingsOnRestart = true;
                 currentDomainName = "";
 
@@ -2035,13 +2136,13 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
                 break;
 
             case R.id.settings:
-                // Reapply the domain settings on returning to `MainWebViewActivity`.
+                // Set the flag to reapply app settings on restart when returning from Settings.
+                reapplyAppSettingsOnRestart = true;
+
+                // Set the flag to reapply the domain settings on restart when returning from Settings.
                 reapplyDomainSettingsOnRestart = true;
                 currentDomainName = "";
 
-                // Mark a flag to reapply app settings on restart only when returning from Settings.
-                returnFromSettings = true;
-
                 // Launch `SettingsActivity`.
                 Intent settingsIntent = new Intent(this, SettingsActivity.class);
                 startActivity(settingsIntent);
@@ -2129,8 +2230,9 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
                     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.
+                        // A `String[]` must be used 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.
@@ -2305,9 +2407,31 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
 
                 // Add a `Download Image` entry.
                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
-                    // Show the `DownloadImageDialog` `AlertDialog` and name this instance `@string/download`.
-                    AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
-                    downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
+                    // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
+                    if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
+                        // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
+
+                        // Store the image URL for use by `onRequestPermissionResult()`.
+                        downloadImageUrl = imageUrl;
+
+                        // Show a dialog if the user has previously denied the permission.
+                        if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
+                            // Get a handle for the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
+                            DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
+
+                            // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
+                            downloadLocationPermissionDialogFragment.show(getFragmentManager(), getString(R.string.download_location));
+                        } else {  // Show the permission request directly.
+                            // Request the permission.  The download dialog will be launched by `onRequestPermissionResult().
+                            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
+                        }
+                    } else {  // The WRITE_EXTERNAL_STORAGE permission has already been granted.
+                        // Get a handle for the download image alert dialog.
+                        AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
+
+                        // Show the download image alert dialog.
+                        downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
+                    }
                     return false;
                 });
 
@@ -2342,9 +2466,31 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
 
                 // Add a `Download Image` entry.
                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
-                    // Show the `DownloadImageDialog` `AlertDialog` and name this instance `@string/download`.
-                    AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
-                    downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
+                    // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
+                    if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
+                        // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
+
+                        // Store the image URL for use by `onRequestPermissionResult()`.
+                        downloadImageUrl = imageUrl;
+
+                        // Show a dialog if the user has previously denied the permission.
+                        if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
+                            // Get a handle for the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
+                            DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
+
+                            // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
+                            downloadLocationPermissionDialogFragment.show(getFragmentManager(), getString(R.string.download_location));
+                        } else {  // Show the permission request directly.
+                            // Request the permission.  The download dialog will be launched by `onRequestPermissionResult().
+                            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
+                        }
+                    } else {  // The WRITE_EXTERNAL_STORAGE permission has already been granted.
+                        // Get a handle for the download image alert dialog.
+                        AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
+
+                        // Show the download image alert dialog.
+                        downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
+                    }
                     return false;
                 });
 
@@ -2364,33 +2510,6 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
         }
     }
 
-    @Override
-    public void onAddDomain(AppCompatDialogFragment dialogFragment) {
-        // Reapply the domain settings on returning to `MainWebViewActivity`.
-        reapplyDomainSettingsOnRestart = true;
-        currentDomainName = "";
-
-        // Get the new domain name `String` from `dialogFragment`.
-        EditText domainNameEditText = dialogFragment.getDialog().findViewById(R.id.domain_name_edittext);
-        String domainNameString = domainNameEditText.getText().toString();
-
-        // Initialize the database handler.  `this` specifies the context.  The two `nulls` do not specify the database name or a `CursorFactory`.
-        // The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
-        DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
-
-        // Create the domain and store the database ID in `currentDomainDatabaseId`.
-        int newDomainDatabaseId = domainsDatabaseHelper.addDomain(domainNameString);
-
-        // Create an intent to launch the domains activity.
-        Intent domainsIntent = new Intent(this, DomainsActivity.class);
-
-        // Put extra information instructing the domains activity to directly load the current domain.
-        domainsIntent.putExtra("LoadDomain", newDomainDatabaseId);
-
-        // Make it so.
-        startActivity(domainsIntent);
-    }
-
     @Override
     public void onCreateBookmark(AppCompatDialogFragment dialogFragment) {
         // Get the `EditTexts` from the `dialogFragment`.
@@ -2493,6 +2612,58 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
         ShortcutManagerCompat.requestPinShortcut(this, shortcutInfoBuilder.build(), null);
     }
 
+    @Override
+    public void onCloseDownloadLocationPermissionDialog(int downloadType) {
+        switch (downloadType) {
+            case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
+                // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
+                break;
+
+            case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
+                // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
+                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
+                break;
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
+        switch (requestCode) {
+            case DOWNLOAD_FILE_REQUEST_CODE:
+                // Show the download file alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
+                AppCompatDialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
+
+                // On API 23, displaying the fragment must be delayed or the app will crash.
+                if (Build.VERSION.SDK_INT == 23) {
+                    new Handler().postDelayed(() -> downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)), 500);
+                } else {
+                    downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
+                }
+
+                // Reset the download variables.
+                downloadUrl = "";
+                downloadContentDisposition = "";
+                downloadContentLength = 0;
+                break;
+
+            case DOWNLOAD_IMAGE_REQUEST_CODE:
+                // Show the download image alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
+                AppCompatDialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
+
+                // On API 23, displaying the fragment must be delayed or the app will crash.
+                if (Build.VERSION.SDK_INT == 23) {
+                    new Handler().postDelayed(() -> downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download)), 500);
+                } else {
+                    downloadImageDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
+                }
+
+                // Reset the image URL variable.
+                downloadImageUrl = "";
+                break;
+        }
+    }
+
     @Override
     public void onDownloadImage(AppCompatDialogFragment dialogFragment, String imageUrl) {
         // Download the image if it has an HTTP or HTTPS URI.
@@ -2513,15 +2684,17 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
                 downloadRequest.addRequestHeader("Cookie", cookies);
             }
 
-            // Get the file name from `dialogFragment`.
+            // Get the file name from the dialog fragment.
             EditText downloadImageNameEditText = 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);
+            // Specify the download location.
+            if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // External write permission granted.
+                // Download to the public download directory.
+                downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
+            } else {  // External write permission denied.
+                // Download to the app's external download directory.
+                downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
             }
 
             // Allow `MediaScanner` to index the download if it is a media file.
@@ -2547,7 +2720,6 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
     public void onDownloadFile(AppCompatDialogFragment dialogFragment, String downloadUrl) {
         // Download the file if it has an HTTP or HTTPS URI.
         if (downloadUrl.startsWith("http")) {
-
             // Get a handle for the system `DOWNLOAD_SERVICE`.
             DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
 
@@ -2564,15 +2736,17 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
                 downloadRequest.addRequestHeader("Cookie", cookies);
             }
 
-            // Get the file name from `dialogFragment`.
+            // Get the file name from the dialog fragment.
             EditText downloadFileNameEditText = 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);
+            // Specify the download location.
+            if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // External write permission granted.
+                // Download to the public download directory.
+                downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
+            } else {  // External write permission denied.
+                // Download to the app's external download directory.
+                downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
             }
 
             // Allow `MediaScanner` to index the download if it is a media file.
@@ -2835,7 +3009,7 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
 
     private void loadUrl(String url) {
         // Apply any custom domain settings.
-        applyDomainSettings(url);
+        applyDomainSettings(url, true);
 
         // Load the URL.
         mainWebView.loadUrl(url, customHeaders);
@@ -2882,13 +3056,9 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
         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", "");
-        easyListEnabled = sharedPreferences.getBoolean("easylist", true);
-        easyPrivacyEnabled = sharedPreferences.getBoolean("easyprivacy", true);
-        fanboyAnnoyanceListEnabled = sharedPreferences.getBoolean("fanboy_annoyance_list", true);
-        fanboySocialBlockingListEnabled = sharedPreferences.getBoolean("fanboy_social_blocking_list", true);
         incognitoModeEnabled = sharedPreferences.getBoolean("incognito_mode", false);
         boolean doNotTrackEnabled = sharedPreferences.getBoolean("do_not_track", false);
-        boolean proxyThroughOrbot = sharedPreferences.getBoolean("proxy_through_orbot", false);
+        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);
@@ -3031,9 +3201,10 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
         }
     }
 
-    // We have to use the deprecated `.getDrawable()` until the minimum API >= 21.
+    //
+    // The deprecated `.getDrawable()` must be used until the minimum API >= 21.
     @SuppressWarnings("deprecation")
-    private void applyDomainSettings(String url) {
+    private void applyDomainSettings(String url, boolean resetFavoriteIcon) {
         // Reset `navigatingHistory`.
         navigatingHistory = false;
 
@@ -3063,9 +3234,11 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
             // Reset `ignorePinnedSslCertificate`.
             ignorePinnedSslCertificate = false;
 
-            // Reset `favoriteIconBitmap` and display it in the `appbar`.
-            favoriteIconBitmap = favoriteIconDefaultBitmap;
-            favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true));
+            // Reset the favorite icon if specified.
+            if (resetFavoriteIcon) {
+                favoriteIconBitmap = favoriteIconDefaultBitmap;
+                favoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(favoriteIconBitmap, 64, 64, true));
+            }
 
             // Initialize the database handler.  `this` specifies the context.  The two `nulls` do not specify the database name or a `CursorFactory`.
             // The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
@@ -3136,6 +3309,10 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
                 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);
+                easyListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
+                easyPrivacyEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
+                fanboysAnnoyanceListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
+                fanboysSocialBlockingListEnabled = (currentHostDomainSettingsCursor.getInt(currentHostDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
                 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));
@@ -3249,6 +3426,10 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
                 thirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies_enabled", false);
                 domStorageEnabled = sharedPreferences.getBoolean("dom_storage_enabled", false);
                 saveFormDataEnabled = sharedPreferences.getBoolean("save_form_data_enabled", false);
+                easyListEnabled = sharedPreferences.getBoolean("easylist", true);
+                easyPrivacyEnabled = sharedPreferences.getBoolean("easyprivacy", true);
+                fanboysAnnoyanceListEnabled = sharedPreferences.getBoolean("fanboy_annoyance_list", true);
+                fanboysSocialBlockingListEnabled = sharedPreferences.getBoolean("fanboy_social_blocking_list", true);
 
                 // Set `javaScriptEnabled` to be `true` if `night_mode` is `true`.
                 if (nightMode) {
@@ -3471,4 +3652,4 @@ public class MainWebViewActivity extends AppCompatActivity implements AddDomainD
             bookmarksTitleTextView.setText(currentBookmarksFolder);
         }
     }
-}
+}
\ No newline at end of file