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