]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Update Download with External App summary. https://redmine.stoutner.com/issues/921
[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.OnBackPressedCallback;
103 import androidx.activity.result.ActivityResultCallback;
104 import androidx.activity.result.ActivityResultLauncher;
105 import androidx.activity.result.contract.ActivityResultContracts;
106 import androidx.annotation.NonNull;
107 import androidx.appcompat.app.ActionBar;
108 import androidx.appcompat.app.ActionBarDrawerToggle;
109 import androidx.appcompat.app.AppCompatActivity;
110 import androidx.appcompat.app.AppCompatDelegate;
111 import androidx.appcompat.widget.Toolbar;
112 import androidx.coordinatorlayout.widget.CoordinatorLayout;
113 import androidx.core.content.res.ResourcesCompat;
114 import androidx.core.view.GravityCompat;
115 import androidx.drawerlayout.widget.DrawerLayout;
116 import androidx.fragment.app.DialogFragment;
117 import androidx.fragment.app.Fragment;
118 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
119 import androidx.viewpager.widget.ViewPager;
120 import androidx.webkit.WebSettingsCompat;
121 import androidx.webkit.WebViewFeature;
122
123 import com.google.android.material.appbar.AppBarLayout;
124 import com.google.android.material.floatingactionbutton.FloatingActionButton;
125 import com.google.android.material.navigation.NavigationView;
126 import com.google.android.material.snackbar.Snackbar;
127 import com.google.android.material.tabs.TabLayout;
128
129 import com.stoutner.privacybrowser.R;
130 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
131 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
132 import com.stoutner.privacybrowser.asynctasks.PopulateBlocklists;
133 import com.stoutner.privacybrowser.asynctasks.PrepareSaveDialog;
134 import com.stoutner.privacybrowser.asynctasks.SaveUrl;
135 import com.stoutner.privacybrowser.asynctasks.SaveWebpageImage;
136 import com.stoutner.privacybrowser.dataclasses.PendingDialog;
137 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
138 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
139 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
140 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
141 import com.stoutner.privacybrowser.dialogs.FontSizeDialog;
142 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
143 import com.stoutner.privacybrowser.dialogs.OpenDialog;
144 import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog;
145 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
146 import com.stoutner.privacybrowser.dialogs.SaveDialog;
147 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
148 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
149 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
150 import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog;
151 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
152 import com.stoutner.privacybrowser.helpers.BlocklistHelper;
153 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
154 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
155 import com.stoutner.privacybrowser.helpers.ProxyHelper;
156 import com.stoutner.privacybrowser.helpers.SanitizeUrlHelper;
157 import com.stoutner.privacybrowser.views.NestedScrollWebView;
158
159 import java.io.ByteArrayInputStream;
160 import java.io.ByteArrayOutputStream;
161 import java.io.File;
162 import java.io.FileInputStream;
163 import java.io.FileOutputStream;
164 import java.io.IOException;
165 import java.io.InputStream;
166 import java.io.OutputStream;
167 import java.io.UnsupportedEncodingException;
168
169 import java.net.MalformedURLException;
170 import java.net.URL;
171 import java.net.URLDecoder;
172 import java.net.URLEncoder;
173
174 import java.text.NumberFormat;
175
176 import java.util.ArrayList;
177 import java.util.Date;
178 import java.util.HashSet;
179 import java.util.List;
180 import java.util.Objects;
181 import java.util.Set;
182 import java.util.concurrent.ExecutorService;
183 import java.util.concurrent.Executors;
184
185 import kotlin.Pair;
186
187 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
188         EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener,
189         PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveDialog.SaveListener, UrlHistoryDialog.NavigateHistoryListener,
190         WebViewTabFragment.NewTabListener {
191
192     // Define the public static variables.
193     public static final ExecutorService executorService = Executors.newFixedThreadPool(4);
194     public static String orbotStatus = "unknown";
195     public static final ArrayList<PendingDialog> pendingDialogsArrayList =  new ArrayList<>();
196     public static String proxyMode = ProxyHelper.NONE;
197
198     // Declare the public static variables.
199     public static String currentBookmarksFolder = "";
200     public static boolean restartFromBookmarksActivity;
201     public static WebViewPagerAdapter webViewPagerAdapter;
202
203     // Declare the public static views.
204     public static AppBarLayout appBarLayout;
205
206     // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
207     public final static int UNRECOGNIZED_USER_AGENT = -1;
208     public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
209     public final static int SETTINGS_CUSTOM_USER_AGENT = 11;
210     public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
211     public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
212     public final static int DOMAINS_CUSTOM_USER_AGENT = 12;
213
214     // Define the start activity for result request codes.  The public static entry is accessed from `OpenDialog()`.
215     private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 0;
216     public final static int BROWSE_OPEN_REQUEST_CODE = 1;
217
218     // Define the saved instance state constants.
219     private final String SAVED_STATE_ARRAY_LIST = "saved_state_array_list";
220     private final String SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list";
221     private final String SAVED_TAB_POSITION = "saved_tab_position";
222     private final String PROXY_MODE = "proxy_mode";
223
224     // Define the saved instance state variables.
225     private ArrayList<Bundle> savedStateArrayList;
226     private ArrayList<Bundle> savedNestedScrollWebViewStateArrayList;
227     private int savedTabPosition;
228     private String savedProxyMode;
229
230     // Define the class variables.
231     @SuppressWarnings("rawtypes")
232     AsyncTask populateBlocklists;
233
234     // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
235     // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxy()`, and `applyDomainSettings()`.
236     private NestedScrollWebView currentWebView;
237
238     // The search URL is set in `applyAppSettings()` and used in `onNewIntent()`, `loadUrlFromTextBox()`, `initializeApp()`, and `initializeWebView()`.
239     private String searchURL;
240
241     // The blocklists are populated in `finishedPopulatingBlocklists()` and accessed from `initializeWebView()`.
242     private ArrayList<List<String[]>> easyList;
243     private ArrayList<List<String[]>> easyPrivacy;
244     private ArrayList<List<String[]>> fanboysAnnoyanceList;
245     private ArrayList<List<String[]>> fanboysSocialList;
246     private ArrayList<List<String[]>> ultraList;
247     private ArrayList<List<String[]>> ultraPrivacy;
248
249     // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
250     private ActionBarDrawerToggle actionBarDrawerToggle;
251
252     // The color spans are used in `onCreate()` and `highlightUrlText()`.
253     private ForegroundColorSpan redColorSpan;
254     private ForegroundColorSpan initialGrayColorSpan;
255     private ForegroundColorSpan finalGrayColorSpan;
256
257     // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
258     private Cursor bookmarksCursor;
259
260     // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
261     private CursorAdapter bookmarksCursorAdapter;
262
263     // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
264     private String oldFolderNameString;
265
266     // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
267     private ValueCallback<Uri[]> fileChooserCallback;
268
269     // The default progress view offsets are set in `onCreate()` and used in `initializeWebView()`.
270     private int appBarHeight;
271     private int defaultProgressViewStartOffset;
272     private int defaultProgressViewEndOffset;
273
274     // Declare the helpers.
275     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
276     private DomainsDatabaseHelper domainsDatabaseHelper;
277     private ProxyHelper proxyHelper;
278     private SanitizeUrlHelper sanitizeUrlHelper;
279
280     // Declare the class variables
281     private boolean bottomAppBar;
282     private boolean displayAdditionalAppBarIcons;
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 boolean reapplyAppSettingsOnRestart;
292     private boolean reapplyDomainSettingsOnRestart;
293     private boolean sanitizeAmpRedirects;
294     private boolean sanitizeTrackingQueries;
295     private boolean scrollAppBar;
296     private boolean waitingForProxy;
297     private String webViewDefaultUserAgent;
298
299     // Define the class variables.
300     private ObjectAnimator objectAnimator = new ObjectAnimator();
301     private String saveUrlString = "";
302
303     // Declare the class views.
304     private FrameLayout rootFrameLayout;
305     private DrawerLayout drawerLayout;
306     private CoordinatorLayout coordinatorLayout;
307     private Toolbar toolbar;
308     private RelativeLayout urlRelativeLayout;
309     private EditText urlEditText;
310     private ActionBar actionBar;
311     private LinearLayout findOnPageLinearLayout;
312     private LinearLayout tabsLinearLayout;
313     private TabLayout tabLayout;
314     private SwipeRefreshLayout swipeRefreshLayout;
315     private ViewPager webViewPager;
316     private FrameLayout fullScreenVideoFrameLayout;
317
318     // Declare the class menus.
319     private Menu optionsMenu;
320
321     // Declare the class menu items.
322     private MenuItem navigationBackMenuItem;
323     private MenuItem navigationForwardMenuItem;
324     private MenuItem navigationHistoryMenuItem;
325     private MenuItem navigationRequestsMenuItem;
326     private MenuItem optionsPrivacyMenuItem;
327     private MenuItem optionsRefreshMenuItem;
328     private MenuItem optionsCookiesMenuItem;
329     private MenuItem optionsDomStorageMenuItem;
330     private MenuItem optionsSaveFormDataMenuItem;
331     private MenuItem optionsClearDataMenuItem;
332     private MenuItem optionsClearCookiesMenuItem;
333     private MenuItem optionsClearDomStorageMenuItem;
334     private MenuItem optionsClearFormDataMenuItem;
335     private MenuItem optionsBlocklistsMenuItem;
336     private MenuItem optionsEasyListMenuItem;
337     private MenuItem optionsEasyPrivacyMenuItem;
338     private MenuItem optionsFanboysAnnoyanceListMenuItem;
339     private MenuItem optionsFanboysSocialBlockingListMenuItem;
340     private MenuItem optionsUltraListMenuItem;
341     private MenuItem optionsUltraPrivacyMenuItem;
342     private MenuItem optionsBlockAllThirdPartyRequestsMenuItem;
343     private MenuItem optionsProxyMenuItem;
344     private MenuItem optionsProxyNoneMenuItem;
345     private MenuItem optionsProxyTorMenuItem;
346     private MenuItem optionsProxyI2pMenuItem;
347     private MenuItem optionsProxyCustomMenuItem;
348     private MenuItem optionsUserAgentMenuItem;
349     private MenuItem optionsUserAgentPrivacyBrowserMenuItem;
350     private MenuItem optionsUserAgentWebViewDefaultMenuItem;
351     private MenuItem optionsUserAgentFirefoxOnAndroidMenuItem;
352     private MenuItem optionsUserAgentChromeOnAndroidMenuItem;
353     private MenuItem optionsUserAgentSafariOnIosMenuItem;
354     private MenuItem optionsUserAgentFirefoxOnLinuxMenuItem;
355     private MenuItem optionsUserAgentChromiumOnLinuxMenuItem;
356     private MenuItem optionsUserAgentFirefoxOnWindowsMenuItem;
357     private MenuItem optionsUserAgentChromeOnWindowsMenuItem;
358     private MenuItem optionsUserAgentEdgeOnWindowsMenuItem;
359     private MenuItem optionsUserAgentInternetExplorerOnWindowsMenuItem;
360     private MenuItem optionsUserAgentSafariOnMacosMenuItem;
361     private MenuItem optionsUserAgentCustomMenuItem;
362     private MenuItem optionsSwipeToRefreshMenuItem;
363     private MenuItem optionsWideViewportMenuItem;
364     private MenuItem optionsDisplayImagesMenuItem;
365     private MenuItem optionsDarkWebViewMenuItem;
366     private MenuItem optionsFontSizeMenuItem;
367     private MenuItem optionsAddOrEditDomainMenuItem;
368
369     // This variable won't be needed once the class is migrated to Kotlin, as can be seen in LogcatActivity or AboutVersionFragment.
370     private Activity resultLauncherActivityHandle;
371
372     // Define the save URL activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
373     private final ActivityResultLauncher<String> saveUrlActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("*/*"),
374             new ActivityResultCallback<Uri>() {
375                 @Override
376                 public void onActivityResult(Uri fileUri) {
377                     // Only save the URL if the file URI is not null, which happens if the user exited the file picker by pressing back.
378                     if (fileUri != null) {
379                         new SaveUrl(getApplicationContext(), resultLauncherActivityHandle, fileUri, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()).execute(saveUrlString);
380                     }
381
382                     // Reset the save URL string.
383                     saveUrlString = "";
384                 }
385             });
386
387     // Define the save webpage archive activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
388     private final ActivityResultLauncher<String> saveWebpageArchiveActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("multipart/related"),
389             new ActivityResultCallback<Uri>() {
390                 @Override
391                 public void onActivityResult(Uri fileUri) {
392                     // Only save the webpage archive if the file URI is not null, which happens if the user exited the file picker by pressing back.
393                     if (fileUri != null) {
394                         // Initialize the file name string from the file URI last path segment.
395                         String temporaryFileNameString = fileUri.getLastPathSegment();
396
397                         // Query the exact file name if the API >= 26.
398                         if (Build.VERSION.SDK_INT >= 26) {
399                             // Get a cursor from the content resolver.
400                             Cursor contentResolverCursor = resultLauncherActivityHandle.getContentResolver().query(fileUri, null, null, null);
401
402                             // Move to the fist row.
403                             contentResolverCursor.moveToFirst();
404
405                             // Get the file name from the cursor.
406                             temporaryFileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
407
408                             // Close the cursor.
409                             contentResolverCursor.close();
410                         }
411
412                         // Save the final file name string so it can be used inside the lambdas.  This will no longer be needed once this activity has transitioned to Kotlin.
413                         String finalFileNameString = temporaryFileNameString;
414
415                         try {
416                             // Create a temporary MHT file.
417                             File temporaryMhtFile = File.createTempFile("temporary_mht_file", ".mht", getCacheDir());
418                             currentWebView.saveWebArchive(temporaryMhtFile.toString(), false, callbackValue -> {
419                                 if (callbackValue != null) {  // The temporary MHT file was saved successfully.
420                                     try {
421                                         // Create a temporary MHT file input stream.
422                                         FileInputStream temporaryMhtFileInputStream = new FileInputStream(temporaryMhtFile);
423
424                                         // Get an output stream for the save webpage file path.
425                                         OutputStream mhtOutputStream = getContentResolver().openOutputStream(fileUri);
426
427                                         // Create a transfer byte array.
428                                         byte[] transferByteArray = new byte[1024];
429
430                                         // Create an integer to track the number of bytes read.
431                                         int bytesRead;
432
433                                         // Copy the temporary MHT file input stream to the MHT output stream.
434                                         while ((bytesRead = temporaryMhtFileInputStream.read(transferByteArray)) > 0) {
435                                             mhtOutputStream.write(transferByteArray, 0, bytesRead);
436                                         }
437
438                                         // Close the streams.
439                                         mhtOutputStream.close();
440                                         temporaryMhtFileInputStream.close();
441
442                                         // Display a snackbar.
443                                         Snackbar.make(currentWebView, getString(R.string.saved, finalFileNameString), Snackbar.LENGTH_SHORT).show();
444                                     } catch (Exception exception) {
445                                         // Display a snackbar with the exception.
446                                         Snackbar.make(currentWebView, getString(R.string.error_saving_file, finalFileNameString, exception), Snackbar.LENGTH_INDEFINITE).show();
447                                     } finally {
448                                         // Delete the temporary MHT file.
449                                         //noinspection ResultOfMethodCallIgnored
450                                         temporaryMhtFile.delete();
451                                     }
452                                 } else {  // There was an unspecified error while saving the temporary MHT file.
453                                     // Display an error snackbar.
454                                     Snackbar.make(currentWebView, getString(R.string.error_saving_file, finalFileNameString, getString(R.string.unknown_error)), Snackbar.LENGTH_INDEFINITE).show();
455                                 }
456                             });
457                         } catch (IOException ioException) {
458                             // Display a snackbar with the IO exception.
459                             Snackbar.make(currentWebView, getString(R.string.error_saving_file, finalFileNameString, ioException), Snackbar.LENGTH_INDEFINITE).show();
460                         }
461                     }
462                 }
463             });
464
465     // Define the save webpage image activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
466     private final ActivityResultLauncher<String> saveWebpageImageActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("image/png"),
467             new ActivityResultCallback<Uri>() {
468                 @Override
469                 public void onActivityResult(Uri fileUri) {
470                     // Only save the webpage image if the file URI is not null, which happens if the user exited the file picker by pressing back.
471                     if (fileUri != null) {
472                         // Save the webpage image.
473                         new SaveWebpageImage(resultLauncherActivityHandle, fileUri, currentWebView).execute();
474                     }
475                 }
476             });
477
478     // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with WebView.
479     @SuppressLint("ClickableViewAccessibility")
480     @Override
481     protected void onCreate(Bundle savedInstanceState) {
482         // Run the default commands.
483         super.onCreate(savedInstanceState);
484
485         // Populate the result launcher activity.  This will no longer be needed once the activity has transitioned to Kotlin.
486         resultLauncherActivityHandle = this;
487
488         // Check to see if the activity has been restarted.
489         if (savedInstanceState != null) {
490             // Store the saved instance state variables.
491             savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST);
492             savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST);
493             savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION);
494             savedProxyMode = savedInstanceState.getString(PROXY_MODE);
495         }
496
497         // Initialize the default preference values the first time the program is run.  `false` keeps this command from resetting any current preferences back to default.
498         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
499
500         // Get a handle for the shared preferences.
501         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
502
503         // Get the preferences.
504         String appTheme = sharedPreferences.getString(getString(R.string.app_theme_key), getString(R.string.app_theme_default_value));
505         boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
506         bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false);
507         displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false);
508
509         // Get the theme entry values string array.
510         String[] appThemeEntryValuesStringArray = getResources().getStringArray(R.array.app_theme_entry_values);
511
512         // 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.
513         if (appTheme.equals(appThemeEntryValuesStringArray[1])) {  // The light theme is selected.
514             // Apply the light theme.
515             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
516         } else if (appTheme.equals(appThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
517             // Apply the dark theme.
518             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
519         } else {  // The system default theme is selected.
520             if (Build.VERSION.SDK_INT >= 28) {  // The system default theme is supported.
521                 // Follow the system default theme.
522                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
523             } else {  // The system default theme is not supported.
524                 // Follow the battery saver mode.
525                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
526             }
527         }
528
529         // Disable screenshots if not allowed.
530         if (!allowScreenshots) {
531             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
532         }
533
534         // 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.
535         WebView.enableSlowWholeDocumentDraw();
536
537         // Set the content view according to the position of the app bar.
538         if (bottomAppBar) setContentView(R.layout.main_framelayout_bottom_appbar);
539         else setContentView(R.layout.main_framelayout_top_appbar);
540
541         // Get handles for the views.
542         rootFrameLayout = findViewById(R.id.root_framelayout);
543         drawerLayout = findViewById(R.id.drawerlayout);
544         coordinatorLayout = findViewById(R.id.coordinatorlayout);
545         appBarLayout = findViewById(R.id.appbar_layout);
546         toolbar = findViewById(R.id.toolbar);
547         findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
548         tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
549         tabLayout = findViewById(R.id.tablayout);
550         swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
551         webViewPager = findViewById(R.id.webviewpager);
552         NavigationView navigationView = findViewById(R.id.navigationview);
553         fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
554
555         // Get a handle for the navigation menu.
556         Menu navigationMenu = navigationView.getMenu();
557
558         // Get handles for the navigation menu items.
559         navigationBackMenuItem = navigationMenu.findItem(R.id.back);
560         navigationForwardMenuItem = navigationMenu.findItem(R.id.forward);
561         navigationHistoryMenuItem = navigationMenu.findItem(R.id.history);
562         navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests);
563
564         // Listen for touches on the navigation menu.
565         navigationView.setNavigationItemSelectedListener(this);
566
567         // Get a handle for the app compat delegate.
568         AppCompatDelegate appCompatDelegate = getDelegate();
569
570         // Set the support action bar.
571         appCompatDelegate.setSupportActionBar(toolbar);
572
573         // Get a handle for the action bar.
574         actionBar = appCompatDelegate.getSupportActionBar();
575
576         // Remove the incorrect lint warning below that the action bar might be null.
577         assert actionBar != null;
578
579         // Add the custom layout, which shows the URL text bar.
580         actionBar.setCustomView(R.layout.url_app_bar);
581         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
582
583         // Get handles for the views in the URL app bar.
584         urlRelativeLayout = findViewById(R.id.url_relativelayout);
585         urlEditText = findViewById(R.id.url_edittext);
586
587         // Create the hamburger icon at the start of the AppBar.
588         actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
589
590         // Initially disable the sliding drawers.  They will be enabled once the blocklists are loaded.
591         drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
592
593         // Initially hide the user interface so that only the blocklist loading screen is shown (if reloading).
594         drawerLayout.setVisibility(View.GONE);
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 helpers.
606         bookmarksDatabaseHelper = new BookmarksDatabaseHelper(this);
607         domainsDatabaseHelper = new DomainsDatabaseHelper(this);
608         proxyHelper = new ProxyHelper();
609         sanitizeUrlHelper = new SanitizeUrlHelper();
610
611         // Initialize the app.
612         initializeApp();
613
614         // Apply the app settings from the shared preferences.
615         applyAppSettings();
616
617         // Control what the system back command does.
618         OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
619             @Override
620             public void handleOnBackPressed() {
621                 // Process the different back options.
622                 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {  // The navigation drawer is open.
623                     // Close the navigation drawer.
624                     drawerLayout.closeDrawer(GravityCompat.START);
625                 } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){  // The bookmarks drawer is open.
626                     // close the bookmarks drawer.
627                     drawerLayout.closeDrawer(GravityCompat.END);
628                 } else if (displayingFullScreenVideo) {  // A full screen video is shown.
629                     // Exit the full screen video.
630                     exitFullScreenVideo();
631                 } else if (currentWebView.canGoBack()) {  // There is at least one item in the current WebView history.
632                     // Get the current web back forward list.
633                     WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
634
635                     // Get the previous entry URL.
636                     String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
637
638                     // Apply the domain settings.
639                     applyDomainSettings(currentWebView, previousUrl, false, false, false);
640
641                     // Go back.
642                     currentWebView.goBack();
643                 } else if (tabLayout.getTabCount() > 1) {  // There are at least two tabs.
644                     // Close the current tab.
645                     closeCurrentTab();
646                 } else {  // There isn't anything to do in Privacy Browser.
647                     // Close Privacy Browser.  `finishAndRemoveTask()` also removes Privacy Browser from the recent app list.
648                     finishAndRemoveTask();
649
650                     // Manually kill Privacy Browser.  Otherwise, it is glitchy when restarted.
651                     System.exit(0);
652                 }
653             }
654         };
655
656         // Register the on back pressed callback.
657         getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
658
659         // Populate the blocklists.
660         populateBlocklists = new PopulateBlocklists(this, this).execute();
661     }
662
663     @Override
664     protected void onNewIntent(Intent intent) {
665         // Run the default commands.
666         super.onNewIntent(intent);
667
668         // Check to see if the app is being restarted from a saved state.
669         if (savedStateArrayList == null || savedStateArrayList.size() == 0) {  // The activity is not being restarted from a saved state.
670             // Get the information from the intent.
671             String intentAction = intent.getAction();
672             Uri intentUriData = intent.getData();
673             String intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
674
675             // Determine if this is a web search.
676             boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH));
677
678             // 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.
679             if (intentUriData != null || intentStringExtra != null || isWebSearch) {
680                 // Exit the full screen video if it is displayed.
681                 if (displayingFullScreenVideo) {
682                     // Exit full screen video mode.
683                     exitFullScreenVideo();
684
685                     // Reload the current WebView.  Otherwise, it can display entirely black.
686                     currentWebView.reload();
687                 }
688
689                 // Get the shared preferences.
690                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
691
692                 // Create a URL string.
693                 String url;
694
695                 // If the intent action is a web search, perform the search.
696                 if (isWebSearch) {  // The intent is a web search.
697                     // Create an encoded URL string.
698                     String encodedUrlString;
699
700                     // Sanitize the search input and convert it to a search.
701                     try {
702                         encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
703                     } catch (UnsupportedEncodingException exception) {
704                         encodedUrlString = "";
705                     }
706
707                     // Add the base search URL.
708                     url = searchURL + encodedUrlString;
709                 } else if (intentUriData != null) {  // The intent contains a URL formatted as a URI.
710                     // Set the intent data as the URL.
711                     url = intentUriData.toString();
712                 } else {  // The intent contains a string, which might be a URL.
713                     // Set the intent string as the URL.
714                     url = intentStringExtra;
715                 }
716
717                 // Add a new tab if specified in the preferences.
718                 if (sharedPreferences.getBoolean(getString(R.string.open_intents_in_new_tab_key), true)) {  // Load the URL in a new tab.
719                     // Set the loading new intent flag.
720                     loadingNewIntent = true;
721
722                     // Add a new tab.
723                     addNewTab(url, true);
724                 } else {  // Load the URL in the current tab.
725                     // Make it so.
726                     loadUrl(currentWebView, url);
727                 }
728
729                 // Close the navigation drawer if it is open.
730                 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
731                     drawerLayout.closeDrawer(GravityCompat.START);
732                 }
733
734                 // Close the bookmarks drawer if it is open.
735                 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
736                     drawerLayout.closeDrawer(GravityCompat.END);
737                 }
738             }
739         } else {  // The app has been restarted.
740             // Replace the intent that started the app with this one.  This will load the tab after the others have been restored.
741             setIntent(intent);
742         }
743     }
744
745     @Override
746     public void onRestart() {
747         // Run the default commands.
748         super.onRestart();
749
750         // Apply the app settings if returning from the Settings activity.
751         if (reapplyAppSettingsOnRestart) {
752             // Reset the reapply app settings on restart tracker.
753             reapplyAppSettingsOnRestart = false;
754
755             // Apply the app settings.
756             applyAppSettings();
757         }
758
759         // Apply the domain settings if returning from the settings or domains activity.
760         if (reapplyDomainSettingsOnRestart) {
761             // Reset the reapply domain settings on restart tracker.
762             reapplyDomainSettingsOnRestart = false;
763
764             // Reapply the domain settings for each tab.
765             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
766                 // Get the WebView tab fragment.
767                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
768
769                 // Get the fragment view.
770                 View fragmentView = webViewTabFragment.getView();
771
772                 // Only reload the WebViews if they exist.
773                 if (fragmentView != null) {
774                     // Get the nested scroll WebView from the tab fragment.
775                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
776
777                     // Reset the current domain name so the domain settings will be reapplied.
778                     nestedScrollWebView.setCurrentDomainName("");
779
780                     // Reapply the domain settings if the URL is not null, which can happen if an empty tab is active when returning from settings.
781                     if (nestedScrollWebView.getUrl() != null) {
782                         applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true, false);
783                     }
784                 }
785             }
786         }
787
788         // Update the bookmarks drawer if returning from the Bookmarks activity.
789         if (restartFromBookmarksActivity) {
790             // Close the bookmarks drawer.
791             drawerLayout.closeDrawer(GravityCompat.END);
792
793             // Reload the bookmarks drawer.
794             loadBookmarksFolder();
795
796             // Reset `restartFromBookmarksActivity`.
797             restartFromBookmarksActivity = false;
798         }
799
800         // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.  This can be important if the screen was rotated.
801         updatePrivacyIcons(true);
802     }
803
804     // `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.
805     @Override
806     public void onStart() {
807         // Run the default commands.
808         super.onStart();
809
810         // Resume any WebViews.
811         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
812             // Get the WebView tab fragment.
813             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
814
815             // Get the fragment view.
816             View fragmentView = webViewTabFragment.getView();
817
818             // Only resume the WebViews if they exist (they won't when the app is first created).
819             if (fragmentView != null) {
820                 // Get the nested scroll WebView from the tab fragment.
821                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
822
823                 // Resume the nested scroll WebView.
824                 nestedScrollWebView.onResume();
825             }
826         }
827
828         // Resume the nested scroll WebView JavaScript timers.  This is a global command that resumes JavaScript timers on all WebViews.
829         if (currentWebView != null) {
830             currentWebView.resumeTimers();
831         }
832
833         // Reapply the proxy settings if the system is using a proxy.  This redisplays the appropriate alert dialog.
834         if (!proxyMode.equals(ProxyHelper.NONE)) {
835             applyProxy(false);
836         }
837
838         // Reapply any system UI flags.
839         if (displayingFullScreenVideo || inFullScreenBrowsingMode) {  // The system is displaying a website or a video in full screen mode.
840             /* Hide the system bars.
841              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
842              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
843              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
844              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
845              */
846             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
847                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
848         }
849
850         // Show any pending dialogs.
851         for (int i = 0; i < pendingDialogsArrayList.size(); i++) {
852             // Get the pending dialog from the array list.
853             PendingDialog pendingDialog = pendingDialogsArrayList.get(i);
854
855             // Show the pending dialog.
856             pendingDialog.dialogFragment.show(getSupportFragmentManager(), pendingDialog.tag);
857         }
858
859         // Clear the pending dialogs array list.
860         pendingDialogsArrayList.clear();
861     }
862
863     // `onStop()` runs after `onPause()`.  It is used instead of `onPause()` so the commands are not called every time the screen is partially hidden.
864     @Override
865     public void onStop() {
866         // Run the default commands.
867         super.onStop();
868
869         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
870             // Get the WebView tab fragment.
871             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
872
873             // Get the fragment view.
874             View fragmentView = webViewTabFragment.getView();
875
876             // Only pause the WebViews if they exist (they won't when the app is first created).
877             if (fragmentView != null) {
878                 // Get the nested scroll WebView from the tab fragment.
879                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
880
881                 // Pause the nested scroll WebView.
882                 nestedScrollWebView.onPause();
883             }
884         }
885
886         // Pause the WebView JavaScript timers.  This is a global command that pauses JavaScript on all WebViews.
887         if (currentWebView != null) {
888             currentWebView.pauseTimers();
889         }
890     }
891
892     @Override
893     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
894         // Run the default commands.
895         super.onSaveInstanceState(savedInstanceState);
896
897         // Create the saved state array lists.
898         ArrayList<Bundle> savedStateArrayList = new ArrayList<>();
899         ArrayList<Bundle> savedNestedScrollWebViewStateArrayList = new ArrayList<>();
900
901         // Get the URLs from each tab.
902         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
903             // Get the WebView tab fragment.
904             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
905
906             // Get the fragment view.
907             View fragmentView = webViewTabFragment.getView();
908
909             if (fragmentView != null) {
910                 // Get the nested scroll WebView from the tab fragment.
911                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
912
913                 // Create saved state bundle.
914                 Bundle savedStateBundle = new Bundle();
915
916                 // Get the current states.
917                 nestedScrollWebView.saveState(savedStateBundle);
918                 Bundle savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState();
919
920                 // Store the saved states in the array lists.
921                 savedStateArrayList.add(savedStateBundle);
922                 savedNestedScrollWebViewStateArrayList.add(savedNestedScrollWebViewStateBundle);
923             }
924         }
925
926         // Get the current tab position.
927         int currentTabPosition = tabLayout.getSelectedTabPosition();
928
929         // Store the saved states in the bundle.
930         savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList);
931         savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList);
932         savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition);
933         savedInstanceState.putString(PROXY_MODE, proxyMode);
934     }
935
936     @Override
937     public void onDestroy() {
938         // Unregister the orbot status broadcast receiver if it exists.
939         if (orbotStatusBroadcastReceiver != null) {
940             this.unregisterReceiver(orbotStatusBroadcastReceiver);
941         }
942
943         // Close the bookmarks cursor if it exists.
944         if (bookmarksCursor != null) {
945             bookmarksCursor.close();
946         }
947
948         // Close the bookmarks database if it exists.
949         if (bookmarksDatabaseHelper != null) {
950             bookmarksDatabaseHelper.close();
951         }
952
953         // Stop populating the blocklists if the AsyncTask is running in the background.
954         if (populateBlocklists != null) {
955             populateBlocklists.cancel(true);
956         }
957
958         // Run the default commands.
959         super.onDestroy();
960     }
961
962     @Override
963     public boolean onCreateOptionsMenu(Menu menu) {
964         // Inflate the menu.  This adds items to the action bar if it is present.
965         getMenuInflater().inflate(R.menu.webview_options_menu, menu);
966
967         // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
968         optionsMenu = menu;
969
970         // Get handles for the menu items.
971         optionsPrivacyMenuItem = menu.findItem(R.id.javascript);
972         optionsRefreshMenuItem = menu.findItem(R.id.refresh);
973         MenuItem bookmarksMenuItem = menu.findItem(R.id.bookmarks);
974         optionsCookiesMenuItem = menu.findItem(R.id.cookies);
975         optionsDomStorageMenuItem = menu.findItem(R.id.dom_storage);
976         optionsSaveFormDataMenuItem = menu.findItem(R.id.save_form_data);  // Form data can be removed once the minimum API >= 26.
977         optionsClearDataMenuItem = menu.findItem(R.id.clear_data);
978         optionsClearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
979         optionsClearDomStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
980         optionsClearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
981         optionsBlocklistsMenuItem = menu.findItem(R.id.blocklists);
982         optionsEasyListMenuItem = menu.findItem(R.id.easylist);
983         optionsEasyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
984         optionsFanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
985         optionsFanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
986         optionsUltraListMenuItem = menu.findItem(R.id.ultralist);
987         optionsUltraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
988         optionsBlockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
989         optionsProxyMenuItem = menu.findItem(R.id.proxy);
990         optionsProxyNoneMenuItem = menu.findItem(R.id.proxy_none);
991         optionsProxyTorMenuItem = menu.findItem(R.id.proxy_tor);
992         optionsProxyI2pMenuItem = menu.findItem(R.id.proxy_i2p);
993         optionsProxyCustomMenuItem = menu.findItem(R.id.proxy_custom);
994         optionsUserAgentMenuItem = menu.findItem(R.id.user_agent);
995         optionsUserAgentPrivacyBrowserMenuItem = menu.findItem(R.id.user_agent_privacy_browser);
996         optionsUserAgentWebViewDefaultMenuItem = menu.findItem(R.id.user_agent_webview_default);
997         optionsUserAgentFirefoxOnAndroidMenuItem = menu.findItem(R.id.user_agent_firefox_on_android);
998         optionsUserAgentChromeOnAndroidMenuItem = menu.findItem(R.id.user_agent_chrome_on_android);
999         optionsUserAgentSafariOnIosMenuItem = menu.findItem(R.id.user_agent_safari_on_ios);
1000         optionsUserAgentFirefoxOnLinuxMenuItem = menu.findItem(R.id.user_agent_firefox_on_linux);
1001         optionsUserAgentChromiumOnLinuxMenuItem = menu.findItem(R.id.user_agent_chromium_on_linux);
1002         optionsUserAgentFirefoxOnWindowsMenuItem = menu.findItem(R.id.user_agent_firefox_on_windows);
1003         optionsUserAgentChromeOnWindowsMenuItem = menu.findItem(R.id.user_agent_chrome_on_windows);
1004         optionsUserAgentEdgeOnWindowsMenuItem = menu.findItem(R.id.user_agent_edge_on_windows);
1005         optionsUserAgentInternetExplorerOnWindowsMenuItem = menu.findItem(R.id.user_agent_internet_explorer_on_windows);
1006         optionsUserAgentSafariOnMacosMenuItem = menu.findItem(R.id.user_agent_safari_on_macos);
1007         optionsUserAgentCustomMenuItem = menu.findItem(R.id.user_agent_custom);
1008         optionsSwipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
1009         optionsWideViewportMenuItem = menu.findItem(R.id.wide_viewport);
1010         optionsDisplayImagesMenuItem = menu.findItem(R.id.display_images);
1011         optionsDarkWebViewMenuItem = menu.findItem(R.id.dark_webview);
1012         optionsFontSizeMenuItem = menu.findItem(R.id.font_size);
1013         optionsAddOrEditDomainMenuItem = menu.findItem(R.id.add_or_edit_domain);
1014
1015         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
1016         updatePrivacyIcons(false);
1017
1018         // Only display the form data menu items if the API < 26.
1019         optionsSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1020         optionsClearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
1021
1022         // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
1023         optionsClearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
1024
1025         // 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.
1026         if (displayAdditionalAppBarIcons) {
1027             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
1028             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1029             optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
1030         } else { //Do not display the additional icons.
1031             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1032             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1033             optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
1034         }
1035
1036         // Replace Refresh with Stop if a URL is already loading.
1037         if (currentWebView != null && currentWebView.getProgress() != 100) {
1038             // Set the title.
1039             optionsRefreshMenuItem.setTitle(R.string.stop);
1040
1041             // Set the icon if it is displayed in the app bar.  Once the minimum API is >= 26, the blue and black icons can be combined with a tint list.
1042             if (displayAdditionalAppBarIcons) {
1043                 optionsRefreshMenuItem.setIcon(R.drawable.close_blue);
1044             }
1045         }
1046
1047         // Done.
1048         return true;
1049     }
1050
1051     @Override
1052     public boolean onPrepareOptionsMenu(Menu menu) {
1053         // Get a handle for the cookie manager.
1054         CookieManager cookieManager = CookieManager.getInstance();
1055
1056         // Initialize the current user agent string and the font size.
1057         String currentUserAgent = getString(R.string.user_agent_privacy_browser);
1058         int fontSize = 100;
1059
1060         // 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.
1061         if (currentWebView != null) {
1062             // Set the add or edit domain text.
1063             if (currentWebView.getDomainSettingsApplied()) {
1064                 optionsAddOrEditDomainMenuItem.setTitle(R.string.edit_domain_settings);
1065             } else {
1066                 optionsAddOrEditDomainMenuItem.setTitle(R.string.add_domain_settings);
1067             }
1068
1069             // Get the current user agent from the WebView.
1070             currentUserAgent = currentWebView.getSettings().getUserAgentString();
1071
1072             // Get the current font size from the
1073             fontSize = currentWebView.getSettings().getTextZoom();
1074
1075             // Set the status of the menu item checkboxes.
1076             optionsDomStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1077             optionsSaveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData());  // Form data can be removed once the minimum API >= 26.
1078             optionsEasyListMenuItem.setChecked(currentWebView.getEasyListEnabled());
1079             optionsEasyPrivacyMenuItem.setChecked(currentWebView.getEasyPrivacyEnabled());
1080             optionsFanboysAnnoyanceListMenuItem.setChecked(currentWebView.getFanboysAnnoyanceListEnabled());
1081             optionsFanboysSocialBlockingListMenuItem.setChecked(currentWebView.getFanboysSocialBlockingListEnabled());
1082             optionsUltraListMenuItem.setChecked(currentWebView.getUltraListEnabled());
1083             optionsUltraPrivacyMenuItem.setChecked(currentWebView.getUltraPrivacyEnabled());
1084             optionsBlockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.getBlockAllThirdPartyRequests());
1085             optionsSwipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
1086             optionsWideViewportMenuItem.setChecked(currentWebView.getSettings().getUseWideViewPort());
1087             optionsDisplayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
1088
1089             // Initialize the display names for the blocklists with the number of blocked requests.
1090             optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
1091             optionsEasyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
1092             optionsEasyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
1093             optionsFanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
1094             optionsFanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
1095             optionsUltraListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
1096             optionsUltraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
1097             optionsBlockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
1098
1099             // Enable DOM Storage if JavaScript is enabled.
1100             optionsDomStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
1101
1102             // Get the current theme status.
1103             int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
1104
1105             // Enable dark WebView if the API is < 33 or if night mode is enabled.
1106             optionsDarkWebViewMenuItem.setEnabled((Build.VERSION.SDK_INT < 33) || (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES));
1107
1108             // Set the checkbox status for dark WebView if the WebView supports it.
1109             if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {  // The device is running API >= 33 and algorithmic darkening is supported.
1110                 optionsDarkWebViewMenuItem.setChecked(WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView.getSettings()));
1111             } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {  // The device is running API < 33 and the WebView supports force dark.
1112                 //noinspection deprecation
1113                 optionsDarkWebViewMenuItem.setChecked(WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON);
1114             }
1115         }
1116
1117         // Set the cookies menu item checked status.
1118         optionsCookiesMenuItem.setChecked(cookieManager.acceptCookie());
1119
1120         // Enable Clear Cookies if there are any.
1121         optionsClearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1122
1123         // 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`.
1124         String privateDataDirectoryString = getApplicationInfo().dataDir;
1125
1126         // Get a count of the number of files in the Local Storage directory.
1127         File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1128         int localStorageDirectoryNumberOfFiles = 0;
1129         if (localStorageDirectory.exists()) {
1130             // `Objects.requireNonNull` removes a lint warning that `localStorageDirectory.list` might produce a null pointed exception if it is dereferenced.
1131             localStorageDirectoryNumberOfFiles = Objects.requireNonNull(localStorageDirectory.list()).length;
1132         }
1133
1134         // Get a count of the number of files in the IndexedDB directory.
1135         File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1136         int indexedDBDirectoryNumberOfFiles = 0;
1137         if (indexedDBDirectory.exists()) {
1138             // `Objects.requireNonNull` removes a lint warning that `indexedDBDirectory.list` might produce a null pointed exception if it is dereferenced.
1139             indexedDBDirectoryNumberOfFiles = Objects.requireNonNull(indexedDBDirectory.list()).length;
1140         }
1141
1142         // Enable Clear DOM Storage if there is any.
1143         optionsClearDomStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1144
1145         // Enable Clear Form Data is there is any.  This can be removed once the minimum API >= 26.
1146         if (Build.VERSION.SDK_INT < 26) {
1147             // Get the WebView database.
1148             WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
1149
1150             // Enable the clear form data menu item if there is anything to clear.
1151             optionsClearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
1152         }
1153
1154         // Enable Clear Data if any of the submenu items are enabled.
1155         optionsClearDataMenuItem.setEnabled(optionsClearCookiesMenuItem.isEnabled() || optionsClearDomStorageMenuItem.isEnabled() || optionsClearFormDataMenuItem.isEnabled());
1156
1157         // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1158         optionsFanboysSocialBlockingListMenuItem.setEnabled(!optionsFanboysAnnoyanceListMenuItem.isChecked());
1159
1160         // Set the proxy title and check the applied proxy.
1161         switch (proxyMode) {
1162             case ProxyHelper.NONE:
1163                 // Set the proxy title.
1164                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_none));
1165
1166                 // Check the proxy None radio button.
1167                 optionsProxyNoneMenuItem.setChecked(true);
1168                 break;
1169
1170             case ProxyHelper.TOR:
1171                 // Set the proxy title.
1172                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_tor));
1173
1174                 // Check the proxy Tor radio button.
1175                 optionsProxyTorMenuItem.setChecked(true);
1176                 break;
1177
1178             case ProxyHelper.I2P:
1179                 // Set the proxy title.
1180                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_i2p));
1181
1182                 // Check the proxy I2P radio button.
1183                 optionsProxyI2pMenuItem.setChecked(true);
1184                 break;
1185
1186             case ProxyHelper.CUSTOM:
1187                 // Set the proxy title.
1188                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_custom));
1189
1190                 // Check the proxy Custom radio button.
1191                 optionsProxyCustomMenuItem.setChecked(true);
1192                 break;
1193         }
1194
1195         // Select the current user agent menu item.  A switch statement cannot be used because the user agents are not compile time constants.
1196         if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) {  // Privacy Browser.
1197             // Update the user agent menu item title.
1198             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_privacy_browser));
1199
1200             // Select the Privacy Browser radio box.
1201             optionsUserAgentPrivacyBrowserMenuItem.setChecked(true);
1202         } else if (currentUserAgent.equals(webViewDefaultUserAgent)) {  // WebView Default.
1203             // Update the user agent menu item title.
1204             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_webview_default));
1205
1206             // Select the WebView Default radio box.
1207             optionsUserAgentWebViewDefaultMenuItem.setChecked(true);
1208         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) {  // Firefox on Android.
1209             // Update the user agent menu item title.
1210             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_android));
1211
1212             // Select the Firefox on Android radio box.
1213             optionsUserAgentFirefoxOnAndroidMenuItem.setChecked(true);
1214         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) {  // Chrome on Android.
1215             // Update the user agent menu item title.
1216             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_android));
1217
1218             // Select the Chrome on Android radio box.
1219             optionsUserAgentChromeOnAndroidMenuItem.setChecked(true);
1220         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) {  // Safari on iOS.
1221             // Update the user agent menu item title.
1222             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_ios));
1223
1224             // Select the Safari on iOS radio box.
1225             optionsUserAgentSafariOnIosMenuItem.setChecked(true);
1226         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) {  // Firefox on Linux.
1227             // Update the user agent menu item title.
1228             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_linux));
1229
1230             // Select the Firefox on Linux radio box.
1231             optionsUserAgentFirefoxOnLinuxMenuItem.setChecked(true);
1232         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) {  // Chromium on Linux.
1233             // Update the user agent menu item title.
1234             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chromium_on_linux));
1235
1236             // Select the Chromium on Linux radio box.
1237             optionsUserAgentChromiumOnLinuxMenuItem.setChecked(true);
1238         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) {  // Firefox on Windows.
1239             // Update the user agent menu item title.
1240             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_windows));
1241
1242             // Select the Firefox on Windows radio box.
1243             optionsUserAgentFirefoxOnWindowsMenuItem.setChecked(true);
1244         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) {  // Chrome on Windows.
1245             // Update the user agent menu item title.
1246             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_windows));
1247
1248             // Select the Chrome on Windows radio box.
1249             optionsUserAgentChromeOnWindowsMenuItem.setChecked(true);
1250         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) {  // Edge on Windows.
1251             // Update the user agent menu item title.
1252             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_edge_on_windows));
1253
1254             // Select the Edge on Windows radio box.
1255             optionsUserAgentEdgeOnWindowsMenuItem.setChecked(true);
1256         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) {  // Internet Explorer on Windows.
1257             // Update the user agent menu item title.
1258             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_internet_explorer_on_windows));
1259
1260             // Select the Internet on Windows radio box.
1261             optionsUserAgentInternetExplorerOnWindowsMenuItem.setChecked(true);
1262         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) {  // Safari on macOS.
1263             // Update the user agent menu item title.
1264             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_macos));
1265
1266             // Select the Safari on macOS radio box.
1267             optionsUserAgentSafariOnMacosMenuItem.setChecked(true);
1268         } else {  // Custom user agent.
1269             // Update the user agent menu item title.
1270             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_custom));
1271
1272             // Select the Custom radio box.
1273             optionsUserAgentCustomMenuItem.setChecked(true);
1274         }
1275
1276         // Set the font size title.
1277         optionsFontSizeMenuItem.setTitle(getString(R.string.font_size) + " - " + fontSize + "%");
1278
1279         // Run all the other default commands.
1280         super.onPrepareOptionsMenu(menu);
1281
1282         // Display the menu.
1283         return true;
1284     }
1285
1286     @Override
1287     public boolean onOptionsItemSelected(MenuItem menuItem) {
1288         // Get a handle for the shared preferences.
1289         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1290
1291         // Get a handle for the cookie manager.
1292         CookieManager cookieManager = CookieManager.getInstance();
1293
1294         // Get the selected menu item ID.
1295         int menuItemId = menuItem.getItemId();
1296
1297         // Run the commands that correlate to the selected menu item.
1298         if (menuItemId == R.id.javascript) {  // JavaScript.
1299             // Toggle the JavaScript status.
1300             currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
1301
1302             // Update the privacy icon.
1303             updatePrivacyIcons(true);
1304
1305             // Display a `Snackbar`.
1306             if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScrip is enabled.
1307                 Snackbar.make(webViewPager, R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1308             } else if (cookieManager.acceptCookie()) {  // JavaScript is disabled, but first-party cookies are enabled.
1309                 Snackbar.make(webViewPager, R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1310             } else {  // Privacy mode.
1311                 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1312             }
1313
1314             // Reload the current WebView.
1315             currentWebView.reload();
1316
1317             // Consume the event.
1318             return true;
1319         } else if (menuItemId == R.id.refresh) {  // Refresh.
1320             // Run the command that correlates to the current status of the menu item.
1321             if (menuItem.getTitle().equals(getString(R.string.refresh))) {  // The refresh button was pushed.
1322                 // Reload the current WebView.
1323                 currentWebView.reload();
1324             } else {  // The stop button was pushed.
1325                 // Stop the loading of the WebView.
1326                 currentWebView.stopLoading();
1327             }
1328
1329             // Consume the event.
1330             return true;
1331         } else if (menuItemId == R.id.bookmarks) {  // Bookmarks.
1332             // Open the bookmarks drawer.
1333             drawerLayout.openDrawer(GravityCompat.END);
1334
1335             // Consume the event.
1336             return true;
1337         } else if (menuItemId == R.id.cookies) {  // Cookies.
1338             // Switch the first-party cookie status.
1339             cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
1340
1341             // Store the cookie status.
1342             currentWebView.setAcceptCookies(cookieManager.acceptCookie());
1343
1344             // Update the menu checkbox.
1345             menuItem.setChecked(cookieManager.acceptCookie());
1346
1347             // Update the privacy icon.
1348             updatePrivacyIcons(true);
1349
1350             // Display a snackbar.
1351             if (cookieManager.acceptCookie()) {  // Cookies are enabled.
1352                 Snackbar.make(webViewPager, R.string.cookies_enabled, Snackbar.LENGTH_SHORT).show();
1353             } else if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is still enabled.
1354                 Snackbar.make(webViewPager, R.string.cookies_disabled, Snackbar.LENGTH_SHORT).show();
1355             } else {  // Privacy mode.
1356                 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1357             }
1358
1359             // Reload the current WebView.
1360             currentWebView.reload();
1361
1362             // Consume the event.
1363             return true;
1364         } else if (menuItemId == R.id.dom_storage) {  // DOM storage.
1365             // Toggle the status of domStorageEnabled.
1366             currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1367
1368             // Update the menu checkbox.
1369             menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1370
1371             // Update the privacy icon.
1372             updatePrivacyIcons(true);
1373
1374             // Display a snackbar.
1375             if (currentWebView.getSettings().getDomStorageEnabled()) {
1376                 Snackbar.make(webViewPager, R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1377             } else {
1378                 Snackbar.make(webViewPager, R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1379             }
1380
1381             // Reload the current WebView.
1382             currentWebView.reload();
1383
1384             // Consume the event.
1385             return true;
1386         } else if (menuItemId == R.id.save_form_data) {  // Form data.  This can be removed once the minimum API >= 26.
1387             // Switch the status of saveFormDataEnabled.
1388             currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
1389
1390             // Update the menu checkbox.
1391             menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
1392
1393             // Display a snackbar.
1394             if (currentWebView.getSettings().getSaveFormData()) {
1395                 Snackbar.make(webViewPager, R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1396             } else {
1397                 Snackbar.make(webViewPager, R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1398             }
1399
1400             // Update the privacy icon.
1401             updatePrivacyIcons(true);
1402
1403             // Reload the current WebView.
1404             currentWebView.reload();
1405
1406             // Consume the event.
1407             return true;
1408         } else if (menuItemId == R.id.clear_cookies) {  // Clear cookies.
1409             // Create a snackbar.
1410             Snackbar.make(webViewPager, R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1411                     .setAction(R.string.undo, v -> {
1412                         // Do nothing because everything will be handled by `onDismissed()` below.
1413                     })
1414                     .addCallback(new Snackbar.Callback() {
1415                         @Override
1416                         public void onDismissed(Snackbar snackbar, int event) {
1417                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1418                                 // Delete the cookies.
1419                                 cookieManager.removeAllCookies(null);
1420                             }
1421                         }
1422                     })
1423                     .show();
1424
1425             // Consume the event.
1426             return true;
1427         } else if (menuItemId == R.id.clear_dom_storage) {  // Clear DOM storage.
1428             // Create a snackbar.
1429             Snackbar.make(webViewPager, R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1430                     .setAction(R.string.undo, v -> {
1431                         // Do nothing because everything will be handled by `onDismissed()` below.
1432                     })
1433                     .addCallback(new Snackbar.Callback() {
1434                         @Override
1435                         public void onDismissed(Snackbar snackbar, int event) {
1436                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1437                                 // Delete the DOM Storage.
1438                                 WebStorage webStorage = WebStorage.getInstance();
1439                                 webStorage.deleteAllData();
1440
1441                                 // Initialize a handler to manually delete the DOM storage files and directories.
1442                                 Handler deleteDomStorageHandler = new Handler();
1443
1444                                 // Setup a runnable to manually delete the DOM storage files and directories.
1445                                 Runnable deleteDomStorageRunnable = () -> {
1446                                     try {
1447                                         // Get a handle for the runtime.
1448                                         Runtime runtime = Runtime.getRuntime();
1449
1450                                         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1451                                         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1452                                         String privateDataDirectoryString = getApplicationInfo().dataDir;
1453
1454                                         // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1455                                         Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1456
1457                                         // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1458                                         Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1459                                         Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1460                                         Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1461                                         Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1462
1463                                         // Wait for the processes to finish.
1464                                         deleteLocalStorageProcess.waitFor();
1465                                         deleteIndexProcess.waitFor();
1466                                         deleteQuotaManagerProcess.waitFor();
1467                                         deleteQuotaManagerJournalProcess.waitFor();
1468                                         deleteDatabasesProcess.waitFor();
1469                                     } catch (Exception exception) {
1470                                         // Do nothing if an error is thrown.
1471                                     }
1472                                 };
1473
1474                                 // Manually delete the DOM storage files after 200 milliseconds.
1475                                 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1476                             }
1477                         }
1478                     })
1479                     .show();
1480
1481             // Consume the event.
1482             return true;
1483         } else if (menuItemId == R.id.clear_form_data) {  // Clear form data.  This can be remove once the minimum API >= 26.
1484             // Create a snackbar.
1485             Snackbar.make(webViewPager, R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1486                     .setAction(R.string.undo, v -> {
1487                         // Do nothing because everything will be handled by `onDismissed()` below.
1488                     })
1489                     .addCallback(new Snackbar.Callback() {
1490                         @Override
1491                         public void onDismissed(Snackbar snackbar, int event) {
1492                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1493                                 // Get a handle for the webView database.
1494                                 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1495
1496                                 // Delete the form data.
1497                                 webViewDatabase.clearFormData();
1498                             }
1499                         }
1500                     })
1501                     .show();
1502
1503             // Consume the event.
1504             return true;
1505         } else if (menuItemId == R.id.easylist) {  // EasyList.
1506             // Toggle the EasyList status.
1507             currentWebView.setEasyListEnabled(!currentWebView.getEasyListEnabled());
1508
1509             // Update the menu checkbox.
1510             menuItem.setChecked(currentWebView.getEasyListEnabled());
1511
1512             // Reload the current WebView.
1513             currentWebView.reload();
1514
1515             // Consume the event.
1516             return true;
1517         } else if (menuItemId == R.id.easyprivacy) {  // EasyPrivacy.
1518             // Toggle the EasyPrivacy status.
1519             currentWebView.setEasyPrivacyEnabled(!currentWebView.getEasyPrivacyEnabled());
1520
1521             // Update the menu checkbox.
1522             menuItem.setChecked(currentWebView.getEasyPrivacyEnabled());
1523
1524             // Reload the current WebView.
1525             currentWebView.reload();
1526
1527             // Consume the event.
1528             return true;
1529         } else if (menuItemId == R.id.fanboys_annoyance_list) {  // Fanboy's Annoyance List.
1530             // Toggle Fanboy's Annoyance List status.
1531             currentWebView.setFanboysAnnoyanceListEnabled(!currentWebView.getFanboysAnnoyanceListEnabled());
1532
1533             // Update the menu checkbox.
1534             menuItem.setChecked(currentWebView.getFanboysAnnoyanceListEnabled());
1535
1536             // Update the status of Fanboy's Social Blocking List.
1537             optionsFanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.getFanboysAnnoyanceListEnabled());
1538
1539             // Reload the current WebView.
1540             currentWebView.reload();
1541
1542             // Consume the event.
1543             return true;
1544         } else if (menuItemId == R.id.fanboys_social_blocking_list) {  // Fanboy's Social Blocking List.
1545             // Toggle Fanboy's Social Blocking List status.
1546             currentWebView.setFanboysSocialBlockingListEnabled(!currentWebView.getFanboysSocialBlockingListEnabled());
1547
1548             // Update the menu checkbox.
1549             menuItem.setChecked(currentWebView.getFanboysSocialBlockingListEnabled());
1550
1551             // Reload the current WebView.
1552             currentWebView.reload();
1553
1554             // Consume the event.
1555             return true;
1556         } else if (menuItemId == R.id.ultralist) {  // UltraList.
1557             // Toggle the UltraList status.
1558             currentWebView.setUltraListEnabled(!currentWebView.getUltraListEnabled());
1559
1560             // Update the menu checkbox.
1561             menuItem.setChecked(currentWebView.getUltraListEnabled());
1562
1563             // Reload the current WebView.
1564             currentWebView.reload();
1565
1566             // Consume the event.
1567             return true;
1568         } else if (menuItemId == R.id.ultraprivacy) {  // UltraPrivacy.
1569             // Toggle the UltraPrivacy status.
1570             currentWebView.setUltraPrivacyEnabled(!currentWebView.getUltraPrivacyEnabled());
1571
1572             // Update the menu checkbox.
1573             menuItem.setChecked(currentWebView.getUltraPrivacyEnabled());
1574
1575             // Reload the current WebView.
1576             currentWebView.reload();
1577
1578             // Consume the event.
1579             return true;
1580         } else if (menuItemId == R.id.block_all_third_party_requests) {  // Block all third-party requests.
1581             //Toggle the third-party requests blocker status.
1582             currentWebView.setBlockAllThirdPartyRequests(!currentWebView.getBlockAllThirdPartyRequests());
1583
1584             // Update the menu checkbox.
1585             menuItem.setChecked(currentWebView.getBlockAllThirdPartyRequests());
1586
1587             // Reload the current WebView.
1588             currentWebView.reload();
1589
1590             // Consume the event.
1591             return true;
1592         } else if (menuItemId == R.id.proxy_none) {  // Proxy - None.
1593             // Update the proxy mode.
1594             proxyMode = ProxyHelper.NONE;
1595
1596             // Apply the proxy mode.
1597             applyProxy(true);
1598
1599             // Consume the event.
1600             return true;
1601         } else if (menuItemId == R.id.proxy_tor) {  // Proxy - Tor.
1602             // Update the proxy mode.
1603             proxyMode = ProxyHelper.TOR;
1604
1605             // Apply the proxy mode.
1606             applyProxy(true);
1607
1608             // Consume the event.
1609             return true;
1610         } else if (menuItemId == R.id.proxy_i2p) {  // Proxy - I2P.
1611             // Update the proxy mode.
1612             proxyMode = ProxyHelper.I2P;
1613
1614             // Apply the proxy mode.
1615             applyProxy(true);
1616
1617             // Consume the event.
1618             return true;
1619         } else if (menuItemId == R.id.proxy_custom) {  // Proxy - Custom.
1620             // Update the proxy mode.
1621             proxyMode = ProxyHelper.CUSTOM;
1622
1623             // Apply the proxy mode.
1624             applyProxy(true);
1625
1626             // Consume the event.
1627             return true;
1628         } else if (menuItemId == R.id.user_agent_privacy_browser) {  // User Agent - Privacy Browser.
1629             // Update the user agent.
1630             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1631
1632             // Reload the current WebView.
1633             currentWebView.reload();
1634
1635             // Consume the event.
1636             return true;
1637         } else if (menuItemId == R.id.user_agent_webview_default) {  // User Agent - WebView Default.
1638             // Update the user agent.
1639             currentWebView.getSettings().setUserAgentString("");
1640
1641             // Reload the current WebView.
1642             currentWebView.reload();
1643
1644             // Consume the event.
1645             return true;
1646         } else if (menuItemId == R.id.user_agent_firefox_on_android) {  // User Agent - Firefox on Android.
1647             // Update the user agent.
1648             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1649
1650             // Reload the current WebView.
1651             currentWebView.reload();
1652
1653             // Consume the event.
1654             return true;
1655         } else if (menuItemId == R.id.user_agent_chrome_on_android) {  // User Agent - Chrome on Android.
1656             // Update the user agent.
1657             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1658
1659             // Reload the current WebView.
1660             currentWebView.reload();
1661
1662             // Consume the event.
1663             return true;
1664         } else if (menuItemId == R.id.user_agent_safari_on_ios) {  // User Agent - Safari on iOS.
1665             // Update the user agent.
1666             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1667
1668             // Reload the current WebView.
1669             currentWebView.reload();
1670
1671             // Consume the event.
1672             return true;
1673         } else if (menuItemId == R.id.user_agent_firefox_on_linux) {  // User Agent - Firefox on Linux.
1674             // Update the user agent.
1675             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1676
1677             // Reload the current WebView.
1678             currentWebView.reload();
1679
1680             // Consume the event.
1681             return true;
1682         } else if (menuItemId == R.id.user_agent_chromium_on_linux) {  // User Agent - Chromium on Linux.
1683             // Update the user agent.
1684             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1685
1686             // Reload the current WebView.
1687             currentWebView.reload();
1688
1689             // Consume the event.
1690             return true;
1691         } else if (menuItemId == R.id.user_agent_firefox_on_windows) {  // User Agent - Firefox on Windows.
1692             // Update the user agent.
1693             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1694
1695             // Reload the current WebView.
1696             currentWebView.reload();
1697
1698             // Consume the event.
1699             return true;
1700         } else if (menuItemId == R.id.user_agent_chrome_on_windows) {  // User Agent - Chrome on Windows.
1701             // Update the user agent.
1702             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1703
1704             // Reload the current WebView.
1705             currentWebView.reload();
1706
1707             // Consume the event.
1708             return true;
1709         } else if (menuItemId == R.id.user_agent_edge_on_windows) {  // User Agent - Edge on Windows.
1710             // Update the user agent.
1711             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1712
1713             // Reload the current WebView.
1714             currentWebView.reload();
1715
1716             // Consume the event.
1717             return true;
1718         } else if (menuItemId == R.id.user_agent_internet_explorer_on_windows) {  // User Agent - Internet Explorer on Windows.
1719             // Update the user agent.
1720             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1721
1722             // Reload the current WebView.
1723             currentWebView.reload();
1724
1725             // Consume the event.
1726             return true;
1727         } else if (menuItemId == R.id.user_agent_safari_on_macos) {  // User Agent - Safari on macOS.
1728             // Update the user agent.
1729             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1730
1731             // Reload the current WebView.
1732             currentWebView.reload();
1733
1734             // Consume the event.
1735             return true;
1736         } else if (menuItemId == R.id.user_agent_custom) {  // User Agent - Custom.
1737             // Update the user agent.
1738             currentWebView.getSettings().setUserAgentString(sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value)));
1739
1740             // Reload the current WebView.
1741             currentWebView.reload();
1742
1743             // Consume the event.
1744             return true;
1745         } else if (menuItemId == R.id.font_size) {  // Font size.
1746             // Instantiate the font size dialog.
1747             DialogFragment fontSizeDialogFragment = FontSizeDialog.displayDialog(currentWebView.getSettings().getTextZoom());
1748
1749             // Show the font size dialog.
1750             fontSizeDialogFragment.show(getSupportFragmentManager(), getString(R.string.font_size));
1751
1752             // Consume the event.
1753             return true;
1754         } else if (menuItemId == R.id.swipe_to_refresh) {  // Swipe to refresh.
1755             // Toggle the stored status of swipe to refresh.
1756             currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
1757
1758             // Update the swipe refresh layout.
1759             if (currentWebView.getSwipeToRefresh()) {  // Swipe to refresh is enabled.
1760                 // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
1761                 swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
1762             } else {  // Swipe to refresh is disabled.
1763                 // Disable the swipe refresh layout.
1764                 swipeRefreshLayout.setEnabled(false);
1765             }
1766
1767             // Consume the event.
1768             return true;
1769         } else if (menuItemId == R.id.wide_viewport) {  // Wide viewport.
1770             // Toggle the viewport.
1771             currentWebView.getSettings().setUseWideViewPort(!currentWebView.getSettings().getUseWideViewPort());
1772
1773             // Consume the event.
1774             return true;
1775         } else if (menuItemId == R.id.display_images) {  // Display images.
1776             // Toggle the displaying of images.
1777             if (currentWebView.getSettings().getLoadsImagesAutomatically()) {  // Images are currently loaded automatically.
1778                 // Disable loading of images.
1779                 currentWebView.getSettings().setLoadsImagesAutomatically(false);
1780
1781                 // Reload the website to remove existing images.
1782                 currentWebView.reload();
1783             } else {  // Images are not currently loaded automatically.
1784                 // Enable loading of images.  Missing images will be loaded without the need for a reload.
1785                 currentWebView.getSettings().setLoadsImagesAutomatically(true);
1786             }
1787
1788             // Consume the event.
1789             return true;
1790         } else if (menuItemId == R.id.dark_webview) {  // Dark WebView.
1791             // Check to see if dark WebView is supported by this WebView.
1792             if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {  // The device is running API >= 33 and algorithmic darkening is supported.
1793                 // Toggle algorithmic darkening.
1794                 WebSettingsCompat.setAlgorithmicDarkeningAllowed(currentWebView.getSettings(), !WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView.getSettings()));
1795             } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {  // The device is running API < 33 and the WebView supports force dark.
1796                 // Toggle the dark WebView setting.
1797                 //noinspection deprecation
1798                 if (WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON) {  // Dark WebView is currently enabled.
1799                     // Turn off dark WebView.
1800                     //noinspection deprecation
1801                     WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
1802                 } else {  // Dark WebView is currently disabled.
1803                     // Turn on dark WebView.
1804                     //noinspection deprecation
1805                     WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
1806                 }
1807             }
1808
1809             // Consume the event.
1810             return true;
1811         } else if (menuItemId == R.id.find_on_page) {  // Find on page.
1812             // Get a handle for the views.
1813             Toolbar toolbar = findViewById(R.id.toolbar);
1814             LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1815             EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
1816
1817             // Set the minimum height of the find on page linear layout to match the toolbar.
1818             findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight());
1819
1820             // Hide the toolbar.
1821             toolbar.setVisibility(View.GONE);
1822
1823             // Show the find on page linear layout.
1824             findOnPageLinearLayout.setVisibility(View.VISIBLE);
1825
1826             // Display the keyboard.  The app must wait 200 ms before running the command to work around a bug in Android.
1827             // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1828             findOnPageEditText.postDelayed(() -> {
1829                 // Set the focus on the find on page edit text.
1830                 findOnPageEditText.requestFocus();
1831
1832                 // Get a handle for the input method manager.
1833                 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
1834
1835                 // Remove the lint warning below that the input method manager might be null.
1836                 assert inputMethodManager != null;
1837
1838                 // Display the keyboard.  `0` sets no input flags.
1839                 inputMethodManager.showSoftInput(findOnPageEditText, 0);
1840             }, 200);
1841
1842             // Consume the event.
1843             return true;
1844         } else if (menuItemId == R.id.print) {  // Print.
1845             // Get a print manager instance.
1846             PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
1847
1848             // Remove the lint error below that print manager might be null.
1849             assert printManager != null;
1850
1851             // Create a print document adapter from the current WebView.
1852             PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter(getString(R.string.print));
1853
1854             // Print the document.
1855             printManager.print(getString(R.string.privacy_browser_webpage), printDocumentAdapter, null);
1856
1857             // Consume the event.
1858             return true;
1859         } else if (menuItemId == R.id.save_url) {  // Save URL.
1860             // Check the download preference.
1861             if (downloadWithExternalApp) {  // Download with an external app.
1862                 downloadUrlWithExternalApp(currentWebView.getCurrentUrl());
1863             } else {  // Handle the download inside of Privacy Browser.
1864                 // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
1865                 new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
1866                         currentWebView.getAcceptCookies()).execute(currentWebView.getCurrentUrl());
1867             }
1868
1869             // Consume the event.
1870             return true;
1871         } else if (menuItemId == R.id.save_archive) {
1872             // Open the file picker with a default file name built from the current domain name.
1873             saveWebpageArchiveActivityResultLauncher.launch(currentWebView.getCurrentDomainName() + ".mht");
1874
1875             // Consume the event.
1876             return true;
1877         } else if (menuItemId == R.id.save_image) {  // Save image.
1878             // Open the file picker with a default file name built from the current domain name.
1879             saveWebpageImageActivityResultLauncher.launch(currentWebView.getCurrentDomainName() + ".png");
1880
1881             // Consume the event.
1882             return true;
1883         } else if (menuItemId == R.id.add_to_homescreen) {  // Add to homescreen.
1884             // Instantiate the create home screen shortcut dialog.
1885             DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
1886                     currentWebView.getFavoriteOrDefaultIcon());
1887
1888             // Show the create home screen shortcut dialog.
1889             createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
1890
1891             // Consume the event.
1892             return true;
1893         } else if (menuItemId == R.id.view_source) {  // View source.
1894             // Create an intent to launch the view source activity.
1895             Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
1896
1897             // Add the variables to the intent.
1898             viewSourceIntent.putExtra(ViewSourceActivityKt.CURRENT_URL, currentWebView.getUrl());
1899             viewSourceIntent.putExtra(ViewSourceActivityKt.USER_AGENT, currentWebView.getSettings().getUserAgentString());
1900
1901             // Make it so.
1902             startActivity(viewSourceIntent);
1903
1904             // Consume the event.
1905             return true;
1906         } else if (menuItemId == R.id.share_message) {  // Share a message.
1907             // Prepare the share string.
1908             String shareString = currentWebView.getTitle() + " â€“ " + currentWebView.getUrl();
1909
1910             // Create the share intent.
1911             Intent shareMessageIntent = new Intent(Intent.ACTION_SEND);
1912
1913             // Add the share string to the intent.
1914             shareMessageIntent.putExtra(Intent.EXTRA_TEXT, shareString);
1915
1916             // Set the MIME type.
1917             shareMessageIntent.setType("text/plain");
1918
1919             // Set the intent to open in a new task.
1920             shareMessageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1921
1922             // Make it so.
1923             startActivity(Intent.createChooser(shareMessageIntent, getString(R.string.share_message)));
1924
1925             // Consume the event.
1926             return true;
1927         } else if (menuItemId == R.id.share_url) {  // Share URL.
1928             // Create the share intent.
1929             Intent shareUrlIntent = new Intent(Intent.ACTION_SEND);
1930
1931             // Add the URL to the intent.
1932             shareUrlIntent.putExtra(Intent.EXTRA_TEXT, currentWebView.getUrl());
1933
1934             // Add the title to the intent.
1935             shareUrlIntent.putExtra(Intent.EXTRA_SUBJECT, currentWebView.getTitle());
1936
1937             // Set the MIME type.
1938             shareUrlIntent.setType("text/plain");
1939
1940             // Set the intent to open in a new task.
1941             shareUrlIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1942
1943             //Make it so.
1944             startActivity(Intent.createChooser(shareUrlIntent, getString(R.string.share_url)));
1945
1946             // Consume the event.
1947             return true;
1948         } else if (menuItemId == R.id.open_with_app) {  // Open with app.
1949             // Open the URL with an outside app.
1950             openWithApp(currentWebView.getUrl());
1951
1952             // Consume the event.
1953             return true;
1954         } else if (menuItemId == R.id.open_with_browser) {  // Open with browser.
1955             // Open the URL with an outside browser.
1956             openWithBrowser(currentWebView.getUrl());
1957
1958             // Consume the event.
1959             return true;
1960         } else if (menuItemId == R.id.add_or_edit_domain) {  // Add or edit domain.
1961             // Reapply the domain settings on returning to `MainWebViewActivity`.
1962             reapplyDomainSettingsOnRestart = true;
1963
1964             // Check if domain settings currently exist.
1965             if (currentWebView.getDomainSettingsApplied()) {  // Edit the current domain settings.
1966                 // Create an intent to launch the domains activity.
1967                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1968
1969                 // Add the extra information to the intent.
1970                 domainsIntent.putExtra(DomainsActivity.LOAD_DOMAIN, currentWebView.getDomainSettingsDatabaseId());
1971                 domainsIntent.putExtra(DomainsActivity.CLOSE_ON_BACK, true);
1972                 domainsIntent.putExtra(DomainsActivity.CURRENT_URL, currentWebView.getUrl());
1973                 domainsIntent.putExtra(DomainsActivity.CURRENT_IP_ADDRESSES, currentWebView.getCurrentIpAddresses());
1974
1975                 // Get the current certificate.
1976                 SslCertificate sslCertificate = currentWebView.getCertificate();
1977
1978                 // Check to see if the SSL certificate is populated.
1979                 if (sslCertificate != null) {
1980                     // Extract the certificate to strings.
1981                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
1982                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
1983                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
1984                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
1985                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
1986                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
1987                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1988                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1989
1990                     // Add the certificate to the intent.
1991                     domainsIntent.putExtra(DomainsActivity.SSL_ISSUED_TO_CNAME, issuedToCName);
1992                     domainsIntent.putExtra(DomainsActivity.SSL_ISSUED_TO_ONAME, issuedToOName);
1993                     domainsIntent.putExtra(DomainsActivity.SSL_ISSUED_TO_UNAME, issuedToUName);
1994                     domainsIntent.putExtra(DomainsActivity.SSL_ISSUED_BY_CNAME, issuedByCName);
1995                     domainsIntent.putExtra(DomainsActivity.SSL_ISSUED_BY_ONAME, issuedByOName);
1996                     domainsIntent.putExtra(DomainsActivity.SSL_ISSUED_BY_UNAME, issuedByUName);
1997                     domainsIntent.putExtra(DomainsActivity.SSL_START_DATE, startDateLong);
1998                     domainsIntent.putExtra(DomainsActivity.SSL_END_DATE, endDateLong);
1999                 }
2000
2001                 // Make it so.
2002                 startActivity(domainsIntent);
2003             } else {  // Add a new domain.
2004                 // Get the current URI.
2005                 Uri currentUri = Uri.parse(currentWebView.getUrl());
2006
2007                 // Get the current domain from the URI.
2008                 String currentDomain = currentUri.getHost();
2009
2010                 // Set an empty domain if it is null.
2011                 if (currentDomain == null)
2012                     currentDomain = "";
2013
2014                 // Create the domain and store the database ID.
2015                 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
2016
2017                 // Create an intent to launch the domains activity.
2018                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
2019
2020                 // Add the extra information to the intent.
2021                 domainsIntent.putExtra(DomainsActivity.LOAD_DOMAIN, newDomainDatabaseId);
2022                 domainsIntent.putExtra(DomainsActivity.CLOSE_ON_BACK, true);
2023                 domainsIntent.putExtra(DomainsActivity.CURRENT_URL, currentWebView.getUrl());
2024                 domainsIntent.putExtra(DomainsActivity.CURRENT_IP_ADDRESSES, currentWebView.getCurrentIpAddresses());
2025
2026                 // Get the current certificate.
2027                 SslCertificate sslCertificate = currentWebView.getCertificate();
2028
2029                 // Check to see if the SSL certificate is populated.
2030                 if (sslCertificate != null) {
2031                     // Extract the certificate to strings.
2032                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
2033                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
2034                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
2035                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
2036                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
2037                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
2038                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
2039                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
2040
2041                     // Add the certificate to the intent.
2042                     domainsIntent.putExtra(DomainsActivity.SSL_ISSUED_TO_CNAME, issuedToCName);
2043                     domainsIntent.putExtra(DomainsActivity.SSL_ISSUED_TO_ONAME, issuedToOName);
2044                     domainsIntent.putExtra(DomainsActivity.SSL_ISSUED_TO_UNAME, issuedToUName);
2045                     domainsIntent.putExtra(DomainsActivity.SSL_ISSUED_BY_CNAME, issuedByCName);
2046                     domainsIntent.putExtra(DomainsActivity.SSL_ISSUED_BY_ONAME, issuedByOName);
2047                     domainsIntent.putExtra(DomainsActivity.SSL_ISSUED_BY_UNAME, issuedByUName);
2048                     domainsIntent.putExtra(DomainsActivity.SSL_START_DATE, startDateLong);
2049                     domainsIntent.putExtra(DomainsActivity.SSL_END_DATE, endDateLong);
2050                 }
2051
2052                 // Make it so.
2053                 startActivity(domainsIntent);
2054             }
2055
2056             // Consume the event.
2057             return true;
2058         } else {  // There is no match with the options menu.  Pass the event up to the parent method.
2059             // Don't consume the event.
2060             return super.onOptionsItemSelected(menuItem);
2061         }
2062     }
2063
2064     // removeAllCookies is deprecated, but it is required for API < 21.
2065     @Override
2066     public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2067         // Get a handle for the shared preferences.
2068         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2069
2070         // Get the menu item ID.
2071         int menuItemId = menuItem.getItemId();
2072
2073         // Run the commands that correspond to the selected menu item.
2074         if (menuItemId == R.id.clear_and_exit) {  // Clear and exit.
2075             // Clear and exit Privacy Browser.
2076             clearAndExit();
2077         } else if (menuItemId == R.id.home) {  // Home.
2078             // Load the homepage.
2079             loadUrl(currentWebView, sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
2080         } else if (menuItemId == R.id.back) {  // Back.
2081             // Check if the WebView can go back.
2082             if (currentWebView.canGoBack()) {
2083                 // Get the current web back forward list.
2084                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2085
2086                 // Get the previous entry URL.
2087                 String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
2088
2089                 // Apply the domain settings.
2090                 applyDomainSettings(currentWebView, previousUrl, false, false, false);
2091
2092                 // Load the previous website in the history.
2093                 currentWebView.goBack();
2094             }
2095         } else if (menuItemId == R.id.forward) {  // Forward.
2096             // Check if the WebView can go forward.
2097             if (currentWebView.canGoForward()) {
2098                 // Get the current web back forward list.
2099                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2100
2101                 // Get the next entry URL.
2102                 String nextUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() + 1).getUrl();
2103
2104                 // Apply the domain settings.
2105                 applyDomainSettings(currentWebView, nextUrl, false, false, false);
2106
2107                 // Load the next website in the history.
2108                 currentWebView.goForward();
2109             }
2110         } else if (menuItemId == R.id.history) {  // History.
2111             // Instantiate the URL history dialog.
2112             DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId());
2113
2114             // Show the URL history dialog.
2115             urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2116         } else if (menuItemId == R.id.open) {  // Open.
2117             // Instantiate the open file dialog.
2118             DialogFragment openDialogFragment = new OpenDialog();
2119
2120             // Show the open file dialog.
2121             openDialogFragment.show(getSupportFragmentManager(), getString(R.string.open));
2122         } else if (menuItemId == R.id.requests) {  // Requests.
2123             // Populate the resource requests.
2124             RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2125
2126             // Create an intent to launch the Requests activity.
2127             Intent requestsIntent = new Intent(this, RequestsActivity.class);
2128
2129             // Add the block third-party requests status to the intent.
2130             requestsIntent.putExtra("block_all_third_party_requests", currentWebView.getBlockAllThirdPartyRequests());
2131
2132             // Make it so.
2133             startActivity(requestsIntent);
2134         } else if (menuItemId == R.id.downloads) {  // Downloads.
2135             // Try the default system download manager.
2136             try {
2137                 // Launch the default system Download Manager.
2138                 Intent defaultDownloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2139
2140                 // Launch as a new task so that the download manager and Privacy Browser show as separate windows in the recent tasks list.
2141                 defaultDownloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2142
2143                 // Make it so.
2144                 startActivity(defaultDownloadManagerIntent);
2145             } catch (Exception defaultDownloadManagerException) {
2146                 // Try a generic file manager.
2147                 try {
2148                     // Create a generic file manager intent.
2149                     Intent genericFileManagerIntent = new Intent(Intent.ACTION_VIEW);
2150
2151                     // Open the download directory.
2152                     genericFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), DocumentsContract.Document.MIME_TYPE_DIR);
2153
2154                     // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2155                     genericFileManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2156
2157                     // Make it so.
2158                     startActivity(genericFileManagerIntent);
2159                 } catch (Exception genericFileManagerException) {
2160                     // Try an alternate file manager.
2161                     try {
2162                         // Create an alternate file manager intent.
2163                         Intent alternateFileManagerIntent = new Intent(Intent.ACTION_VIEW);
2164
2165                         // Open the download directory.
2166                         alternateFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), "resource/folder");
2167
2168                         // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2169                         alternateFileManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2170
2171                         // Open the alternate file manager.
2172                         startActivity(alternateFileManagerIntent);
2173                     } catch (Exception alternateFileManagerException) {
2174                         // Display a snackbar.
2175                         Snackbar.make(currentWebView, R.string.no_file_manager_detected, Snackbar.LENGTH_INDEFINITE).show();
2176                     }
2177                 }
2178             }
2179         } else if (menuItemId == R.id.domains) {  // Domains.
2180             // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2181             reapplyDomainSettingsOnRestart = true;
2182
2183             // Launch the domains activity.
2184             Intent domainsIntent = new Intent(this, DomainsActivity.class);
2185
2186             // Add the extra information to the intent.
2187             domainsIntent.putExtra(DomainsActivity.CURRENT_URL, currentWebView.getUrl());
2188             domainsIntent.putExtra(DomainsActivity.CURRENT_IP_ADDRESSES, currentWebView.getCurrentIpAddresses());
2189
2190             // Get the current certificate.
2191             SslCertificate sslCertificate = currentWebView.getCertificate();
2192
2193             // Check to see if the SSL certificate is populated.
2194             if (sslCertificate != null) {
2195                 // Extract the certificate to strings.
2196                 String issuedToCName = sslCertificate.getIssuedTo().getCName();
2197                 String issuedToOName = sslCertificate.getIssuedTo().getOName();
2198                 String issuedToUName = sslCertificate.getIssuedTo().getUName();
2199                 String issuedByCName = sslCertificate.getIssuedBy().getCName();
2200                 String issuedByOName = sslCertificate.getIssuedBy().getOName();
2201                 String issuedByUName = sslCertificate.getIssuedBy().getUName();
2202                 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
2203                 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
2204
2205                 // Add the certificate to the intent.
2206                 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
2207                 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
2208                 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
2209                 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
2210                 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
2211                 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
2212                 domainsIntent.putExtra("ssl_start_date", startDateLong);
2213                 domainsIntent.putExtra("ssl_end_date", endDateLong);
2214             }
2215
2216             // Make it so.
2217             startActivity(domainsIntent);
2218         } else if (menuItemId == R.id.settings) {  // Settings.
2219             // Set the flag to reapply app settings on restart when returning from Settings.
2220             reapplyAppSettingsOnRestart = true;
2221
2222             // Set the flag to reapply the domain settings on restart when returning from Settings.
2223             reapplyDomainSettingsOnRestart = true;
2224
2225             // Launch the settings activity.
2226             Intent settingsIntent = new Intent(this, SettingsActivity.class);
2227             startActivity(settingsIntent);
2228         } else if (menuItemId == R.id.import_export) { // Import/Export.
2229             // Create an intent to launch the import/export activity.
2230             Intent importExportIntent = new Intent(this, ImportExportActivity.class);
2231
2232             // Make it so.
2233             startActivity(importExportIntent);
2234         } else if (menuItemId == R.id.logcat) {  // Logcat.
2235             // Create an intent to launch the logcat activity.
2236             Intent logcatIntent = new Intent(this, LogcatActivity.class);
2237
2238             // Make it so.
2239             startActivity(logcatIntent);
2240         } else if (menuItemId == R.id.webview_devtools) {  // WebView Dev.
2241             // Create a WebView DevTools intent.
2242             Intent webViewDevToolsIntent = new Intent("com.android.webview.SHOW_DEV_UI");
2243
2244             // Launch as a new task so that the WebView DevTools and Privacy Browser show as a separate windows in the recent tasks list.
2245             webViewDevToolsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2246
2247             // Make it so.
2248             startActivity(webViewDevToolsIntent);
2249         } else if (menuItemId == R.id.guide) {  // Guide.
2250             // Create an intent to launch the guide activity.
2251             Intent guideIntent = new Intent(this, GuideActivity.class);
2252
2253             // Make it so.
2254             startActivity(guideIntent);
2255         } else if (menuItemId == R.id.about) {  // About
2256             // Create an intent to launch the about activity.
2257             Intent aboutIntent = new Intent(this, AboutActivity.class);
2258
2259             // Create a string array for the blocklist versions.
2260             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],
2261                     ultraList.get(0).get(0)[0], ultraPrivacy.get(0).get(0)[0]};
2262
2263             // Add the blocklist versions to the intent.
2264             aboutIntent.putExtra(AboutActivity.BLOCKLIST_VERSIONS, blocklistVersions);
2265
2266             // Make it so.
2267             startActivity(aboutIntent);
2268         }
2269
2270         // Close the navigation drawer.
2271         drawerLayout.closeDrawer(GravityCompat.START);
2272         return true;
2273     }
2274
2275     @Override
2276     public void onPostCreate(Bundle savedInstanceState) {
2277         // Run the default commands.
2278         super.onPostCreate(savedInstanceState);
2279
2280         // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished.  This creates the navigation drawer icon.
2281         actionBarDrawerToggle.syncState();
2282     }
2283
2284     @Override
2285     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2286         // Get the hit test result.
2287         final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2288
2289         // Define the URL strings.
2290         final String imageUrl;
2291         final String linkUrl;
2292
2293         // Get handles for the system managers.
2294         final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2295
2296         // Remove the lint errors below that the clipboard manager might be null.
2297         assert clipboardManager != null;
2298
2299         // Process the link according to the type.
2300         switch (hitTestResult.getType()) {
2301             // `SRC_ANCHOR_TYPE` is a link.
2302             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2303                 // Get the target URL.
2304                 linkUrl = hitTestResult.getExtra();
2305
2306                 // Set the target URL as the title of the `ContextMenu`.
2307                 menu.setHeaderTitle(linkUrl);
2308
2309                 // Add an Open in New Tab entry.
2310                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2311                     // Load the link URL in a new tab and move to it.
2312                     addNewTab(linkUrl, true);
2313
2314                     // Consume the event.
2315                     return true;
2316                 });
2317
2318                 // Add an Open in Background entry.
2319                 menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
2320                     // Load the link URL in a new tab but do not move to it.
2321                     addNewTab(linkUrl, false);
2322
2323                     // Consume the event.
2324                     return true;
2325                 });
2326
2327                 // Add an Open with App entry.
2328                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2329                     openWithApp(linkUrl);
2330
2331                     // Consume the event.
2332                     return true;
2333                 });
2334
2335                 // Add an Open with Browser entry.
2336                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2337                     openWithBrowser(linkUrl);
2338
2339                     // Consume the event.
2340                     return true;
2341                 });
2342
2343                 // Add a Copy URL entry.
2344                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2345                     // Save the link URL in a `ClipData`.
2346                     ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2347
2348                     // Set the `ClipData` as the clipboard's primary clip.
2349                     clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2350
2351                     // Consume the event.
2352                     return true;
2353                 });
2354
2355                 // Add a Save URL entry.
2356                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
2357                     // Check the download preference.
2358                     if (downloadWithExternalApp) {  // Download with an external app.
2359                         downloadUrlWithExternalApp(linkUrl);
2360                     } else {  // Handle the download inside of Privacy Browser.
2361                         // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2362                         new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
2363                                 currentWebView.getAcceptCookies()).execute(linkUrl);
2364                     }
2365
2366                     // Consume the event.
2367                     return true;
2368                 });
2369
2370                 // Add an empty Cancel entry, which by default closes the context menu.
2371                 menu.add(R.string.cancel);
2372                 break;
2373
2374             // `IMAGE_TYPE` is an image.
2375             case WebView.HitTestResult.IMAGE_TYPE:
2376                 // Get the image URL.
2377                 imageUrl = hitTestResult.getExtra();
2378
2379                 // Remove the incorrect lint warning below that the image URL might be null.
2380                 assert imageUrl != null;
2381
2382                 // Set the context menu title.
2383                 if (imageUrl.startsWith("data:")) {  // The image data is contained in within the URL, making it exceedingly long.
2384                     // Truncate the image URL before making it the title.
2385                     menu.setHeaderTitle(imageUrl.substring(0, 100));
2386                 } else {  // The image URL does not contain the full image data.
2387                     // Set the image URL as the title of the context menu.
2388                     menu.setHeaderTitle(imageUrl);
2389                 }
2390
2391                 // Add an Open in New Tab entry.
2392                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2393                     // Load the image in a new tab.
2394                     addNewTab(imageUrl, true);
2395
2396                     // Consume the event.
2397                     return true;
2398                 });
2399
2400                 // Add an Open with App entry.
2401                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2402                     // Open the image URL with an external app.
2403                     openWithApp(imageUrl);
2404
2405                     // Consume the event.
2406                     return true;
2407                 });
2408
2409                 // Add an Open with Browser entry.
2410                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2411                     // Open the image URL with an external browser.
2412                     openWithBrowser(imageUrl);
2413
2414                     // Consume the event.
2415                     return true;
2416                 });
2417
2418                 // Add a View Image entry.
2419                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2420                     // Load the image in the current tab.
2421                     loadUrl(currentWebView, imageUrl);
2422
2423                     // Consume the event.
2424                     return true;
2425                 });
2426
2427                 // Add a Save Image entry.
2428                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
2429                     // Check the download preference.
2430                     if (downloadWithExternalApp) {  // Download with an external app.
2431                         downloadUrlWithExternalApp(imageUrl);
2432                     } else {  // Handle the download inside of Privacy Browser.
2433                         // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2434                         new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
2435                                 currentWebView.getAcceptCookies()).execute(imageUrl);
2436                     }
2437
2438                     // Consume the event.
2439                     return true;
2440                 });
2441
2442                 // Add a Copy URL entry.
2443                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2444                     // Save the image URL in a clip data.
2445                     ClipData imageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2446
2447                     // Set the clip data as the clipboard's primary clip.
2448                     clipboardManager.setPrimaryClip(imageTypeClipData);
2449
2450                     // Consume the event.
2451                     return true;
2452                 });
2453
2454                 // Add an empty Cancel entry, which by default closes the context menu.
2455                 menu.add(R.string.cancel);
2456                 break;
2457
2458             // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2459             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2460                 // Get the image URL.
2461                 imageUrl = hitTestResult.getExtra();
2462
2463                 // Instantiate a handler.
2464                 Handler handler = new Handler();
2465
2466                 // Get a message from the handler.
2467                 Message message = handler.obtainMessage();
2468
2469                 // Request the image details from the last touched node be returned in the message.
2470                 currentWebView.requestFocusNodeHref(message);
2471
2472                 // Get the link URL from the message data.
2473                 linkUrl = message.getData().getString("url");
2474
2475                 // Set the link URL as the title of the context menu.
2476                 menu.setHeaderTitle(linkUrl);
2477
2478                 // Add an Open in New Tab entry.
2479                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2480                     // Load the link URL in a new tab and move to it.
2481                     addNewTab(linkUrl, true);
2482
2483                     // Consume the event.
2484                     return true;
2485                 });
2486
2487                 // Add an Open in Background entry.
2488                 menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
2489                     // Lod the link URL in a new tab but do not move to it.
2490                     addNewTab(linkUrl, false);
2491
2492                     // Consume the event.
2493                     return true;
2494                 });
2495
2496                 // Add an Open Image in New Tab entry.
2497                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2498                     // Load the image in a new tab and move to it.
2499                     addNewTab(imageUrl, true);
2500
2501                     // Consume the event.
2502                     return true;
2503                 });
2504
2505                 // Add an Open with App entry.
2506                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2507                     // Open the link URL with an external app.
2508                     openWithApp(linkUrl);
2509
2510                     // Consume the event.
2511                     return true;
2512                 });
2513
2514                 // Add an Open with Browser entry.
2515                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2516                     // Open the link URL with an external browser.
2517                     openWithBrowser(linkUrl);
2518
2519                     // Consume the event.
2520                     return true;
2521                 });
2522
2523                 // Add a View Image entry.
2524                 menu.add(R.string.view_image).setOnMenuItemClickListener((MenuItem item) -> {
2525                    // View the image in the current tab.
2526                    loadUrl(currentWebView, imageUrl);
2527
2528                    // Consume the event.
2529                    return true;
2530                 });
2531
2532                 // Add a Save Image entry.
2533                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
2534                     // Check the download preference.
2535                     if (downloadWithExternalApp) {  // Download with an external app.
2536                         downloadUrlWithExternalApp(imageUrl);
2537                     } else {  // Handle the download inside of Privacy Browser.
2538                         // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2539                         new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
2540                                 currentWebView.getAcceptCookies()).execute(imageUrl);
2541                     }
2542
2543                     // Consume the event.
2544                     return true;
2545                 });
2546
2547                 // Add a Copy URL entry.
2548                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2549                     // Save the link URL in a clip data.
2550                     ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2551
2552                     // Set the clip data as the clipboard's primary clip.
2553                     clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2554
2555                     // Consume the event.
2556                     return true;
2557                 });
2558
2559                 // Add a Save URL entry.
2560                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
2561                     // Check the download preference.
2562                     if (downloadWithExternalApp) {  // Download with an external app.
2563                         downloadUrlWithExternalApp(linkUrl);
2564                     } else {  // Handle the download inside of Privacy Browser.
2565                         // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2566                         new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
2567                                 currentWebView.getAcceptCookies()).execute(linkUrl);
2568                     }
2569
2570                     // Consume the event.
2571                     return true;
2572                 });
2573
2574                 // Add an empty Cancel entry, which by default closes the context menu.
2575                 menu.add(R.string.cancel);
2576                 break;
2577
2578             case WebView.HitTestResult.EMAIL_TYPE:
2579                 // Get the target URL.
2580                 linkUrl = hitTestResult.getExtra();
2581
2582                 // Set the target URL as the title of the `ContextMenu`.
2583                 menu.setHeaderTitle(linkUrl);
2584
2585                 // Add a Write Email entry.
2586                 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2587                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2588                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2589
2590                     // Parse the url and set it as the data for the `Intent`.
2591                     emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2592
2593                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2594                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2595
2596                     try {
2597                         // Make it so.
2598                         startActivity(emailIntent);
2599                     } catch (ActivityNotFoundException exception) {
2600                         // Display a snackbar.
2601                         Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
2602                     }
2603
2604                     // Consume the event.
2605                     return true;
2606                 });
2607
2608                 // Add a Copy Email Address entry.
2609                 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2610                     // Save the email address in a `ClipData`.
2611                     ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2612
2613                     // Set the `ClipData` as the clipboard's primary clip.
2614                     clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2615
2616                     // Consume the event.
2617                     return true;
2618                 });
2619
2620                 // Add an empty Cancel entry, which by default closes the context menu.
2621                 menu.add(R.string.cancel);
2622                 break;
2623         }
2624     }
2625
2626     @Override
2627     public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2628         // Get a handle for the bookmarks list view.
2629         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2630
2631         // Get the dialog.
2632         Dialog dialog = dialogFragment.getDialog();
2633
2634         // Remove the incorrect lint warning below that the dialog might be null.
2635         assert dialog != null;
2636
2637         // Get the views from the dialog fragment.
2638         EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
2639         EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
2640
2641         // Extract the strings from the edit texts.
2642         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2643         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2644
2645         // Create a favorite icon byte array output stream.
2646         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2647
2648         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2649         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2650
2651         // Convert the favorite icon byte array stream to a byte array.
2652         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2653
2654         // Display the new bookmark below the current items in the (0 indexed) list.
2655         int newBookmarkDisplayOrder = bookmarksListView.getCount();
2656
2657         // Create the bookmark.
2658         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2659
2660         // Update the bookmarks cursor with the current contents of this folder.
2661         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2662
2663         // Update the list view.
2664         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2665
2666         // Scroll to the new bookmark.
2667         bookmarksListView.setSelection(newBookmarkDisplayOrder);
2668     }
2669
2670     @Override
2671     public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) {
2672         // Get a handle for the bookmarks list view.
2673         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2674
2675         // Get the dialog.
2676         Dialog dialog = dialogFragment.getDialog();
2677
2678         // Remove the incorrect lint warning below that the dialog might be null.
2679         assert dialog != null;
2680
2681         // Get handles for the views in the dialog fragment.
2682         EditText folderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
2683         RadioButton defaultIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
2684         ImageView defaultIconImageView = dialog.findViewById(R.id.default_icon_imageview);
2685
2686         // Get new folder name string.
2687         String folderNameString = folderNameEditText.getText().toString();
2688
2689         // Create a folder icon bitmap.
2690         Bitmap folderIconBitmap;
2691
2692         // Set the folder icon bitmap according to the dialog.
2693         if (defaultIconRadioButton.isChecked()) {  // Use the default folder icon.
2694             // Get the default folder icon drawable.
2695             Drawable folderIconDrawable = defaultIconImageView.getDrawable();
2696
2697             // Convert the folder icon drawable to a bitmap drawable.
2698             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2699
2700             // Convert the folder icon bitmap drawable to a bitmap.
2701             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2702         } else {  // Use the WebView favorite icon.
2703             // Copy the favorite icon bitmap to the folder icon bitmap.
2704             folderIconBitmap = favoriteIconBitmap;
2705         }
2706
2707         // Create a folder icon byte array output stream.
2708         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2709
2710         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2711         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2712
2713         // Convert the folder icon byte array stream to a byte array.
2714         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2715
2716         // Move all the bookmarks down one in the display order.
2717         for (int i = 0; i < bookmarksListView.getCount(); i++) {
2718             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2719             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2720         }
2721
2722         // Create the folder, which will be placed at the top of the `ListView`.
2723         bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2724
2725         // Update the bookmarks cursor with the current contents of this folder.
2726         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2727
2728         // Update the list view.
2729         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2730
2731         // Scroll to the new folder.
2732         bookmarksListView.setSelection(0);
2733     }
2734
2735     @Override
2736     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
2737         // Remove the incorrect lint warning below that the dialog fragment might be null.
2738         assert dialogFragment != null;
2739
2740         // Get the dialog.
2741         Dialog dialog = dialogFragment.getDialog();
2742
2743         // Remove the incorrect lint warning below that the dialog might be null.
2744         assert dialog != null;
2745
2746         // Get handles for the views from the dialog.
2747         RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
2748         RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
2749         ImageView defaultFolderIconImageView = dialog.findViewById(R.id.default_icon_imageview);
2750         EditText editFolderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
2751
2752         // Get the new folder name.
2753         String newFolderNameString = editFolderNameEditText.getText().toString();
2754
2755         // Check if the favorite icon has changed.
2756         if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
2757             // Update the name in the database.
2758             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2759         } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
2760             // Create the new folder icon Bitmap.
2761             Bitmap folderIconBitmap;
2762
2763             // Populate the new folder icon bitmap.
2764             if (defaultFolderIconRadioButton.isChecked()) {
2765                 // Get the default folder icon drawable.
2766                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2767
2768                 // Convert the folder icon drawable to a bitmap drawable.
2769                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2770
2771                 // Convert the folder icon bitmap drawable to a bitmap.
2772                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2773             } else {  // Use the `WebView` favorite icon.
2774                 // Copy the favorite icon bitmap to the folder icon bitmap.
2775                 folderIconBitmap = favoriteIconBitmap;
2776             }
2777
2778             // Create a folder icon byte array output stream.
2779             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2780
2781             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2782             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2783
2784             // Convert the folder icon byte array stream to a byte array.
2785             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2786
2787             // Update the folder icon in the database.
2788             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2789         } else {  // The folder icon and the name have changed.
2790             // Get the new folder icon bitmap.
2791             Bitmap folderIconBitmap;
2792             if (defaultFolderIconRadioButton.isChecked()) {
2793                 // Get the default folder icon drawable.
2794                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2795
2796                 // Convert the folder icon drawable to a bitmap drawable.
2797                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2798
2799                 // Convert the folder icon bitmap drawable to a bitmap.
2800                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2801             } else {  // Use the `WebView` favorite icon.
2802                 // Copy the favorite icon bitmap to the folder icon bitmap.
2803                 folderIconBitmap = favoriteIconBitmap;
2804             }
2805
2806             // Create a folder icon byte array output stream.
2807             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2808
2809             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2810             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2811
2812             // Convert the folder icon byte array stream to a byte array.
2813             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2814
2815             // Update the folder name and icon in the database.
2816             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
2817         }
2818
2819         // Update the bookmarks cursor with the current contents of this folder.
2820         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2821
2822         // Update the list view.
2823         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2824     }
2825
2826     // Process the results of a file browse.
2827     @Override
2828     public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) {
2829         // Run the default commands.
2830         super.onActivityResult(requestCode, resultCode, returnedIntent);
2831
2832         // Run the commands that correlate to the specified request code.
2833         switch (requestCode) {
2834             case BROWSE_FILE_UPLOAD_REQUEST_CODE:
2835                 // Pass the file to the WebView.
2836                 fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, returnedIntent));
2837                 break;
2838
2839             case BROWSE_OPEN_REQUEST_CODE:
2840                 // Don't do anything if the user pressed back from the file picker.
2841                 if (resultCode == Activity.RESULT_OK) {
2842                     // Get a handle for the open dialog fragment.
2843                     DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open));
2844
2845                     // Only update the file name if the dialog still exists.
2846                     if (openDialogFragment != null) {
2847                         // Get a handle for the open dialog.
2848                         Dialog openDialog = openDialogFragment.getDialog();
2849
2850                         // Remove the incorrect lint warning below that the dialog might be null.
2851                         assert openDialog != null;
2852
2853                         // Get a handle for the file name edit text.
2854                         EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext);
2855
2856                         // Get the file name URI from the intent.
2857                         Uri fileNameUri = returnedIntent.getData();
2858
2859                         // Get the file name string from the URI.
2860                         String fileNameString = fileNameUri.toString();
2861
2862                         // Set the file name text.
2863                         fileNameEditText.setText(fileNameString);
2864
2865                         // Move the cursor to the end of the file name edit text.
2866                         fileNameEditText.setSelection(fileNameString.length());
2867                     }
2868                 }
2869                 break;
2870         }
2871     }
2872
2873     private void loadUrlFromTextBox() {
2874         // Get the text from urlTextBox and convert it to a string.  trim() removes white spaces from the beginning and end of the string.
2875         String unformattedUrlString = urlEditText.getText().toString().trim();
2876
2877         // Initialize the formatted URL string.
2878         String url = "";
2879
2880         // Check to see if the unformatted URL string is a valid URL.  Otherwise, convert it into a search.
2881         if (unformattedUrlString.startsWith("content://")) {  // This is a Content URL.
2882             // Load the entire content URL.
2883             url = unformattedUrlString;
2884         } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
2885                 unformattedUrlString.startsWith("file://")) {  // This is a standard URL.
2886             // Add `https://` at the beginning if there is no protocol.  Otherwise the app will segfault.
2887             if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://")) {
2888                 unformattedUrlString = "https://" + unformattedUrlString;
2889             }
2890
2891             // Initialize the unformatted URL.
2892             URL unformattedUrl = null;
2893
2894             // Convert the unformatted URL string to a URL, then to a URI, and then back to a string, which sanitizes the input and adds in any missing components.
2895             try {
2896                 unformattedUrl = new URL(unformattedUrlString);
2897             } catch (MalformedURLException e) {
2898                 e.printStackTrace();
2899             }
2900
2901             // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
2902             String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
2903             String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
2904             String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
2905             String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
2906             String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
2907
2908             // Build the URI.
2909             Uri.Builder uri = new Uri.Builder();
2910             uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
2911
2912             // Decode the URI as a UTF-8 string in.
2913             try {
2914                 url = URLDecoder.decode(uri.build().toString(), "UTF-8");
2915             } catch (UnsupportedEncodingException exception) {
2916                 // Do nothing.  The formatted URL string will remain blank.
2917             }
2918         } else if (!unformattedUrlString.isEmpty()){  // This is not a URL, but rather a search string.
2919             // Create an encoded URL String.
2920             String encodedUrlString;
2921
2922             // Sanitize the search input.
2923             try {
2924                 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
2925             } catch (UnsupportedEncodingException exception) {
2926                 encodedUrlString = "";
2927             }
2928
2929             // Add the base search URL.
2930             url = searchURL + encodedUrlString;
2931         }
2932
2933         // 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.
2934         urlEditText.clearFocus();
2935
2936         // Make it so.
2937         loadUrl(currentWebView, url);
2938     }
2939
2940     private void loadUrl(NestedScrollWebView nestedScrollWebView, String url) {
2941         // Sanitize the URL.
2942         url = sanitizeUrl(url);
2943
2944         // Apply the domain settings and load the URL.
2945         applyDomainSettings(nestedScrollWebView, url, true, false, true);
2946     }
2947
2948     public void findPreviousOnPage(View view) {
2949         // Go to the previous highlighted phrase on the page.  `false` goes backwards instead of forwards.
2950         currentWebView.findNext(false);
2951     }
2952
2953     public void findNextOnPage(View view) {
2954         // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
2955         currentWebView.findNext(true);
2956     }
2957
2958     public void closeFindOnPage(View view) {
2959         // Get a handle for the views.
2960         Toolbar toolbar = findViewById(R.id.toolbar);
2961         LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
2962         EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
2963
2964         // Delete the contents of `find_on_page_edittext`.
2965         findOnPageEditText.setText(null);
2966
2967         // Clear the highlighted phrases if the WebView is not null.
2968         if (currentWebView != null) {
2969             currentWebView.clearMatches();
2970         }
2971
2972         // Hide the find on page linear layout.
2973         findOnPageLinearLayout.setVisibility(View.GONE);
2974
2975         // Show the toolbar.
2976         toolbar.setVisibility(View.VISIBLE);
2977
2978         // Get a handle for the input method manager.
2979         InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
2980
2981         // Remove the lint warning below that the input method manager might be null.
2982         assert inputMethodManager != null;
2983
2984         // Hide the keyboard.
2985         inputMethodManager.hideSoftInputFromWindow(toolbar.getWindowToken(), 0);
2986     }
2987
2988     @Override
2989     public void onApplyNewFontSize(DialogFragment dialogFragment) {
2990         // Remove the incorrect lint warning below that the dialog fragment might be null.
2991         assert dialogFragment != null;
2992
2993         // Get the dialog.
2994         Dialog dialog = dialogFragment.getDialog();
2995
2996         // Remove the incorrect lint warning below tha the dialog might be null.
2997         assert dialog != null;
2998
2999         // Get a handle for the font size edit text.
3000         EditText fontSizeEditText = dialog.findViewById(R.id.font_size_edittext);
3001
3002         // Initialize the new font size variable with the current font size.
3003         int newFontSize = currentWebView.getSettings().getTextZoom();
3004
3005         // Get the font size from the edit text.
3006         try {
3007             newFontSize = Integer.parseInt(fontSizeEditText.getText().toString());
3008         } catch (Exception exception) {
3009             // If the edit text does not contain a valid font size do nothing.
3010         }
3011
3012         // Apply the new font size.
3013         currentWebView.getSettings().setTextZoom(newFontSize);
3014     }
3015
3016     @Override
3017     public void onOpen(DialogFragment dialogFragment) {
3018         // Get the dialog.
3019         Dialog dialog = dialogFragment.getDialog();
3020
3021         // Remove the incorrect lint warning below that the dialog might be null.
3022         assert dialog != null;
3023
3024         // Get handles for the views.
3025         EditText fileNameEditText = dialog.findViewById(R.id.file_name_edittext);
3026         CheckBox mhtCheckBox = dialog.findViewById(R.id.mht_checkbox);
3027
3028         // Get the file path string.
3029         String openFilePath = fileNameEditText.getText().toString();
3030
3031         // Apply the domain settings.  This resets the favorite icon and removes any domain settings.
3032         applyDomainSettings(currentWebView, openFilePath, true, false, false);
3033
3034         // Open the file according to the type.
3035         if (mhtCheckBox.isChecked()) {  // Force opening of an MHT file.
3036             try {
3037                 // Get the MHT file input stream.
3038                 InputStream mhtFileInputStream = getContentResolver().openInputStream(Uri.parse(openFilePath));
3039
3040                 // Create a temporary MHT file.
3041                 File temporaryMhtFile = File.createTempFile("temporary_mht_file", ".mht", getCacheDir());
3042
3043                 // Get a file output stream for the temporary MHT file.
3044                 FileOutputStream temporaryMhtFileOutputStream = new FileOutputStream(temporaryMhtFile);
3045
3046                 // Create a transfer byte array.
3047                 byte[] transferByteArray = new byte[1024];
3048
3049                 // Create an integer to track the number of bytes read.
3050                 int bytesRead;
3051
3052                 // Copy the temporary MHT file input stream to the MHT output stream.
3053                 while ((bytesRead = mhtFileInputStream.read(transferByteArray)) > 0) {
3054                     temporaryMhtFileOutputStream.write(transferByteArray, 0, bytesRead);
3055                 }
3056
3057                 // Flush the temporary MHT file output stream.
3058                 temporaryMhtFileOutputStream.flush();
3059
3060                 // Close the streams.
3061                 temporaryMhtFileOutputStream.close();
3062                 mhtFileInputStream.close();
3063
3064                 // Load the temporary MHT file.
3065                 currentWebView.loadUrl(temporaryMhtFile.toString());
3066             } catch (Exception exception) {
3067                 // Display a snackbar.
3068                 Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
3069             }
3070         } else {  // Let the WebView handle opening of the file.
3071             // Open the file.
3072             currentWebView.loadUrl(openFilePath);
3073         }
3074     }
3075
3076     private void downloadUrlWithExternalApp(String url) {
3077         // Create a download intent.  Not specifying the action type will display the maximum number of options.
3078         Intent downloadIntent = new Intent();
3079
3080         // Set the URI and the mime type.
3081         downloadIntent.setDataAndType(Uri.parse(url), "text/html");
3082
3083         // Flag the intent to open in a new task.
3084         downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
3085
3086         // Show the chooser.
3087         startActivity(Intent.createChooser(downloadIntent, getString(R.string.download_with_external_app)));
3088     }
3089
3090     public void onSaveUrl(@NonNull String originalUrlString, @NonNull String fileNameString, @NonNull DialogFragment dialogFragment) {
3091         // Store the URL.  This will be used in the save URL activity result launcher.
3092         if (originalUrlString.startsWith("data:")) {
3093             // Save the original URL.
3094             saveUrlString = originalUrlString;
3095         } else {
3096             // Get the dialog.
3097             Dialog dialog = dialogFragment.getDialog();
3098
3099             // Remove the incorrect lint warning below that the dialog might be null.
3100             assert dialog != null;
3101
3102             // Get a handle for the dialog URL edit text.
3103             EditText dialogUrlEditText = dialog.findViewById(R.id.url_edittext);
3104
3105             // Get the URL from the edit text, which may have been modified.
3106             saveUrlString = dialogUrlEditText.getText().toString();
3107         }
3108
3109         // Open the file picker.
3110         saveUrlActivityResultLauncher.launch(fileNameString);
3111     }
3112     
3113     // Remove the warning that `OnTouchListener()` needs to override `performClick()`, as the only purpose of setting the `OnTouchListener()` is to make it do nothing.
3114     @SuppressLint("ClickableViewAccessibility")
3115     private void initializeApp() {
3116         // Get a handle for the input method.
3117         InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
3118
3119         // Remove the lint warning below that the input method manager might be null.
3120         assert inputMethodManager != null;
3121
3122         // Initialize the color spans for highlighting the URLs.
3123         initialGrayColorSpan = new ForegroundColorSpan(getColor(R.color.gray_500));
3124         finalGrayColorSpan = new ForegroundColorSpan(getColor(R.color.gray_500));
3125         redColorSpan = new ForegroundColorSpan(getColor(R.color.red_text));
3126
3127         // Remove the formatting from the URL edit text when the user is editing the text.
3128         urlEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
3129             if (hasFocus) {  // The user is editing the URL text box.
3130                 // Remove the highlighting.
3131                 urlEditText.getText().removeSpan(redColorSpan);
3132                 urlEditText.getText().removeSpan(initialGrayColorSpan);
3133                 urlEditText.getText().removeSpan(finalGrayColorSpan);
3134             } else {  // The user has stopped editing the URL text box.
3135                 // Move to the beginning of the string.
3136                 urlEditText.setSelection(0);
3137
3138                 // Reapply the highlighting.
3139                 highlightUrlText();
3140             }
3141         });
3142
3143         // Set the go button on the keyboard to load the URL in `urlTextBox`.
3144         urlEditText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
3145             // If the event is a key-down event on the `enter` button, load the URL.
3146             if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
3147                 // Load the URL into the mainWebView and consume the event.
3148                 loadUrlFromTextBox();
3149
3150                 // If the enter key was pressed, consume the event.
3151                 return true;
3152             } else {
3153                 // If any other key was pressed, do not consume the event.
3154                 return false;
3155             }
3156         });
3157
3158         // Create an Orbot status broadcast receiver.
3159         orbotStatusBroadcastReceiver = new BroadcastReceiver() {
3160             @Override
3161             public void onReceive(Context context, Intent intent) {
3162                 // Store the content of the status message in `orbotStatus`.
3163                 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS");
3164
3165                 // If Privacy Browser is waiting on the proxy, load the website now that Orbot is connected.
3166                 if ((orbotStatus != null) && orbotStatus.equals(ProxyHelper.ORBOT_STATUS_ON) && waitingForProxy) {
3167                     // Reset the waiting for proxy status.
3168                     waitingForProxy = false;
3169
3170                     // Get a list of the current fragments.
3171                     List<Fragment> fragmentList = getSupportFragmentManager().getFragments();
3172
3173                     // Check each fragment to see if it is a waiting for proxy dialog.  Sometimes more than one is displayed.
3174                     for (int i = 0; i < fragmentList.size(); i++) {
3175                         // Get the fragment tag.
3176                         String fragmentTag = fragmentList.get(i).getTag();
3177
3178                         // Check to see if it is the waiting for proxy dialog.
3179                         if ((fragmentTag!= null) && fragmentTag.equals(getString(R.string.waiting_for_proxy_dialog))) {
3180                             // Dismiss the waiting for proxy dialog.
3181                             ((DialogFragment) fragmentList.get(i)).dismiss();
3182                         }
3183                     }
3184
3185                     // Reload existing URLs and load any URLs that are waiting for the proxy.
3186                     for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3187                         // Get the WebView tab fragment.
3188                         WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3189
3190                         // Get the fragment view.
3191                         View fragmentView = webViewTabFragment.getView();
3192
3193                         // Only process the WebViews if they exist.
3194                         if (fragmentView != null) {
3195                             // Get the nested scroll WebView from the tab fragment.
3196                             NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3197
3198                             // Get the waiting for proxy URL string.
3199                             String waitingForProxyUrlString = nestedScrollWebView.getWaitingForProxyUrlString();
3200
3201                             // Load the pending URL if it exists.
3202                             if (!waitingForProxyUrlString.isEmpty()) {  // A URL is waiting to be loaded.
3203                                 // Load the URL.
3204                                 loadUrl(nestedScrollWebView, waitingForProxyUrlString);
3205
3206                                 // Reset the waiting for proxy URL string.
3207                                 nestedScrollWebView.setWaitingForProxyUrlString("");
3208                             } else {  // No URL is waiting to be loaded.
3209                                 // Reload the existing URL.
3210                                 nestedScrollWebView.reload();
3211                             }
3212                         }
3213                     }
3214                 }
3215             }
3216         };
3217
3218         // Register the Orbot status broadcast receiver on `this` context.
3219         this.registerReceiver(orbotStatusBroadcastReceiver, new IntentFilter("org.torproject.android.intent.action.STATUS"));
3220
3221         // Get handles for views that need to be modified.
3222         LinearLayout bookmarksHeaderLinearLayout = findViewById(R.id.bookmarks_header_linearlayout);
3223         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
3224         FloatingActionButton launchBookmarksActivityFab = findViewById(R.id.launch_bookmarks_activity_fab);
3225         FloatingActionButton createBookmarkFolderFab = findViewById(R.id.create_bookmark_folder_fab);
3226         FloatingActionButton createBookmarkFab = findViewById(R.id.create_bookmark_fab);
3227         EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
3228
3229         // Update the web view pager every time a tab is modified.
3230         webViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
3231             @Override
3232             public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
3233                 // Do nothing.
3234             }
3235
3236             @Override
3237             public void onPageSelected(int position) {
3238                 // Close the find on page bar if it is open.
3239                 closeFindOnPage(null);
3240
3241                 // Set the current WebView.
3242                 setCurrentWebView(position);
3243
3244                 // Select the corresponding tab if it does not match the currently selected page.  This will happen if the page was scrolled by creating a new tab.
3245                 if (tabLayout.getSelectedTabPosition() != position) {
3246                     // Wait until the new tab has been created.
3247                     tabLayout.post(() -> {
3248                         // Get a handle for the tab.
3249                         TabLayout.Tab tab = tabLayout.getTabAt(position);
3250
3251                         // Assert that the tab is not null.
3252                         assert tab != null;
3253
3254                         // Select the tab.
3255                         tab.select();
3256                     });
3257                 }
3258             }
3259
3260             @Override
3261             public void onPageScrollStateChanged(int state) {
3262                 // Do nothing.
3263             }
3264         });
3265
3266         // Display the View SSL Certificate dialog when the currently selected tab is reselected.
3267         tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
3268             @Override
3269             public void onTabSelected(TabLayout.Tab tab) {
3270                 // Select the same page in the view pager.
3271                 webViewPager.setCurrentItem(tab.getPosition());
3272             }
3273
3274             @Override
3275             public void onTabUnselected(TabLayout.Tab tab) {
3276                 // Do nothing.
3277             }
3278
3279             @Override
3280             public void onTabReselected(TabLayout.Tab tab) {
3281                 // Instantiate the View SSL Certificate dialog.
3282                 DialogFragment viewSslCertificateDialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView.getWebViewFragmentId(), currentWebView.getFavoriteOrDefaultIcon());
3283
3284                 // Display the View SSL Certificate dialog.
3285                 viewSslCertificateDialogFragment.show(getSupportFragmentManager(), getString(R.string.view_ssl_certificate));
3286             }
3287         });
3288
3289         // Set a touch listener on the bookmarks header linear layout so that touches don't pass through to the button underneath.
3290         bookmarksHeaderLinearLayout.setOnTouchListener((view, motionEvent) -> {
3291             // Consume the touch.
3292             return true;
3293         });
3294
3295         // Set the launch bookmarks activity FAB to launch the bookmarks activity.
3296         launchBookmarksActivityFab.setOnClickListener(v -> {
3297             // Get a copy of the favorite icon bitmap.
3298             Bitmap favoriteIconBitmap = currentWebView.getFavoriteOrDefaultIcon();
3299
3300             // Create a favorite icon byte array output stream.
3301             ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
3302
3303             // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
3304             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
3305
3306             // Convert the favorite icon byte array stream to a byte array.
3307             byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
3308
3309             // Create an intent to launch the bookmarks activity.
3310             Intent bookmarksIntent = new Intent(getApplicationContext(), BookmarksActivity.class);
3311
3312             // Add the extra information to the intent.
3313             bookmarksIntent.putExtra("current_url", currentWebView.getUrl());
3314             bookmarksIntent.putExtra("current_title", currentWebView.getTitle());
3315             bookmarksIntent.putExtra("current_folder", currentBookmarksFolder);
3316             bookmarksIntent.putExtra("favorite_icon_byte_array", favoriteIconByteArray);
3317
3318             // Make it so.
3319             startActivity(bookmarksIntent);
3320         });
3321
3322         // Set the create new bookmark folder FAB to display an alert dialog.
3323         createBookmarkFolderFab.setOnClickListener(v -> {
3324             // Create a create bookmark folder dialog.
3325             DialogFragment createBookmarkFolderDialog = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView.getFavoriteOrDefaultIcon());
3326
3327             // Show the create bookmark folder dialog.
3328             createBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.create_folder));
3329         });
3330
3331         // Set the create new bookmark FAB to display an alert dialog.
3332         createBookmarkFab.setOnClickListener(view -> {
3333             // Instantiate the create bookmark dialog.
3334             DialogFragment createBookmarkDialog = CreateBookmarkDialog.createBookmark(currentWebView.getUrl(), currentWebView.getTitle(), currentWebView.getFavoriteOrDefaultIcon());
3335
3336             // Display the create bookmark dialog.
3337             createBookmarkDialog.show(getSupportFragmentManager(), getString(R.string.create_bookmark));
3338         });
3339
3340         // Search for the string on the page whenever a character changes in the `findOnPageEditText`.
3341         findOnPageEditText.addTextChangedListener(new TextWatcher() {
3342             @Override
3343             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3344                 // Do nothing.
3345             }
3346
3347             @Override
3348             public void onTextChanged(CharSequence s, int start, int before, int count) {
3349                 // Do nothing.
3350             }
3351
3352             @Override
3353             public void afterTextChanged(Editable s) {
3354                 // Search for the text in the WebView if it is not null.  Sometimes on resume after a period of non-use the WebView will be null.
3355                 if (currentWebView != null) {
3356                     currentWebView.findAllAsync(findOnPageEditText.getText().toString());
3357                 }
3358             }
3359         });
3360
3361         // Set the `check mark` button for the `findOnPageEditText` keyboard to close the soft keyboard.
3362         findOnPageEditText.setOnKeyListener((v, keyCode, event) -> {
3363             if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {  // The `enter` key was pressed.
3364                 // Hide the soft keyboard.
3365                 inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3366
3367                 // Consume the event.
3368                 return true;
3369             } else {  // A different key was pressed.
3370                 // Do not consume the event.
3371                 return false;
3372             }
3373         });
3374
3375         // Implement swipe to refresh.
3376         swipeRefreshLayout.setOnRefreshListener(() -> {
3377             // Reload the website.
3378             currentWebView.reload();
3379         });
3380
3381         // Store the default progress view offsets for use later in `initializeWebView()`.
3382         defaultProgressViewStartOffset = swipeRefreshLayout.getProgressViewStartOffset();
3383         defaultProgressViewEndOffset = swipeRefreshLayout.getProgressViewEndOffset();
3384
3385         // Set the refresh color scheme according to the theme.
3386         swipeRefreshLayout.setColorSchemeResources(R.color.blue_text);
3387
3388         // Initialize a color background typed value.
3389         TypedValue colorBackgroundTypedValue = new TypedValue();
3390
3391         // Get the color background from the theme.
3392         getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true);
3393
3394         // Get the color background int from the typed value.
3395         int colorBackgroundInt = colorBackgroundTypedValue.data;
3396
3397         // Set the swipe refresh background color.
3398         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt);
3399
3400         // The drawer titles identify the drawer layouts in accessibility mode.
3401         drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer));
3402         drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks));
3403
3404         // Load the bookmarks folder.
3405         loadBookmarksFolder();
3406
3407         // Handle clicks on bookmarks.
3408         bookmarksListView.setOnItemClickListener((parent, view, position, id) -> {
3409             // Convert the id from long to int to match the format of the bookmarks database.
3410             int databaseId = (int) id;
3411
3412             // Get the bookmark cursor for this ID.
3413             Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
3414
3415             // Move the bookmark cursor to the first row.
3416             bookmarkCursor.moveToFirst();
3417
3418             // Act upon the bookmark according to the type.
3419             if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {  // The selected bookmark is a folder.
3420                 // Store the new folder name in `currentBookmarksFolder`.
3421                 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
3422
3423                 // Load the new folder.
3424                 loadBookmarksFolder();
3425             } else {  // The selected bookmark is not a folder.
3426                 // Load the bookmark URL.
3427                 loadUrl(currentWebView, bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)));
3428
3429                 // Close the bookmarks drawer.
3430                 drawerLayout.closeDrawer(GravityCompat.END);
3431             }
3432
3433             // Close the `Cursor`.
3434             bookmarkCursor.close();
3435         });
3436
3437         bookmarksListView.setOnItemLongClickListener((parent, view, position, id) -> {
3438             // Convert the database ID from `long` to `int`.
3439             int databaseId = (int) id;
3440
3441             // Find out if the selected bookmark is a folder.
3442             boolean isFolder = bookmarksDatabaseHelper.isFolder(databaseId);
3443
3444             // Check to see if the bookmark is a folder.
3445             if (isFolder) {  // The bookmark is a folder.
3446                 // Save the current folder name, which is used in `onSaveEditBookmarkFolder()`.
3447                 oldFolderNameString = bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
3448
3449                 // Instantiate the edit folder bookmark dialog.
3450                 DialogFragment editBookmarkFolderDialog = EditBookmarkFolderDialog.folderDatabaseId(databaseId, currentWebView.getFavoriteOrDefaultIcon());
3451
3452                 // Show the edit folder bookmark dialog.
3453                 editBookmarkFolderDialog.show(getSupportFragmentManager(), getString(R.string.edit_folder));
3454             } else {  // The bookmark is not a folder.
3455                 // Get the bookmark cursor for this ID.
3456                 Cursor bookmarkCursor = bookmarksDatabaseHelper.getBookmark(databaseId);
3457
3458                 // Move the bookmark cursor to the first row.
3459                 bookmarkCursor.moveToFirst();
3460
3461                 // Load the bookmark in a new tab.
3462                 addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), true);
3463
3464                 // Close the bookmarks drawer.
3465                 drawerLayout.closeDrawer(GravityCompat.END);
3466             }
3467
3468             // Consume the event.
3469             return true;
3470         });
3471
3472         // The drawer listener is used to update the navigation menu.
3473         drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
3474             @Override
3475             public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
3476             }
3477
3478             @Override
3479             public void onDrawerOpened(@NonNull View drawerView) {
3480             }
3481
3482             @Override
3483             public void onDrawerClosed(@NonNull View drawerView) {
3484                 // Reset the drawer icon when the drawer is closed.  Otherwise, it is an arrow if the drawer is open when the app is restarted.
3485                 actionBarDrawerToggle.syncState();
3486             }
3487
3488             @Override
3489             public void onDrawerStateChanged(int newState) {
3490                 if ((newState == DrawerLayout.STATE_SETTLING) || (newState == DrawerLayout.STATE_DRAGGING)) {  // A drawer is opening or closing.
3491                     // Update the navigation menu items if the WebView is not null.
3492                     if (currentWebView != null) {
3493                         navigationBackMenuItem.setEnabled(currentWebView.canGoBack());
3494                         navigationForwardMenuItem.setEnabled(currentWebView.canGoForward());
3495                         navigationHistoryMenuItem.setEnabled((currentWebView.canGoBack() || currentWebView.canGoForward()));
3496                         navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
3497
3498                         // Hide the keyboard (if displayed).
3499                         inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
3500                     }
3501
3502                     // Clear the focus from from the URL text box.  This removes any text selection markers and context menus, which otherwise draw above the open drawers.
3503                     urlEditText.clearFocus();
3504
3505                     // Clear the focus from from the WebView if it is not null, which can happen if a user opens a drawer while the browser is being resumed.
3506                     if (currentWebView != null) {
3507                         // Clearing the focus from the WebView removes any text selection markers and context menus, which otherwise draw above the open drawers.
3508                         currentWebView.clearFocus();
3509                     }
3510                 }
3511             }
3512         });
3513
3514         // Inflate a bare WebView to get the default user agent.  It is not used to render content on the screen.
3515         @SuppressLint("InflateParams") View webViewLayout = getLayoutInflater().inflate(R.layout.bare_webview, null, false);
3516
3517         // Get a handle for the WebView.
3518         WebView bareWebView = webViewLayout.findViewById(R.id.bare_webview);
3519
3520         // Store the default user agent.
3521         webViewDefaultUserAgent = bareWebView.getSettings().getUserAgentString();
3522
3523         // Destroy the bare WebView.
3524         bareWebView.destroy();
3525     }
3526
3527     private void applyAppSettings() {
3528         // Get a handle for the shared preferences.
3529         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3530
3531         // Store the values from the shared preferences in variables.
3532         incognitoModeEnabled = sharedPreferences.getBoolean(getString(R.string.incognito_mode_key), false);
3533         sanitizeTrackingQueries = sharedPreferences.getBoolean(getString(R.string.tracking_queries_key), true);
3534         sanitizeAmpRedirects = sharedPreferences.getBoolean(getString(R.string.amp_redirects_key), true);
3535         proxyMode = sharedPreferences.getString(getString(R.string.proxy_key), getString(R.string.proxy_default_value));
3536         fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean(getString(R.string.full_screen_browsing_mode_key), false);
3537         hideAppBar = sharedPreferences.getBoolean(getString(R.string.hide_app_bar_key), true);
3538         downloadWithExternalApp = sharedPreferences.getBoolean(getString(R.string.download_with_external_app_key), false);
3539         scrollAppBar = sharedPreferences.getBoolean(getString(R.string.scroll_app_bar_key), true);
3540
3541         // Apply the saved proxy mode if the app has been restarted.
3542         if (savedProxyMode != null) {
3543             // Apply the saved proxy mode.
3544             proxyMode = savedProxyMode;
3545
3546             // Reset the saved proxy mode.
3547             savedProxyMode = null;
3548         }
3549
3550         // Get the search string.
3551         String searchString = sharedPreferences.getString(getString(R.string.search_key), getString(R.string.search_default_value));
3552
3553         // Set the search string.
3554         if (searchString.equals(getString(R.string.custom_url_item)))
3555             searchURL = sharedPreferences.getString(getString(R.string.search_custom_url_key), getString(R.string.search_custom_url_default_value));
3556         else
3557             searchURL = searchString;
3558
3559         // Apply the proxy.
3560         applyProxy(false);
3561
3562         // Adjust the layout and scrolling parameters according to the position of the app bar.
3563         if (bottomAppBar) {  // The app bar is on the bottom.
3564             // Adjust the UI.
3565             if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) {  // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
3566                 // Reset the WebView padding to fill the available space.
3567                 swipeRefreshLayout.setPadding(0, 0, 0, 0);
3568             } else {  // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
3569                 // Move the WebView above the app bar layout.
3570                 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight);
3571
3572                 // Show the app bar if it is scrolled off the screen.
3573                 if (appBarLayout.getTranslationY() != 0) {
3574                     // Animate the bottom app bar onto the screen.
3575                     objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0);
3576
3577                     // Make it so.
3578                     objectAnimator.start();
3579                 }
3580             }
3581         } else {  // The app bar is on the top.
3582             // Get the current layout parameters.  Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command.
3583             CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
3584             AppBarLayout.LayoutParams toolbarLayoutParams = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
3585             AppBarLayout.LayoutParams findOnPageLayoutParams = (AppBarLayout.LayoutParams) findOnPageLinearLayout.getLayoutParams();
3586             AppBarLayout.LayoutParams tabsLayoutParams = (AppBarLayout.LayoutParams) tabsLinearLayout.getLayoutParams();
3587
3588             // Add the scrolling behavior to the layout parameters.
3589             if (scrollAppBar) {
3590                 // Enable scrolling of the app bar.
3591                 swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior());
3592                 toolbarLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3593                 findOnPageLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3594                 tabsLayoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS | AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
3595             } else {
3596                 // Disable scrolling of the app bar.
3597                 swipeRefreshLayoutParams.setBehavior(null);
3598                 toolbarLayoutParams.setScrollFlags(0);
3599                 findOnPageLayoutParams.setScrollFlags(0);
3600                 tabsLayoutParams.setScrollFlags(0);
3601
3602                 // Expand the app bar if it is currently collapsed.
3603                 appBarLayout.setExpanded(true);
3604             }
3605
3606             // Set the app bar scrolling for each WebView.
3607             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
3608                 // Get the WebView tab fragment.
3609                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
3610
3611                 // Get the fragment view.
3612                 View fragmentView = webViewTabFragment.getView();
3613
3614                 // Only modify the WebViews if they exist.
3615                 if (fragmentView != null) {
3616                     // Get the nested scroll WebView from the tab fragment.
3617                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
3618
3619                     // Set the app bar scrolling.
3620                     nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar);
3621                 }
3622             }
3623         }
3624
3625         // Update the full screen browsing mode settings.
3626         if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
3627             // Update the visibility of the app bar, which might have changed in the settings.
3628             if (hideAppBar) {
3629                 // Hide the tab linear layout.
3630                 tabsLinearLayout.setVisibility(View.GONE);
3631
3632                 // Hide the action bar.
3633                 actionBar.hide();
3634             } else {
3635                 // Show the tab linear layout.
3636                 tabsLinearLayout.setVisibility(View.VISIBLE);
3637
3638                 // Show the action bar.
3639                 actionBar.show();
3640             }
3641
3642             /* Hide the system bars.
3643              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3644              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3645              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3646              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3647              */
3648             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
3649                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
3650         } else {  // Privacy Browser is not in full screen browsing mode.
3651             // Reset the full screen tracker, which could be true if Privacy Browser was in full screen mode before entering settings and full screen browsing was disabled.
3652             inFullScreenBrowsingMode = false;
3653
3654             // Show the tab linear layout.
3655             tabsLinearLayout.setVisibility(View.VISIBLE);
3656
3657             // Show the action bar.
3658             actionBar.show();
3659
3660             // Remove the `SYSTEM_UI` flags from the root frame layout.
3661             rootFrameLayout.setSystemUiVisibility(0);
3662         }
3663     }
3664
3665     @Override
3666     public void navigateHistory(@NonNull String url, int steps) {
3667         // Apply the domain settings.
3668         applyDomainSettings(currentWebView, url, false, false, false);
3669
3670         // Load the history entry.
3671         currentWebView.goBackOrForward(steps);
3672     }
3673
3674     @Override
3675     public void pinnedErrorGoBack() {
3676         // Get the current web back forward list.
3677         WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
3678
3679         // Get the previous entry URL.
3680         String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
3681
3682         // Apply the domain settings.
3683         applyDomainSettings(currentWebView, previousUrl, false, false, false);
3684
3685         // Go back.
3686         currentWebView.goBack();
3687     }
3688
3689     // `reloadWebsite` is used if returning from the Domains activity.  Otherwise JavaScript might not function correctly if it is newly enabled.
3690     @SuppressLint("SetJavaScriptEnabled")
3691     private void applyDomainSettings(NestedScrollWebView nestedScrollWebView, String url, boolean resetTab, boolean reloadWebsite, boolean loadUrl) {
3692         // Store the current URL.
3693         nestedScrollWebView.setCurrentUrl(url);
3694
3695         // Parse the URL into a URI.
3696         Uri uri = Uri.parse(url);
3697
3698         // Extract the domain from `uri`.
3699         String newHostName = uri.getHost();
3700
3701         // Strings don't like to be null.
3702         if (newHostName == null) {
3703             newHostName = "";
3704         }
3705
3706         // Apply the domain settings if a new domain is being loaded or if the new domain is blank.  This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc.
3707         if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName) || newHostName.equals("")) {
3708             // Set the new host name as the current domain name.
3709             nestedScrollWebView.setCurrentDomainName(newHostName);
3710
3711             // Reset the ignoring of pinned domain information.
3712             nestedScrollWebView.setIgnorePinnedDomainInformation(false);
3713
3714             // Clear any pinned SSL certificate or IP addresses.
3715             nestedScrollWebView.clearPinnedSslCertificate();
3716             nestedScrollWebView.setPinnedIpAddresses("");
3717
3718             // Reset the favorite icon if specified.
3719             if (resetTab) {
3720                 // Initialize the favorite icon.
3721                 nestedScrollWebView.initializeFavoriteIcon();
3722
3723                 // Get the current page position.
3724                 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
3725
3726                 // Get the corresponding tab.
3727                 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
3728
3729                 // Update the tab if it isn't null, which sometimes happens when restarting from the background.
3730                 if (tab != null) {
3731                     // Get the tab custom view.
3732                     View tabCustomView = tab.getCustomView();
3733
3734                     // Remove the warning below that the tab custom view might be null.
3735                     assert tabCustomView != null;
3736
3737                     // Get the tab views.
3738                     ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview);
3739                     TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview);
3740
3741                     // Set the default favorite icon as the favorite icon for this tab.
3742                     tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
3743
3744                     // Set the loading title text.
3745                     tabTitleTextView.setText(R.string.loading);
3746                 }
3747             }
3748
3749             // Get a full domain name cursor.
3750             Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3751
3752             // Initialize `domainSettingsSet`.
3753             Set<String> domainSettingsSet = new HashSet<>();
3754
3755             // Get the domain name column index.
3756             int domainNameColumnIndex = domainNameCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME);
3757
3758             // Populate the domain settings set.
3759             for (int i = 0; i < domainNameCursor.getCount(); i++) {
3760                 // Move the domains cursor to the current row.
3761                 domainNameCursor.moveToPosition(i);
3762
3763                 // Store the domain name in the domain settings set.
3764                 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3765             }
3766
3767             // Close the domain name cursor.
3768             domainNameCursor.close();
3769
3770             // Initialize the domain name in database variable.
3771             String domainNameInDatabase = null;
3772
3773             // Check the hostname against the domain settings set.
3774             if (domainSettingsSet.contains(newHostName)) {  // The hostname is contained in the domain settings set.
3775                 // Record the domain name in the database.
3776                 domainNameInDatabase = newHostName;
3777
3778                 // Set the domain settings applied tracker to true.
3779                 nestedScrollWebView.setDomainSettingsApplied(true);
3780             } else {  // The hostname is not contained in the domain settings set.
3781                 // Set the domain settings applied tracker to false.
3782                 nestedScrollWebView.setDomainSettingsApplied(false);
3783             }
3784
3785             // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3786             while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) {  // Stop checking if domain settings are already applied or there are no more `.` in the hostname.
3787                 if (domainSettingsSet.contains("*." + newHostName)) {  // Check the host name prepended by `*.`.
3788                     // Set the domain settings applied tracker to true.
3789                     nestedScrollWebView.setDomainSettingsApplied(true);
3790
3791                     // Store the applied domain names as it appears in the database.
3792                     domainNameInDatabase = "*." + newHostName;
3793                 }
3794
3795                 // Strip out the lowest subdomain of of the host name.
3796                 newHostName = newHostName.substring(newHostName.indexOf(".") + 1);
3797             }
3798
3799
3800             // Get a handle for the shared preferences.
3801             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3802
3803             // Store the general preference information.
3804             String defaultFontSizeString = sharedPreferences.getString(getString(R.string.font_size_key), getString(R.string.font_size_default_value));
3805             String defaultUserAgentName = sharedPreferences.getString(getString(R.string.user_agent_key), getString(R.string.user_agent_default_value));
3806             boolean defaultSwipeToRefresh = sharedPreferences.getBoolean(getString(R.string.swipe_to_refresh_key), true);
3807             String webViewTheme = sharedPreferences.getString(getString(R.string.webview_theme_key), getString(R.string.webview_theme_default_value));
3808             boolean wideViewport = sharedPreferences.getBoolean(getString(R.string.wide_viewport_key), true);
3809             boolean displayWebpageImages = sharedPreferences.getBoolean(getString(R.string.display_webpage_images_key), true);
3810
3811             // Get the WebView theme entry values string array.
3812             String[] webViewThemeEntryValuesStringArray = getResources().getStringArray(R.array.webview_theme_entry_values);
3813
3814             // Get a handle for the cookie manager.
3815             CookieManager cookieManager = CookieManager.getInstance();
3816
3817             // Initialize the user agent array adapter and string array.
3818             ArrayAdapter<CharSequence> userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
3819             String[] userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
3820
3821             if (nestedScrollWebView.getDomainSettingsApplied()) {  // The url has custom domain settings.
3822                 // Remove the incorrect lint warning below that the domain name in database might be null.
3823                 assert domainNameInDatabase != null;
3824
3825                 // Get a cursor for the current host.
3826                 Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3827
3828                 // Move to the first position.
3829                 currentDomainSettingsCursor.moveToFirst();
3830
3831                 // Get the settings from the cursor.
3832                 nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ID)));
3833                 nestedScrollWebView.getSettings().setJavaScriptEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3834                 nestedScrollWebView.setAcceptCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.COOKIES)) == 1);
3835                 nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3836                 // Form data can be removed once the minimum API >= 26.
3837                 boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
3838                 nestedScrollWebView.setEasyListEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3839                 nestedScrollWebView.setEasyPrivacyEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3840                 nestedScrollWebView.setFanboysAnnoyanceListEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(
3841                         DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3842                 nestedScrollWebView.setFanboysSocialBlockingListEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(
3843                         DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3844                 nestedScrollWebView.setUltraListEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ULTRALIST)) == 1);
3845                 nestedScrollWebView.setUltraPrivacyEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3846                 nestedScrollWebView.setBlockAllThirdPartyRequests(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(
3847                         DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3848                 String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.USER_AGENT));
3849                 int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.FONT_SIZE));
3850                 int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3851                 int webViewThemeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WEBVIEW_THEME));
3852                 int wideViewportInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WIDE_VIEWPORT));
3853                 int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DISPLAY_IMAGES));
3854                 boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3855                 String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3856                 String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3857                 String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3858                 String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3859                 String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3860                 String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3861                 Date pinnedSslStartDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_START_DATE)));
3862                 Date pinnedSslEndDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_END_DATE)));
3863                 boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3864                 String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.IP_ADDRESSES));
3865
3866                 // Close the current host domain settings cursor.
3867                 currentDomainSettingsCursor.close();
3868
3869                 // If there is a pinned SSL certificate, store it in the WebView.
3870                 if (pinnedSslCertificate) {
3871                     nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3872                             pinnedSslStartDate, pinnedSslEndDate);
3873                 }
3874
3875                 // If there is a pinned IP address, store it in the WebView.
3876                 if (pinnedIpAddresses) {
3877                     nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses);
3878                 }
3879
3880                 // Apply the cookie domain settings.
3881                 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptCookies());
3882
3883                 // Apply the form data setting if the API < 26.
3884                 if (Build.VERSION.SDK_INT < 26) {
3885                     nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3886                 }
3887
3888                 // Apply the font size.
3889                 try {  // Try the specified font size to see if it is valid.
3890                     if (fontSize == 0) {  // Apply the default font size.
3891                             // Try to set the font size from the value in the app settings.
3892                             nestedScrollWebView.getSettings().setTextZoom(Integer.parseInt(defaultFontSizeString));
3893                     } else {  // Apply the font size from domain settings.
3894                         nestedScrollWebView.getSettings().setTextZoom(fontSize);
3895                     }
3896                 } catch (Exception exception) {  // The specified font size is invalid
3897                     // Set the font size to be 100%
3898                     nestedScrollWebView.getSettings().setTextZoom(100);
3899                 }
3900
3901                 // Set the user agent.
3902                 if (userAgentName.equals(getString(R.string.system_default_user_agent))) {  // Use the system default user agent.
3903                     // Get the array position of the default user agent name.
3904                     int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3905
3906                     // Set the user agent according to the system default.
3907                     switch (defaultUserAgentArrayPosition) {
3908                         case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
3909                             // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3910                             nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3911                             break;
3912
3913                         case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3914                             // Set the user agent to `""`, which uses the default value.
3915                             nestedScrollWebView.getSettings().setUserAgentString("");
3916                             break;
3917
3918                         case SETTINGS_CUSTOM_USER_AGENT:
3919                             // Set the default custom user agent.
3920                             nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value)));
3921                             break;
3922
3923                         default:
3924                             // Get the user agent string from the user agent data array
3925                             nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
3926                     }
3927                 } else {  // Set the user agent according to the stored name.
3928                     // Get the array position of the user agent name.
3929                     int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
3930
3931                     switch (userAgentArrayPosition) {
3932                         case UNRECOGNIZED_USER_AGENT:  // The user agent name contains a custom user agent.
3933                             nestedScrollWebView.getSettings().setUserAgentString(userAgentName);
3934                             break;
3935
3936                         case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3937                             // Set the user agent to `""`, which uses the default value.
3938                             nestedScrollWebView.getSettings().setUserAgentString("");
3939                             break;
3940
3941                         default:
3942                             // Get the user agent string from the user agent data array.
3943                             nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3944                     }
3945                 }
3946
3947                 // Set swipe to refresh.
3948                 switch (swipeToRefreshInt) {
3949                     case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3950                         // Store the swipe to refresh status in the nested scroll WebView.
3951                         nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3952
3953                         // Update the swipe refresh layout.
3954                         if (defaultSwipeToRefresh) {  // Swipe to refresh is enabled.
3955                             // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
3956                             swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
3957                         } else {  // Swipe to refresh is disabled.
3958                             // Disable the swipe refresh layout.
3959                             swipeRefreshLayout.setEnabled(false);
3960                         }
3961                         break;
3962
3963                     case DomainsDatabaseHelper.ENABLED:
3964                         // Store the swipe to refresh status in the nested scroll WebView.
3965                         nestedScrollWebView.setSwipeToRefresh(true);
3966
3967                         // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
3968                         swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
3969                         break;
3970
3971                     case DomainsDatabaseHelper.DISABLED:
3972                         // Store the swipe to refresh status in the nested scroll WebView.
3973                         nestedScrollWebView.setSwipeToRefresh(false);
3974
3975                         // Disable swipe to refresh.
3976                         swipeRefreshLayout.setEnabled(false);
3977                         break;
3978                 }
3979
3980                 // Check to see if WebView themes are supported.
3981                 if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {  // The device is running API >= 33 and algorithmic darkening is supported.
3982                     // Set the WebView theme.
3983                     switch (webViewThemeInt) {
3984                         case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3985                             // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
3986                             if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
3987                                 // Turn off algorithmic darkening.
3988                                 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), false);
3989                             } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
3990                                 // Turn on algorithmic darkening.
3991                                 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true);
3992                             } else {  // The system default theme is selected.
3993                                 // Get the current system theme status.
3994                                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
3995
3996                                 // Set the algorithmic darkening according to the current system theme status.
3997                                 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES));
3998                             }
3999                             break;
4000
4001                         case DomainsDatabaseHelper.LIGHT_THEME:
4002                             // Turn off algorithmic darkening.
4003                             WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), false);
4004                             break;
4005
4006                         case DomainsDatabaseHelper.DARK_THEME:
4007                             // Turn on algorithmic darkening.
4008                             WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true);
4009                             break;
4010                     }
4011                 } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {  // The device is running API < 33 and the WebView supports force dark.
4012                     // Set the WebView theme.
4013                     switch (webViewThemeInt) {
4014                         case DomainsDatabaseHelper.SYSTEM_DEFAULT:
4015                             // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
4016                             if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
4017                                 // Turn off the WebView dark mode.
4018                                 //noinspection deprecation
4019                                 WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
4020                             } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
4021                                 // Turn on the WebView dark mode.
4022                                 //noinspection deprecation
4023                                 WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
4024                             } else {  // The system default theme is selected.
4025                                 // Get the current system theme status.
4026                                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
4027
4028                                 // Set the WebView theme according to the current system theme status.
4029                                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
4030                                     // Turn off the WebView dark mode.
4031                                     //noinspection deprecation
4032                                     WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
4033                                 } else {  // The system is in night mode.
4034                                     // Turn on the WebView dark mode.
4035                                     //noinspection deprecation
4036                                     WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
4037                                 }
4038                             }
4039                             break;
4040
4041                         case DomainsDatabaseHelper.LIGHT_THEME:
4042                             // Turn off the WebView dark mode.
4043                             //noinspection deprecation
4044                             WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
4045                             break;
4046
4047                         case DomainsDatabaseHelper.DARK_THEME:
4048                             // Turn on the WebView dark mode.
4049                             //noinspection deprecation
4050                             WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
4051                             break;
4052                     }
4053                 }
4054
4055                 // Set the viewport.
4056                 switch (wideViewportInt) {
4057                     case DomainsDatabaseHelper.SYSTEM_DEFAULT:
4058                         nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
4059                         break;
4060
4061                     case DomainsDatabaseHelper.ENABLED:
4062                         nestedScrollWebView.getSettings().setUseWideViewPort(true);
4063                         break;
4064
4065                     case DomainsDatabaseHelper.DISABLED:
4066                         nestedScrollWebView.getSettings().setUseWideViewPort(false);
4067                         break;
4068                 }
4069
4070                 // Set the loading of webpage images.
4071                 switch (displayWebpageImagesInt) {
4072                     case DomainsDatabaseHelper.SYSTEM_DEFAULT:
4073                         nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4074                         break;
4075
4076                     case DomainsDatabaseHelper.ENABLED:
4077                         nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true);
4078                         break;
4079
4080                     case DomainsDatabaseHelper.DISABLED:
4081                         nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false);
4082                         break;
4083                 }
4084
4085                 // Set a background on the URL relative layout to indicate that custom domain settings are being used.
4086                 urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.domain_settings_url_background, null));
4087             } else {  // The new URL does not have custom domain settings.  Load the defaults.
4088                 // Store the values from the shared preferences.
4089                 nestedScrollWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean(getString(R.string.javascript_key), false));
4090                 nestedScrollWebView.setAcceptCookies(sharedPreferences.getBoolean(getString(R.string.cookies_key), false));
4091                 nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean(getString(R.string.dom_storage_key), false));
4092                 boolean saveFormData = sharedPreferences.getBoolean(getString(R.string.save_form_data_key), false);  // Form data can be removed once the minimum API >= 26.
4093                 nestedScrollWebView.setEasyListEnabled(sharedPreferences.getBoolean(getString(R.string.easylist_key), true));
4094                 nestedScrollWebView.setEasyPrivacyEnabled(sharedPreferences.getBoolean(getString(R.string.easyprivacy_key), true));
4095                 nestedScrollWebView.setFanboysAnnoyanceListEnabled(sharedPreferences.getBoolean(getString(R.string.fanboys_annoyance_list_key), true));
4096                 nestedScrollWebView.setFanboysSocialBlockingListEnabled(sharedPreferences.getBoolean(getString(R.string.fanboys_social_blocking_list_key), true));
4097                 nestedScrollWebView.setUltraListEnabled(sharedPreferences.getBoolean(getString(R.string.ultralist_key), true));
4098                 nestedScrollWebView.setUltraPrivacyEnabled(sharedPreferences.getBoolean(getString(R.string.ultraprivacy_key), true));
4099                 nestedScrollWebView.setBlockAllThirdPartyRequests(sharedPreferences.getBoolean(getString(R.string.block_all_third_party_requests_key), false));
4100
4101                 // Apply the default cookie setting.
4102                 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptCookies());
4103
4104                 // Apply the default font size setting.
4105                 try {
4106                     // Try to set the font size from the value in the app settings.
4107                     nestedScrollWebView.getSettings().setTextZoom(Integer.parseInt(defaultFontSizeString));
4108                 } catch (Exception exception) {
4109                     // If the app settings value is invalid, set the font size to 100%.
4110                     nestedScrollWebView.getSettings().setTextZoom(100);
4111                 }
4112
4113                 // Apply the form data setting if the API < 26.
4114                 if (Build.VERSION.SDK_INT < 26) {
4115                     nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
4116                 }
4117
4118                 // Store the swipe to refresh status in the nested scroll WebView.
4119                 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
4120
4121                 // Update the swipe refresh layout.
4122                 if (defaultSwipeToRefresh) {  // Swipe to refresh is enabled.
4123                     // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
4124                     swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
4125                 } else {  // Swipe to refresh is disabled.
4126                     // Disable the swipe refresh layout.
4127                     swipeRefreshLayout.setEnabled(false);
4128                 }
4129
4130                 // Reset the pinned variables.
4131                 nestedScrollWebView.setDomainSettingsDatabaseId(-1);
4132
4133                 // Get the array position of the user agent name.
4134                 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
4135
4136                 // Set the user agent.
4137                 switch (userAgentArrayPosition) {
4138                     case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
4139                         // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
4140                         nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
4141                         break;
4142
4143                     case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4144                         // Set the user agent to `""`, which uses the default value.
4145                         nestedScrollWebView.getSettings().setUserAgentString("");
4146                         break;
4147
4148                     case SETTINGS_CUSTOM_USER_AGENT:
4149                         // Set the default custom user agent.
4150                         nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value)));
4151                         break;
4152
4153                     default:
4154                         // Get the user agent string from the user agent data array
4155                         nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
4156                 }
4157
4158                 // Apply the WebView theme if supported by the installed WebView.
4159                 if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {  // The device is running API >= 33 and algorithmic darkening is supported.
4160                     // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
4161                     if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // the light theme is selected.
4162                         // Turn off algorithmic darkening.
4163                         WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), false);
4164                     } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
4165                         // Turn on algorithmic darkening.
4166                         WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true);
4167                     } else {  // The system default theme is selected.
4168                         // Get the current system theme status.
4169                         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
4170
4171                         // Set the algorithmic darkening according to the current system theme status.
4172                         WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), currentThemeStatus == Configuration.UI_MODE_NIGHT_YES);
4173                     }
4174                 } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {  // The device is running API < 33 and the WebView supports force dark.
4175                     // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
4176                     if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
4177                         // Turn off the WebView dark mode.
4178                         //noinspection deprecation
4179                         WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
4180                     } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
4181                         // Turn on the WebView dark mode.
4182                         //noinspection deprecation
4183                         WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
4184                     } else {  // The system default theme is selected.
4185                         // Get the current system theme status.
4186                         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
4187
4188                         // Set the WebView theme according to the current system theme status.
4189                         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
4190                             // Turn off the WebView dark mode.
4191                             //noinspection deprecation
4192                             WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
4193                         } else {  // The system is in night mode.
4194                             // Turn on the WebView dark mode.
4195                             //noinspection deprecation
4196                             WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
4197                         }
4198                     }
4199                 }
4200
4201                 // Set the viewport.
4202                 nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
4203
4204                 // Set the loading of webpage images.
4205                 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4206
4207                 // Set a transparent background on the URL relative layout.
4208                 urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.color.transparent, null));
4209             }
4210
4211             // Close the domains database helper.
4212             domainsDatabaseHelper.close();
4213
4214             // Update the privacy icons.
4215             updatePrivacyIcons(true);
4216         }
4217
4218         // Reload the website if returning from the Domains activity.
4219         if (reloadWebsite) {
4220             nestedScrollWebView.reload();
4221         }
4222
4223         // Load the URL if directed.  This makes sure that the domain settings are properly loaded before the URL.  By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent.
4224         if (loadUrl) {
4225             nestedScrollWebView.loadUrl(url);
4226         }
4227     }
4228
4229     private void applyProxy(boolean reloadWebViews) {
4230         // Set the proxy according to the mode.
4231         proxyHelper.setProxy(getApplicationContext(), appBarLayout, proxyMode);
4232
4233         // Reset the waiting for proxy tracker.
4234         waitingForProxy = false;
4235
4236         // Get the current theme status.
4237         int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
4238
4239         // Update the user interface and reload the WebViews if requested.
4240         switch (proxyMode) {
4241             case ProxyHelper.NONE:
4242                 // Initialize a color background typed value.
4243                 TypedValue colorBackgroundTypedValue = new TypedValue();
4244
4245                 // Get the color background from the theme.
4246                 getTheme().resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true);
4247
4248                 // Get the color background int from the typed value.
4249                 int colorBackgroundInt = colorBackgroundTypedValue.data;
4250
4251                 // Set the default app bar layout background.
4252                 appBarLayout.setBackgroundColor(colorBackgroundInt);
4253                 break;
4254
4255             case ProxyHelper.TOR:
4256                 // Set the app bar background to indicate proxying through Orbot is enabled.
4257                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
4258                     appBarLayout.setBackgroundResource(R.color.blue_50);
4259                 } else {
4260                     appBarLayout.setBackgroundResource(R.color.dark_blue_30);
4261                 }
4262
4263                 // Check to see if Orbot is installed.
4264                 try {
4265                     // Get the package manager.
4266                     PackageManager packageManager = getPackageManager();
4267
4268                     // Check to see if Orbot is in the list.  This will throw an error and drop to the catch section if it isn't installed.
4269                     packageManager.getPackageInfo("org.torproject.android", 0);
4270
4271                     // Check to see if the proxy is ready.
4272                     if (!orbotStatus.equals(ProxyHelper.ORBOT_STATUS_ON)) {  // Orbot is not ready.
4273                         // Set the waiting for proxy status.
4274                         waitingForProxy = true;
4275
4276                         // Show the waiting for proxy dialog if it isn't already displayed.
4277                         if (getSupportFragmentManager().findFragmentByTag(getString(R.string.waiting_for_proxy_dialog)) == null) {
4278                             // Get a handle for the waiting for proxy alert dialog.
4279                             DialogFragment waitingForProxyDialogFragment = new WaitingForProxyDialog();
4280
4281                             // Try to show the dialog.  Sometimes the window is not yet active if returning from Settings.
4282                             try {
4283                                 // Show the waiting for proxy alert dialog.
4284                                 waitingForProxyDialogFragment.show(getSupportFragmentManager(), getString(R.string.waiting_for_proxy_dialog));
4285                             } catch (Exception waitingForTorException) {
4286                                 // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
4287                                 pendingDialogsArrayList.add(new PendingDialog(waitingForProxyDialogFragment, getString(R.string.waiting_for_proxy_dialog)));
4288                             }
4289                         }
4290                     }
4291                 } catch (PackageManager.NameNotFoundException exception) {  // Orbot is not installed.
4292                     // Show the Orbot not installed dialog if it is not already displayed.
4293                     if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
4294                         // Get a handle for the Orbot not installed alert dialog.
4295                         DialogFragment orbotNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode);
4296
4297                         // Try to show the dialog.  Sometimes the window is not yet active if returning from Settings.
4298                         try {
4299                             // Display the Orbot not installed alert dialog.
4300                             orbotNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog));
4301                         } catch (Exception orbotNotInstalledException) {
4302                             // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
4303                             pendingDialogsArrayList.add(new PendingDialog(orbotNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)));
4304                         }
4305                     }
4306                 }
4307                 break;
4308
4309             case ProxyHelper.I2P:
4310                 // Set the app bar background to indicate proxying through Orbot is enabled.
4311                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
4312                     appBarLayout.setBackgroundResource(R.color.blue_50);
4313                 } else {
4314                     appBarLayout.setBackgroundResource(R.color.dark_blue_30);
4315                 }
4316                 // Get the package manager.
4317                 PackageManager packageManager = getPackageManager();
4318
4319                 // Check to see if I2P is installed.
4320                 try {
4321                     // Check to see if the F-Droid flavor is installed.  This will throw an error and drop to the catch section if it isn't installed.
4322                     packageManager.getPackageInfo("net.i2p.android.router", 0);
4323                 } catch (PackageManager.NameNotFoundException fdroidException) {  // The F-Droid flavor is not installed.
4324                     try {
4325                         // Check to see if the Google Play flavor is installed.  This will throw an error and drop to the catch section if it isn't installed.
4326                         packageManager.getPackageInfo("net.i2p.android", 0);
4327                     } catch (PackageManager.NameNotFoundException googlePlayException) {  // The Google Play flavor is not installed.
4328                         // Sow the I2P not installed dialog if it is not already displayed.
4329                         if (getSupportFragmentManager().findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
4330                             // Get a handle for the waiting for proxy alert dialog.
4331                             DialogFragment i2pNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode);
4332
4333                             // Try to show the dialog.  Sometimes the window is not yet active if returning from Settings.
4334                             try {
4335                                 // Display the I2P not installed alert dialog.
4336                                 i2pNotInstalledDialogFragment.show(getSupportFragmentManager(), getString(R.string.proxy_not_installed_dialog));
4337                             } catch (Exception i2pNotInstalledException) {
4338                                 // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
4339                                 pendingDialogsArrayList.add(new PendingDialog(i2pNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)));
4340                             }
4341                         }
4342                     }
4343                 }
4344                 break;
4345
4346             case ProxyHelper.CUSTOM:
4347                 // Set the app bar background to indicate proxying through Orbot is enabled.
4348                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
4349                     appBarLayout.setBackgroundResource(R.color.blue_50);
4350                 } else {
4351                     appBarLayout.setBackgroundResource(R.color.dark_blue_30);
4352                 }
4353                 break;
4354         }
4355
4356         // Reload the WebViews if requested and not waiting for the proxy.
4357         if (reloadWebViews && !waitingForProxy) {
4358             // Reload the WebViews.
4359             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4360                 // Get the WebView tab fragment.
4361                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4362
4363                 // Get the fragment view.
4364                 View fragmentView = webViewTabFragment.getView();
4365
4366                 // Only reload the WebViews if they exist.
4367                 if (fragmentView != null) {
4368                     // Get the nested scroll WebView from the tab fragment.
4369                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4370
4371                     // Reload the WebView.
4372                     nestedScrollWebView.reload();
4373                 }
4374             }
4375         }
4376     }
4377
4378     private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
4379         // Only update the privacy icons if the options menu and the current WebView have already been populated.
4380         if ((optionsMenu != null) && (currentWebView != null)) {
4381             // Update the privacy icon.
4382             if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is enabled.
4383                 optionsPrivacyMenuItem.setIcon(R.drawable.javascript_enabled);
4384             } else if (currentWebView.getAcceptCookies()) {  // JavaScript is disabled but cookies are enabled.
4385                 optionsPrivacyMenuItem.setIcon(R.drawable.warning);
4386             } else {  // All the dangerous features are disabled.
4387                 optionsPrivacyMenuItem.setIcon(R.drawable.privacy_mode);
4388             }
4389
4390             // Update the cookies icon.
4391             if (currentWebView.getAcceptCookies()) {
4392                 optionsCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
4393             } else {
4394                 optionsCookiesMenuItem.setIcon(R.drawable.cookies_disabled);
4395             }
4396
4397             // Update the refresh icon.
4398             if (optionsRefreshMenuItem.getTitle() == getString(R.string.refresh)) {  // The refresh icon is displayed.
4399                 // Set the icon.  Once the minimum API is >= 26, the blue and black icons can be combined with a tint list.
4400                 optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled);
4401             } else {  // The stop icon is displayed.
4402                 // Set the icon.  Once the minimum API is >= 26, the blue and black icons can be combined with a tint list.
4403                 optionsRefreshMenuItem.setIcon(R.drawable.close_blue);
4404             }
4405
4406             // `invalidateOptionsMenu()` calls `onPrepareOptionsMenu()` and redraws the icons in the app bar.
4407             if (runInvalidateOptionsMenu) {
4408                 invalidateOptionsMenu();
4409             }
4410         }
4411     }
4412
4413     private void highlightUrlText() {
4414         // Only highlight the URL text if the box is not currently selected.
4415         if (!urlEditText.hasFocus()) {
4416             // Get the URL string.
4417             String urlString = urlEditText.getText().toString();
4418
4419             // Highlight the URL according to the protocol.
4420             if (urlString.startsWith("file://") || urlString.startsWith("content://")) {  // This is a file or content URL.
4421                 // De-emphasize everything before the file name.
4422                 urlEditText.getText().setSpan(initialGrayColorSpan, 0, urlString.lastIndexOf("/") + 1,Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4423             } else {  // This is a web URL.
4424                 // Get the index of the `/` immediately after the domain name.
4425                 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4426
4427                 // Create a base URL string.
4428                 String baseUrl;
4429
4430                 // Get the base URL.
4431                 if (endOfDomainName > 0) {  // There is at least one character after the base URL.
4432                     // Get the base URL.
4433                     baseUrl = urlString.substring(0, endOfDomainName);
4434                 } else {  // There are no characters after the base URL.
4435                     // Set the base URL to be the entire URL string.
4436                     baseUrl = urlString;
4437                 }
4438
4439                 // Get the index of the last `.` in the domain.
4440                 int lastDotIndex = baseUrl.lastIndexOf(".");
4441
4442                 // Get the index of the penultimate `.` in the domain.
4443                 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4444
4445                 // Markup the beginning of the URL.
4446                 if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
4447                     urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4448
4449                     // De-emphasize subdomains.
4450                     if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4451                         urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4452                     }
4453                 } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
4454                     if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4455                         // De-emphasize the protocol and the additional subdomains.
4456                         urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4457                     } else {  // There is only one subdomain in the domain name.
4458                         // De-emphasize only the protocol.
4459                         urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4460                     }
4461                 }
4462
4463                 // De-emphasize the text after the domain name.
4464                 if (endOfDomainName > 0) {
4465                     urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4466                 }
4467             }
4468         }
4469     }
4470
4471     private void loadBookmarksFolder() {
4472         // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4473         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4474
4475         // Populate the bookmarks cursor adapter.
4476         bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4477             @Override
4478             public View newView(Context context, Cursor cursor, ViewGroup parent) {
4479                 // Inflate the individual item layout.
4480                 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4481             }
4482
4483             @Override
4484             public void bindView(View view, Context context, Cursor cursor) {
4485                 // Get handles for the views.
4486                 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4487                 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4488
4489                 // Get the favorite icon byte array from the cursor.
4490                 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON));
4491
4492                 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4493                 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4494
4495                 // Display the bitmap in `bookmarkFavoriteIcon`.
4496                 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4497
4498                 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4499                 String bookmarkNameString = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME));
4500                 bookmarkNameTextView.setText(bookmarkNameString);
4501
4502                 // Make the font bold for folders.
4503                 if (cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4504                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4505                 } else {  // Reset the font to default for normal bookmarks.
4506                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4507                 }
4508             }
4509         };
4510
4511         // Get a handle for the bookmarks list view.
4512         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
4513
4514         // Populate the list view with the adapter.
4515         bookmarksListView.setAdapter(bookmarksCursorAdapter);
4516
4517         // Get a handle for the bookmarks title text view.
4518         TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
4519
4520         // Set the bookmarks drawer title.
4521         if (currentBookmarksFolder.isEmpty()) {
4522             bookmarksTitleTextView.setText(R.string.bookmarks);
4523         } else {
4524             bookmarksTitleTextView.setText(currentBookmarksFolder);
4525         }
4526     }
4527
4528     private void openWithApp(String url) {
4529         // Create an open with app intent with `ACTION_VIEW`.
4530         Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4531
4532         // Set the URI but not the MIME type.  This should open all available apps.
4533         openWithAppIntent.setData(Uri.parse(url));
4534
4535         // Flag the intent to open in a new task.
4536         openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4537
4538         // Try the intent.
4539         try {
4540             // Show the chooser.
4541             startActivity(openWithAppIntent);
4542         } catch (ActivityNotFoundException exception) {  // There are no apps available to open the URL.
4543             // Show a snackbar with the error.
4544             Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
4545         }
4546     }
4547
4548     private void openWithBrowser(String url) {
4549         // Create an open with browser intent with `ACTION_VIEW`.
4550         Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4551
4552         // Set the URI and the MIME type.  `"text/html"` should load browser options.
4553         openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4554
4555         // Flag the intent to open in a new task.
4556         openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4557
4558         // Try the intent.
4559         try {
4560             // Show the chooser.
4561             startActivity(openWithBrowserIntent);
4562         } catch (ActivityNotFoundException exception) {  // There are no browsers available to open the URL.
4563             // Show a snackbar with the error.
4564             Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
4565         }
4566     }
4567
4568     private String sanitizeUrl(String url) {
4569         // Sanitize tracking queries.
4570         if (sanitizeTrackingQueries)
4571             url = sanitizeUrlHelper.sanitizeTrackingQueries(url);
4572
4573         // Sanitize AMP redirects.
4574         if (sanitizeAmpRedirects)
4575             url = sanitizeUrlHelper.sanitizeAmpRedirects(url);
4576
4577         // Return the sanitized URL.
4578         return url;
4579     }
4580
4581     public void finishedPopulatingBlocklists(ArrayList<ArrayList<List<String[]>>> combinedBlocklists) {
4582         // Store the blocklists.
4583         easyList = combinedBlocklists.get(0);
4584         easyPrivacy = combinedBlocklists.get(1);
4585         fanboysAnnoyanceList = combinedBlocklists.get(2);
4586         fanboysSocialList = combinedBlocklists.get(3);
4587         ultraList = combinedBlocklists.get(4);
4588         ultraPrivacy = combinedBlocklists.get(5);
4589
4590         // Check to see if the activity has been restarted with a saved state.
4591         if ((savedStateArrayList == null) || (savedStateArrayList.size() == 0)) {  // The activity has not been restarted or it was restarted on start to force the night theme.
4592             // Add the first tab.
4593             addNewTab("", true);
4594         } else {  // The activity has been restarted.
4595             // Restore each tab.  Once the minimum API >= 24, a `forEach()` command can be used.
4596             for (int i = 0; i < savedStateArrayList.size(); i++) {
4597                 // Add a new tab.
4598                 tabLayout.addTab(tabLayout.newTab());
4599
4600                 // Get the new tab.
4601                 TabLayout.Tab newTab = tabLayout.getTabAt(i);
4602
4603                 // Remove the lint warning below that the current tab might be null.
4604                 assert newTab != null;
4605
4606                 // Set a custom view on the new tab.
4607                 newTab.setCustomView(R.layout.tab_custom_view);
4608
4609                 // Add the new page.
4610                 webViewPagerAdapter.restorePage(savedStateArrayList.get(i), savedNestedScrollWebViewStateArrayList.get(i));
4611             }
4612
4613             // Reset the saved state variables.
4614             savedStateArrayList = null;
4615             savedNestedScrollWebViewStateArrayList = null;
4616
4617             // Restore the selected tab position.
4618             if (savedTabPosition == 0) {  // The first tab is selected.
4619                 // Set the first page as the current WebView.
4620                 setCurrentWebView(0);
4621             } else {  // the first tab is not selected.
4622                 // Move to the selected tab.
4623                 webViewPager.setCurrentItem(savedTabPosition);
4624             }
4625
4626             // Get the intent that started the app.
4627             Intent intent = getIntent();
4628
4629             // Reset the intent.  This prevents a duplicate tab from being created on restart.
4630             setIntent(new Intent());
4631
4632             // Get the information from the intent.
4633             String intentAction = intent.getAction();
4634             Uri intentUriData = intent.getData();
4635             String intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
4636
4637             // Determine if this is a web search.
4638             boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH));
4639
4640             // 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.
4641             if (intentUriData != null || intentStringExtra != null || isWebSearch) {
4642                 // Get the shared preferences.
4643                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4644
4645                 // Create a URL string.
4646                 String url;
4647
4648                 // If the intent action is a web search, perform the search.
4649                 if (isWebSearch) {  // The intent is a web search.
4650                     // Create an encoded URL string.
4651                     String encodedUrlString;
4652
4653                     // Sanitize the search input and convert it to a search.
4654                     try {
4655                         encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
4656                     } catch (UnsupportedEncodingException exception) {
4657                         encodedUrlString = "";
4658                     }
4659
4660                     // Add the base search URL.
4661                     url = searchURL + encodedUrlString;
4662                 } else if (intentUriData != null) {  // The intent contains a URL formatted as a URI.
4663                     // Set the intent data as the URL.
4664                     url = intentUriData.toString();
4665                 } else {  // The intent contains a string, which might be a URL.
4666                     // Set the intent string as the URL.
4667                     url = intentStringExtra;
4668                 }
4669
4670                 // Add a new tab if specified in the preferences.
4671                 if (sharedPreferences.getBoolean(getString(R.string.open_intents_in_new_tab_key), true)) {  // Load the URL in a new tab.
4672                     // Set the loading new intent flag.
4673                     loadingNewIntent = true;
4674
4675                     // Add a new tab.
4676                     addNewTab(url, true);
4677                 } else {  // Load the URL in the current tab.
4678                     // Make it so.
4679                     loadUrl(currentWebView, url);
4680                 }
4681             }
4682         }
4683     }
4684
4685     public void addTab(View view) {
4686         // Add a new tab with a blank URL.
4687         addNewTab("", true);
4688     }
4689
4690     private void addNewTab(String url, boolean moveToTab) {
4691         // Clear the focus from the URL edit text, so that it will be populated with the information from the new tab.
4692         urlEditText.clearFocus();
4693
4694         // Get the new page number.  The page numbers are 0 indexed, so the new page number will match the current count.
4695         int newTabNumber = tabLayout.getTabCount();
4696
4697         // Add a new tab.
4698         tabLayout.addTab(tabLayout.newTab());
4699
4700         // Get the new tab.
4701         TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
4702
4703         // Remove the lint warning below that the current tab might be null.
4704         assert newTab != null;
4705
4706         // Set a custom view on the new tab.
4707         newTab.setCustomView(R.layout.tab_custom_view);
4708
4709         // Add the new WebView page.
4710         webViewPagerAdapter.addPage(newTabNumber, webViewPager, url, moveToTab);
4711
4712         // Show the app bar if it is at the bottom of the screen and the new tab is taking focus.
4713         if (bottomAppBar && moveToTab && (appBarLayout.getTranslationY() != 0)) {
4714             // Animate the bottom app bar onto the screen.
4715             objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0);
4716
4717             // Make it so.
4718             objectAnimator.start();
4719         }
4720     }
4721
4722     public void closeTab(View view) {
4723         // Run the command according to the number of tabs.
4724         if (tabLayout.getTabCount() > 1) {  // There is more than one tab open.
4725             // Close the current tab.
4726             closeCurrentTab();
4727         } else {  // There is only one tab open.
4728             clearAndExit();
4729         }
4730     }
4731
4732     private void closeCurrentTab() {
4733         // Get the current tab number.
4734         int currentTabNumber = tabLayout.getSelectedTabPosition();
4735
4736         // Delete the current tab.
4737         tabLayout.removeTabAt(currentTabNumber);
4738
4739         // Delete the current page.  If the selected page number did not change during the delete (because the newly selected tab has has same number as the previously deleted tab), it will return true,
4740         // meaning that the current WebView must be reset.  Otherwise it will happen automatically as the selected tab number changes.
4741         if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
4742             setCurrentWebView(currentTabNumber);
4743         }
4744     }
4745
4746     private void exitFullScreenVideo() {
4747         // Re-enable the screen timeout.
4748         fullScreenVideoFrameLayout.setKeepScreenOn(false);
4749
4750         // Unset the full screen video flag.
4751         displayingFullScreenVideo = false;
4752
4753         // Remove all the views from the full screen video frame layout.
4754         fullScreenVideoFrameLayout.removeAllViews();
4755
4756         // Hide the full screen video frame layout.
4757         fullScreenVideoFrameLayout.setVisibility(View.GONE);
4758
4759         // Enable the sliding drawers.
4760         drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
4761
4762         // Show the coordinator layout.
4763         coordinatorLayout.setVisibility(View.VISIBLE);
4764
4765         // Apply the appropriate full screen mode flags.
4766         if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
4767             // Hide the app bar if specified.
4768             if (hideAppBar) {
4769                 // Hide the tab linear layout.
4770                 tabsLinearLayout.setVisibility(View.GONE);
4771
4772                 // Hide the action bar.
4773                 actionBar.hide();
4774             }
4775
4776             /* Hide the system bars.
4777              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4778              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4779              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4780              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4781              */
4782             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4783                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4784         } else {  // Switch to normal viewing mode.
4785             // Remove the `SYSTEM_UI` flags from the root frame layout.
4786             rootFrameLayout.setSystemUiVisibility(0);
4787         }
4788     }
4789
4790     private void clearAndExit() {
4791         // Get a handle for the shared preferences.
4792         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4793
4794         // Close the bookmarks cursor and database.
4795         bookmarksCursor.close();
4796         bookmarksDatabaseHelper.close();
4797
4798         // Get the status of the clear everything preference.
4799         boolean clearEverything = sharedPreferences.getBoolean(getString(R.string.clear_everything_key), true);
4800
4801         // Get a handle for the runtime.
4802         Runtime runtime = Runtime.getRuntime();
4803
4804         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
4805         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
4806         String privateDataDirectoryString = getApplicationInfo().dataDir;
4807
4808         // Clear cookies.
4809         if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_cookies_key), true)) {
4810             // Request the cookies be deleted.
4811             CookieManager.getInstance().removeAllCookies(null);
4812
4813             // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4814             try {
4815                 // Two commands must be used because `Runtime.exec()` does not like `*`.
4816                 Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
4817                 Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
4818
4819                 // Wait until the processes have finished.
4820                 deleteCookiesProcess.waitFor();
4821                 deleteCookiesJournalProcess.waitFor();
4822             } catch (Exception exception) {
4823                 // Do nothing if an error is thrown.
4824             }
4825         }
4826
4827         // Clear DOM storage.
4828         if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_dom_storage_key), true)) {
4829             // Ask `WebStorage` to clear the DOM storage.
4830             WebStorage webStorage = WebStorage.getInstance();
4831             webStorage.deleteAllData();
4832
4833             // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4834             try {
4835                 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4836                 Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
4837
4838                 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
4839                 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
4840                 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
4841                 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
4842                 Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
4843
4844                 // Wait until the processes have finished.
4845                 deleteLocalStorageProcess.waitFor();
4846                 deleteIndexProcess.waitFor();
4847                 deleteQuotaManagerProcess.waitFor();
4848                 deleteQuotaManagerJournalProcess.waitFor();
4849                 deleteDatabaseProcess.waitFor();
4850             } catch (Exception exception) {
4851                 // Do nothing if an error is thrown.
4852             }
4853         }
4854
4855         // Clear form data if the API < 26.
4856         if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_form_data_key), true))) {
4857             WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
4858             webViewDatabase.clearFormData();
4859
4860             // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4861             try {
4862                 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
4863                 Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
4864                 Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
4865
4866                 // Wait until the processes have finished.
4867                 deleteWebDataProcess.waitFor();
4868                 deleteWebDataJournalProcess.waitFor();
4869             } catch (Exception exception) {
4870                 // Do nothing if an error is thrown.
4871             }
4872         }
4873
4874         // Clear the logcat.
4875         if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_logcat_key), true)) {
4876             try {
4877                 // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
4878                 Process process = Runtime.getRuntime().exec("logcat -b all -c");
4879
4880                 // Wait for the process to finish.
4881                 process.waitFor();
4882             } catch (IOException|InterruptedException exception) {
4883                 // Do nothing.
4884             }
4885         }
4886
4887         // Clear the cache.
4888         if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_cache_key), true)) {
4889             // Clear the cache from each WebView.
4890             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4891                 // Get the WebView tab fragment.
4892                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4893
4894                 // Get the WebView fragment view.
4895                 View webViewFragmentView = webViewTabFragment.getView();
4896
4897                 // Only clear the cache if the WebView exists.
4898                 if (webViewFragmentView != null) {
4899                     // Get the nested scroll WebView from the tab fragment.
4900                     NestedScrollWebView nestedScrollWebView = webViewFragmentView.findViewById(R.id.nestedscroll_webview);
4901
4902                     // Clear the cache for this WebView.
4903                     nestedScrollWebView.clearCache(true);
4904                 }
4905             }
4906
4907             // Manually delete the cache directories.
4908             try {
4909                 // Delete the main cache directory.
4910                 Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
4911
4912                 // Delete the secondary `Service Worker` cache directory.
4913                 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4914                 Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Default/Service Worker/"});
4915
4916                 // Wait until the processes have finished.
4917                 deleteCacheProcess.waitFor();
4918                 deleteServiceWorkerProcess.waitFor();
4919             } catch (Exception exception) {
4920                 // Do nothing if an error is thrown.
4921             }
4922         }
4923
4924         // Wipe out each WebView.
4925         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4926             // Get the WebView tab fragment.
4927             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4928
4929             // Get the WebView frame layout.
4930             FrameLayout webViewFrameLayout = (FrameLayout) webViewTabFragment.getView();
4931
4932             // Only wipe out the WebView if it exists.
4933             if (webViewFrameLayout != null) {
4934                 // Get the nested scroll WebView from the tab fragment.
4935                 NestedScrollWebView nestedScrollWebView = webViewFrameLayout.findViewById(R.id.nestedscroll_webview);
4936
4937                 // Clear SSL certificate preferences for this WebView.
4938                 nestedScrollWebView.clearSslPreferences();
4939
4940                 // Clear the back/forward history for this WebView.
4941                 nestedScrollWebView.clearHistory();
4942
4943                 // Remove all the views from the frame layout.
4944                 webViewFrameLayout.removeAllViews();
4945
4946                 // Destroy the internal state of the WebView.
4947                 nestedScrollWebView.destroy();
4948             }
4949         }
4950
4951         // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
4952         // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
4953         if (clearEverything) {
4954             try {
4955                 // Delete the folder.
4956                 Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
4957
4958                 // Wait until the process has finished.
4959                 deleteAppWebviewProcess.waitFor();
4960             } catch (Exception exception) {
4961                 // Do nothing if an error is thrown.
4962             }
4963         }
4964
4965         // Close Privacy Browser.  `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
4966         finishAndRemoveTask();
4967
4968         // Remove the terminated program from RAM.  The status code is `0`.
4969         System.exit(0);
4970     }
4971
4972     public void bookmarksBack(View view) {
4973         if (currentBookmarksFolder.isEmpty()) {  // The home folder is displayed.
4974             // close the bookmarks drawer.
4975             drawerLayout.closeDrawer(GravityCompat.END);
4976         } else {  // A subfolder is displayed.
4977             // Place the former parent folder in `currentFolder`.
4978             currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
4979
4980             // Load the new folder.
4981             loadBookmarksFolder();
4982         }
4983     }
4984
4985     private void setCurrentWebView(int pageNumber) {
4986         // Stop the swipe to refresh indicator if it is running
4987         swipeRefreshLayout.setRefreshing(false);
4988
4989         // Get the WebView tab fragment.
4990         WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber);
4991
4992         // Get the fragment view.
4993         View webViewFragmentView = webViewTabFragment.getView();
4994
4995         // Set the current WebView if the fragment view is not null.
4996         if (webViewFragmentView != null) {  // The fragment has been populated.
4997             // Store the current WebView.
4998             currentWebView = webViewFragmentView.findViewById(R.id.nestedscroll_webview);
4999
5000             // Update the status of swipe to refresh.
5001             if (currentWebView.getSwipeToRefresh()) {  // Swipe to refresh is enabled.
5002                 // Enable the swipe refresh layout if the WebView is scrolled all the way to the top.  It is updated every time the scroll changes.
5003                 swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
5004             } else {  // Swipe to refresh is disabled.
5005                 // Disable the swipe refresh layout.
5006                 swipeRefreshLayout.setEnabled(false);
5007             }
5008
5009             // Get a handle for the cookie manager.
5010             CookieManager cookieManager = CookieManager.getInstance();
5011
5012             // Set the cookie status.
5013             cookieManager.setAcceptCookie(currentWebView.getAcceptCookies());
5014
5015             // Update the privacy icons.  `true` redraws the icons in the app bar.
5016             updatePrivacyIcons(true);
5017
5018             // Get a handle for the input method manager.
5019             InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
5020
5021             // Remove the lint warning below that the input method manager might be null.
5022             assert inputMethodManager != null;
5023
5024             // Get the current URL.
5025             String url = currentWebView.getUrl();
5026
5027             // Update the URL edit text if not loading a new intent.  Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`.
5028             if (!loadingNewIntent) {  // A new intent is not being loaded.
5029                 if ((url == null) || url.equals("about:blank")) {  // The WebView is blank.
5030                     // Display the hint in the URL edit text.
5031                     urlEditText.setText("");
5032
5033                     // Request focus for the URL text box.
5034                     urlEditText.requestFocus();
5035
5036                     // Display the keyboard.
5037                     inputMethodManager.showSoftInput(urlEditText, 0);
5038                 } else {  // The WebView has a loaded URL.
5039                     // Clear the focus from the URL text box.
5040                     urlEditText.clearFocus();
5041
5042                     // Hide the soft keyboard.
5043                     inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
5044
5045                     // Display the current URL in the URL text box.
5046                     urlEditText.setText(url);
5047
5048                     // Highlight the URL text.
5049                     highlightUrlText();
5050                 }
5051             } else {  // A new intent is being loaded.
5052                 // Reset the loading new intent tracker.
5053                 loadingNewIntent = false;
5054             }
5055
5056             // Set the background to indicate the domain settings status.
5057             if (currentWebView.getDomainSettingsApplied()) {
5058                 // Set a background on the URL relative layout to indicate that custom domain settings are being used.
5059                 urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.drawable.domain_settings_url_background, null));
5060             } else {
5061                 // Remove any background on the URL relative layout.
5062                 urlRelativeLayout.setBackground(ResourcesCompat.getDrawable(getResources(), R.color.transparent, null));
5063             }
5064         } else {  // The fragment has not been populated.  Try again in 100 milliseconds.
5065             // Create a handler to set the current WebView.
5066             Handler setCurrentWebViewHandler = new Handler();
5067
5068             // Create a runnable to set the current WebView.
5069             Runnable setCurrentWebWebRunnable = () -> {
5070                 // Set the current WebView.
5071                 setCurrentWebView(pageNumber);
5072             };
5073
5074             // Try setting the current WebView again after 100 milliseconds.
5075             setCurrentWebViewHandler.postDelayed(setCurrentWebWebRunnable, 100);
5076         }
5077     }
5078
5079     @SuppressLint("ClickableViewAccessibility")
5080     @Override
5081     public void initializeWebView(@NonNull NestedScrollWebView nestedScrollWebView, int pageNumber, @NonNull ProgressBar progressBar, @NonNull String url, boolean restoringState) {
5082         // Get a handle for the shared preferences.
5083         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
5084
5085         // Get the WebView theme.
5086         String webViewTheme = sharedPreferences.getString(getString(R.string.webview_theme_key), getString(R.string.webview_theme_default_value));
5087
5088         // Get the WebView theme entry values string array.
5089         String[] webViewThemeEntryValuesStringArray = getResources().getStringArray(R.array.webview_theme_entry_values);
5090
5091         // Apply the WebView theme if supported by the installed WebView.
5092         if ((Build.VERSION.SDK_INT >= 33) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {  // The device is running API >= 33 and algorithmic darkening is supported.
5093             // Set the WebView them.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
5094             if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
5095                 // Turn off algorithmic darkening.
5096                 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), false);
5097
5098                 // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
5099                 // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
5100                 nestedScrollWebView.setVisibility(View.VISIBLE);
5101             } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
5102                 // Turn on algorithmic darkening.
5103                 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true);
5104             } else {
5105                 // The system default theme is selected.
5106                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
5107
5108                 // Set the algorithmic darkening according to the current system theme status.
5109                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
5110                     // Turn off algorithmic darkening.
5111                     WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), false);
5112
5113                     // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
5114                     // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
5115                     nestedScrollWebView.setVisibility(View.VISIBLE);
5116                 } else {  // The system is in night mode.
5117                     // Turn on algorithmic darkening.
5118                     WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.getSettings(), true);
5119                 }
5120             }
5121         } else if ((Build.VERSION.SDK_INT < 33) && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {  // The device is running API < 33 and the WebView supports force dark.
5122             // Set the WebView theme.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
5123             if (webViewTheme.equals(webViewThemeEntryValuesStringArray[1])) {  // The light theme is selected.
5124                 // Turn off the WebView dark mode.
5125                 //noinspection deprecation
5126                 WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
5127
5128                 // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
5129                 // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
5130                 nestedScrollWebView.setVisibility(View.VISIBLE);
5131             } else if (webViewTheme.equals(webViewThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
5132                 // Turn on the WebView dark mode.
5133                 //noinspection deprecation
5134                 WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
5135             } else {  // The system default theme is selected.
5136                 // Get the current system theme status.
5137                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
5138
5139                 // Set the WebView theme according to the current system theme status.
5140                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
5141                     // Turn off the WebView dark mode.
5142                     //noinspection deprecation
5143                     WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
5144
5145                     // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
5146                     // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
5147                     nestedScrollWebView.setVisibility(View.VISIBLE);
5148                 } else {  // The system is in night mode.
5149                     // Turn on the WebView dark mode.
5150                     //noinspection deprecation
5151                     WebSettingsCompat.setForceDark(nestedScrollWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
5152                 }
5153             }
5154         }
5155
5156         // Get a handle for the activity
5157         Activity activity = this;
5158
5159         // Get a handle for the input method manager.
5160         InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
5161
5162         // Instantiate the blocklist helper.
5163         BlocklistHelper blocklistHelper = new BlocklistHelper();
5164
5165         // Remove the lint warning below that the input method manager might be null.
5166         assert inputMethodManager != null;
5167
5168         // Set the app bar scrolling.
5169         nestedScrollWebView.setNestedScrollingEnabled(scrollAppBar);
5170
5171         // Allow pinch to zoom.
5172         nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
5173
5174         // Hide zoom controls.
5175         nestedScrollWebView.getSettings().setDisplayZoomControls(false);
5176
5177         // Don't allow mixed content (HTTP and HTTPS) on the same website.
5178         nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
5179
5180         // Set the WebView to load in overview mode (zoomed out to the maximum width).
5181         nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
5182
5183         // Explicitly disable geolocation.
5184         nestedScrollWebView.getSettings().setGeolocationEnabled(false);
5185
5186         // Allow loading of file:// URLs.  This is necessary for opening MHT web archives, which are copies into a temporary cache location.
5187         nestedScrollWebView.getSettings().setAllowFileAccess(true);
5188
5189         // Create a double-tap gesture detector to toggle full-screen mode.
5190         GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
5191             // Override `onDoubleTap()`.  All other events are handled using the default settings.
5192             @Override
5193             public boolean onDoubleTap(MotionEvent event) {
5194                 if (fullScreenBrowsingModeEnabled) {  // Only process the double-tap if full screen browsing mode is enabled.
5195                     // Toggle the full screen browsing mode tracker.
5196                     inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
5197
5198                     // Toggle the full screen browsing mode.
5199                     if (inFullScreenBrowsingMode) {  // Switch to full screen mode.
5200                         // Hide the app bar if specified.
5201                         if (hideAppBar) {  // The app bar is hidden.
5202                             // Close the find on page bar if it is visible.
5203                             closeFindOnPage(null);
5204
5205                             // Hide the tab linear layout.
5206                             tabsLinearLayout.setVisibility(View.GONE);
5207
5208                             // Hide the action bar.
5209                             actionBar.hide();
5210
5211                             // Set layout and scrolling parameters according to the position of the app bar.
5212                             if (bottomAppBar) {  // The app bar is at the bottom.
5213                                 // Reset the WebView padding to fill the available space.
5214                                 swipeRefreshLayout.setPadding(0, 0, 0, 0);
5215                             } else {  // The app bar is at the top.
5216                                 // Check to see if the app bar is normally scrolled.
5217                                 if (scrollAppBar) {  // The app bar is scrolled when it is displayed.
5218                                     // Get the swipe refresh layout parameters.
5219                                     CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
5220
5221                                     // Remove the off-screen scrolling layout.
5222                                     swipeRefreshLayoutParams.setBehavior(null);
5223                                 } else {  // The app bar is not scrolled when it is displayed.
5224                                     // Remove the padding from the top of the swipe refresh layout.
5225                                     swipeRefreshLayout.setPadding(0, 0, 0, 0);
5226
5227                                     // The swipe refresh circle must be moved above the now removed status bar location.
5228                                     swipeRefreshLayout.setProgressViewOffset(false, -200, defaultProgressViewEndOffset);
5229                                 }
5230                             }
5231                         } else {  // The app bar is not hidden.
5232                             // Adjust the UI for the bottom app bar.
5233                             if (bottomAppBar) {
5234                                 // Adjust the UI according to the scrolling of the app bar.
5235                                 if (scrollAppBar) {
5236                                     // Reset the WebView padding to fill the available space.
5237                                     swipeRefreshLayout.setPadding(0, 0, 0, 0);
5238                                 } else {
5239                                     // Move the WebView above the app bar layout.
5240                                     swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight);
5241                                 }
5242                             }
5243                         }
5244
5245                         /* Hide the system bars.
5246                          * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5247                          * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5248                          * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5249                          * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5250                          */
5251                         rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5252                                 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5253                     } else {  // Switch to normal viewing mode.
5254                         // Show the app bar if it was hidden.
5255                         if (hideAppBar) {
5256                             // Show the tab linear layout.
5257                             tabsLinearLayout.setVisibility(View.VISIBLE);
5258
5259                             // Show the action bar.
5260                             actionBar.show();
5261                         }
5262
5263                         // Set layout and scrolling parameters according to the position of the app bar.
5264                         if (bottomAppBar) {  // The app bar is at the bottom.
5265                             // Adjust the UI.
5266                             if (scrollAppBar) {
5267                                 // Reset the WebView padding to fill the available space.
5268                                 swipeRefreshLayout.setPadding(0, 0, 0, 0);
5269                             } else {
5270                                 // Move the WebView above the app bar layout.
5271                                 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight);
5272                             }
5273                         } else {  // The app bar is at the top.
5274                             // Check to see if the app bar is normally scrolled.
5275                             if (scrollAppBar) {  // The app bar is scrolled when it is displayed.
5276                                 // Get the swipe refresh layout parameters.
5277                                 CoordinatorLayout.LayoutParams swipeRefreshLayoutParams = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
5278
5279                                 // Add the off-screen scrolling layout.
5280                                 swipeRefreshLayoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior());
5281                             } else {  // The app bar is not scrolled when it is displayed.
5282                                 // The swipe refresh layout must be manually moved below the app bar layout.
5283                                 swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0);
5284
5285                                 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5286                                 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight);
5287                             }
5288                         }
5289
5290                         // Remove the `SYSTEM_UI` flags from the root frame layout.
5291                         rootFrameLayout.setSystemUiVisibility(0);
5292                     }
5293
5294                     // Consume the double-tap.
5295                     return true;
5296                 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
5297                     return false;
5298                 }
5299             }
5300
5301             @Override
5302             public boolean onFling(MotionEvent motionEvent1, MotionEvent motionEvent2, float velocityX, float velocityY) {
5303                 // Scroll the bottom app bar if enabled.
5304                 if (bottomAppBar && scrollAppBar && !objectAnimator.isRunning()) {
5305                     // Calculate the Y change.
5306                     float motionY = motionEvent2.getY() - motionEvent1.getY();
5307
5308                     // Scroll the app bar if the change is greater than 50 pixels.
5309                     if (motionY > 50) {
5310                         // Animate the bottom app bar onto the screen.
5311                         objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0);
5312                     } else if (motionY < -50) {
5313                         // Animate the bottom app bar off the screen.
5314                         objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", appBarLayout.getHeight());
5315                     }
5316
5317                     // Make it so.
5318                     objectAnimator.start();
5319                 }
5320
5321                 // Do not consume the event.
5322                 return false;
5323             }
5324         });
5325
5326         // Pass all touch events on the WebView through the double-tap gesture detector.
5327         nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
5328             // Call `performClick()` on the view, which is required for accessibility.
5329             view.performClick();
5330
5331             // Send the event to the gesture detector.
5332             return doubleTapGestureDetector.onTouchEvent(event);
5333         });
5334
5335         // Register the WebView for a context menu.  This is used to see link targets and download images.
5336         registerForContextMenu(nestedScrollWebView);
5337
5338         // Allow the downloading of files.
5339         nestedScrollWebView.setDownloadListener((String downloadUrl, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
5340             // Check the download preference.
5341             if (downloadWithExternalApp) {  // Download with an external app.
5342                 downloadUrlWithExternalApp(downloadUrl);
5343             } else {  // Handle the download inside of Privacy Browser.
5344                 // Define a formatted file size string.
5345                 String formattedFileSizeString;
5346
5347                 // Process the content length if it contains data.
5348                 if (contentLength > 0) {  // The content length is greater than 0.
5349                     // Format the content length as a string.
5350                     formattedFileSizeString = NumberFormat.getInstance().format(contentLength) + " " + getString(R.string.bytes);
5351                 } else {  // The content length is not greater than 0.
5352                     // Set the formatted file size string to be `unknown size`.
5353                     formattedFileSizeString = getString(R.string.unknown_size);
5354                 }
5355
5356                 // Get the file name from the content disposition.
5357                 String fileNameString = PrepareSaveDialog.getFileNameFromHeaders(this, contentDisposition, mimetype, downloadUrl);
5358
5359                 // Instantiate the save dialog.
5360                 DialogFragment saveDialogFragment = SaveDialog.saveUrl(downloadUrl, formattedFileSizeString, fileNameString, userAgent,
5361                         nestedScrollWebView.getAcceptCookies());
5362
5363                 // Try to show the dialog.  The download listener continues to function even when the WebView is paused.  Attempting to display a dialog in that state leads to a crash.
5364                 try {
5365                     // Show the save dialog.  It must be named `save_dialog` so that the file picker can update the file name.
5366                     saveDialogFragment.show(getSupportFragmentManager(), getString(R.string.save_dialog));
5367                 } catch (Exception exception) {  // The dialog could not be shown.
5368                     // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
5369                     pendingDialogsArrayList.add(new PendingDialog(saveDialogFragment, getString(R.string.save_dialog)));
5370                 }
5371             }
5372         });
5373
5374         // Update the find on page count.
5375         nestedScrollWebView.setFindListener(new WebView.FindListener() {
5376             // Get a handle for `findOnPageCountTextView`.
5377             final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
5378
5379             @Override
5380             public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
5381                 if ((isDoneCounting) && (numberOfMatches == 0)) {  // There are no matches.
5382                     // Set `findOnPageCountTextView` to `0/0`.
5383                     findOnPageCountTextView.setText(R.string.zero_of_zero);
5384                 } else if (isDoneCounting) {  // There are matches.
5385                     // `activeMatchOrdinal` is zero-based.
5386                     int activeMatch = activeMatchOrdinal + 1;
5387
5388                     // Build the match string.
5389                     String matchString = activeMatch + "/" + numberOfMatches;
5390
5391                     // Set `findOnPageCountTextView`.
5392                     findOnPageCountTextView.setText(matchString);
5393                 }
5394             }
5395         });
5396
5397         // Process scroll changes.
5398         nestedScrollWebView.setOnScrollChangeListener((view, scrollX, scrollY, oldScrollX, oldScrollY) -> {
5399             // Set the swipe to refresh status.
5400             if (nestedScrollWebView.getSwipeToRefresh()) {
5401                 // Only enable swipe to refresh if the WebView is scrolled to the top.
5402                 swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0);
5403             } else {
5404                 // Disable swipe to refresh.
5405                 swipeRefreshLayout.setEnabled(false);
5406             }
5407
5408             // Reinforce the system UI visibility flags if in full screen browsing mode.
5409             // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard.
5410             if (inFullScreenBrowsingMode) {
5411                 /* Hide the system bars.
5412                  * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5413                  * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5414                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5415                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5416                  */
5417                 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5418                         View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5419             }
5420         });
5421
5422         // Set the web chrome client.
5423         nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
5424             // Update the progress bar when a page is loading.
5425             @Override
5426             public void onProgressChanged(WebView view, int progress) {
5427                 // Update the progress bar.
5428                 progressBar.setProgress(progress);
5429
5430                 // Set the visibility of the progress bar.
5431                 if (progress < 100) {
5432                     // Show the progress bar.
5433                     progressBar.setVisibility(View.VISIBLE);
5434                 } else {
5435                     // Hide the progress bar.
5436                     progressBar.setVisibility(View.GONE);
5437
5438                     //Stop the swipe to refresh indicator if it is running
5439                     swipeRefreshLayout.setRefreshing(false);
5440
5441                     // Make the current WebView visible.  If this is a new tab, the current WebView would have been created invisible in `webview_framelayout` to prevent a white background splash in night mode.
5442                     nestedScrollWebView.setVisibility(View.VISIBLE);
5443                 }
5444             }
5445
5446             // Set the favorite icon when it changes.
5447             @Override
5448             public void onReceivedIcon(WebView view, Bitmap icon) {
5449                 // Only update the favorite icon if the website has finished loading.
5450                 if (progressBar.getVisibility() == View.GONE) {
5451                     // Store the new favorite icon.
5452                     nestedScrollWebView.setFavoriteOrDefaultIcon(icon);
5453
5454                     // Get the current page position.
5455                     int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5456
5457                     // Get the current tab.
5458                     TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5459
5460                     // Check to see if the tab has been populated.
5461                     if (tab != null) {
5462                         // Get the custom view from the tab.
5463                         View tabView = tab.getCustomView();
5464
5465                         // Check to see if the custom tab view has been populated.
5466                         if (tabView != null) {
5467                             // Get the favorite icon image view from the tab.
5468                             ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview);
5469
5470                             // Display the favorite icon in the tab.
5471                             tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
5472                         }
5473                     }
5474                 }
5475             }
5476
5477             // Save a copy of the title when it changes.
5478             @Override
5479             public void onReceivedTitle(WebView view, String title) {
5480                 // Get the current page position.
5481                 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5482
5483                 // Get the current tab.
5484                 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5485
5486                 // Only populate the title text view if the tab has been fully created.
5487                 if (tab != null) {
5488                     // Get the custom view from the tab.
5489                     View tabView = tab.getCustomView();
5490
5491                     // Only populate the title text view if the tab view has been fully populated.
5492                     if (tabView != null) {
5493                         // Get the title text view from the tab.
5494                         TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5495
5496                         // Set the title according to the URL.
5497                         if (title.equals("about:blank")) {
5498                             // Set the title to indicate a new tab.
5499                             tabTitleTextView.setText(R.string.new_tab);
5500                         } else {
5501                             // Set the title as the tab text.
5502                             tabTitleTextView.setText(title);
5503                         }
5504                     }
5505                 }
5506             }
5507
5508             // Enter full screen video.
5509             @Override
5510             public void onShowCustomView(View video, CustomViewCallback callback) {
5511                 // Set the full screen video flag.
5512                 displayingFullScreenVideo = true;
5513
5514                 // Hide the keyboard.
5515                 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5516
5517                 // Hide the coordinator layout.
5518                 coordinatorLayout.setVisibility(View.GONE);
5519
5520                 /* Hide the system bars.
5521                  * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5522                  * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5523                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5524                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5525                  */
5526                 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5527                         View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5528
5529                 // Disable the sliding drawers.
5530                 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
5531
5532                 // Add the video view to the full screen video frame layout.
5533                 fullScreenVideoFrameLayout.addView(video);
5534
5535                 // Show the full screen video frame layout.
5536                 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
5537
5538                 // Disable the screen timeout while the video is playing.  YouTube does this automatically, but not all other videos do.
5539                 fullScreenVideoFrameLayout.setKeepScreenOn(true);
5540             }
5541
5542             // Exit full screen video.
5543             @Override
5544             public void onHideCustomView() {
5545                 // Exit the full screen video.
5546                 exitFullScreenVideo();
5547             }
5548
5549             // Upload files.
5550             @Override
5551             public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
5552                 // Store the file path callback.
5553                 fileChooserCallback = filePathCallback;
5554
5555                 // Create an intent to open a chooser based on the file chooser parameters.
5556                 Intent fileChooserIntent = fileChooserParams.createIntent();
5557
5558                 // Get a handle for the package manager.
5559                 PackageManager packageManager = getPackageManager();
5560
5561                 // Check to see if the file chooser intent resolves to an installed package.
5562                 if (fileChooserIntent.resolveActivity(packageManager) != null) {  // The file chooser intent is fine.
5563                     // Start the file chooser intent.
5564                     startActivityForResult(fileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE);
5565                 } else {  // The file chooser intent will cause a crash.
5566                     // Create a generic intent to open a chooser.
5567                     Intent genericFileChooserIntent = new Intent(Intent.ACTION_GET_CONTENT);
5568
5569                     // Request an openable file.
5570                     genericFileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE);
5571
5572                     // Set the file type to everything.
5573                     genericFileChooserIntent.setType("*/*");
5574
5575                     // Start the generic file chooser intent.
5576                     startActivityForResult(genericFileChooserIntent, BROWSE_FILE_UPLOAD_REQUEST_CODE);
5577                 }
5578                 return true;
5579             }
5580         });
5581
5582         nestedScrollWebView.setWebViewClient(new WebViewClient() {
5583             // `shouldOverrideUrlLoading` makes this WebView the default handler for URLs inside the app, so that links are not kicked out to other apps.
5584             // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
5585             @Override
5586             public boolean shouldOverrideUrlLoading(WebView view, String url) {
5587                 // Sanitize the url.
5588                 url = sanitizeUrl(url);
5589
5590                 // Handle the URL according to the type.
5591                 if (url.startsWith("http")) {  // Load the URL in Privacy Browser.
5592                     // Load the URL.  By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent.
5593                     loadUrl(nestedScrollWebView, url);
5594
5595                     // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
5596                     // Custom headers cannot be added if false is returned and the WebView handles the loading of the URL.
5597                     return true;
5598                 } else if (url.startsWith("mailto:")) {  // Load the email address in an external email program.
5599                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
5600                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
5601
5602                     // Parse the url and set it as the data for the intent.
5603                     emailIntent.setData(Uri.parse(url));
5604
5605                     // Open the email program in a new task instead of as part of Privacy Browser.
5606                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5607
5608                     try {
5609                         // Make it so.
5610                         startActivity(emailIntent);
5611                     } catch (ActivityNotFoundException exception) {
5612                         // Display a snackbar.
5613                         Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
5614                     }
5615
5616
5617                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5618                     return true;
5619                 } else if (url.startsWith("tel:")) {  // Load the phone number in the dialer.
5620                     // Open the dialer and load the phone number, but wait for the user to place the call.
5621                     Intent dialIntent = new Intent(Intent.ACTION_DIAL);
5622
5623                     // Add the phone number to the intent.
5624                     dialIntent.setData(Uri.parse(url));
5625
5626                     // Open the dialer in a new task instead of as part of Privacy Browser.
5627                     dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5628
5629                     try {
5630                         // Make it so.
5631                         startActivity(dialIntent);
5632                     } catch (ActivityNotFoundException exception) {
5633                         // Display a snackbar.
5634                         Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
5635                     }
5636
5637                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5638                     return true;
5639                 } else {  // Load a system chooser to select an app that can handle the URL.
5640                     // Open an app that can handle the URL.
5641                     Intent genericIntent = new Intent(Intent.ACTION_VIEW);
5642
5643                     // Add the URL to the intent.
5644                     genericIntent.setData(Uri.parse(url));
5645
5646                     // List all apps that can handle the URL instead of just opening the first one.
5647                     genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
5648
5649                     // Open the app in a new task instead of as part of Privacy Browser.
5650                     genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5651
5652                     // Start the app or display a snackbar if no app is available to handle the URL.
5653                     try {
5654                         startActivity(genericIntent);
5655                     } catch (ActivityNotFoundException exception) {
5656                         Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + "  " + url, Snackbar.LENGTH_SHORT).show();
5657                     }
5658
5659                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5660                     return true;
5661                 }
5662             }
5663
5664             // Check requests against the block lists.  The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
5665             @Override
5666             public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest webResourceRequest) {
5667                 // Get the URL.
5668                 String url = webResourceRequest.getUrl().toString();
5669
5670                 // Check to see if the resource request is for the main URL.
5671                 if (url.equals(nestedScrollWebView.getCurrentUrl())) {
5672                     // `return null` loads the resource request, which should never be blocked if it is the main URL.
5673                     return null;
5674                 }
5675
5676                 // Wait until the blocklists have been populated.  When Privacy Browser is being resumed after having the process killed in the background it will try to load the URLs immediately.
5677                 while (ultraPrivacy == null) {
5678                     // The wait must be synchronized, which only lets one thread run on it at a time, or `java.lang.IllegalMonitorStateException` is thrown.
5679                     synchronized (this) {
5680                         try {
5681                             // Check to see if the blocklists have been populated after 100 ms.
5682                             wait(100);
5683                         } catch (InterruptedException exception) {
5684                             // Do nothing.
5685                         }
5686                     }
5687                 }
5688
5689                 // Create an empty web resource response to be used if the resource request is blocked.
5690                 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
5691
5692                 // Reset the whitelist results tracker.
5693                 String[] whitelistResultStringArray = null;
5694
5695                 // Initialize the third party request tracker.
5696                 boolean isThirdPartyRequest = false;
5697
5698                 // Get the current URL.  `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
5699                 String currentBaseDomain = nestedScrollWebView.getCurrentDomainName();
5700
5701                 // Store a copy of the current domain for use in later requests.
5702                 String currentDomain = currentBaseDomain;
5703
5704                 // Get the request host name.
5705                 String requestBaseDomain = webResourceRequest.getUrl().getHost();
5706
5707                 // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
5708                 if (!currentBaseDomain.isEmpty() && (requestBaseDomain != null)) {
5709                     // Determine the current base domain.
5710                     while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) {  // There is at least one subdomain.
5711                         // Remove the first subdomain.
5712                         currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
5713                     }
5714
5715                     // Determine the request base domain.
5716                     while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) {  // There is at least one subdomain.
5717                         // Remove the first subdomain.
5718                         requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
5719                     }
5720
5721                     // Update the third party request tracker.
5722                     isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
5723                 }
5724
5725                 // Get the current WebView page position.
5726                 int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5727
5728                 // Determine if the WebView is currently displayed.
5729                 boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
5730
5731                 // Block third-party requests if enabled.
5732                 if (isThirdPartyRequest && nestedScrollWebView.getBlockAllThirdPartyRequests()) {
5733                     // Add the result to the resource requests.
5734                     nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_THIRD_PARTY, url});
5735
5736                     // Increment the blocked requests counters.
5737                     nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5738                     nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS);
5739
5740                     // Update the titles of the blocklist menu items if the WebView is currently displayed.
5741                     if (webViewDisplayed) {
5742                         // Updating the UI must be run from the UI thread.
5743                         activity.runOnUiThread(() -> {
5744                             // Update the menu item titles.
5745                             navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5746
5747                             // Update the options menu if it has been populated.
5748                             if (optionsMenu != null) {
5749                                 optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5750                                 optionsBlockAllThirdPartyRequestsMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " +
5751                                         getString(R.string.block_all_third_party_requests));
5752                             }
5753                         });
5754                     }
5755
5756                     // Return an empty web resource response.
5757                     return emptyWebResourceResponse;
5758                 }
5759
5760                 // Check UltraList if it is enabled.
5761                 if (nestedScrollWebView.getUltraListEnabled()) {
5762                     // Check the URL against UltraList.
5763                     String[] ultraListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraList);
5764
5765                     // Process the UltraList results.
5766                     if (ultraListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched UltraList's blacklist.
5767                         // Add the result to the resource requests.
5768                         nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5769
5770                         // Increment the blocked requests counters.
5771                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5772                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRALIST);
5773
5774                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5775                         if (webViewDisplayed) {
5776                             // Updating the UI must be run from the UI thread.
5777                             activity.runOnUiThread(() -> {
5778                                 // Update the menu item titles.
5779                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5780
5781                                 // Update the options menu if it has been populated.
5782                                 if (optionsMenu != null) {
5783                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5784                                     optionsUltraListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
5785                                 }
5786                             });
5787                         }
5788
5789                         // The resource request was blocked.  Return an empty web resource response.
5790                         return emptyWebResourceResponse;
5791                     } else if (ultraListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched UltraList's whitelist.
5792                         // Add a whitelist entry to the resource requests array.
5793                         nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5794
5795                         // The resource request has been allowed by UltraPrivacy.  `return null` loads the requested resource.
5796                         return null;
5797                     }
5798                 }
5799
5800                 // Check UltraPrivacy if it is enabled.
5801                 if (nestedScrollWebView.getUltraPrivacyEnabled()) {
5802                     // Check the URL against UltraPrivacy.
5803                     String[] ultraPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy);
5804
5805                     // Process the UltraPrivacy results.
5806                     if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched UltraPrivacy's blacklist.
5807                         // Add the result to the resource requests.
5808                         nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5809                                 ultraPrivacyResults[5]});
5810
5811                         // Increment the blocked requests counters.
5812                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5813                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRAPRIVACY);
5814
5815                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5816                         if (webViewDisplayed) {
5817                             // Updating the UI must be run from the UI thread.
5818                             activity.runOnUiThread(() -> {
5819                                 // Update the menu item titles.
5820                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5821
5822                                 // Update the options menu if it has been populated.
5823                                 if (optionsMenu != null) {
5824                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5825                                     optionsUltraPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
5826                                 }
5827                             });
5828                         }
5829
5830                         // The resource request was blocked.  Return an empty web resource response.
5831                         return emptyWebResourceResponse;
5832                     } else if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched UltraPrivacy's whitelist.
5833                         // Add a whitelist entry to the resource requests array.
5834                         nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5835                                 ultraPrivacyResults[5]});
5836
5837                         // The resource request has been allowed by UltraPrivacy.  `return null` loads the requested resource.
5838                         return null;
5839                     }
5840                 }
5841
5842                 // Check EasyList if it is enabled.
5843                 if (nestedScrollWebView.getEasyListEnabled()) {
5844                     // Check the URL against EasyList.
5845                     String[] easyListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList);
5846
5847                     // Process the EasyList results.
5848                     if (easyListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched EasyList's blacklist.
5849                         // Add the result to the resource requests.
5850                         nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]});
5851
5852                         // Increment the blocked requests counters.
5853                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5854                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYLIST);
5855
5856                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5857                         if (webViewDisplayed) {
5858                             // Updating the UI must be run from the UI thread.
5859                             activity.runOnUiThread(() -> {
5860                                 // Update the menu item titles.
5861                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5862
5863                                 // Update the options menu if it has been populated.
5864                                 if (optionsMenu != null) {
5865                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5866                                     optionsEasyListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
5867                                 }
5868                             });
5869                         }
5870
5871                         // The resource request was blocked.  Return an empty web resource response.
5872                         return emptyWebResourceResponse;
5873                     } else if (easyListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched EasyList's whitelist.
5874                         // Update the whitelist result string array tracker.
5875                         whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]};
5876                     }
5877                 }
5878
5879                 // Check EasyPrivacy if it is enabled.
5880                 if (nestedScrollWebView.getEasyPrivacyEnabled()) {
5881                     // Check the URL against EasyPrivacy.
5882                     String[] easyPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy);
5883
5884                     // Process the EasyPrivacy results.
5885                     if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched EasyPrivacy's blacklist.
5886                         // Add the result to the resource requests.
5887                         nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4],
5888                                 easyPrivacyResults[5]});
5889
5890                         // Increment the blocked requests counters.
5891                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5892                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYPRIVACY);
5893
5894                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5895                         if (webViewDisplayed) {
5896                             // Updating the UI must be run from the UI thread.
5897                             activity.runOnUiThread(() -> {
5898                                 // Update the menu item titles.
5899                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5900
5901                                 // Update the options menu if it has been populated.
5902                                 if (optionsMenu != null) {
5903                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5904                                     optionsEasyPrivacyMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
5905                                 }
5906                             });
5907                         }
5908
5909                         // The resource request was blocked.  Return an empty web resource response.
5910                         return emptyWebResourceResponse;
5911                     } else if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched EasyPrivacy's whitelist.
5912                         // Update the whitelist result string array tracker.
5913                         whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]};
5914                     }
5915                 }
5916
5917                 // Check Fanboy’s Annoyance List if it is enabled.
5918                 if (nestedScrollWebView.getFanboysAnnoyanceListEnabled()) {
5919                     // Check the URL against Fanboy's Annoyance List.
5920                     String[] fanboysAnnoyanceListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList);
5921
5922                     // Process the Fanboy's Annoyance List results.
5923                     if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched Fanboy's Annoyance List's blacklist.
5924                         // Add the result to the resource requests.
5925                         nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5926                                 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]});
5927
5928                         // Increment the blocked requests counters.
5929                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5930                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST);
5931
5932                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5933                         if (webViewDisplayed) {
5934                             // Updating the UI must be run from the UI thread.
5935                             activity.runOnUiThread(() -> {
5936                                 // Update the menu item titles.
5937                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5938
5939                                 // Update the options menu if it has been populated.
5940                                 if (optionsMenu != null) {
5941                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5942                                     optionsFanboysAnnoyanceListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " +
5943                                             getString(R.string.fanboys_annoyance_list));
5944                                 }
5945                             });
5946                         }
5947
5948                         // The resource request was blocked.  Return an empty web resource response.
5949                         return emptyWebResourceResponse;
5950                     } else if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)){  // The resource request matched Fanboy's Annoyance List's whitelist.
5951                         // Update the whitelist result string array tracker.
5952                         whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5953                                 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]};
5954                     }
5955                 } else if (nestedScrollWebView.getFanboysSocialBlockingListEnabled()) {  // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5956                     // Check the URL against Fanboy's Annoyance List.
5957                     String[] fanboysSocialListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList);
5958
5959                     // Process the Fanboy's Social Blocking List results.
5960                     if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched Fanboy's Social Blocking List's blacklist.
5961                         // Add the result to the resource requests.
5962                         nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5963                                 fanboysSocialListResults[4], fanboysSocialListResults[5]});
5964
5965                         // Increment the blocked requests counters.
5966                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5967                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST);
5968
5969                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5970                         if (webViewDisplayed) {
5971                             // Updating the UI must be run from the UI thread.
5972                             activity.runOnUiThread(() -> {
5973                                 // Update the menu item titles.
5974                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5975
5976                                 // Update the options menu if it has been populated.
5977                                 if (optionsMenu != null) {
5978                                     optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5979                                     optionsFanboysSocialBlockingListMenuItem.setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " +
5980                                             getString(R.string.fanboys_social_blocking_list));
5981                                 }
5982                             });
5983                         }
5984
5985                         // The resource request was blocked.  Return an empty web resource response.
5986                         return emptyWebResourceResponse;
5987                     } else if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched Fanboy's Social Blocking List's whitelist.
5988                         // Update the whitelist result string array tracker.
5989                         whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5990                                 fanboysSocialListResults[4], fanboysSocialListResults[5]};
5991                     }
5992                 }
5993
5994                 // Add the request to the log because it hasn't been processed by any of the previous checks.
5995                 if (whitelistResultStringArray != null) {  // The request was processed by a whitelist.
5996                     nestedScrollWebView.addResourceRequest(whitelistResultStringArray);
5997                 } else {  // The request didn't match any blocklist entry.  Log it as a default request.
5998                     nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_DEFAULT, url});
5999                 }
6000
6001                 // The resource request has not been blocked.  `return null` loads the requested resource.
6002                 return null;
6003             }
6004
6005             // Handle HTTP authentication requests.
6006             @Override
6007             public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
6008                 // Store the handler.
6009                 nestedScrollWebView.setHttpAuthHandler(handler);
6010
6011                 // Instantiate an HTTP authentication dialog.
6012                 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.getWebViewFragmentId());
6013
6014                 // Show the HTTP authentication dialog.
6015                 httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication));
6016             }
6017
6018             @Override
6019             public void onPageStarted(WebView view, String url, Bitmap favicon) {
6020                 // Get the app bar layout height.  This can't be done in `applyAppSettings()` because the app bar is not yet populated there.
6021                 // This should only be populated if it is greater than 0 because otherwise it will be reset to 0 if the app bar is hidden in full screen browsing mode.
6022                 if (appBarLayout.getHeight() > 0) appBarHeight = appBarLayout.getHeight();
6023
6024                 // Set the padding and layout settings according to the position of the app bar.
6025                 if (bottomAppBar) {  // The app bar is on the bottom.
6026                     // Adjust the UI.
6027                     if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) {  // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
6028                         // Reset the WebView padding to fill the available space.
6029                         swipeRefreshLayout.setPadding(0, 0, 0, 0);
6030                     } else {  // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
6031                         // Move the WebView above the app bar layout.
6032                         swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight);
6033                     }
6034                 } else {  // The app bar is on the top.
6035                     // Set the top padding of the swipe refresh layout according to the app bar scrolling preference.  This can't be done in `appAppSettings()` because the app bar is not yet populated there.
6036                     if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) {
6037                         // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior.
6038                         swipeRefreshLayout.setPadding(0, 0, 0, 0);
6039
6040                         // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
6041                         swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset);
6042                     } else {
6043                         // The swipe refresh layout must be manually moved below the app bar layout.
6044                         swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0);
6045
6046                         // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
6047                         swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight);
6048                     }
6049                 }
6050
6051                 // Reset the list of resource requests.
6052                 nestedScrollWebView.clearResourceRequests();
6053
6054                 // Reset the requests counters.
6055                 nestedScrollWebView.resetRequestsCounters();
6056
6057                 // Get the current page position.
6058                 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
6059
6060                 // Update the URL text bar if the page is currently selected and the URL edit text is not currently being edited.
6061                 if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus()) {
6062                     // Display the formatted URL text.
6063                     urlEditText.setText(url);
6064
6065                     // Apply text highlighting to the URL text box.
6066                     highlightUrlText();
6067
6068                     // Hide the keyboard.
6069                     inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
6070                 }
6071
6072                 // Reset the list of host IP addresses.
6073                 nestedScrollWebView.setCurrentIpAddresses("");
6074
6075                 // Get a URI for the current URL.
6076                 Uri currentUri = Uri.parse(url);
6077
6078                 // Get the IP addresses for the host.
6079                 new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
6080
6081                 // Replace Refresh with Stop if the options menu has been created.  (The first WebView typically begins loading before the menu items are instantiated.)
6082                 if (optionsMenu != null) {
6083                     // Set the title.
6084                     optionsRefreshMenuItem.setTitle(R.string.stop);
6085
6086                     // Set the icon if it is displayed in the AppBar.
6087                     if (displayAdditionalAppBarIcons)
6088                         optionsRefreshMenuItem.setIcon(R.drawable.close_blue);
6089                 }
6090             }
6091
6092             @Override
6093             public void onPageFinished(WebView view, String url) {
6094                 // Flush any cookies to persistent storage.  The cookie manager has become very lazy about flushing cookies in recent versions.
6095                 if (nestedScrollWebView.getAcceptCookies()) {
6096                     CookieManager.getInstance().flush();
6097                 }
6098
6099                 // Update the Refresh menu item if the options menu has been created.
6100                 if (optionsMenu != null) {
6101                     // Reset the Refresh title.
6102                     optionsRefreshMenuItem.setTitle(R.string.refresh);
6103
6104                     // Reset the icon if it is displayed in the app bar.
6105                     if (displayAdditionalAppBarIcons)
6106                         optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled);
6107                 }
6108
6109                  // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
6110                 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
6111                 String privateDataDirectoryString = getApplicationInfo().dataDir;
6112
6113                 // Clear the cache, history, and logcat if Incognito Mode is enabled.
6114                 if (incognitoModeEnabled) {
6115                     // Clear the cache.  `true` includes disk files.
6116                     nestedScrollWebView.clearCache(true);
6117
6118                     // Clear the back/forward history.
6119                     nestedScrollWebView.clearHistory();
6120
6121                     // Manually delete cache folders.
6122                     try {
6123                         // Delete the main cache directory.
6124                         Runtime.getRuntime().exec("rm -rf " + privateDataDirectoryString + "/cache");
6125                     } catch (IOException exception) {
6126                         // Do nothing if an error is thrown.
6127                     }
6128
6129                     // Clear the logcat.
6130                     try {
6131                         // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
6132                         Runtime.getRuntime().exec("logcat -b all -c");
6133                     } catch (IOException exception) {
6134                         // Do nothing.
6135                     }
6136                 }
6137
6138                 // Clear the `Service Worker` directory.
6139                 try {
6140                     // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
6141                     Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Default/Service Worker/"});
6142                 } catch (IOException exception) {
6143                     // Do nothing.
6144                 }
6145
6146                 // Get the current page position.
6147                 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
6148
6149                 // Get the current URL from the nested scroll WebView.  This is more accurate than using the URL passed into the method, which is sometimes not the final one.
6150                 String currentUrl = nestedScrollWebView.getUrl();
6151
6152                 // Get the current tab.
6153                 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
6154
6155                 // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text.
6156                 // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point.
6157                 // Probably some sort of race condition when Privacy Browser is being resumed.
6158                 if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) {
6159                     // Check to see if the URL is `about:blank`.
6160                     if (currentUrl.equals("about:blank")) {  // The WebView is blank.
6161                         // Display the hint in the URL edit text.
6162                         urlEditText.setText("");
6163
6164                         // Request focus for the URL text box.
6165                         urlEditText.requestFocus();
6166
6167                         // Display the keyboard.
6168                         inputMethodManager.showSoftInput(urlEditText, 0);
6169
6170                         // Apply the domain settings.  This clears any settings from the previous domain.
6171                         applyDomainSettings(nestedScrollWebView, "", true, false, false);
6172
6173                         // Only populate the title text view if the tab has been fully created.
6174                         if (tab != null) {
6175                             // Get the custom view from the tab.
6176                             View tabView = tab.getCustomView();
6177
6178                             // Remove the incorrect warning below that the current tab view might be null.
6179                             assert tabView != null;
6180
6181                             // Get the title text view from the tab.
6182                             TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
6183
6184                             // Set the title as the tab text.
6185                             tabTitleTextView.setText(R.string.new_tab);
6186                         }
6187                     } else {  // The WebView has loaded a webpage.
6188                         // Update the URL edit text if it is not currently being edited.
6189                         if (!urlEditText.hasFocus()) {
6190                             // Sanitize the current URL.  This removes unwanted URL elements that were added by redirects, so that they won't be included if the URL is shared.
6191                             String sanitizedUrl = sanitizeUrl(currentUrl);
6192
6193                             // Display the final URL.  Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
6194                             urlEditText.setText(sanitizedUrl);
6195
6196                             // Apply text highlighting to the URL.
6197                             highlightUrlText();
6198                         }
6199
6200                         // Only populate the title text view if the tab has been fully created.
6201                         if (tab != null) {
6202                             // Get the custom view from the tab.
6203                             View tabView = tab.getCustomView();
6204
6205                             // Remove the incorrect warning below that the current tab view might be null.
6206                             assert tabView != null;
6207
6208                             // Get the title text view from the tab.
6209                             TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
6210
6211                             // Set the title as the tab text.  Sometimes `onReceivedTitle()` is not called, especially when navigating history.
6212                             tabTitleTextView.setText(nestedScrollWebView.getTitle());
6213                         }
6214                     }
6215                 }
6216             }
6217
6218             // Handle SSL Certificate errors.  Suppress the lint warning that ignoring the error might be dangerous.
6219             @SuppressLint("WebViewClientOnReceivedSslError")
6220             @Override
6221             public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
6222                 // Get the current website SSL certificate.
6223                 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
6224
6225                 // Extract the individual pieces of information from the current website SSL certificate.
6226                 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
6227                 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
6228                 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
6229                 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
6230                 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
6231                 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
6232                 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
6233                 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
6234
6235                 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
6236                 if (nestedScrollWebView.hasPinnedSslCertificate()) {
6237                     // Get the pinned SSL certificate.
6238                     Pair<String[], Date[]> pinnedSslCertificatePair = nestedScrollWebView.getPinnedSslCertificate();
6239
6240                     // Extract the arrays from the array list.
6241                     String[] pinnedSslCertificateStringArray = pinnedSslCertificatePair.getFirst();
6242                     Date[] pinnedSslCertificateDateArray = pinnedSslCertificatePair.getSecond();
6243
6244                     // Check if the current SSL certificate matches the pinned certificate.
6245                     if (currentWebsiteIssuedToCName.equals(pinnedSslCertificateStringArray[0]) && currentWebsiteIssuedToOName.equals(pinnedSslCertificateStringArray[1]) &&
6246                         currentWebsiteIssuedToUName.equals(pinnedSslCertificateStringArray[2]) && currentWebsiteIssuedByCName.equals(pinnedSslCertificateStringArray[3]) &&
6247                         currentWebsiteIssuedByOName.equals(pinnedSslCertificateStringArray[4]) && currentWebsiteIssuedByUName.equals(pinnedSslCertificateStringArray[5]) &&
6248                         currentWebsiteSslStartDate.equals(pinnedSslCertificateDateArray[0]) && currentWebsiteSslEndDate.equals(pinnedSslCertificateDateArray[1])) {
6249
6250                         // An SSL certificate is pinned and matches the current domain certificate.  Proceed to the website without displaying an error.
6251                         handler.proceed();
6252                     }
6253                 } else {  // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
6254                     // Store the SSL error handler.
6255                     nestedScrollWebView.setSslErrorHandler(handler);
6256
6257                     // Instantiate an SSL certificate error alert dialog.
6258                     DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.getWebViewFragmentId());
6259
6260                     // Try to show the dialog.  The SSL error handler continues to function even when the WebView is paused.  Attempting to display a dialog in that state leads to a crash.
6261                     try {
6262                         // Show the SSL certificate error dialog.
6263                         sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
6264                     } catch (Exception exception) {
6265                         // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
6266                         pendingDialogsArrayList.add(new PendingDialog(sslCertificateErrorDialogFragment, getString(R.string.ssl_certificate_error)));
6267                     }
6268                 }
6269             }
6270         });
6271
6272         // Check to see if the state is being restored.
6273         if (restoringState) {  // The state is being restored.
6274             // Resume the nested scroll WebView JavaScript timers.
6275             nestedScrollWebView.resumeTimers();
6276         } else if (pageNumber == 0) {  // The first page is being loaded.
6277             // Set this nested scroll WebView as the current WebView.
6278             currentWebView = nestedScrollWebView;
6279
6280             // Initialize the URL to load string.
6281             String urlToLoadString;
6282
6283             // Get the intent that started the app.
6284             Intent launchingIntent = getIntent();
6285
6286             // Reset the intent.  This prevents a duplicate tab from being created on restart.
6287             setIntent(new Intent());
6288
6289             // Get the information from the intent.
6290             String launchingIntentAction = launchingIntent.getAction();
6291             Uri launchingIntentUriData = launchingIntent.getData();
6292             String launchingIntentStringExtra = launchingIntent.getStringExtra(Intent.EXTRA_TEXT);
6293
6294             // Parse the launching intent URL.
6295             if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {  // The intent contains a search string.
6296                 // Create an encoded URL string.
6297                 String encodedUrlString;
6298
6299                 // Sanitize the search input and convert it to a search.
6300                 try {
6301                     encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
6302                 } catch (UnsupportedEncodingException exception) {
6303                     encodedUrlString = "";
6304                 }
6305
6306                 // Store the web search as the URL to load.
6307                 urlToLoadString = searchURL + encodedUrlString;
6308             } else if (launchingIntentUriData != null) {  // The launching intent contains a URL formatted as a URI.
6309                 // Store the URI as a URL.
6310                 urlToLoadString = launchingIntentUriData.toString();
6311             } else if (launchingIntentStringExtra != null) {  // The launching intent contains text that might be a URL.
6312                 // Store the URL.
6313                 urlToLoadString = launchingIntentStringExtra;
6314             } else if (!url.equals("")) {  // The activity has been restarted.
6315                 // Load the saved URL.
6316                 urlToLoadString = url;
6317             } else {  // The is no URL in the intent.
6318                 // Store the homepage to be loaded.
6319                 urlToLoadString = sharedPreferences.getString("homepage", getString(R.string.homepage_default_value));
6320             }
6321
6322             // Load the website if not waiting for the proxy.
6323             if (waitingForProxy) {  // Store the URL to be loaded in the Nested Scroll WebView.
6324                 nestedScrollWebView.setWaitingForProxyUrlString(urlToLoadString);
6325             } else {  // Load the URL.
6326                 loadUrl(nestedScrollWebView, urlToLoadString);
6327             }
6328
6329             // Reset the intent.  This prevents a duplicate tab from being created on a subsequent restart if loading an link from a new intent on restart.
6330             // For example, this prevents a duplicate tab if a link is loaded from the Guide after changing the theme in the guide and then changing the theme again in the main activity.
6331             setIntent(new Intent());
6332         } else {  // This is not the first tab.
6333             // Load the URL.
6334             loadUrl(nestedScrollWebView, url);
6335
6336             // Set the focus and display the keyboard if the URL is blank.
6337             if (url.equals("")) {
6338                 // Request focus for the URL text box.
6339                 urlEditText.requestFocus();
6340
6341                 // Create a display keyboard handler.
6342                 Handler displayKeyboardHandler = new Handler();
6343
6344                 // Create a display keyboard runnable.
6345                 Runnable displayKeyboardRunnable = () -> {
6346                     // Display the keyboard.
6347                     inputMethodManager.showSoftInput(urlEditText, 0);
6348                 };
6349
6350                 // Display the keyboard after 100 milliseconds, which leaves enough time for the tab to transition.
6351                 displayKeyboardHandler.postDelayed(displayKeyboardRunnable, 100);
6352             }
6353         }
6354     }
6355 }