]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blobdiff - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Clear domain settings before opening a file. https://redmine.stoutner.com/issues/554
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.java
index b2d9afa3fa48e3b801c6b293b912aa2d637910f6..9107d5af73da0b4bbc3b01badde3ea66e963a0fb 100644 (file)
@@ -31,6 +31,7 @@ import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ClipData;
 import android.content.ClipboardManager;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -103,6 +104,7 @@ import androidx.appcompat.widget.Toolbar;
 import androidx.coordinatorlayout.widget.CoordinatorLayout;
 import androidx.core.app.ActivityCompat;
 import androidx.core.content.ContextCompat;
+import androidx.core.content.FileProvider;
 import androidx.core.content.res.ResourcesCompat;
 import androidx.core.view.GravityCompat;
 import androidx.drawerlayout.widget.DrawerLayout;
@@ -136,7 +138,7 @@ import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
 import com.stoutner.privacybrowser.dialogs.OpenDialog;
 import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog;
 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
-import com.stoutner.privacybrowser.dialogs.SaveDialog;
+import com.stoutner.privacybrowser.dialogs.SaveWebpageDialog;
 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
 import com.stoutner.privacybrowser.dialogs.StoragePermissionDialog;
 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
@@ -175,7 +177,7 @@ import java.util.concurrent.Executors;
 
 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
         EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener,
-        PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener,
+        PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveWebpageDialog.SaveWebpageListener, StoragePermissionDialog.StoragePermissionDialogListener,
         UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener {
 
     // The executor service handles background tasks.  It is accessed from `ViewSourceActivity`.
@@ -187,10 +189,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // The WebView pager adapter is accessed from `HttpAuthenticationDialog`, `PinnedMismatchDialog`, and `SslCertificateErrorDialog`.  It is also used in `onCreate()`, `onResume()`, and `addTab()`.
     public static WebViewPagerAdapter webViewPagerAdapter;
 
-    // The load URL on restart variables are public static so they can be accessed from `BookmarksActivity`.  They are used in `onRestart()`.
-    public static boolean loadUrlOnRestart;
-    public static String urlToLoadOnRestart;
-
     // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onRestart()`.
     public static boolean restartFromBookmarksActivity;
 
@@ -216,13 +214,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     // It will be updated in `applyAppSettings()`, but it needs to be initialized here or the first run of `onPrepareOptionsMenu()` crashes.
     public static String proxyMode = ProxyHelper.NONE;
 
-
-    // The permission result request codes are used in `onCreateContextMenu()`, `onRequestPermissionResult()`, `onSaveWebpage()`, `onCloseStoragePermissionDialog()`, and `initializeWebView()`.
-    private final int PERMISSION_OPEN_REQUEST_CODE = 0;
-    private final int PERMISSION_SAVE_URL_REQUEST_CODE = 1;
-    private final int PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE = 2;
-    private final int PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE = 3;
-
     // Define the saved instance state constants.
     private final String SAVED_STATE_ARRAY_LIST = "saved_state_array_list";
     private final String SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list";
@@ -235,11 +226,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     private int savedTabPosition;
     private String savedProxyMode;
 
-    // Define the class views.
-    private AppBarLayout appBarLayout;
-    private TabLayout tabLayout;
-    private ViewPager webViewPager;
-
     // Define the class variables.
     @SuppressWarnings("rawtypes")
     AsyncTask populateBlocklists;
@@ -338,6 +324,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     private String saveWebpageUrl;
     private String saveWebpageFilePath;
 
+    // Declare the class views.
+    private DrawerLayout drawerLayout;
+    private AppBarLayout appBarLayout;
+    private TabLayout tabLayout;
+    private ViewPager webViewPager;
+
     @Override
     // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
     @SuppressLint("ClickableViewAccessibility")
@@ -362,7 +354,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
         // Get the screenshot preference.
         String appTheme = sharedPreferences.getString("app_theme", getString(R.string.app_theme_default_value));
-        boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
+        boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
 
         // Get the theme entry values string array.
         String[] appThemeEntryValuesStringArray = getResources().getStringArray(R.array.app_theme_entry_values);
@@ -401,7 +393,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         setContentView(R.layout.main_framelayout);
 
         // Get handles for the views.
-        DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
+        drawerLayout = findViewById(R.id.drawerlayout);
         appBarLayout = findViewById(R.id.appbar_layout);
         Toolbar toolbar = findViewById(R.id.toolbar);
         tabLayout = findViewById(R.id.tablayout);
@@ -504,9 +496,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     loadUrl(currentWebView, url);
                 }
 
