e086503d8c80dc073c3ea80989887ce87aee6396
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.java
1 /*
2  * Copyright © 2015-2022 Soren Stoutner <soren@stoutner.com>.
3  *
4  * Download cookie code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
5  *
6  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
7  *
8  * Privacy Browser Android is free software: you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation, either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * Privacy Browser Android is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
20  */
21
22 package com.stoutner.privacybrowser.activities;
23
24 import android.animation.ObjectAnimator;
25 import android.annotation.SuppressLint;
26 import android.app.Activity;
27 import android.app.Dialog;
28 import android.app.DownloadManager;
29 import android.app.SearchManager;
30 import android.content.ActivityNotFoundException;
31 import android.content.BroadcastReceiver;
32 import android.content.ClipData;
33 import android.content.ClipboardManager;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.IntentFilter;
37 import android.content.SharedPreferences;
38 import android.content.pm.PackageManager;
39 import android.content.res.Configuration;
40 import android.database.Cursor;
41 import android.graphics.Bitmap;
42 import android.graphics.BitmapFactory;
43 import android.graphics.Typeface;
44 import android.graphics.drawable.BitmapDrawable;
45 import android.graphics.drawable.Drawable;
46 import android.net.Uri;
47 import android.net.http.SslCertificate;
48 import android.net.http.SslError;
49 import android.os.AsyncTask;
50 import android.os.Build;
51 import android.os.Bundle;
52 import android.os.Environment;
53 import android.os.Handler;
54 import android.os.Message;
55 import android.preference.PreferenceManager;
56 import android.print.PrintDocumentAdapter;
57 import android.print.PrintManager;
58 import android.provider.DocumentsContract;
59 import android.provider.OpenableColumns;
60 import android.text.Editable;
61 import android.text.Spanned;
62 import android.text.TextWatcher;
63 import android.text.style.ForegroundColorSpan;
64 import android.util.Patterns;
65 import android.util.TypedValue;
66 import android.view.ContextMenu;
67 import android.view.GestureDetector;
68 import android.view.KeyEvent;
69 import android.view.Menu;
70 import android.view.MenuItem;
71 import android.view.MotionEvent;
72 import android.view.View;
73 import android.view.ViewGroup;
74 import android.view.WindowManager;
75 import android.view.inputmethod.InputMethodManager;
76 import android.webkit.CookieManager;
77 import android.webkit.HttpAuthHandler;
78 import android.webkit.SslErrorHandler;
79 import android.webkit.ValueCallback;
80 import android.webkit.WebBackForwardList;
81 import android.webkit.WebChromeClient;
82 import android.webkit.WebResourceRequest;
83 import android.webkit.WebResourceResponse;
84 import android.webkit.WebSettings;
85 import android.webkit.WebStorage;
86 import android.webkit.WebView;
87 import android.webkit.WebViewClient;
88 import android.webkit.WebViewDatabase;
89 import android.widget.ArrayAdapter;
90 import android.widget.CheckBox;
91 import android.widget.CursorAdapter;
92 import android.widget.EditText;
93 import android.widget.FrameLayout;
94 import android.widget.ImageView;
95 import android.widget.LinearLayout;
96 import android.widget.ListView;
97 import android.widget.ProgressBar;
98 import android.widget.RadioButton;
99 import android.widget.RelativeLayout;
100 import android.widget.TextView;
101
102 import androidx.activity.result.ActivityResultCallback;
103 import androidx.activity.result.ActivityResultLauncher;
104 import androidx.activity.result.contract.ActivityResultContracts;
105 import androidx.annotation.NonNull;
106 import androidx.appcompat.app.ActionBar;
107 import androidx.appcompat.app.ActionBarDrawerToggle;
108 import androidx.appcompat.app.AppCompatActivity;
109 import androidx.appcompat.app.AppCompatDelegate;
110 import androidx.appcompat.widget.Toolbar;
111 import androidx.coordinatorlayout.widget.CoordinatorLayout;
112 import androidx.core.content.res.ResourcesCompat;
113 import androidx.core.view.GravityCompat;
114 import androidx.drawerlayout.widget.DrawerLayout;
115 import androidx.fragment.app.DialogFragment;
116 import androidx.fragment.app.Fragment;
117 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
118 import androidx.viewpager.widget.ViewPager;
119 import androidx.webkit.WebSettingsCompat;
120 import androidx.webkit.WebViewFeature;
121
122 import com.google.android.material.appbar.AppBarLayout;
123 import com.google.android.material.floatingactionbutton.FloatingActionButton;
124 import com.google.android.material.navigation.NavigationView;
125 import com.google.android.material.snackbar.Snackbar;
126 import com.google.android.material.tabs.TabLayout;
127
128 import com.stoutner.privacybrowser.R;
129 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
130 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
131 import com.stoutner.privacybrowser.asynctasks.PopulateBlocklists;
132 import com.stoutner.privacybrowser.asynctasks.PrepareSaveDialog;
133 import com.stoutner.privacybrowser.asynctasks.SaveUrl;
134 import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage;
135 import com.stoutner.privacybrowser.dataclasses.PendingDialog;
136 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
137 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
138 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
139 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
140 import com.stoutner.privacybrowser.dialogs.FontSizeDialog;
141 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
142 import com.stoutner.privacybrowser.dialogs.OpenDialog;
143 import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog;
144 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
145 import com.stoutner.privacybrowser.dialogs.SaveDialog;
146 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
147 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
148 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
149 import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog;
150 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
151 import com.stoutner.privacybrowser.helpers.BlocklistHelper;
152 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
153 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
154 import com.stoutner.privacybrowser.helpers.ProxyHelper;
155 import com.stoutner.privacybrowser.views.NestedScrollWebView;
156
157 import java.io.ByteArrayInputStream;
158 import java.io.ByteArrayOutputStream;
159 import java.io.File;
160 import java.io.FileInputStream;
161 import java.io.FileOutputStream;
162 import java.io.IOException;
163 import java.io.InputStream;
164 import java.io.OutputStream;
165 import java.io.UnsupportedEncodingException;
166
167 import java.net.MalformedURLException;
168 import java.net.URL;
169 import java.net.URLDecoder;
170 import java.net.URLEncoder;
171
172 import java.text.NumberFormat;
173
174 import java.util.ArrayList;
175 import java.util.Date;
176 import java.util.HashMap;
177 import java.util.HashSet;
178 import java.util.List;
179 import java.util.Map;
180 import java.util.Objects;
181 import java.util.Set;
182 import java.util.concurrent.ExecutorService;
183 import java.util.concurrent.Executors;
184
185 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
186         EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener,
187         PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveDialog.SaveListener, UrlHistoryDialog.NavigateHistoryListener,
188         WebViewTabFragment.NewTabListener {
189
190     // Define the public static variables.
191     public static ExecutorService executorService = Executors.newFixedThreadPool(4);
192     public static String orbotStatus = "unknown";
193     public static ArrayList<PendingDialog> pendingDialogsArrayList =  new ArrayList<>();
194     public static String proxyMode = ProxyHelper.NONE;
195
196     // Declare the public static variables.
197     public static String currentBookmarksFolder;
198     public static boolean restartFromBookmarksActivity;
199     public static WebViewPagerAdapter webViewPagerAdapter;
200
201     // Declare the public static views.
202     public static AppBarLayout appBarLayout;
203
204     // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
205     public final static int UNRECOGNIZED_USER_AGENT = -1;
206     public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
207     public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
208     public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
209     public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
210     public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
211
212     // Define the start activity for result request codes.  The public static entry is accessed from `OpenDialog()`.
213     private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 0;
214     public final static int BROWSE_OPEN_REQUEST_CODE = 1;
215
216     // Define the saved instance state constants.
217     private final String SAVED_STATE_ARRAY_LIST = "saved_state_array_list";
218     private final String SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list";
219     private final String SAVED_TAB_POSITION = "saved_tab_position";
220     private final String PROXY_MODE = "proxy_mode";
221
222     // Define the saved instance state variables.
223     private ArrayList<Bundle> savedStateArrayList;
224     private ArrayList<Bundle> savedNestedScrollWebViewStateArrayList;
225     private int savedTabPosition;
226     private String savedProxyMode;
227
228     // Define the class variables.
229     @SuppressWarnings("rawtypes")
230     AsyncTask populateBlocklists;
231
232     // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
233     // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxy()`, and `applyDomainSettings()`.
234     private NestedScrollWebView currentWebView;
235
236     // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
237     private final Map<String, String> customHeaders = new HashMap<>();
238
239     // The search URL is set in `applyAppSettings()` and used in `onNewIntent()`, `loadUrlFromTextBox()`, `initializeApp()`, and `initializeWebView()`.
240     private String searchURL;
241
242     // The blocklists are populated in `finishedPopulatingBlocklists()` and accessed from `initializeWebView()`.
243     private ArrayList<List<String[]>> easyList;
244     private ArrayList<List<String[]>> easyPrivacy;
245     private ArrayList<List<String[]>> fanboysAnnoyanceList;
246     private ArrayList<List<String[]>> fanboysSocialList;
247     private ArrayList<List<String[]>> ultraList;
248     private ArrayList<List<String[]>> ultraPrivacy;
249
250     // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
251     private ActionBarDrawerToggle actionBarDrawerToggle;
252
253     // The color spans are used in `onCreate()` and `highlightUrlText()`.
254     private ForegroundColorSpan redColorSpan;
255     private ForegroundColorSpan initialGrayColorSpan;
256     private ForegroundColorSpan finalGrayColorSpan;
257
258     // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
259     private Cursor bookmarksCursor;
260
261     // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
262     private CursorAdapter bookmarksCursorAdapter;
263
264     // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
265     private String oldFolderNameString;
266
267     // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
268     private ValueCallback<Uri[]> fileChooserCallback;
269
270     // The default progress view offsets are set in `onCreate()` and used in `initializeWebView()`.
271     private int appBarHeight;
272     private int defaultProgressViewStartOffset;
273     private int defaultProgressViewEndOffset;
274
275     // The URL sanitizers are set in `applyAppSettings()` and used in `sanitizeUrl()`.
276     private boolean sanitizeGoogleAnalytics;
277     private boolean sanitizeFacebookClickIds;
278     private boolean sanitizeTwitterAmpRedirects;
279
280     // Declare the class variables
281     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
282     private boolean bottomAppBar;
283     private boolean displayingFullScreenVideo;
284     private boolean downloadWithExternalApp;
285     private boolean fullScreenBrowsingModeEnabled;
286     private boolean hideAppBar;
287     private boolean incognitoModeEnabled;
288     private boolean inFullScreenBrowsingMode;
289     private boolean loadingNewIntent;
290     private BroadcastReceiver orbotStatusBroadcastReceiver;
291     private ProxyHelper proxyHelper;
292     private boolean reapplyAppSettingsOnRestart;
293     private boolean reapplyDomainSettingsOnRestart;
294     private boolean scrollAppBar;
295     private boolean waitingForProxy;
296     private String webViewDefaultUserAgent;
297
298     // Define the class variables.
299     private ObjectAnimator objectAnimator = new ObjectAnimator();
300     private String saveUrlString = "";
301
302     // Declare the class views.
303     private FrameLayout rootFrameLayout;
304     private DrawerLayout drawerLayout;
305     private CoordinatorLayout coordinatorLayout;
306     private Toolbar toolbar;
307     private RelativeLayout urlRelativeLayout;
308     private EditText urlEditText;
309     private ActionBar actionBar;
310     private LinearLayout findOnPageLinearLayout;
311     private LinearLayout tabsLinearLayout;
312     private TabLayout tabLayout;
313     private SwipeRefreshLayout swipeRefreshLayout;
314     private ViewPager webViewPager;
315     private FrameLayout fullScreenVideoFrameLayout;
316
317     // Declare the class menus.
318     private Menu optionsMenu;
319
320     // Declare the class menu items.
321     private MenuItem navigationBackMenuItem;
322     private MenuItem navigationForwardMenuItem;
323     private MenuItem navigationHistoryMenuItem;
324     private MenuItem navigationRequestsMenuItem;
325     private MenuItem optionsPrivacyMenuItem;
326     private MenuItem optionsRefreshMenuItem;
327     private MenuItem optionsCookiesMenuItem;
328     private MenuItem optionsDomStorageMenuItem;
329     private MenuItem optionsSaveFormDataMenuItem;
330     private MenuItem optionsClearDataMenuItem;
331     private MenuItem optionsClearCookiesMenuItem;
332     private MenuItem optionsClearDomStorageMenuItem;
333     private MenuItem optionsClearFormDataMenuItem;
334     private MenuItem optionsBlocklistsMenuItem;
335     private MenuItem optionsEasyListMenuItem;
336     private MenuItem optionsEasyPrivacyMenuItem;
337     private MenuItem optionsFanboysAnnoyanceListMenuItem;
338     private MenuItem optionsFanboysSocialBlockingListMenuItem;
339     private MenuItem optionsUltraListMenuItem;
340     private MenuItem optionsUltraPrivacyMenuItem;
341     private MenuItem optionsBlockAllThirdPartyRequestsMenuItem;
342     private MenuItem optionsProxyMenuItem;
343     private MenuItem optionsProxyNoneMenuItem;
344     private MenuItem optionsProxyTorMenuItem;
345     private MenuItem optionsProxyI2pMenuItem;
346     private MenuItem optionsProxyCustomMenuItem;
347     private MenuItem optionsUserAgentMenuItem;
348     private MenuItem optionsUserAgentPrivacyBrowserMenuItem;
349     private MenuItem optionsUserAgentWebViewDefaultMenuItem;
350     private MenuItem optionsUserAgentFirefoxOnAndroidMenuItem;
351     private MenuItem optionsUserAgentChromeOnAndroidMenuItem;
352     private MenuItem optionsUserAgentSafariOnIosMenuItem;
353     private MenuItem optionsUserAgentFirefoxOnLinuxMenuItem;
354     private MenuItem optionsUserAgentChromiumOnLinuxMenuItem;
355     private MenuItem optionsUserAgentFirefoxOnWindowsMenuItem;
356     private MenuItem optionsUserAgentChromeOnWindowsMenuItem;
357     private MenuItem optionsUserAgentEdgeOnWindowsMenuItem;
358     private MenuItem optionsUserAgentInternetExplorerOnWindowsMenuItem;
359     private MenuItem optionsUserAgentSafariOnMacosMenuItem;
360     private MenuItem optionsUserAgentCustomMenuItem;
361     private MenuItem optionsSwipeToRefreshMenuItem;
362     private MenuItem optionsWideViewportMenuItem;
363     private MenuItem optionsDisplayImagesMenuItem;
364     private MenuItem optionsDarkWebViewMenuItem;
365     private MenuItem optionsFontSizeMenuItem;
366     private MenuItem optionsAddOrEditDomainMenuItem;
367
368     // This variable won't be needed once the class is migrated to Kotlin, as can be seen in LogcatActivity or AboutVersionFragment.
369     private Activity resultLauncherActivityHandle;
370
371     // Define the save URL activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
372     private final ActivityResultLauncher<String> saveUrlActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument(),
373             new ActivityResultCallback<Uri>() {
374                 @Override
375                 public void onActivityResult(Uri fileUri) {
376                     // Only save the URL if the file URI is not null, which happens if the user exited the file picker by pressing back.
377                     if (fileUri != null) {
378                         new SaveUrl(getApplicationContext(), resultLauncherActivityHandle, fileUri, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()).execute(saveUrlString);
379                     }
380
381                     // Reset the save URL string.
382                     saveUrlString = "";
383                 }
384             });
385
386     // Define the save webpage archive activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
387     private final ActivityResultLauncher<String> saveWebpageArchiveActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument(),
388             new ActivityResultCallback<Uri>() {
389                 @Override
390                 public void onActivityResult(Uri fileUri) {
391                     // Only save the webpage archive if the file URI is not null, which happens if the user exited the file picker by pressing back.
392                     if (fileUri != null) {
393                         try {
394                             // Create a temporary MHT file.
395                             File temporaryMhtFile = File.createTempFile("temporary_mht_file", ".mht", getCacheDir());
396
397                             // Save the temporary MHT file.
398                             currentWebView.saveWebArchive(temporaryMhtFile.toString(), false, callbackValue -> {
399                                 if (callbackValue != null) {  // The temporary MHT file was saved successfully.
400                                     try {
401                                         // Create a temporary MHT file input stream.
402                                         FileInputStream temporaryMhtFileInputStream = new FileInputStream(temporaryMhtFile);
403
404                                         // Get an output stream for the save webpage file path.
405                                         OutputStream mhtOutputStream = getContentResolver().openOutputStream(fileUri);
406
407                                         // Create a transfer byte array.
408                                         byte[] transferByteArray = new byte[1024];
409
410                                         // Create an integer to track the number of bytes read.
411                                         int bytesRead;
412
413                                         // Copy the temporary MHT file input stream to the MHT output stream.
414                                         while ((bytesRead = temporaryMhtFileInputStream.read(transferByteArray)) > 0) {
415                                             mhtOutputStream.write(transferByteArray, 0, bytesRead);
416                                         }
417
418                                         // Close the streams.
419                                         mhtOutputStream.close();
420                                         temporaryMhtFileInputStream.close();
421
422                                         // Initialize the file name string from the file URI last path segment.
423                                         String fileNameString = fileUri.getLastPathSegment();
424
425                                         // Query the exact file name if the API >= 26.
426                                         if (Build.VERSION.SDK_INT >= 26) {
427                                             // Get a cursor from the content resolver.
428                                             Cursor contentResolverCursor = resultLauncherActivityHandle.getContentResolver().query(fileUri, null, null, null);
429
430                                             // Move to the fist row.
431                                             contentResolverCursor.moveToFirst();
432
433                                             // Get the file name from the cursor.
434                                             fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
435
436                                             // Close the cursor.
437                                             contentResolverCursor.close();
438                                         }
439
440                                         // Display a snackbar.
441                                         Snackbar.make(currentWebView, getString(R.string.file_saved) + "  " + fileNameString, Snackbar.LENGTH_SHORT).show();
442                                     } catch (Exception exception) {
443                                         // Display a snackbar with the exception.
444                                         Snackbar.make(currentWebView, getString(R.string.error_saving_file) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
445                                     } finally {
446                                         // Delete the temporary MHT file.
447                                         //noinspection ResultOfMethodCallIgnored
448                                         temporaryMhtFile.delete();
449                                     }
450                                 } else {  // There was an unspecified error while saving the temporary MHT file.
451                                     // Display an error snackbar.
452                                     Snackbar.make(currentWebView, getString(R.string.error_saving_file), Snackbar.LENGTH_INDEFINITE).show();
453                                 }
454                             });
455                         } catch (IOException ioException) {
456                             // Display a snackbar with the IO exception.
457                             Snackbar.make(currentWebView, getString(R.string.error_saving_file) + "  " + ioException.toString(), Snackbar.LENGTH_INDEFINITE).show();
458                         }
459                     }
460                 }
461             });
462
463     // Define the save webpage image activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
464     private final ActivityResultLauncher<String> saveWebpageImageActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument(),
465             new ActivityResultCallback<Uri>() {
466                 @Override
467                 public void onActivityResult(Uri fileUri) {
468                     // Only save the webpage image if the file URI is not null, which happens if the user exited the file picker by pressing back.
469                     if (fileUri != null) {
470                         // Save the webpage image.
471                         new SaveWebpageImage(resultLauncherActivityHandle, fileUri, currentWebView).execute();
472                     }
473                 }
474             });
475
476     // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with WebView.
477     @SuppressLint("ClickableViewAccessibility")
478     @Override
479     protected void onCreate(Bundle savedInstanceState) {
480         // Run the default commands.
481         super.onCreate(savedInstanceState);
482
483         // Populate the result launcher activity.  This will no longer be needed once the activity has transitioned to Kotlin.
484         resultLauncherActivityHandle = this;
485
486         // Check to see if the activity has been restarted.
487         if (savedInstanceState != null) {
488             // Store the saved instance state variables.
489             savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST);
490             savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST);
491             savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION);
492             savedProxyMode = savedInstanceState.getString(PROXY_MODE);
493         }
494
495         // Initialize the default preference values the first time the program is run.  `false` keeps this command from resetting any current preferences back to default.
496         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
497
498         // Get a handle for the shared preferences.
499         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
500
501         // Get the preferences.
502         String appTheme = sharedPreferences.getString("app_theme", getString(R.string.app_theme_default_value));
503         boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
504         bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false);
505
506         // Get the theme entry values string array.
507         String[] appThemeEntryValuesStringArray = getResources().getStringArray(R.array.app_theme_entry_values);
508
509         // Set the app theme according to the preference.  A switch statement cannot be used because the theme entry values string array is not a compile time constant.
510         if (appTheme.equals(appThemeEntryValuesStringArray[1])) {  // The light theme is selected.
511             // Apply the light theme.
512             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
513         } else if (appTheme.equals(appThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
514             // Apply the dark theme.
515             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
516         } else {  // The system default theme is selected.
517             if (Build.VERSION.SDK_INT >= 28) {  // The system default theme is supported.
518                 // Follow the system default theme.
519                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
520             } else {  // The system default theme is not supported.
521                 // Follow the battery saver mode.
522                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
523             }
524         }
525
526         // Disable screenshots if not allowed.
527         if (!allowScreenshots) {
528             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
529         }
530
531         // Enable the drawing of the entire webpage.  This makes it possible to save a website image.  This must be done before anything else happens with the WebView.
532         WebView.enableSlowWholeDocumentDraw();
533
534         // Set the theme.
535         setTheme(R.style.PrivacyBrowser);
536
537         // Set the content view.
538         if (bottomAppBar) {
539             setContentView(R.layout.main_framelayout_bottom_appbar);
540         } else {
541             setContentView(R.layout.main_framelayout_top_appbar);
542         }
543
544         // Get handles for the views.
545         rootFrameLayout = findViewById(R.id.root_framelayout);
546         drawerLayout = findViewById(R.id.drawerlayout);
547         coordinatorLayout = findViewById(R.id.coordinatorlayout);
548         appBarLayout = findViewById(R.id.appbar_layout);
549         toolbar = findViewById(R.id.toolbar);
550         findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
551         tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
552         tabLayout = findViewById(R.id.tablayout);
553         swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
554         webViewPager = findViewById(R.id.webviewpager);
555         NavigationView navigationView = findViewById(R.id.navigationview);
556         fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
557
558         // Get a handle for the navigation menu.
559         Menu navigationMenu = navigationView.getMenu();
560
561         // Get handles for the navigation menu items.
562         navigationBackMenuItem = navigationMenu.findItem(R.id.back);
563         navigationForwardMenuItem = navigationMenu.findItem(R.id.forward);
564         navigationHistoryMenuItem = navigationMenu.findItem(R.id.history);
565         navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests);
566
567         // Listen for touches on the navigation menu.
568         navigationView.setNavigationItemSelectedListener(this);
569
570         // Get a handle for the app compat delegate.
571         AppCompatDelegate appCompatDelegate = getDelegate();
572
573         // Set the support action bar.
574         appCompatDelegate.setSupportActionBar(toolbar);
575
576         // Get a handle for the action bar.
577         actionBar = appCompatDelegate.getSupportActionBar();
578
579         // Remove the incorrect lint warning below that the action bar might be null.
580         assert actionBar != null;
581
582         // Add the custom layout, which shows the URL text bar.
583         actionBar.setCustomView(R.layout.url_app_bar);
584         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
585
586         // Get handles for the views in the URL app bar.
587         urlRelativeLayout = findViewById(R.id.url_relativelayout);
588         urlEditText = findViewById(R.id.url_edittext);
589
590         // Create the hamburger icon at the start of the AppBar.
591         actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
592
593         // Initially disable the sliding drawers.  They will be enabled once the blocklists are loaded.
594         drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
595
596         // Initialize the web view pager adapter.
597         webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
598
599         // Set the pager adapter on the web view pager.
600         webViewPager.setAdapter(webViewPagerAdapter);
601
602         // Store up to 100 tabs in memory.
603         webViewPager.setOffscreenPageLimit(100);
604
605         // Instantiate the proxy helper.
606         proxyHelper = new ProxyHelper();
607
608         // Initialize the app.
609         initializeApp();
610
611         // Apply the app settings from the shared preferences.
612         applyAppSettings();
613
614         // Populate the blocklists.
615         populateBlocklists = new PopulateBlocklists(this, this).execute();
616     }
617
618     @Override
619     protected void onNewIntent(Intent intent) {
620         // Run the default commands.
621         super.onNewIntent(intent);
622
623         // Replace the intent that started the app with this one.
624         setIntent(intent);
625
626         // Check to see if the app is being restarted from a saved state.
627         if (savedStateArrayList == null || savedStateArrayList.size() == 0) {  // The activity is not being restarted from a saved state.
628             // Get the information from the intent.
629             String intentAction = intent.getAction();
630             Uri intentUriData = intent.getData();
631             String intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
632
633             // Determine if this is a web search.
634             boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH));
635
636             // Only process the URI if it contains data or it is a web search.  If the user pressed the desktop icon after the app was already running the URI will be null.
637             if (intentUriData != null || intentStringExtra != null || isWebSearch) {
638                 // Exit the full screen video if it is displayed.
639                 if (displayingFullScreenVideo) {
640                     // Exit full screen video mode.
641                     exitFullScreenVideo();
642
643                     // Reload the current WebView.  Otherwise, it can display entirely black.
644                     currentWebView.reload();
645                 }
646
647                 // Get the shared preferences.
648                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
649
650                 // Create a URL string.
651                 String url;
652
653                 // If the intent action is a web search, perform the search.
654                 if (isWebSearch) {  // The intent is a web search.
655                     // Create an encoded URL string.
656                     String encodedUrlString;
657
658                     // Sanitize the search input and convert it to a search.
659                     try {
660                         encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
661                     } catch (UnsupportedEncodingException exception) {
662                         encodedUrlString = "";
663                     }
664
665                     // Add the base search URL.
666                     url = searchURL + encodedUrlString;
667                 } else if (intentUriData != null) {  // The intent contains a URL formatted as a URI.
668                     // Set the intent data as the URL.
669                     url = intentUriData.toString();
670                 } else {  // The intent contains a string, which might be a URL.
671                     // Set the intent string as the URL.
672                     url = intentStringExtra;
673                 }
674
675                 // Add a new tab if specified in the preferences.
676                 if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) {  // Load the URL in a new tab.
677                     // Set the loading new intent flag.
678                     loadingNewIntent = true;
679
680                     // Add a new tab.
681                     addNewTab(url, true);
682                 } else {  // Load the URL in the current tab.
683                     // Make it so.
684                     loadUrl(currentWebView, url);
685                 }
686
687                 // Close the navigation drawer if it is open.
688                 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
689                     drawerLayout.closeDrawer(GravityCompat.START);
690                 }
691
692                 // Close the bookmarks drawer if it is open.
693                 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
694                     drawerLayout.closeDrawer(GravityCompat.END);
695                 }
696             }
697         }
698     }
699
700     @Override
701     public void onRestart() {
702         // Run the default commands.
703         super.onRestart();
704
705         // Apply the app settings if returning from the Settings activity.
706         if (reapplyAppSettingsOnRestart) {
707             // Reset the reapply app settings on restart tracker.
708             reapplyAppSettingsOnRestart = false;
709
710             // Apply the app settings.
711             applyAppSettings();
712         }
713
714         // Apply the domain settings if returning from the settings or domains activity.
715         if (reapplyDomainSettingsOnRestart) {
716             // Reset the reapply domain settings on restart tracker.
717             reapplyDomainSettingsOnRestart = false;
718
719             // Reapply the domain settings for each tab.
720             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
721                 // Get the WebView tab fragment.
722                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
723
724                 // Get the fragment view.
725                 View fragmentView = webViewTabFragment.getView();
726
727                 // Only reload the WebViews if they exist.
728                 if (fragmentView != null) {
729                     // Get the nested scroll WebView from the tab fragment.
730                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
731
732                     // Reset the current domain name so the domain settings will be reapplied.
733                     nestedScrollWebView.setCurrentDomainName("");
734
735                     // Reapply the domain settings if the URL is not null, which can happen if an empty tab is active when returning from settings.
736                     if (nestedScrollWebView.getUrl() != null) {
737                         applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true, false);
738                     }
739                 }
740             }
741         }
742
743         // Update the bookmarks drawer if returning from the Bookmarks activity.
744         if (restartFromBookmarksActivity) {
745             // Close the bookmarks drawer.
746             drawerLayout.closeDrawer(GravityCompat.END);
747
748             // Reload the bookmarks drawer.
749             loadBookmarksFolder();
750
751             // Reset `restartFromBookmarksActivity`.
752             restartFromBookmarksActivity = false;
753         }
754
755         // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.  This can be important if the screen was rotated.
756         updatePrivacyIcons(true);
757     }
758
759     // `onStart()` runs after `onCreate()` or `onRestart()`.  This is used instead of `onResume()` so the commands aren't called every time the screen is partially hidden.
760     @Override
761     public void onStart() {
762         // Run the default commands.
763         super.onStart();
764
765         // Resume any WebViews.
766         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
767             // Get the WebView tab fragment.
768             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
769
770             // Get the fragment view.
771             View fragmentView = webViewTabFragment.getView();
772
773             // Only resume the WebViews if they exist (they won't when the app is first created).
774             if (fragmentView != null) {
775                 // Get the nested scroll WebView from the tab fragment.
776                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
777
778                 // Resume the nested scroll WebView.
779                 nestedScrollWebView.onResume();
780             }
781         }
782
783         // Resume the nested scroll WebView JavaScript timers.  This is a global command that resumes JavaScript timers on all WebViews.
784         if (currentWebView != null) {
785             currentWebView.resumeTimers();
786         }
787
788         // Reapply the proxy settings if the system is using a proxy.  This redisplays the appropriate alert dialog.
789         if (!proxyMode.equals(ProxyHelper.NONE)) {
790             applyProxy(false);
791         }
792
793         // Reapply any system UI flags.
794         if (displayingFullScreenVideo || inFullScreenBrowsingMode) {  // The system is displaying a website or a video in full screen mode.
795             /* Hide the system bars.
796              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
797              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
798              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
799              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
800              */
801             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
802                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
803         }
804
805         // Show any pending dialogs.
806         for (int i = 0; i < pendingDialogsArrayList.size(); i++) {
807             // Get the pending dialog from the array list.
808             PendingDialog pendingDialog = pendingDialogsArrayList.get(i);
809
810             // Show the pending dialog.
811             pendingDialog.dialogFragment.show(getSupportFragmentManager(), pendingDialog.tag);
812         }
813
814         // Clear the pending dialogs array list.
815         pendingDialogsArrayList.clear();
816     }
817
818     // `onStop()` runs after `onPause()`.  It is used instead of `onPause()` so the commands are not called every time the screen is partially hidden.
819     @Override
820     public void onStop() {
821         // Run the default commands.
822         super.onStop();
823
824         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
825             // Get the WebView tab fragment.
826             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
827
828             // Get the fragment view.
829             View fragmentView = webViewTabFragment.getView();
830
831             // Only pause the WebViews if they exist (they won't when the app is first created).
832             if (fragmentView != null) {
833                 // Get the nested scroll WebView from the tab fragment.
834                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
835
836                 // Pause the nested scroll WebView.
837                 nestedScrollWebView.onPause();
838             }
839         }
840
841         // Pause the WebView JavaScript timers.  This is a global command that pauses JavaScript on all WebViews.
842         if (currentWebView != null) {
843             currentWebView.pauseTimers();
844         }
845     }
846
847     @Override
848     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
849         // Run the default commands.
850         super.onSaveInstanceState(savedInstanceState);
851
852         // Create the saved state array lists.
853         ArrayList<Bundle> savedStateArrayList = new ArrayList<>();
854         ArrayList<Bundle> savedNestedScrollWebViewStateArrayList = new ArrayList<>();
855
856         // Get the URLs from each tab.
857         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
858             // Get the WebView tab fragment.
859             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
860
861             // Get the fragment view.
862             View fragmentView = webViewTabFragment.getView();
863
864             if (fragmentView != null) {
865                 // Get the nested scroll WebView from the tab fragment.
866                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
867
868                 // Create saved state bundle.
869                 Bundle savedStateBundle = new Bundle();
870
871                 // Get the current states.
872                 nestedScrollWebView.saveState(savedStateBundle);
873                 Bundle savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState();
874
875                 // Store the saved states in the array lists.
876                 savedStateArrayList.add(savedStateBundle);
877                 savedNestedScrollWebViewStateArrayList.add(savedNestedScrollWebViewStateBundle);
878             }
879         }
880
881         // Get the current tab position.
882         int currentTabPosition = tabLayout.getSelectedTabPosition();
883
884         // Store the saved states in the bundle.
885         savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList);
886         savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList);
887         savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition);
888         savedInstanceState.putString(PROXY_MODE, proxyMode);
889     }
890
891     @Override
892     public void onDestroy() {
893         // Unregister the orbot status broadcast receiver if it exists.
894         if (orbotStatusBroadcastReceiver != null) {
895             this.unregisterReceiver(orbotStatusBroadcastReceiver);
896         }
897
898         // Close the bookmarks cursor if it exists.
899         if (bookmarksCursor != null) {
900             bookmarksCursor.close();
901         }
902
903         // Close the bookmarks database if it exists.
904         if (bookmarksDatabaseHelper != null) {
905             bookmarksDatabaseHelper.close();
906         }
907
908         // Stop populating the blocklists if the AsyncTask is running in the background.
909         if (populateBlocklists != null) {
910             populateBlocklists.cancel(true);
911         }
912
913         // Run the default commands.
914         super.onDestroy();
915     }
916
917     @Override
918     public boolean onCreateOptionsMenu(Menu menu) {
919         // Inflate the menu.  This adds items to the action bar if it is present.
920         getMenuInflater().inflate(R.menu.webview_options_menu, menu);
921
922         // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
923         optionsMenu = menu;
924
925         // Get handles for the menu items.
926         optionsPrivacyMenuItem = menu.findItem(R.id.javascript);
927         optionsRefreshMenuItem = menu.findItem(R.id.refresh);
928         MenuItem bookmarksMenuItem = menu.findItem(R.id.bookmarks);
929         optionsCookiesMenuItem = menu.findItem(R.id.cookies);
930         optionsDomStorageMenuItem = menu.findItem(R.id.dom_storage);
931         optionsSaveFormDataMenuItem = menu.findItem(R.id.save_form_data);  // Form data can be removed once the minimum API >= 26.
932         optionsClearDataMenuItem = menu.findItem(R.id.clear_data);
933         optionsClearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
934         optionsClearDomStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
935         optionsClearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
936         optionsBlocklistsMenuItem = menu.findItem(R.id.blocklists);
937         optionsEasyListMenuItem = menu.findItem(R.id.easylist);
938         optionsEasyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
939         optionsFanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
940         optionsFanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
941         optionsUltraListMenuItem = menu.findItem(R.id.ultralist);
942         optionsUltraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
943         optionsBlockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
944         optionsProxyMenuItem = menu.findItem(R.id.proxy);
945         optionsProxyNoneMenuItem = menu.findItem(R.id.proxy_none);
946         optionsProxyTorMenuItem = menu.findItem(R.id.proxy_tor);
947         optionsProxyI2pMenuItem = menu.findItem(R.id.proxy_i2p);
948         optionsProxyCustomMenuItem = menu.findItem(R.id.proxy_custom);
949         optionsUserAgentMenuItem = menu.findItem(R.id.user_agent);
950         optionsUserAgentPrivacyBrowserMenuItem = menu.findItem(R.id.user_agent_privacy_browser);
951         optionsUserAgentWebViewDefaultMenuItem = menu.findItem(R.id.user_agent_webview_default);
952         optionsUserAgentFirefoxOnAndroidMenuItem = menu.findItem(R.id.user_agent_firefox_on_android);
953         optionsUserAgentChromeOnAndroidMenuItem = menu.findItem(R.id.user_agent_chrome_on_android);
954         optionsUserAgentSafariOnIosMenuItem = menu.findItem(R.id.user_agent_safari_on_ios);
955         optionsUserAgentFirefoxOnLinuxMenuItem = menu.findItem(R.id.user_agent_firefox_on_linux);
956         optionsUserAgentChromiumOnLinuxMenuItem = menu.findItem(R.id.user_agent_chromium_on_linux);
957         optionsUserAgentFirefoxOnWindowsMenuItem = menu.findItem(R.id.user_agent_firefox_on_windows);
958         optionsUserAgentChromeOnWindowsMenuItem = menu.findItem(R.id.user_agent_chrome_on_windows);
959         optionsUserAgentEdgeOnWindowsMenuItem = menu.findItem(R.id.user_agent_edge_on_windows);
960         optionsUserAgentInternetExplorerOnWindowsMenuItem = menu.findItem(R.id.user_agent_internet_explorer_on_windows);
961         optionsUserAgentSafariOnMacosMenuItem = menu.findItem(R.id.user_agent_safari_on_macos);
962         optionsUserAgentCustomMenuItem = menu.findItem(R.id.user_agent_custom);
963         optionsSwipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
964         optionsWideViewportMenuItem = menu.findItem(R.id.wide_viewport);
965         optionsDisplayImagesMenuItem = menu.findItem(R.id.display_images);
966         optionsDarkWebViewMenuItem = menu.findItem(R.id.dark_webview);
967         optionsFontSizeMenuItem = menu.findItem(R.id.font_size);
968         optionsAddOrEditDomainMenuItem = menu.findItem(R.id.add_or_edit_domain);
969
970         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
971         updatePrivacyIcons(false);
972
973         // Only display the form data menu items if the API < 26.
974         optionsSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
975         optionsClearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
976
977         // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
978         optionsClearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
979
980         // Get the shared preferences.
981         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
982
983         // Get the dark theme and app bar preferences.
984         boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false);
985
986         // Set the status of the additional app bar icons.  Setting the refresh menu item to `SHOW_AS_ACTION_ALWAYS` makes it appear even on small devices like phones.
987         if (displayAdditionalAppBarIcons) {
988             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
989             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
990             optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
991         } else { //Do not display the additional icons.
992             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
993             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
994             optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
995         }
996
997         // Replace Refresh with Stop if a URL is already loading.
998         if (currentWebView != null && currentWebView.getProgress() != 100) {
999             // Set the title.
1000             optionsRefreshMenuItem.setTitle(R.string.stop);
1001
1002             // Set the icon if it is displayed in the app bar.
1003             if (displayAdditionalAppBarIcons) {
1004                 // Get the current theme status.
1005                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
1006
1007                 // Set the icon according to the current theme status.
1008                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
1009                     optionsRefreshMenuItem.setIcon(R.drawable.close_blue_day);
1010                 } else {
1011                     optionsRefreshMenuItem.setIcon(R.drawable.close_blue_night);
1012                 }
1013             }
1014         }
1015
1016         // Done.
1017         return true;
1018     }
1019
1020     @Override
1021     public boolean onPrepareOptionsMenu(Menu menu) {
1022         // Get a handle for the cookie manager.
1023         CookieManager cookieManager = CookieManager.getInstance();
1024
1025         // Initialize the current user agent string and the font size.
1026         String currentUserAgent = getString(R.string.user_agent_privacy_browser);
1027         int fontSize = 100;
1028
1029         // Set items that require the current web view to be populated.  It will be null when the program is first opened, as `onPrepareOptionsMenu()` is called before the first WebView is initialized.
1030         if (currentWebView != null) {
1031             // Set the add or edit domain text.
1032             if (currentWebView.getDomainSettingsApplied()) {
1033                 optionsAddOrEditDomainMenuItem.setTitle(R.string.edit_domain_settings);
1034             } else {
1035                 optionsAddOrEditDomainMenuItem.setTitle(R.string.add_domain_settings);
1036             }
1037
1038             // Get the current user agent from the WebView.
1039             currentUserAgent = currentWebView.getSettings().getUserAgentString();
1040
1041             // Get the current font size from the
1042             fontSize = currentWebView.getSettings().getTextZoom();
1043
1044             // Set the status of the menu item checkboxes.
1045             optionsDomStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1046             optionsSaveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData());  // Form data can be removed once the minimum API >= 26.
1047             optionsEasyListMenuItem.setChecked(currentWebView.getEasyListEnabled());
1048             optionsEasyPrivacyMenuItem.setChecked(currentWebView.getEasyPrivacyEnabled());
1049             optionsFanboysAnnoyanceListMenuItem.setChecked(currentWebView.getFanboysAnnoyanceListEnabled());
1050             optionsFanboysSocialBlockingListMenuItem.setChecked(currentWebView.getFanboysSocialBlockingListEnabled());
1051             optionsUltraListMenuItem.setChecked(currentWebView.getUltraListEnabled());
1052             optionsUltraPrivacyMenuItem.setChecked(currentWebView.getUltraPrivacyEnabled());
1053             optionsBlockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.getBlockAllThirdPartyRequests());
1054             optionsSwipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
1055             optionsWideViewportMenuItem.setChecked(currentWebView.getSettings().getUseWideViewPort());
1056             optionsDisplayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
1057
1058             // Initialize the display names for the blocklists with the number of blocked requests.
1059             optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
1060             optionsEasyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
1061             optionsEasyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
1062             optionsFanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
1063             optionsFanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
1064             optionsUltraListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
1065             optionsUltraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
1066             optionsBlockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
1067
1068             // Enable DOM Storage if JavaScript is enabled.
1069             optionsDomStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
1070
1071             // Set the checkbox status for dark WebView if the WebView supports it.
1072             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
1073                 optionsDarkWebViewMenuItem.setChecked(WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON);
1074             }
1075         }
1076
1077         // Set the cookies menu item checked status.
1078         optionsCookiesMenuItem.setChecked(cookieManager.acceptCookie());
1079
1080         // Enable Clear Cookies if there are any.
1081         optionsClearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1082
1083         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`.
1084         String privateDataDirectoryString = getApplicationInfo().dataDir;
1085
1086         // Get a count of the number of files in the Local Storage directory.
1087         File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1088         int localStorageDirectoryNumberOfFiles = 0;
1089         if (localStorageDirectory.exists()) {
1090             // `Objects.requireNonNull` removes a lint warning that `localStorageDirectory.list` might produce a null pointed exception if it is dereferenced.
1091             localStorageDirectoryNumberOfFiles = Objects.requireNonNull(localStorageDirectory.list()).length;
1092         }
1093
1094         // Get a count of the number of files in the IndexedDB directory.
1095         File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1096         int indexedDBDirectoryNumberOfFiles = 0;
1097         if (indexedDBDirectory.exists()) {
1098             // `Objects.requireNonNull` removes a lint warning that `indexedDBDirectory.list` might produce a null pointed exception if it is dereferenced.
1099             indexedDBDirectoryNumberOfFiles = Objects.requireNonNull(indexedDBDirectory.list()).length;
1100         }
1101
1102         // Enable Clear DOM Storage if there is any.
1103         optionsClearDomStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1104
1105         // Enable Clear Form Data is there is any.  This can be removed once the minimum API >= 26.
1106         if (Build.VERSION.SDK_INT < 26) {
1107             // Get the WebView database.
1108             WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
1109
1110             // Enable the clear form data menu item if there is anything to clear.
1111             optionsClearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
1112         }
1113
1114         // Enable Clear Data if any of the submenu items are enabled.
1115         optionsClearDataMenuItem.setEnabled(optionsClearCookiesMenuItem.isEnabled() || optionsClearDomStorageMenuItem.isEnabled() || optionsClearFormDataMenuItem.isEnabled());
1116
1117         // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1118         optionsFanboysSocialBlockingListMenuItem.setEnabled(!optionsFanboysAnnoyanceListMenuItem.isChecked());
1119
1120         // Set the proxy title and check the applied proxy.
1121         switch (proxyMode) {
1122             case ProxyHelper.NONE:
1123                 // Set the proxy title.
1124                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_none));
1125
1126                 // Check the proxy None radio button.
1127                 optionsProxyNoneMenuItem.setChecked(true);
1128                 break;
1129
1130             case ProxyHelper.TOR:
1131                 // Set the proxy title.
1132                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_tor));
1133
1134                 // Check the proxy Tor radio button.
1135                 optionsProxyTorMenuItem.setChecked(true);
1136                 break;
1137
1138             case ProxyHelper.I2P:
1139                 // Set the proxy title.
1140                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_i2p));
1141
1142                 // Check the proxy I2P radio button.
1143                 optionsProxyI2pMenuItem.setChecked(true);
1144                 break;
1145
1146             case ProxyHelper.CUSTOM:
1147                 // Set the proxy title.
1148                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_custom));
1149
1150                 // Check the proxy Custom radio button.
1151                 optionsProxyCustomMenuItem.setChecked(true);
1152                 break;
1153         }
1154
1155         // Select the current user agent menu item.  A switch statement cannot be used because the user agents are not compile time constants.
1156         if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) {  // Privacy Browser.
1157             // Update the user agent menu item title.
1158             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_privacy_browser));
1159
1160             // Select the Privacy Browser radio box.
1161             optionsUserAgentPrivacyBrowserMenuItem.setChecked(true);
1162         } else if (currentUserAgent.equals(webViewDefaultUserAgent)) {  // WebView Default.
1163             // Update the user agent menu item title.
1164             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_webview_default));
1165
1166             // Select the WebView Default radio box.
1167             optionsUserAgentWebViewDefaultMenuItem.setChecked(true);
1168         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) {  // Firefox on Android.
1169             // Update the user agent menu item title.
1170             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_android));
1171
1172             // Select the Firefox on Android radio box.
1173             optionsUserAgentFirefoxOnAndroidMenuItem.setChecked(true);
1174         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) {  // Chrome on Android.
1175             // Update the user agent menu item title.
1176             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_android));
1177
1178             // Select the Chrome on Android radio box.
1179             optionsUserAgentChromeOnAndroidMenuItem.setChecked(true);
1180         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) {  // Safari on iOS.
1181             // Update the user agent menu item title.
1182             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_ios));
1183
1184             // Select the Safari on iOS radio box.
1185             optionsUserAgentSafariOnIosMenuItem.setChecked(true);
1186         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) {  // Firefox on Linux.
1187             // Update the user agent menu item title.
1188             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_linux));
1189
1190             // Select the Firefox on Linux radio box.
1191             optionsUserAgentFirefoxOnLinuxMenuItem.setChecked(true);
1192         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) {  // Chromium on Linux.
1193             // Update the user agent menu item title.
1194             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chromium_on_linux));
1195
1196             // Select the Chromium on Linux radio box.
1197             optionsUserAgentChromiumOnLinuxMenuItem.setChecked(true);
1198         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) {  // Firefox on Windows.
1199             // Update the user agent menu item title.
1200             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_windows));
1201
1202             // Select the Firefox on Windows radio box.
1203             optionsUserAgentFirefoxOnWindowsMenuItem.setChecked(true);
1204         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) {  // Chrome on Windows.
1205             // Update the user agent menu item title.
1206             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_windows));
1207
1208             // Select the Chrome on Windows radio box.
1209             optionsUserAgentChromeOnWindowsMenuItem.setChecked(true);
1210         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) {  // Edge on Windows.
1211             // Update the user agent menu item title.
1212             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_edge_on_windows));
1213
1214             // Select the Edge on Windows radio box.
1215             optionsUserAgentEdgeOnWindowsMenuItem.setChecked(true);
1216         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) {  // Internet Explorer on Windows.
1217             // Update the user agent menu item title.
1218             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_internet_explorer_on_windows));
1219
1220             // Select the Internet on Windows radio box.
1221             optionsUserAgentInternetExplorerOnWindowsMenuItem.setChecked(true);
1222         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) {  // Safari on macOS.
1223             // Update the user agent menu item title.
1224             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_macos));
1225
1226             // Select the Safari on macOS radio box.
1227             optionsUserAgentSafariOnMacosMenuItem.setChecked(true);
1228         } else {  // Custom user agent.
1229             // Update the user agent menu item title.
1230             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_custom));
1231
1232             // Select the Custom radio box.
1233             optionsUserAgentCustomMenuItem.setChecked(true);
1234         }
1235
1236         // Set the font size title.
1237         optionsFontSizeMenuItem.setTitle(getString(R.string.font_size) + " - " + fontSize + "%");
1238
1239         // Run all the other default commands.
1240         super.onPrepareOptionsMenu(menu);
1241
1242         // Display the menu.
1243         return true;
1244     }
1245
1246     @Override
1247     public boolean onOptionsItemSelected(MenuItem menuItem) {
1248         // Get a handle for the shared preferences.
1249         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1250
1251         // Get a handle for the cookie manager.
1252         CookieManager cookieManager = CookieManager.getInstance();
1253
1254         // Get the selected menu item ID.
1255         int menuItemId = menuItem.getItemId();
1256
1257         // Run the commands that correlate to the selected menu item.
1258         if (menuItemId == R.id.javascript) {  // JavaScript.
1259             // Toggle the JavaScript status.
1260             currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
1261
1262             // Update the privacy icon.
1263             updatePrivacyIcons(true);
1264
1265             // Display a `Snackbar`.
1266             if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScrip is enabled.
1267                 Snackbar.make(webViewPager, R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1268             } else if (cookieManager.acceptCookie()) {  // JavaScript is disabled, but first-party cookies are enabled.
1269                 Snackbar.make(webViewPager, R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1270             } else {  // Privacy mode.
1271                 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1272             }
1273
1274             // Reload the current WebView.
1275             currentWebView.reload();
1276
1277             // Consume the event.
1278             return true;
1279         } else if (menuItemId == R.id.refresh) {  // Refresh.
1280             // Run the command that correlates to the current status of the menu item.
1281             if (menuItem.getTitle().equals(getString(R.string.refresh))) {  // The refresh button was pushed.
1282                 // Reload the current WebView.
1283                 currentWebView.reload();
1284             } else {  // The stop button was pushed.
1285                 // Stop the loading of the WebView.
1286                 currentWebView.stopLoading();
1287             }
1288
1289             // Consume the event.
1290             return true;
1291         } else if (menuItemId == R.id.bookmarks) {  // Bookmarks.
1292             // Open the bookmarks drawer.
1293             drawerLayout.openDrawer(GravityCompat.END);
1294
1295             // Consume the event.
1296             return true;
1297         } else if (menuItemId == R.id.cookies) {  // Cookies.
1298             // Switch the first-party cookie status.
1299             cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
1300
1301             // Store the cookie status.
1302             currentWebView.setAcceptCookies(cookieManager.acceptCookie());
1303
1304             // Update the menu checkbox.
1305             menuItem.setChecked(cookieManager.acceptCookie());
1306
1307             // Update the privacy icon.
1308             updatePrivacyIcons(true);
1309
1310             // Display a snackbar.
1311             if (cookieManager.acceptCookie()) {  // Cookies are enabled.
1312                 Snackbar.make(webViewPager, R.string.cookies_enabled, Snackbar.LENGTH_SHORT).show();
1313             } else if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is still enabled.
1314                 Snackbar.make(webViewPager, R.string.cookies_disabled, Snackbar.LENGTH_SHORT).show();
1315             } else {  // Privacy mode.
1316                 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1317             }
1318
1319             // Reload the current WebView.
1320             currentWebView.reload();
1321
1322             // Consume the event.
1323             return true;
1324         } else if (menuItemId == R.id.dom_storage) {  // DOM storage.
1325             // Toggle the status of domStorageEnabled.
1326             currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1327
1328             // Update the menu checkbox.
1329             menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1330
1331             // Update the privacy icon.
1332             updatePrivacyIcons(true);
1333
1334             // Display a snackbar.
1335             if (currentWebView.getSettings().getDomStorageEnabled()) {
1336                 Snackbar.make(webViewPager, R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1337             } else {
1338                 Snackbar.make(webViewPager, R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1339             }
1340
1341             // Reload the current WebView.
1342             currentWebView.reload();
1343
1344             // Consume the event.
1345             return true;
1346         } else if (menuItemId == R.id.save_form_data) {  // Form data.  This can be removed once the minimum API >= 26.
1347             // Switch the status of saveFormDataEnabled.
1348             currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
1349
1350             // Update the menu checkbox.
1351             menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
1352
1353             // Display a snackbar.
1354             if (currentWebView.getSettings().getSaveFormData()) {
1355                 Snackbar.make(webViewPager, R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1356             } else {
1357                 Snackbar.make(webViewPager, R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1358             }
1359
1360             // Update the privacy icon.
1361             updatePrivacyIcons(true);
1362
1363             // Reload the current WebView.
1364             currentWebView.reload();
1365
1366             // Consume the event.
1367             return true;
1368         } else if (menuItemId == R.id.clear_cookies) {  // Clear cookies.
1369             // Create a snackbar.
1370             Snackbar.make(webViewPager, R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1371                     .setAction(R.string.undo, v -> {
1372                         // Do nothing because everything will be handled by `onDismissed()` below.
1373                     })
1374                     .addCallback(new Snackbar.Callback() {
1375                         @Override
1376                         public void onDismissed(Snackbar snackbar, int event) {
1377                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1378                                 // Delete the cookies.
1379                                 cookieManager.removeAllCookies(null);
1380                             }
1381                         }
1382                     })
1383                     .show();
1384
1385             // Consume the event.
1386             return true;
1387         } else if (menuItemId == R.id.clear_dom_storage) {  // Clear DOM storage.
1388             // Create a snackbar.
1389             Snackbar.make(webViewPager, R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1390                     .setAction(R.string.undo, v -> {
1391                         // Do nothing because everything will be handled by `onDismissed()` below.
1392                     })
1393                     .addCallback(new Snackbar.Callback() {
1394                         @Override
1395                         public void onDismissed(Snackbar snackbar, int event) {
1396                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1397                                 // Delete the DOM Storage.
1398                                 WebStorage webStorage = WebStorage.getInstance();
1399                                 webStorage.deleteAllData();
1400
1401                                 // Initialize a handler to manually delete the DOM storage files and directories.
1402                                 Handler deleteDomStorageHandler = new Handler();
1403
1404                                 // Setup a runnable to manually delete the DOM storage files and directories.
1405                                 Runnable deleteDomStorageRunnable = () -> {
1406                                     try {
1407                                         // Get a handle for the runtime.
1408                                         Runtime runtime = Runtime.getRuntime();
1409
1410                                         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1411                                         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1412                                         String privateDataDirectoryString = getApplicationInfo().dataDir;
1413
1414                                         // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1415                                         Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1416
1417                                         // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1418                                         Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1419                                         Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1420                                         Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1421                                         Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1422
1423                                         // Wait for the processes to finish.
1424                                         deleteLocalStorageProcess.waitFor();
1425                                         deleteIndexProcess.waitFor();
1426                                         deleteQuotaManagerProcess.waitFor();
1427                                         deleteQuotaManagerJournalProcess.waitFor();
1428                                         deleteDatabasesProcess.waitFor();
1429                                     } catch (Exception exception) {
1430                                         // Do nothing if an error is thrown.
1431                                     }
1432                                 };
1433
1434                                 // Manually delete the DOM storage files after 200 milliseconds.
1435                                 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1436                             }
1437                         }
1438                     })
1439                     .show();
1440
1441             // Consume the event.
1442             return true;
1443         } else if (menuItemId == R.id.clear_form_data) {  // Clear form data.  This can be remove once the minimum API >= 26.
1444             // Create a snackbar.
1445             Snackbar.make(webViewPager, R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1446                     .setAction(R.string.undo, v -> {
1447                         // Do nothing because everything will be handled by `onDismissed()` below.
1448                     })
1449                     .addCallback(new Snackbar.Callback() {
1450                         @Override
1451                         public void onDismissed(Snackbar snackbar, int event) {
1452                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1453                                 // Get a handle for the webView database.
1454                                 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1455
1456                                 // Delete the form data.
1457                                 webViewDatabase.clearFormData();
1458                             }
1459                         }
1460                     })
1461                     .show();
1462
1463             // Consume the event.
1464             return true;
1465         } else if (menuItemId == R.id.easylist) {  // EasyList.
1466             // Toggle the EasyList status.
1467             currentWebView.setEasyListEnabled(!currentWebView.getEasyListEnabled());
1468
1469             // Update the menu checkbox.
1470             menuItem.setChecked(currentWebView.getEasyListEnabled());
1471
1472             // Reload the current WebView.
1473             currentWebView.reload();
1474
1475             // Consume the event.
1476             return true;
1477         } else if (menuItemId == R.id.easyprivacy) {  // EasyPrivacy.
1478             // Toggle the EasyPrivacy status.
1479             currentWebView.setEasyPrivacyEnabled(!currentWebView.getEasyPrivacyEnabled());
1480
1481             // Update the menu checkbox.
1482             menuItem.setChecked(currentWebView.getEasyPrivacyEnabled());
1483
1484             // Reload the current WebView.
1485             currentWebView.reload();
1486
1487             // Consume the event.
1488             return true;
1489         } else if (menuItemId == R.id.fanboys_annoyance_list) {  // Fanboy's Annoyance List.
1490             // Toggle Fanboy's Annoyance List status.
1491             currentWebView.setFanboysAnnoyanceListEnabled(!currentWebView.getFanboysAnnoyanceListEnabled());
1492
1493             // Update the menu checkbox.
1494             menuItem.setChecked(currentWebView.getFanboysAnnoyanceListEnabled());
1495
1496             // Update the status of Fanboy's Social Blocking List.
1497             optionsFanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.getFanboysAnnoyanceListEnabled());
1498
1499             // Reload the current WebView.
1500             currentWebView.reload();
1501
1502             // Consume the event.
1503             return true;
1504         } else if (menuItemId == R.id.fanboys_social_blocking_list) {  // Fanboy's Social Blocking List.
1505             // Toggle Fanboy's Social Blocking List status.
1506             currentWebView.setFanboysSocialBlockingListEnabled(!currentWebView.getFanboysSocialBlockingListEnabled());
1507
1508             // Update the menu checkbox.
1509             menuItem.setChecked(currentWebView.getFanboysSocialBlockingListEnabled());
1510
1511             // Reload the current WebView.
1512             currentWebView.reload();
1513
1514             // Consume the event.
1515             return true;
1516         } else if (menuItemId == R.id.ultralist) {  // UltraList.
1517             // Toggle the UltraList status.
1518             currentWebView.setUltraListEnabled(!currentWebView.getUltraListEnabled());
1519
1520             // Update the menu checkbox.
1521             menuItem.setChecked(currentWebView.getUltraListEnabled());
1522
1523             // Reload the current WebView.
1524             currentWebView.reload();
1525
1526             // Consume the event.
1527             return true;
1528         } else if (menuItemId == R.id.ultraprivacy) {  // UltraPrivacy.
1529             // Toggle the UltraPrivacy status.
1530             currentWebView.setUltraPrivacyEnabled(!currentWebView.getUltraPrivacyEnabled());
1531
1532             // Update the menu checkbox.
1533             menuItem.setChecked(currentWebView.getUltraPrivacyEnabled());
1534
1535             // Reload the current WebView.
1536             currentWebView.reload();
1537
1538             // Consume the event.
1539             return true;
1540         } else if (menuItemId == R.id.block_all_third_party_requests) {  // Block all third-party requests.
1541             //Toggle the third-party requests blocker status.
1542             currentWebView.setBlockAllThirdPartyRequests(!currentWebView.getBlockAllThirdPartyRequests());
1543
1544             // Update the menu checkbox.
1545             menuItem.setChecked(currentWebView.getBlockAllThirdPartyRequests());
1546
1547             // Reload the current WebView.
1548             currentWebView.reload();
1549
1550             // Consume the event.
1551             return true;
1552         } else if (menuItemId == R.id.proxy_none) {  // Proxy - None.
1553             // Update the proxy mode.
1554             proxyMode = ProxyHelper.NONE;
1555
1556             // Apply the proxy mode.
1557             applyProxy(true);
1558
1559             // Consume the event.
1560             return true;
1561         } else if (menuItemId == R.id.proxy_tor) {  // Proxy - Tor.
1562             // Update the proxy mode.
1563             proxyMode = ProxyHelper.TOR;
1564
1565             // Apply the proxy mode.
1566             applyProxy(true);
1567
1568             // Consume the event.
1569             return true;
1570         } else if (menuItemId == R.id.proxy_i2p) {  // Proxy - I2P.
1571             // Update the proxy mode.
1572             proxyMode = ProxyHelper.I2P;
1573
1574             // Apply the proxy mode.
1575             applyProxy(true);
1576
1577             // Consume the event.
1578             return true;
1579         } else if (menuItemId == R.id.proxy_custom) {  // Proxy - Custom.
1580             // Update the proxy mode.
1581             proxyMode = ProxyHelper.CUSTOM;
1582
1583             // Apply the proxy mode.
1584             applyProxy(true);
1585
1586             // Consume the event.
1587             return true;
1588         } else if (menuItemId == R.id.user_agent_privacy_browser) {  // User Agent - Privacy Browser.
1589             // Update the user agent.
1590             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1591
1592             // Reload the current WebView.
1593             currentWebView.reload();
1594
1595             // Consume the event.
1596             return true;
1597         } else if (menuItemId == R.id.user_agent_webview_default) {  // User Agent - WebView Default.
1598             // Update the user agent.
1599             currentWebView.getSettings().setUserAgentString("");
1600
1601             // Reload the current WebView.
1602             currentWebView.reload();
1603
1604             // Consume the event.
1605             return true;
1606         } else if (menuItemId == R.id.user_agent_firefox_on_android) {  // User Agent - Firefox on Android.
1607             // Update the user agent.
1608             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1609
1610             // Reload the current WebView.
1611             currentWebView.reload();
1612
1613             // Consume the event.
1614             return true;
1615         } else if (menuItemId == R.id.user_agent_chrome_on_android) {  // User Agent - Chrome on Android.
1616             // Update the user agent.
1617             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1618
1619             // Reload the current WebView.
1620             currentWebView.reload();
1621
1622             // Consume the event.
1623             return true;
1624         } else if (menuItemId == R.id.user_agent_safari_on_ios) {  // User Agent - Safari on iOS.
1625             // Update the user agent.
1626             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1627
1628             // Reload the current WebView.
1629             currentWebView.reload();
1630
1631             // Consume the event.
1632             return true;
1633         } else if (menuItemId == R.id.user_agent_firefox_on_linux) {  // User Agent - Firefox on Linux.
1634             // Update the user agent.
1635             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1636
1637             // Reload the current WebView.
1638             currentWebView.reload();
1639
1640             // Consume the event.
1641             return true;
1642         } else if (menuItemId == R.id.user_agent_chromium_on_linux) {  // User Agent - Chromium on Linux.
1643             // Update the user agent.
1644             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1645
1646             // Reload the current WebView.
1647             currentWebView.reload();
1648
1649             // Consume the event.
1650             return true;
1651         } else if (menuItemId == R.id.user_agent_firefox_on_windows) {  // User Agent - Firefox on Windows.
1652             // Update the user agent.
1653             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1654
1655             // Reload the current WebView.
1656             currentWebView.reload();
1657
1658             // Consume the event.
1659             return true;
1660         } else if (menuItemId == R.id.user_agent_chrome_on_windows) {  // User Agent - Chrome on Windows.
1661             // Update the user agent.
1662             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1663
1664             // Reload the current WebView.
1665             currentWebView.reload();
1666
1667             // Consume the event.
1668             return true;
1669         } else if (menuItemId == R.id.user_agent_edge_on_windows) {  // User Agent - Edge on Windows.
1670             // Update the user agent.
1671             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1672
1673             // Reload the current WebView.
1674             currentWebView.reload();
1675
1676             // Consume the event.
1677             return true;
1678         } else if (menuItemId == R.id.user_agent_internet_explorer_on_windows) {  // User Agent - Internet Explorer on Windows.
1679             // Update the user agent.
1680             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1681
1682             // Reload the current WebView.
1683             currentWebView.reload();
1684
1685             // Consume the event.
1686             return true;
1687         } else if (menuItemId == R.id.user_agent_safari_on_macos) {  // User Agent - Safari on macOS.
1688             // Update the user agent.
1689             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1690
1691             // Reload the current WebView.
1692             currentWebView.reload();
1693
1694             // Consume the event.
1695             return true;
1696         } else if (menuItemId == R.id.user_agent_custom) {  // User Agent - Custom.
1697             // Update the user agent.
1698             currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1699
1700             // Reload the current WebView.
1701             currentWebView.reload();
1702
1703             // Consume the event.
1704             return true;
1705         } else if (menuItemId == R.id.font_size) {  // Font size.
1706             // Instantiate the font size dialog.
1707             DialogFragment fontSizeDialogFragment = FontSizeDialog.displayDialog(currentWebView.getSettings().getTextZoom());
1708
1709             // Show the font size dialog.
1710             fontSizeDialogFragment.show(getSupportFragmentManager(), getString(R.string.font_size));
1711
1712             // Consume the event.
1713             return true;
1714         } else if (menuItemId == R.id.swipe_to_refresh) {  // Swipe to refresh.
1715             // Toggle the stored status of swipe to refresh.
1716             currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
1717
1718             // Update the swipe refresh layout.
1719             if (currentWebView.getSwipeToRefresh()) {  // Swipe to refresh is enabled.
1720                 // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
1721                 swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
1722             } else {  // Swipe to refresh is disabled.
1723                 // Disable the swipe refresh layout.
1724                 swipeRefreshLayout.setEnabled(false);
1725             }
1726
1727             // Consume the event.
1728             return true;
1729         } else if (menuItemId == R.id.wide_viewport) {  // Wide viewport.
1730             // Toggle the viewport.
1731             currentWebView.getSettings().setUseWideViewPort(!currentWebView.getSettings().getUseWideViewPort());
1732
1733             // Consume the event.
1734             return true;
1735         } else if (menuItemId == R.id.display_images) {  // Display images.
1736             // Toggle the displaying of images.
1737             if (currentWebView.getSettings().getLoadsImagesAutomatically()) {  // Images are currently loaded automatically.
1738                 // Disable loading of images.
1739                 currentWebView.getSettings().setLoadsImagesAutomatically(false);
1740
1741                 // Reload the website to remove existing images.
1742                 currentWebView.reload();
1743             } else {  // Images are not currently loaded automatically.
1744                 // Enable loading of images.  Missing images will be loaded without the need for a reload.
1745                 currentWebView.getSettings().setLoadsImagesAutomatically(true);
1746             }
1747
1748             // Consume the event.
1749             return true;
1750         } else if (menuItemId == R.id.dark_webview) {  // Dark WebView.
1751             // Check to see if dark WebView is supported by this WebView.
1752             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
1753                 // Toggle the dark WebView setting.
1754                 if (WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON) {  // Dark WebView is currently enabled.
1755                     // Turn off dark WebView.
1756                     WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
1757                 } else {  // Dark WebView is currently disabled.
1758                     // Turn on dark WebView.
1759                     WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
1760                 }
1761             }
1762
1763             // Consume the event.
1764             return true;
1765         } else if (menuItemId == R.id.find_on_page) {  // Find on page.
1766             // Get a handle for the views.
1767             Toolbar toolbar = findViewById(R.id.toolbar);
1768             LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1769             EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
1770
1771             // Set the minimum height of the find on page linear layout to match the toolbar.
1772             findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight());
1773
1774             // Hide the toolbar.
1775             toolbar.setVisibility(View.GONE);
1776
1777             // Show the find on page linear layout.
1778             findOnPageLinearLayout.setVisibility(View.VISIBLE);
1779
1780             // Display the keyboard.  The app must wait 200 ms before running the command to work around a bug in Android.
1781             // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1782             findOnPageEditText.postDelayed(() -> {
1783                 // Set the focus on the find on page edit text.
1784                 findOnPageEditText.requestFocus();
1785
1786                 // Get a handle for the input method manager.
1787                 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
1788
1789                 // Remove the lint warning below that the input method manager might be null.
1790                 assert inputMethodManager != null;
1791
1792                 // Display the keyboard.  `0` sets no input flags.
1793                 inputMethodManager.showSoftInput(findOnPageEditText, 0);
1794             }, 200);
1795
1796             // Consume the event.
1797             return true;
1798         } else if (menuItemId == R.id.print) {  // Print.
1799             // Get a print manager instance.
1800             PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
1801
1802             // Remove the lint error below that print manager might be null.
1803             assert printManager != null;
1804
1805             // Create a print document adapter from the current WebView.
1806             PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter(getString(R.string.print));
1807
1808             // Print the document.
1809             printManager.print(getString(R.string.privacy_browser_webpage), printDocumentAdapter, null);
1810
1811             // Consume the event.
1812             return true;
1813         } else if (menuItemId == R.id.save_url) {  // Save URL.
1814             // Check the download preference.
1815             if (downloadWithExternalApp) {  // Download with an external app.
1816                 downloadUrlWithExternalApp(currentWebView.getCurrentUrl());
1817             } else {  // Handle the download inside of Privacy Browser.
1818                 // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
1819                 new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
1820                         currentWebView.getAcceptCookies()).execute(currentWebView.getCurrentUrl());
1821             }
1822
1823             // Consume the event.
1824             return true;
1825         } else if (menuItemId == R.id.save_archive) {
1826             // Open the file picker with a default file name built from the current domain name.
1827             saveWebpageArchiveActivityResultLauncher.launch(currentWebView.getCurrentDomainName() + ".mht");
1828
1829             // Consume the event.
1830             return true;
1831         } else if (menuItemId == R.id.save_image) {  // Save image.
1832             // Open the file picker with a default file name built from the current domain name.
1833             saveWebpageImageActivityResultLauncher.launch(currentWebView.getCurrentDomainName() + ".png");
1834
1835             // Consume the event.
1836             return true;
1837         } else if (menuItemId == R.id.add_to_homescreen) {  // Add to homescreen.
1838             // Instantiate the create home screen shortcut dialog.
1839             DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
1840                     currentWebView.getFavoriteOrDefaultIcon());
1841
1842             // Show the create home screen shortcut dialog.
1843             createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
1844
1845             // Consume the event.
1846             return true;
1847         } else if (menuItemId == R.id.view_source) {  // View source.
1848             // Create an intent to launch the view source activity.
1849             Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
1850
1851             // Add the variables to the intent.
1852             viewSourceIntent.putExtra(ViewSourceActivityKt.CURRENT_URL, currentWebView.getUrl());
1853             viewSourceIntent.putExtra(ViewSourceActivityKt.USER_AGENT, currentWebView.getSettings().getUserAgentString());
1854
1855             // Make it so.
1856             startActivity(viewSourceIntent);
1857
1858             // Consume the event.
1859             return true;
1860         } else if (menuItemId == R.id.share_url) {  // Share URL.
1861             // Setup the share string.
1862             String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl();
1863
1864             // Create the share intent.
1865             Intent shareIntent = new Intent(Intent.ACTION_SEND);
1866
1867             // Add the share string to the intent.
1868             shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
1869
1870             // Set the MIME type.
1871             shareIntent.setType("text/plain");
1872
1873             // Set the intent to open in a new task.
1874             shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1875
1876             // Make it so.
1877             startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
1878
1879             // Consume the event.
1880             return true;
1881         } else if (menuItemId == R.id.open_with_app) {  // Open with app.
1882             // Open the URL with an outside app.
1883             openWithApp(currentWebView.getUrl());
1884
1885             // Consume the event.
1886             return true;
1887         } else if (menuItemId == R.id.open_with_browser) {  // Open with browser.
1888             // Open the URL with an outside browser.
1889             openWithBrowser(currentWebView.getUrl());
1890
1891             // Consume the event.
1892             return true;
1893         } else if (menuItemId == R.id.add_or_edit_domain) {  // Add or edit domain.
1894             // Check if domain settings currently exist.
1895             if (currentWebView.getDomainSettingsApplied()) {  // Edit the current domain settings.
1896                 // Reapply the domain settings on returning to `MainWebViewActivity`.
1897                 reapplyDomainSettingsOnRestart = true;
1898
1899                 // Create an intent to launch the domains activity.
1900                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1901
1902                 // Add the extra information to the intent.
1903                 domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
1904                 domainsIntent.putExtra("close_on_back", true);
1905                 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1906                 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1907
1908                 // Get the current certificate.
1909                 SslCertificate sslCertificate = currentWebView.getCertificate();
1910
1911                 // Check to see if the SSL certificate is populated.
1912                 if (sslCertificate != null) {
1913                     // Extract the certificate to strings.
1914                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
1915                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
1916                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
1917                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
1918                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
1919                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
1920                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1921                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1922
1923                     // Add the certificate to the intent.
1924                     domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1925                     domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1926                     domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1927                     domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1928                     domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1929                     domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1930                     domainsIntent.putExtra("ssl_start_date", startDateLong);
1931                     domainsIntent.putExtra("ssl_end_date", endDateLong);
1932                 }
1933
1934                 // Make it so.
1935                 startActivity(domainsIntent);
1936             } else {  // Add a new domain.
1937                 // Apply the new domain settings on returning to `MainWebViewActivity`.
1938                 reapplyDomainSettingsOnRestart = true;
1939
1940                 // Get the current domain
1941                 Uri currentUri = Uri.parse(currentWebView.getUrl());
1942                 String currentDomain = currentUri.getHost();
1943
1944                 // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1945                 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1946
1947                 // Create the domain and store the database ID.
1948                 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1949
1950                 // Create an intent to launch the domains activity.
1951                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1952
1953                 // Add the extra information to the intent.
1954                 domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1955                 domainsIntent.putExtra("close_on_back", true);
1956                 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1957                 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1958
1959                 // Get the current certificate.
1960                 SslCertificate sslCertificate = currentWebView.getCertificate();
1961
1962                 // Check to see if the SSL certificate is populated.
1963                 if (sslCertificate != null) {
1964                     // Extract the certificate to strings.
1965                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
1966                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
1967                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
1968                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
1969                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
1970                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
1971                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1972                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1973
1974                     // Add the certificate to the intent.
1975                     domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1976                     domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1977                     domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1978                     domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1979                     domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1980                     domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1981                     domainsIntent.putExtra("ssl_start_date", startDateLong);
1982                     domainsIntent.putExtra("ssl_end_date", endDateLong);
1983                 }
1984
1985                 // Make it so.
1986                 startActivity(domainsIntent);
1987             }
1988
1989             // Consume the event.
1990             return true;
1991         } else {  // There is no match with the options menu.  Pass the event up to the parent method.
1992             // Don't consume the event.
1993             return super.onOptionsItemSelected(menuItem);
1994         }
1995     }
1996
1997     // removeAllCookies is deprecated, but it is required for API < 21.
1998     @Override
1999     public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2000         // Get a handle for the shared preferences.
2001         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2002
2003         // Get the menu item ID.
2004         int menuItemId = menuItem.getItemId();
2005
2006         // Run the commands that correspond to the selected menu item.
2007         if (menuItemId == R.id.clear_and_exit) {  // Clear and exit.
2008             // Clear and exit Privacy Browser.
2009             clearAndExit();
2010         } else if (menuItemId == R.id.home) {  // Home.
2011             // Load the homepage.
2012             loadUrl(currentWebView, sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
2013         } else if (menuItemId == R.id.back) {  // Back.
2014             // Check if the WebView can go back.
2015             if (currentWebView.canGoBack()) {
2016                 // Get the current web back forward list.
2017                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2018
2019                 // Get the previous entry URL.
2020                 String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
2021
2022                 // Apply the domain settings.
2023                 applyDomainSettings(currentWebView, previousUrl, false, false, false);
2024
2025                 // Load the previous website in the history.
2026                 currentWebView.goBack();
2027             }
2028         } else if (menuItemId == R.id.forward) {  // Forward.
2029             // Check if the WebView can go forward.
2030             if (currentWebView.canGoForward()) {
2031                 // Get the current web back forward list.
2032                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2033
2034                 // Get the next entry URL.
2035                 String nextUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() + 1).getUrl();
2036
2037                 // Apply the domain settings.
2038                 applyDomainSettings(currentWebView, nextUrl, false, false, false);
2039
2040                 // Load the next website in the history.
2041                 currentWebView.goForward();
2042             }
2043         } else if (menuItemId == R.id.history) {  // History.
2044             // Instantiate the URL history dialog.
2045             DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId());
2046
2047             // Show the URL history dialog.
2048             urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2049         } else if (menuItemId == R.id.open) {  // Open.
2050             // Instantiate the open file dialog.
2051             DialogFragment openDialogFragment = new OpenDialog();
2052
2053             // Show the open file dialog.
2054             openDialogFragment.show(getSupportFragmentManager(), getString(R.string.open));
2055         } else if (menuItemId == R.id.requests) {  // Requests.
2056             // Populate the resource requests.
2057             RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2058
2059             // Create an intent to launch the Requests activity.
2060             Intent requestsIntent = new Intent(this, RequestsActivity.class);
2061
2062             // Add the block third-party requests status to the intent.
2063             requestsIntent.putExtra("block_all_third_party_requests", currentWebView.getBlockAllThirdPartyRequests());
2064
2065             // Make it so.
2066             startActivity(requestsIntent);
2067         } else if (menuItemId == R.id.downloads) {  // Downloads.
2068             // Try the default system download manager.
2069             try {
2070                 // Launch the default system Download Manager.
2071                 Intent defaultDownloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2072
2073                 // Launch as a new task so that the download manager and Privacy Browser show as separate windows in the recent tasks list.
2074                 defaultDownloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2075
2076                 // Make it so.
2077                 startActivity(defaultDownloadManagerIntent);
2078             } catch (Exception defaultDownloadManagerException) {
2079                 // Try a generic file manager.
2080                 try {
2081                     // Create a generic file manager intent.
2082                     Intent genericFileManagerIntent = new Intent(Intent.ACTION_VIEW);
2083
2084                     // Open the download directory.
2085                     genericFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), DocumentsContract.Document.MIME_TYPE_DIR);
2086
2087                     // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2088                     genericFileManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2089
2090                     // Make it so.
2091                     startActivity(genericFileManagerIntent);
2092                 } catch (Exception genericFileManagerException) {
2093                     // Try an alternate file manager.
2094                     try {
2095                         // Create an alternate file manager intent.
2096                         Intent alternateFileManagerIntent = new Intent(Intent.ACTION_VIEW);
2097
2098                         // Open the download directory.
2099                         alternateFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), "resource/folder");
2100
2101                         // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2102                         alternateFileManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2103
2104                         // Open the alternate file manager.
2105                         startActivity(alternateFileManagerIntent);
2106                     } catch (Exception alternateFileManagerException) {
2107                         // Display a snackbar.
2108                         Snackbar.make(currentWebView, R.string.no_file_manager_detected, Snackbar.LENGTH_INDEFINITE).show();
2109                     }
2110                 }
2111             }
2112         } else if (menuItemId == R.id.domains) {  // Domains.
2113             // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2114             reapplyDomainSettingsOnRestart = true;
2115
2116             // Launch the domains activity.
2117             Intent domainsIntent = new Intent(this, DomainsActivity.class);
2118
2119             // Add the extra information to the intent.
2120             domainsIntent.putExtra("current_url", currentWebView.getUrl());
2121             domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
2122
2123             // Get the current certificate.
2124             SslCertificate sslCertificate = currentWebView.getCertificate();
2125
2126             // Check to see if the SSL certificate is populated.
2127             if (sslCertificate != null) {
2128                 // Extract the certificate to strings.
2129                 String issuedToCName = sslCertificate.getIssuedTo().getCName();
2130                 String issuedToOName = sslCertificate.getIssuedTo().getOName();
2131                 String issuedToUName = sslCertificate.getIssuedTo().getUName();
2132                 String issuedByCName = sslCertificate.getIssuedBy().getCName();
2133                 String issuedByOName = sslCertificate.getIssuedBy().getOName();
2134                 String issuedByUName = sslCertificate.getIssuedBy().getUName();
2135                 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
2136                 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
2137
2138                 // Add the certificate to the intent.
2139                 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
2140                 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
2141                 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
2142                 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
2143                 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
2144                 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
2145                 domainsIntent.putExtra("ssl_start_date", startDateLong);
2146                 domainsIntent.putExtra("ssl_end_date", endDateLong);
2147             }
2148
2149             // Make it so.
2150             startActivity(domainsIntent);
2151         } else if (menuItemId == R.id.settings) {  // Settings.
2152             // Set the flag to reapply app settings on restart when returning from Settings.
2153             reapplyAppSettingsOnRestart = true;
2154
2155             // Set the flag to reapply the domain settings on restart when returning from Settings.
2156             reapplyDomainSettingsOnRestart = true;
2157
2158             // Launch the settings activity.
2159             Intent settingsIntent = new Intent(this, SettingsActivity.class);
2160             startActivity(settingsIntent);
2161         } else if (menuItemId == R.id.import_export) { // Import/Export.
2162             // Create an intent to launch the import/export activity.
2163             Intent importExportIntent = new Intent(this, ImportExportActivity.class);
2164
2165             // Make it so.
2166             startActivity(importExportIntent);
2167         } else if (menuItemId == R.id.logcat) {  // Logcat.
2168             // Create an intent to launch the logcat activity.
2169             Intent logcatIntent = new Intent(this, LogcatActivity.class);
2170
2171             // Make it so.
2172             startActivity(logcatIntent);
2173         } else if (menuItemId == R.id.guide) {  // Guide.
2174             // Create an intent to launch the guide activity.
2175             Intent guideIntent = new Intent(this, GuideActivity.class);
2176
2177             // Make it so.
2178             startActivity(guideIntent);
2179         } else if (menuItemId == R.id.about) {  // About
2180             // Create an intent to launch the about activity.
2181             Intent aboutIntent = new Intent(this, AboutActivity.class);
2182
2183             // Create a string array for the blocklist versions.
2184             String[] blocklistVersions = new String[]{easyList.get(0).get(0)[0], easyPrivacy.get(0).get(0)[0], fanboysAnnoyanceList.get(0).get(0)[0], fanboysSocialList.get(0).get(0)[0],
2185                     ultraList.get(0).get(0)[0], ultraPrivacy.get(0).get(0)[0]};
2186
2187             // Add the blocklist versions to the intent.
2188             aboutIntent.putExtra(AboutActivity.BLOCKLIST_VERSIONS, blocklistVersions);
2189
2190             // Make it so.
2191             startActivity(aboutIntent);
2192         }
2193
2194         // Close the navigation drawer.
2195         drawerLayout.closeDrawer(GravityCompat.START);
2196         return true;
2197     }
2198
2199     @Override
2200     public void onPostCreate(Bundle savedInstanceState) {
2201         // Run the default commands.
2202         super.onPostCreate(savedInstanceState);
2203
2204         // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished.  This creates the navigation drawer icon.
2205         actionBarDrawerToggle.syncState();
2206     }
2207
2208     @Override
2209     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2210         // Get the hit test result.
2211         final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2212
2213         // Define the URL strings.
2214         final String imageUrl;
2215         final String linkUrl;
2216
2217         // Get handles for the system managers.
2218         final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2219
2220         // Remove the lint errors below that the clipboard manager might be null.
2221         assert clipboardManager != null;
2222
2223         // Process the link according to the type.
2224         switch (hitTestResult.getType()) {
2225             // `SRC_ANCHOR_TYPE` is a link.
2226             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2227                 // Get the target URL.
2228                 linkUrl = hitTestResult.getExtra();
2229
2230                 // Set the target URL as the title of the `ContextMenu`.
2231                 menu.setHeaderTitle(linkUrl);
2232
2233                 // Add an Open in New Tab entry.
2234                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2235                     // Load the link URL in a new tab and move to it.
2236                     addNewTab(linkUrl, true);
2237
2238                     // Consume the event.
2239                     return true;
2240                 });
2241
2242                 // Add an Open in Background entry.
2243                 menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
2244                     // Load the link URL in a new tab but do not move to it.
2245                     addNewTab(linkUrl, false);
2246
2247                     // Consume the event.
2248                     return true;
2249                 });
2250
2251                 // Add an Open with App entry.
2252                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2253                     openWithApp(linkUrl);
2254
2255                     // Consume the event.
2256                     return true;
2257                 });
2258
2259                 // Add an Open with Browser entry.
2260                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2261                     openWithBrowser(linkUrl);
2262
2263                     // Consume the event.
2264                     return true;
2265                 });
2266
2267                 // Add a Copy URL entry.
2268                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2269                     // Save the link URL in a `ClipData`.
2270                     ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2271
2272                     // Set the `ClipData` as the clipboard's primary clip.
2273                     clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2274
2275                     // Consume the event.
2276                     return true;
2277                 });
2278
2279                 // Add a Save URL entry.
2280                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
2281                     // Check the download preference.
2282                     if (downloadWithExternalApp) {  // Download with an external app.
2283                         downloadUrlWithExternalApp(linkUrl);
2284                     } else {  // Handle the download inside of Privacy Browser.
2285                         // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2286                         new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
2287                                 currentWebView.getAcceptCookies()).execute(linkUrl);
2288                     }
2289
2290                     // Consume the event.
2291                     return true;
2292                 });
2293
2294                 // Add an empty Cancel entry, which by default closes the context menu.
2295                 menu.add(R.string.cancel);
2296                 break;
2297
2298             // `IMAGE_TYPE` is an image.
2299             case WebView.HitTestResult.IMAGE_TYPE:
2300                 // Get the image URL.
2301                 imageUrl = hitTestResult.getExtra();
2302
2303                 // Remove the incorrect lint warning below that the image URL might be null.
2304                 assert imageUrl != null;
2305
2306                 // Set the context menu title.
2307                 if (imageUrl.startsWith("data:")) {  // The image data is contained in within the URL, making it exceedingly long.
2308                     // Truncate the image URL before making it the title.
2309                     menu.setHeaderTitle(imageUrl.substring(0, 100));
2310                 } else {  // The image URL does not contain the full image data.
2311                     // Set the image URL as the title of the context menu.
2312                     menu.setHeaderTitle(imageUrl);
2313                 }
2314
2315                 // Add an Open in New Tab entry.
2316                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2317                     // Load the image in a new tab.
2318                     addNewTab(imageUrl, true);
2319
2320                     // Consume the event.
2321                     return true;
2322                 });
2323
2324                 // Add an Open with App entry.
2325                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2326                     // Open the image URL with an external app.
2327                     openWithApp(imageUrl);
2328
2329                     // Consume the event.
2330                     return true;
2331                 });
2332
2333                 // Add an Open with Browser entry.
2334                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2335                     // Open the image URL with an external browser.
2336                     openWithBrowser(imageUrl);
2337
2338                     // Consume the event.
2339                     return true;
2340                 });
2341
2342                 // Add a View Image entry.
2343                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2344                     // Load the image in the current tab.
2345                     loadUrl(currentWebView, imageUrl);
2346
2347                     // Consume the event.
2348                     return true;
2349                 });
2350
2351                 // Add a Save Image entry.
2352                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
2353                     // Check the download preference.
2354                     if (downloadWithExternalApp) {  // Download with an external app.
2355                         downloadUrlWithExternalApp(imageUrl);
2356                     } else {  // Handle the download inside of Privacy Browser.
2357                         // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2358                         new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
2359                                 currentWebView.getAcceptCookies()).execute(imageUrl);
2360                     }
2361
2362                     // Consume the event.
2363                     return true;
2364                 });
2365
2366                 // Add a Copy URL entry.
2367                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2368                     // Save the image URL in a clip data.
2369                     ClipData imageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2370
2371                     // Set the clip data as the clipboard's primary clip.
2372                     clipboardManager.setPrimaryClip(imageTypeClipData);
2373
2374                     // Consume the event.
2375                     return true;
2376                 });
2377
2378                 // Add an empty Cancel entry, which by default closes the context menu.
2379                 menu.add(R.string.cancel);
2380                 break;
2381
2382             // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2383             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2384                 // Get the image URL.
2385                 imageUrl = hitTestResult.getExtra();
2386
2387                 // Instantiate a handler.
2388                 Handler handler = new Handler();
2389
2390                 // Get a message from the handler.
2391                 Message message = handler.obtainMessage();
2392
2393                 // Request the image details from the last touched node be returned in the message.
2394                 currentWebView.requestFocusNodeHref(message);
2395
2396                 // Get the link URL from the message data.
2397                 linkUrl = message.getData().getString("url");
2398
2399                 // Set the link URL as the title of the context menu.
2400                 menu.setHeaderTitle(linkUrl);
2401
2402                 // Add an Open in New Tab entry.
2403                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2404                     // Load the link URL in a new tab and move to it.
2405                     addNewTab(linkUrl, true);
2406
2407                     // Consume the event.
2408                     return true;
2409                 });
2410
2411                 // Add an Open in Background entry.
2412                 menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
2413                     // Lod the link URL in a new tab but do not move to it.
2414                     addNewTab(linkUrl, false);
2415
2416                     // Consume the event.
2417                     return true;
2418                 });
2419
2420                 // Add an Open Image in New Tab entry.
2421                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2422                     // Load the image in a new tab and move to it.
2423                     addNewTab(imageUrl, true);
2424
2425                     // Consume the event.
2426                     return true;
2427                 });
2428
2429                 // Add an Open with App entry.
2430                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2431                     // Open the link URL with an external app.
2432                     openWithApp(linkUrl);
2433
2434                     // Consume the event.
2435                     return true;
2436                 });
2437
2438                 // Add an Open with Browser entry.
2439                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2440                     // Open the link URL with an external browser.
2441                     openWithBrowser(linkUrl);
2442
2443                     // Consume the event.
2444                     return true;
2445                 });
2446
2447                 // Add a View Image entry.
2448                 menu.add(R.string.view_image).setOnMenuItemClickListener((MenuItem item) -> {
2449                    // View the image in the current tab.
2450                    loadUrl(currentWebView, imageUrl);
2451
2452                    // Consume the event.
2453                    return true;
2454                 });
2455
2456                 // Add a Save Image entry.
2457                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
2458                     // Check the download preference.
2459                     if (downloadWithExternalApp) {  // Download with an external app.
2460                         downloadUrlWithExternalApp(imageUrl);
2461                     } else {  // Handle the download inside of Privacy Browser.
2462                         // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2463                         new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
2464                                 currentWebView.getAcceptCookies()).execute(imageUrl);
2465                     }
2466
2467                     // Consume the event.
2468                     return true;
2469                 });
2470
2471                 // Add a Copy URL entry.
2472                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2473                     // Save the link URL in a clip data.
2474                     ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2475
2476                     // Set the clip data as the clipboard's primary clip.
2477                     clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2478
2479                     // Consume the event.
2480                     return true;
2481                 });
2482
2483                 // Add a Save URL entry.
2484                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
2485                     // Check the download preference.
2486                     if (downloadWithExternalApp) {  // Download with an external app.
2487                         downloadUrlWithExternalApp(linkUrl);
2488                     } else {  // Handle the download inside of Privacy Browser.
2489                         // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2490                         new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
2491                                 currentWebView.getAcceptCookies()).execute(linkUrl);
2492                     }
2493
2494                     // Consume the event.
2495                     return true;
2496                 });
2497
2498                 // Add an empty Cancel entry, which by default closes the context menu.
2499                 menu.add(R.string.cancel);
2500                 break;
2501
2502             case WebView.HitTestResult.EMAIL_TYPE:
2503                 // Get the target URL.
2504                 linkUrl = hitTestResult.getExtra();
2505
2506                 // Set the target URL as the title of the `ContextMenu`.
2507                 menu.setHeaderTitle(linkUrl);
2508
2509                 // Add a Write Email entry.
2510                 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2511                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2512                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2513
2514                     // Parse the url and set it as the data for the `Intent`.
2515                     emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2516
2517                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2518                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2519
2520                     try {
2521                         // Make it so.
2522                         startActivity(emailIntent);
2523                     } catch (ActivityNotFoundException exception) {
2524                         // Display a snackbar.
2525                         Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
2526                     }
2527
2528                     // Consume the event.
2529                     return true;
2530                 });
2531
2532                 // Add a Copy Email Address entry.
2533                 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2534                     // Save the email address in a `ClipData`.
2535                     ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2536
2537                     // Set the `ClipData` as the clipboard's primary clip.
2538                     clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2539
2540                     // Consume the event.
2541                     return true;
2542                 });
2543
2544                 // Add an empty Cancel entry, which by default closes the context menu.
2545                 menu.add(R.string.cancel);
2546                 break;
2547         }
2548     }
2549
2550     @Override
2551     public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2552         // Get a handle for the bookmarks list view.
2553         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2554
2555         // Get the dialog.
2556         Dialog dialog = dialogFragment.getDialog();
2557
2558         // Remove the incorrect lint warning below that the dialog might be null.
2559         assert dialog != null;
2560
2561         // Get the views from the dialog fragment.
2562         EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
2563         EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
2564
2565         // Extract the strings from the edit texts.
2566         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2567         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2568
2569         // Create a favorite icon byte array output stream.
2570         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2571
2572         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2573         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2574
2575         // Convert the favorite icon byte array stream to a byte array.
2576         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2577
2578         // Display the new bookmark below the current items in the (0 indexed) list.
2579         int newBookmarkDisplayOrder = bookmarksListView.getCount();
2580
2581         // Create the bookmark.
2582         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2583
2584         // Update the bookmarks cursor with the current contents of this folder.
2585         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2586
2587         // Update the list view.
2588         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2589
2590         // Scroll to the new bookmark.
2591         bookmarksListView.setSelection(newBookmarkDisplayOrder);
2592     }
2593
2594     @Override
2595     public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) {
2596         // Get a handle for the bookmarks list view.
2597         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2598
2599         // Get the dialog.
2600         Dialog dialog = dialogFragment.getDialog();
2601
2602         // Remove the incorrect lint warning below that the dialog might be null.
2603         assert dialog != null;
2604
2605         // Get handles for the views in the dialog fragment.
2606         EditText folderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
2607         RadioButton defaultIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
2608         ImageView defaultIconImageView = dialog.findViewById(R.id.default_icon_imageview);
2609
2610         // Get new folder name string.
2611         String folderNameString = folderNameEditText.getText().toString();
2612
2613         // Create a folder icon bitmap.
2614         Bitmap folderIconBitmap;
2615
2616         // Set the folder icon bitmap according to the dialog.
2617         if (defaultIconRadioButton.isChecked()) {  // Use the default folder icon.
2618             // Get the default folder icon drawable.
2619             Drawable folderIconDrawable = defaultIconImageView.getDrawable();
2620
2621             // Convert the folder icon drawable to a bitmap drawable.
2622             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2623
2624             // Convert the folder icon bitmap drawable to a bitmap.
2625             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2626         } else {  // Use the WebView favorite icon.
2627             // Copy the favorite icon bitmap to the folder icon bitmap.
2628             folderIconBitmap = favoriteIconBitmap;
2629         }
2630
2631         // Create a folder icon byte array output stream.
2632         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2633
2634         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2635         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2636
2637         // Convert the folder icon byte array stream to a byte array.
2638         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2639
2640         // Move all the bookmarks down one in the display order.
2641         for (int i = 0; i < bookmarksListView.getCount(); i++) {
2642             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2643             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2644         }
2645
2646         // Create the folder, which will be placed at the top of the `ListView`.
2647         bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2648
2649         // Update the bookmarks cursor with the current contents of this folder.
2650         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2651
2652         // Update the `ListView`.
2653         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2654
2655         // Scroll to the new folder.
2656         bookmarksListView.setSelection(0);
2657     }
2658
2659     @Override
2660     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
2661         // Remove the incorrect lint warning below that the dialog fragment might be null.
2662         assert dialogFragment != null;
2663
2664         // Get the dialog.
2665         Dialog dialog = dialogFragment.getDialog();
2666
2667         // Remove the incorrect lint warning below that the dialog might be null.
2668         assert dialog != null;
2669
2670         // Get handles for the views from the dialog.
2671         RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
2672         RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
2673         ImageView defaultFolderIconImageView = dialog.findViewById(R.id.default_icon_imageview);
2674         EditText editFolderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
2675
2676         // Get the new folder name.
2677         String newFolderNameString = editFolderNameEditText.getText().toString();
2678
2679         // Check if the favorite icon has changed.
2680         if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
2681             // Update the name in the database.
2682             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2683         } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
2684             // Create the new folder icon Bitmap.
2685             Bitmap folderIconBitmap;
2686
2687             // Populate the new folder icon bitmap.
2688             if (defaultFolderIconRadioButton.isChecked()) {
2689                 // Get the default folder icon drawable.
2690                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2691
2692                 // Convert the folder icon drawable to a bitmap drawable.
2693                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2694
2695                 // Convert the folder icon bitmap drawable to a bitmap.
2696                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2697             } else {  // Use the `WebView` favorite icon.
2698                 // Copy the favorite icon bitmap to the folder icon bitmap.
2699                 folderIconBitmap = favoriteIconBitmap;
2700             }
2701
2702             // Create a folder icon byte array output stream.
2703             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2704
2705             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2706             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2707
2708             // Convert the folder icon byte array stream to a byte array.
2709             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2710
2711             // Update the folder icon in the database.
2712             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2713         } else {  // The folder icon and the name have changed.
2714             // Get the new folder icon bitmap.
2715             Bitmap folderIconBitmap;
2716             if (defaultFolderIconRadioButton.isChecked()) {
2717                 // Get the default folder icon drawable.
2718                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2719
2720                 // Convert the folder icon drawable to a bitmap drawable.
2721                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2722
2723                 // Convert the folder icon bitmap drawable to a bitmap.
2724                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2725             } else {  // Use the `WebView` favorite icon.
2726                 // Copy the favorite icon bitmap to the folder icon bitmap.
2727                 folderIconBitmap = favoriteIconBitmap;
2728             }
2729
2730             // Create a folder icon byte array output stream.
2731             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2732
2733             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2734             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2735
2736             // Convert the folder icon byte array stream to a byte array.
2737             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2738
2739             // Update the folder name and icon in the database.
2740             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
2741         }
2742
2743         // Update the bookmarks cursor with the current contents of this folder.
2744         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2745
2746         // Update the `ListView`.
2747         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2748     }
2749
2750     // Override `onBackPressed()` to handle the navigation drawer and and the WebViews.
2751     @Override
2752     public void onBackPressed() {
2753         // Check the different options for processing `back`.
2754         if (drawerLayout.isDrawerVisible(GravityCompat.START)) {  // The navigation drawer is open.
2755             // Close the navigation drawer.
2756             drawerLayout.closeDrawer(GravityCompat.START);
2757         } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){  // The bookmarks drawer is open.
2758             // close the bookmarks drawer.
2759             drawerLayout.closeDrawer(GravityCompat.END);
2760         } else if (displayingFullScreenVideo) {  // A full screen video is shown.
2761             // Exit the full screen video.
2762             exitFullScreenVideo();
2763         } else if (currentWebView.canGoBack()) {  // There is at least one item in the current WebView history.
2764             // Get the current web back forward list.
2765             WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2766
2767             // Get the previous entry URL.
2768             String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
2769
2770             // Apply the domain settings.
2771             applyDomainSettings(currentWebView, previousUrl, false, false, false);
2772
2773             // Go back.
2774             currentWebView.goBack();
2775         } else if (tabLayout.getTabCount() > 1) {  // There are at least two tabs.
2776             // Close the current tab.
2777             closeCurrentTab();
2778         } else {  // There isn't anything to do in Privacy Browser.
2779             // Close Privacy Browser.  `finishAndRemoveTask()` also removes Privacy Browser from the recent app list.
2780             finishAndRemoveTask();
2781
2782             // Manually kill Privacy Browser.  Otherwise, it is glitchy when restarted.
2783             System.exit(0);
2784         }
2785     }
2786
2787     // Process the results of a file browse.
2788     @Override
2789     public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) {
2790         // Run the default commands.
2791         super.onActivityResult(requestCode, resultCode, returnedIntent);
2792
2793         // Run the commands that correlate to the specified request code.
2794         switch (requestCode) {
2795             case BROWSE_FILE_UPLOAD_REQUEST_CODE:
2796                 // Pass the file to the WebView.
2797                 fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, returnedIntent));
2798                 break;
2799
2800             case BROWSE_OPEN_REQUEST_CODE:
2801                 // Don't do anything if the user pressed back from the file picker.
2802                 if (resultCode == Activity.RESULT_OK) {
2803                     // Get a handle for the open dialog fragment.
2804                     DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open));
2805
2806                     // Only update the file name if the dialog still exists.
2807                     if (openDialogFragment != null) {
2808                         // Get a handle for the open dialog.
2809                         Dialog openDialog = openDialogFragment.getDialog();
2810
2811                         // Remove the incorrect lint warning below that the dialog might be null.
2812                         assert openDialog != null;
2813
2814                         // Get a handle for the file name edit text.
2815                         EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext);
2816
2817                         // Get the file name URI from the intent.
2818                         Uri fileNameUri = returnedIntent.getData();
2819
2820                         // Get the file name string from the URI.
2821                         String fileNameString = fileNameUri.toString();
2822
2823                         // Set the file name text.
2824                         fileNameEditText.setText(fileNameString);
2825
2826                         // Move the cursor to the end of the file name edit text.
2827                         fileNameEditText.setSelection(fileNameString.length());
2828                     }
2829                 }
2830                 break;
2831         }
2832     }
2833
2834     private void loadUrlFromTextBox() {
2835         // Get the text from urlTextBox and convert it to a string.  trim() removes white spaces from the beginning and end of the string.
2836         String unformattedUrlString = urlEditText.getText().toString().trim();
2837
2838         // Initialize the formatted URL string.
2839         String url = "";
2840
2841         // Check to see if `unformattedUrlString` is a valid URL.  Otherwise, convert it into a search.
2842         if (unformattedUrlString.startsWith("content://")) {  // This is a Content URL.
2843             // Load the entire content URL.
2844             url = unformattedUrlString;
2845         } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
2846                 unformattedUrlString.startsWith("file://")) {  // This is a standard URL.
2847             // Add `https://` at the beginning if there is no protocol.  Otherwise the app will segfault.
2848             if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
2849                 unformattedUrlString = "https://" + unformattedUrlString;
2850             }
2851
2852             // Initialize `unformattedUrl`.
2853             URL unformattedUrl = null;
2854
2855             // Convert `unformattedUrlString` to a `URL`, then to a `URI`, and then back to a `String`, which sanitizes the input and adds in any missing components.
2856             try {
2857                 unformattedUrl = new URL(unformattedUrlString);
2858             } catch (MalformedURLException e) {
2859                 e.printStackTrace();
2860             }
2861
2862             // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
2863             String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
2864             String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
2865             String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
2866             String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
2867             String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
2868
2869             // Build the URI.
2870             Uri.Builder uri = new Uri.Builder();
2871             uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
2872
2873             // Decode the URI as a UTF-8 string in.
2874             try {
2875                 url = URLDecoder.decode(uri.build().toString(), "UTF-8");
2876             } catch (UnsupportedEncodingException exception) {
2877                 // Do nothing.  The formatted URL string will remain blank.
2878             }
2879         } else if (!unformattedUrlString.isEmpty()){  // This is not a URL, but rather a search string.
2880             // Create an encoded URL String.
2881             String encodedUrlString;
2882
2883             // Sanitize the search input.
2884             try {
2885                 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
2886             } catch (UnsupportedEncodingException exception) {
2887                 encodedUrlString = "";
2888             }
2889
2890             // Add the base search URL.
2891             url = searchURL + encodedUrlString;
2892         }
2893
2894         // Clear the focus from the URL edit text.  Otherwise, proximate typing in the box will retain the colorized formatting instead of being reset during refocus.
2895         urlEditText.clearFocus();
2896
2897         // Make it so.
2898         loadUrl(currentWebView, url);
2899     }
2900
2901     private void loadUrl(NestedScrollWebView nestedScrollWebView, String url) {
2902         // Sanitize the URL.
2903         url = sanitizeUrl(url);
2904
2905         // Apply the domain settings and load the URL.
2906         applyDomainSettings(nestedScrollWebView, url, true, false, true);
2907     }
2908
2909     public void findPreviousOnPage(View view) {
2910         // Go to the previous highlighted phrase on the page.  `false` goes backwards instead of forwards.
2911         currentWebView.findNext(false);
2912     }
2913
2914     public void findNextOnPage(View view) {
2915         // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
2916         currentWebView.findNext(true);
2917     }
2918
2919     public void closeFindOnPage(View view) {
2920         // Get a handle for the views.
2921         Toolbar toolbar = findViewById(R.id.toolbar);
2922         LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
2923         EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
2924
2925         // Delete the contents of `find_on_page_edittext`.
2926         findOnPageEditText.setText(null);
2927
2928         // Clear the highlighted phrases if the WebView is not null.
2929         if (currentWebView != null) {
2930             currentWebView.clearMatches();
2931         }
2932
2933         // Hide the find on page linear layout.
2934         findOnPageLinearLayout.setVisibility(View.GONE);
2935
2936         // Show the toolbar.
2937         toolbar.setVisibility(View.VISIBLE);
2938
2939         // Get a handle for the input method manager.