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