-                // Get a handle for the drawer layout.
-                DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
-
                 // Close the navigation drawer if it is open.
                 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
                     drawerLayout.closeDrawer(GravityCompat.START);
@@ -563,20 +552,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             }
         }
 
-        // Load the URL on restart (used when loading a bookmark).
-        if (loadUrlOnRestart) {
-            // Load the specified URL.
-            loadUrl(currentWebView, urlToLoadOnRestart);
-
-            // Reset the load on restart tracker.
-            loadUrlOnRestart = false;
-        }
-
         // Update the bookmarks drawer if returning from the Bookmarks activity.
         if (restartFromBookmarksActivity) {
-            // Get a handle for the drawer layout.
-            DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
-
             // Close the bookmarks drawer.
             drawerLayout.closeDrawer(GravityCompat.END);
 
@@ -1083,7 +1060,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     }
 
     @Override
-    // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
+    // Remove Android Studio's warning about the dangers of enabling JavaScript.  We know.  Oh, how we know.
     @SuppressLint("SetJavaScriptEnabled")
     public boolean onOptionsItemSelected(MenuItem menuItem) {
         // Get the selected menu item ID.
@@ -1132,9 +1109,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 return true;
 
             case R.id.bookmarks:
-                // Get a handle for the drawer layout.
-                DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
-
                 // Open the bookmarks drawer.
                 drawerLayout.openDrawer(GravityCompat.END);
 
@@ -1731,18 +1705,24 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Consume the event.
                 return true;
 
-            case R.id.save_as_archive:
-                // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
-                new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_AS_ARCHIVE, currentWebView.getSettings().getUserAgentString(),
-                        currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl());
+            case R.id.save_archive:
+                // Instantiate the save dialog.
+                DialogFragment saveArchiveFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_ARCHIVE, null, null, getString(R.string.webpage_mht), null,
+                        false);
+
+                // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
+                saveArchiveFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
 
                 // Consume the event.
                 return true;
 
-            case R.id.save_as_image:
-                // Prepare the save dialog.  The dialog will be displayed once the file size adn the content disposition have been acquired.
-                new PrepareSaveDialog(this, this, getSupportFragmentManager(), StoragePermissionDialog.SAVE_AS_IMAGE, currentWebView.getSettings().getUserAgentString(),
-                        currentWebView.getAcceptFirstPartyCookies()).execute(currentWebView.getCurrentUrl());
+            case R.id.save_image:
+                // Instantiate the save dialog.
+                DialogFragment saveImageFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_IMAGE, null, null, getString(R.string.webpage_png), null,
+                        false);
+
+                // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
+                saveImageFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
 
                 // Consume the event.
                 return true;
@@ -1778,9 +1758,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // Create the share intent.
                 Intent shareIntent = new Intent(Intent.ACTION_SEND);
+
+                // Add the share string to the intent.
                 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
+
+                // Set the MIME type.
                 shareIntent.setType("text/plain");
 
+                // Set the intent to open in a new task.
+                shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
                 // Make it so.
                 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
 
@@ -2111,9 +2098,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 break;
         }
 
-        // Get a handle for the drawer layout.
-        DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
-
         // Close the navigation drawer.
         drawerLayout.closeDrawer(GravityCompat.START);
         return true;
@@ -2146,7 +2130,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
     @Override
     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
-        // Store the hit test result.
+        // Get the hit test result.
         final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
 
         // Define the URL strings.
@@ -2234,8 +2218,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Get the image URL.
                 imageUrl = hitTestResult.getExtra();
 
-                // Set the image URL as the title of the context menu.
-                menu.setHeaderTitle(imageUrl);
+                // Remove the incorrect lint warning below that the image URL might be null.
+                assert imageUrl != null;
+
+                // Set the context menu title.
+                if (imageUrl.startsWith("data:")) {  // The image data is contained in within the URL, making it exceedingly long.
+                    // Truncate the image URL before making it the title.
+                    menu.setHeaderTitle(imageUrl.substring(0, 100));
+                } else {  // The image URL does not contain the full image data.
+                    // Set the image URL as the title of the context menu.
+                    menu.setHeaderTitle(imageUrl);
+                }
 
                 // Add an Open in New Tab entry.
                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
