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