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