@@ -2427,8 +2420,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 
-                    // Make it so.
-                    startActivity(emailIntent);
+                    try {
+                        // Make it so.
+                        startActivity(emailIntent);
+                    } catch (ActivityNotFoundException exception) {
+                        // Display a snackbar.
+                        Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
+                    }
 
                     // Consume the event.
                     return true;
@@ -2649,26 +2647,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
     }
 
-    // Override `onBackPressed` to handle the navigation drawer and and the WebViews.
+    // Override `onBackPressed()` to handle the navigation drawer and and the WebViews.
     @Override
     public void onBackPressed() {
-        // Get a handle for the drawer layout.
-        DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
-
+        // Check the different options for processing `back`.
         if (drawerLayout.isDrawerVisible(GravityCompat.START)) {  // The navigation drawer is open.
             // Close the navigation drawer.
             drawerLayout.closeDrawer(GravityCompat.START);
         } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){  // The bookmarks drawer is open.
-            if (currentBookmarksFolder.isEmpty()) {  // The home folder is displayed.
-                // close the bookmarks drawer.
-                drawerLayout.closeDrawer(GravityCompat.END);
-            } else {  // A subfolder is displayed.
-                // Place the former parent folder in `currentFolder`.
-                currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
-
-                // Load the new folder.
-                loadBookmarksFolder();
-            }
+            // close the bookmarks drawer.
+            drawerLayout.closeDrawer(GravityCompat.END);
         } else if (displayingFullScreenVideo) {  // A full screen video is shown.
             // Get a handle for the layouts.
             FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
@@ -3004,6 +2992,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         // Get the file path string.
         openFilePath = fileNameEditText.getText().toString();
 
+        // Apply the domain settings.  This resets the favorite icon and removes any domain settings.
+        applyDomainSettings(currentWebView, "file://" + openFilePath, true, false);
+
         // Check to see if the storage permission is needed.
         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
             // Open the file.
@@ -3032,14 +3023,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
                 } else {  // Show the permission request directly.
                     // Request the write external storage permission.  The file will be opened when it finishes.
-                    ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_OPEN_REQUEST_CODE);
+                    ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, StoragePermissionDialog.OPEN);
                 }
             }
         }
     }
 
     @Override
-    public void onSaveWebpage(int saveType, DialogFragment dialogFragment) {
+    public void onSaveWebpage(int saveType, String originalUrlString, DialogFragment dialogFragment) {
         // Get the dialog.
         Dialog dialog = dialogFragment.getDialog();
 
@@ -3050,8 +3041,16 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         EditText urlEditText = dialog.findViewById(R.id.url_edittext);
         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
 
-        // Get the strings from the edit texts.
-        saveWebpageUrl = urlEditText.getText().toString();
+        // Store the URL.
+        if ((originalUrlString != null) && originalUrlString.startsWith("data:")) {
+            // Save the original URL.
+            saveWebpageUrl = originalUrlString;
+        } else {
+            // Get the URL from the edit text, which may have been modified.
+            saveWebpageUrl = urlEditText.getText().toString();
+        }
+
+        // Get the file path from the edit text.
         saveWebpageFilePath = fileNameEditText.getText().toString();
 
         // Check to see if the storage permission is needed.
@@ -3063,16 +3062,20 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
                     break;
 
-                case StoragePermissionDialog.SAVE_AS_ARCHIVE:
+                case StoragePermissionDialog.SAVE_ARCHIVE:
                     // Save the webpage archive.
-                    currentWebView.saveWebArchive(saveWebpageFilePath);
+                    saveWebpageArchive(saveWebpageFilePath);
                     break;
 
-                case StoragePermissionDialog.SAVE_AS_IMAGE:
+                case StoragePermissionDialog.SAVE_IMAGE:
                     // Save the webpage image.
-                    new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
+                    new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
                     break;
             }
+
+            // Reset the strings.
+            saveWebpageUrl = "";
+            saveWebpageFilePath = "";
         } else {  // The storage permission has not been granted.
             // Get the external private directory file.
             File externalPrivateDirectoryFile = getExternalFilesDir(null);
@@ -3092,16 +3095,20 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         new SaveUrl(this, this, saveWebpageFilePath, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptFirstPartyCookies()).execute(saveWebpageUrl);
                         break;
 
-                    case StoragePermissionDialog.SAVE_AS_ARCHIVE:
+                    case StoragePermissionDialog.SAVE_ARCHIVE:
                         // Save the webpage archive.
-                        currentWebView.saveWebArchive(saveWebpageFilePath);
+                        saveWebpageArchive(saveWebpageFilePath);
                         break;
 
-                    case StoragePermissionDialog.SAVE_AS_IMAGE:
+                    case StoragePermissionDialog.SAVE_IMAGE:
                         // Save the webpage image.
-                        new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
+                        new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
                         break;
                 }
