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