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