+
+                // Reset the strings.
+                saveWebpageUrl = "";
+                saveWebpageFilePath = "";
             } else {  // The file path is in a public directory.
                 // Check if the user has previously denied the storage permission.
                 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
@@ -3111,21 +3118,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
                     storagePermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.storage_permission));
                 } else {  // Show the permission request directly.
-                    switch (saveType) {
-                        case StoragePermissionDialog.SAVE_URL:
-                            // Request the write external storage permission.  The URL will be saved when it finishes.
-                            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_URL_REQUEST_CODE);
-
-                        case StoragePermissionDialog.SAVE_AS_ARCHIVE:
-                            // Request the write external storage permission.  The webpage archive will be saved when it finishes.
-                            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE);
-                            break;
-
-                        case StoragePermissionDialog.SAVE_AS_IMAGE:
-                            // Request the write external storage permission.  The webpage image will be saved when it finishes.
-                            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE);
-                            break;
-                    }
+                    // Request the write external storage permission according to the save type.  The URL will be saved when it finishes.
+                    ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, saveType);
                 }
             }
         }
@@ -3133,27 +3127,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
     @Override
     public void onCloseStoragePermissionDialog(int requestType) {
-        switch (requestType) {
-            case StoragePermissionDialog.OPEN:
-                // Request the write external storage permission.  The file will be opened when it finishes.
-                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_OPEN_REQUEST_CODE);
-                break;
-
-            case StoragePermissionDialog.SAVE_URL:
-                // Request the write external storage permission.  The URL will be saved when it finishes.
-                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_URL_REQUEST_CODE);
-                break;
-
-            case StoragePermissionDialog.SAVE_AS_ARCHIVE:
-                // Request the write external storage permission.  The webpage archive will be saved when it finishes.
-                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE);
-                break;
+        // Request the write external storage permission according to the request type.  The file will be opened when it finishes.
+        ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestType);
 
-            case StoragePermissionDialog.SAVE_AS_IMAGE:
-                // Request the write external storage permission.  The webpage image will be saved when it finishes.
-                ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE);
-                break;
-        }
     }
 
     @Override
@@ -3161,7 +3137,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         //Only process the results if they exist (this method is triggered when a dialog is presented the first time for an app, but no grant results are included).
         if (grantResults.length > 0) {
             switch (requestCode) {
-                case PERMISSION_OPEN_REQUEST_CODE:
+                case StoragePermissionDialog.OPEN:
                     // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
                     if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {  // The storage permission was granted.
                         // Load the file.
@@ -3170,12 +3146,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         // Display an error snackbar.
                         Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
                     }
-
-                    // Reset the open file path.
-                    openFilePath = "";
                     break;
 
-                case PERMISSION_SAVE_URL_REQUEST_CODE:
+                case StoragePermissionDialog.SAVE_URL:
                     // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
                     if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {  // The storage permission was granted.
                         // Save the raw URL.
@@ -3184,40 +3157,35 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         // Display an error snackbar.
                         Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
                     }
-
-                    // Reset the save strings.
-                    saveWebpageUrl = "";
-                    saveWebpageFilePath = "";
                     break;
 
-                case PERMISSION_SAVE_AS_ARCHIVE_REQUEST_CODE:
+                case StoragePermissionDialog.SAVE_ARCHIVE:
                     // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
                     if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {  // The storage permission was granted.
                         // Save the webpage archive.
-                        currentWebView.saveWebArchive(saveWebpageFilePath);
+                        saveWebpageArchive(saveWebpageFilePath);
                     } else {  // The storage permission was not granted.
                         // Display an error snackbar.
                         Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
                     }
-
-                    // Reset the save webpage file path.
-                    saveWebpageFilePath = "";
                     break;
 
-                case PERMISSION_SAVE_AS_IMAGE_REQUEST_CODE:
+                case StoragePermissionDialog.SAVE_IMAGE:
                     // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
                     if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {  // The storage permission was granted.
                         // Save the webpage image.
-                        new SaveWebpageImage(this, currentWebView).execute(saveWebpageFilePath);
+                        new SaveWebpageImage(this, this, saveWebpageFilePath, currentWebView).execute();
                     } else {  // The storage permission was not granted.
                         // Display an error snackbar.
                         Snackbar.make(currentWebView, getString(R.string.cannot_use_location), Snackbar.LENGTH_LONG).show();
                     }
-
-                    // Reset the save webpage file path.
-                    saveWebpageFilePath = "";
                     break;
             }
+
+            // Reset the strings.
+            openFilePath = "";
+            saveWebpageUrl = "";
+            saveWebpageFilePath = "";
         }
     }
 
