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