@@ -3333,7 +3301,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
 
         // Get handles for views that need to be modified.
-        DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
         NavigationView navigationView = findViewById(R.id.navigationview);
         SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
@@ -3369,11 +3336,8 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                 // Select the corresponding tab if it does not match the currently selected page.  This will happen if the page was scrolled by creating a new tab.
                 if (tabLayout.getSelectedTabPosition() != position) {
-                    // Create a handler to select the tab.
-                    Handler selectTabHandler = new Handler();
-
-                    // Create a runnable to select the tab.
-                    Runnable selectTabRunnable = () -> {
+                    // Wait until the new tab has been created.
+                    tabLayout.post(() -> {
                         // Get a handle for the tab.
                         TabLayout.Tab tab = tabLayout.getTabAt(position);
 
@@ -3382,10 +3346,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
                         // Select the tab.
                         tab.select();
-                    };
-
-                    // Select the tab layout after 150 milliseconds, which leaves enough time for a new tab to be inflated.  TODO.
-                    selectTabHandler.postDelayed(selectTabRunnable, 150);
+                    });
                 }
             }
 
@@ -4797,6 +4758,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Get the intent that started the app.
             Intent intent = getIntent();
 
+            // Reset the intent.  This prevents a duplicate tab from being created on restart.
+            setIntent(new Intent());
+
             // Get the information from the intent.
             String intentAction = intent.getAction();
             Uri intentUriData = intent.getData();
@@ -4882,6 +4846,12 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
     }
 
     private void closeCurrentTab() {
+        // Pause the current WebView.
+        currentWebView.onPause();
+
+        // Pause the current WebView JavaScript timers.
+        currentWebView.pauseTimers();
+
         // Get the current tab number.
         int currentTabNumber = tabLayout.getSelectedTabPosition();
 
@@ -4897,6 +4867,48 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         appBarLayout.setExpanded(true);
     }
 
+    private void saveWebpageArchive(String filePath) {
+        // Save the webpage archive.
+        currentWebView.saveWebArchive(filePath);
+
+        // Display a snackbar.
+        Snackbar saveWebpageArchiveSnackbar = Snackbar.make(currentWebView, getString(R.string.file_saved) + "  " + filePath, Snackbar.LENGTH_SHORT);
+
+        // Add an open option to the snackbar.
+        saveWebpageArchiveSnackbar.setAction(R.string.open, (View view) -> {
+            // Get a file for the file name string.
+            File file = new File(filePath);
+
+            // Declare a file URI variable.
+            Uri fileUri;
+
+            // Get the URI for the file according to the Android version.
+            if (Build.VERSION.SDK_INT >= 24) {  // Use a file provider.
+                fileUri = FileProvider.getUriForFile(this, getString(R.string.file_provider), file);
+            } else {  // Get the raw file path URI.
+                fileUri = Uri.fromFile(file);
+            }
+
+            // Get a handle for the content resolver.
+            ContentResolver contentResolver = getContentResolver();
+
+            // Create an open intent with `ACTION_VIEW`.
+            Intent openIntent = new Intent(Intent.ACTION_VIEW);
+
+            // Set the URI and the MIME type.
+            openIntent.setDataAndType(fileUri, contentResolver.getType(fileUri));
+
+            // Allow the app to read the file URI.
+            openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+            // Show the chooser.
+            startActivity(Intent.createChooser(openIntent, getString(R.string.open)));
+        });
+
+        // Show the snackbar.
+        saveWebpageArchiveSnackbar.show();
+    }
+
     private void clearAndExit() {
         // Get a handle for the shared preferences.
         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
@@ -4985,6 +4997,19 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             }
         }
 
+        // Clear the logcat.
+        if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_logcat_key), true)) {
+            try {
+                // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
+                Process process = Runtime.getRuntime().exec("logcat -b all -c");
+
+                // Wait for the process to finish.
+                process.waitFor();
+            } catch (IOException|InterruptedException exception) {
+                // Do nothing.
+            }
+        }
+
         // Clear the cache.
         if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
             // Clear the cache from each WebView.
@@ -5041,7 +5066,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                 // Clear the back/forward history for this WebView.
                 nestedScrollWebView.clearHistory();
 
-                // Destroy the internal state of `mainWebView`.
+                // Destroy the internal state of the WebView.
                 nestedScrollWebView.destroy();
             }
         }
@@ -5074,6 +5099,19 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
         System.exit(0);
     }
 
+    public void bookmarksBack(View view) {
+        if (currentBookmarksFolder.isEmpty()) {  // The home folder is displayed.
+            // close the bookmarks drawer.
+            drawerLayout.closeDrawer(GravityCompat.END);
+        } else {  // A subfolder is displayed.
+            // Place the former parent folder in `currentFolder`.
+            currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
+
+            // Load the new folder.
+            loadBookmarksFolder();
+        }
+    }
+
     private void setCurrentWebView(int pageNumber) {
         // Get handles for the URL views.
         RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
@@ -5227,7 +5265,6 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
 
         // Get handles for the activity views.
         FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
-        DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
         RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
         ActionBar actionBar = appCompatDelegate.getSupportActionBar();
         LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
@@ -5381,10 +5418,10 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             }
 
             // Get the file name from the content disposition.
-            String fileNameString = PrepareSaveDialog.getFileNameFromContentDisposition(this, contentDisposition, downloadUrl);
+            String fileNameString = PrepareSaveDialog.getFileNameFromHeaders(this, contentDisposition, mimetype, downloadUrl);
 
             // Instantiate the save dialog.
-            DialogFragment saveDialogFragment = SaveDialog.saveUrl(StoragePermissionDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent,
+            DialogFragment saveDialogFragment = SaveWebpageDialog.saveWebpage(StoragePermissionDialog.SAVE_URL, downloadUrl, formattedFileSizeString, fileNameString, userAgent,
                     nestedScrollWebView.getAcceptFirstPartyCookies());
 
             // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
@@ -5719,8 +5756,14 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     // 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);
+                    try {
+                        // Make it so.
+                        startActivity(emailIntent);
+                    } catch (ActivityNotFoundException exception) {
+                        // Display a snackbar.
+                        Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
+                    }
+
 
                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
                     return true;
@@ -5734,8 +5777,13 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     // 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);
+                    try {
+                        // Make it so.
+                        startActivity(dialIntent);
+                    } catch (ActivityNotFoundException exception) {
+                        // Display a snackbar.
+                        Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
+                    }
 
                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
                     return true;
@@ -6247,7 +6295,7 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                     }
                 }
 
-                // Clear the cache and history if Incognito Mode is enabled.
+                // Clear the cache, history, and logcat if Incognito Mode is enabled.
                 if (incognitoModeEnabled) {
                     // Clear the cache.  `true` includes disk files.
                     nestedScrollWebView.clearCache(true);
@@ -6267,9 +6315,17 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
                         // 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.
                         Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
-                    } catch (IOException e) {
+                    } catch (IOException exception) {
                         // Do nothing if an error is thrown.
                     }
+
+                    // Clear the logcat.
+                    try {
+                        // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
+                        Runtime.getRuntime().exec("logcat -b all -c");
+                    } catch (IOException exception) {
+                        // Do nothing.
+                    }
                 }
 
                 // Get the current page position.
@@ -6411,6 +6467,9 @@ public class MainWebViewActivity extends AppCompatActivity implements CreateBook
             // Get the intent that started the app.
             Intent launchingIntent = getIntent();
 
+            // Reset the intent.  This prevents a duplicate tab from being created on restart.
+            setIntent(new Intent());
+
             // Get the information from the intent.
             String launchingIntentAction = launchingIntent.getAction();
             Uri launchingIntentUriData = launchingIntent.getData();