]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.java
Never block the resource request for the main URL. https://redmine.stoutner.com...
[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         // Store the current URL.
3595         nestedScrollWebView.setCurrentUrl(url);
3596
3597         // Parse the URL into a URI.
3598         Uri uri = Uri.parse(url);
3599
3600         // Extract the domain from `uri`.
3601         String newHostName = uri.getHost();
3602
3603         // Strings don't like to be null.
3604         if (newHostName == null) {
3605             newHostName = "";
3606         }
3607
3608         // 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.
3609         if (!nestedScrollWebView.getCurrentDomainName().equals(newHostName) || newHostName.equals("")) {
3610             // Set the new host name as the current domain name.
3611             nestedScrollWebView.setCurrentDomainName(newHostName);
3612
3613             // Reset the ignoring of pinned domain information.
3614             nestedScrollWebView.setIgnorePinnedDomainInformation(false);
3615
3616             // Clear any pinned SSL certificate or IP addresses.
3617             nestedScrollWebView.clearPinnedSslCertificate();
3618             nestedScrollWebView.clearPinnedIpAddresses();
3619
3620             // Reset the favorite icon if specified.
3621             if (resetTab) {
3622                 // Initialize the favorite icon.
3623                 nestedScrollWebView.initializeFavoriteIcon();
3624
3625                 // Get the current page position.
3626                 int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
3627
3628                 // Get a handle for the tab layout.
3629                 TabLayout tabLayout = findViewById(R.id.tablayout);
3630
3631                 // Get the corresponding tab.
3632                 TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
3633
3634                 // Update the tab if it isn't null, which sometimes happens when restarting from the background.
3635                 if (tab != null) {
3636                     // Get the tab custom view.
3637                     View tabCustomView = tab.getCustomView();
3638
3639                     // Remove the warning below that the tab custom view might be null.
3640                     assert tabCustomView != null;
3641
3642                     // Get the tab views.
3643                     ImageView tabFavoriteIconImageView = tabCustomView.findViewById(R.id.favorite_icon_imageview);
3644                     TextView tabTitleTextView = tabCustomView.findViewById(R.id.title_textview);
3645
3646                     // Set the default favorite icon as the favorite icon for this tab.
3647                     tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteOrDefaultIcon(), 64, 64, true));
3648
3649                     // Set the loading title text.
3650                     tabTitleTextView.setText(R.string.loading);
3651                 }
3652             }
3653
3654             // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
3655             DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
3656
3657             // Get a full cursor from `domainsDatabaseHelper`.
3658             Cursor domainNameCursor = domainsDatabaseHelper.getDomainNameCursorOrderedByDomain();
3659
3660             // Initialize `domainSettingsSet`.
3661             Set<String> domainSettingsSet = new HashSet<>();
3662
3663             // Get the domain name column index.
3664             int domainNameColumnIndex = domainNameCursor.getColumnIndex(DomainsDatabaseHelper.DOMAIN_NAME);
3665
3666             // Populate `domainSettingsSet`.
3667             for (int i = 0; i < domainNameCursor.getCount(); i++) {
3668                 // Move `domainsCursor` to the current row.
3669                 domainNameCursor.moveToPosition(i);
3670
3671                 // Store the domain name in `domainSettingsSet`.
3672                 domainSettingsSet.add(domainNameCursor.getString(domainNameColumnIndex));
3673             }
3674
3675             // Close `domainNameCursor.
3676             domainNameCursor.close();
3677
3678             // Initialize the domain name in database variable.
3679             String domainNameInDatabase = null;
3680
3681             // Check the hostname against the domain settings set.
3682             if (domainSettingsSet.contains(newHostName)) {  // The hostname is contained in the domain settings set.
3683                 // Record the domain name in the database.
3684                 domainNameInDatabase = newHostName;
3685
3686                 // Set the domain settings applied tracker to true.
3687                 nestedScrollWebView.setDomainSettingsApplied(true);
3688             } else {  // The hostname is not contained in the domain settings set.
3689                 // Set the domain settings applied tracker to false.
3690                 nestedScrollWebView.setDomainSettingsApplied(false);
3691             }
3692
3693             // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3694             while (!nestedScrollWebView.getDomainSettingsApplied() && newHostName.contains(".")) {  // Stop checking if domain settings are already applied or there are no more `.` in the host name.
3695                 if (domainSettingsSet.contains("*." + newHostName)) {  // Check the host name prepended by `*.`.
3696                     // Set the domain settings applied tracker to true.
3697                     nestedScrollWebView.setDomainSettingsApplied(true);
3698
3699                     // Store the applied domain names as it appears in the database.
3700                     domainNameInDatabase = "*." + newHostName;
3701                 }
3702
3703                 // Strip out the lowest subdomain of of the host name.
3704                 newHostName = newHostName.substring(newHostName.indexOf(".") + 1);
3705             }
3706
3707
3708             // Get a handle for the shared preferences.
3709             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
3710
3711             // Store the general preference information.
3712             String defaultFontSizeString = sharedPreferences.getString("font_size", getString(R.string.font_size_default_value));
3713             String defaultUserAgentName = sharedPreferences.getString("user_agent", getString(R.string.user_agent_default_value));
3714             boolean defaultSwipeToRefresh = sharedPreferences.getBoolean("swipe_to_refresh", true);
3715             boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
3716             boolean wideViewport = sharedPreferences.getBoolean("wide_viewport", true);
3717             boolean displayWebpageImages = sharedPreferences.getBoolean("display_webpage_images", true);
3718
3719             // Get a handle for the cookie manager.
3720             CookieManager cookieManager = CookieManager.getInstance();
3721
3722             // Get handles for the views.
3723             RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
3724             SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
3725
3726             // Initialize the user agent array adapter and string array.
3727             ArrayAdapter<CharSequence> userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item);
3728             String[] userAgentDataArray = getResources().getStringArray(R.array.user_agent_data);
3729
3730             if (nestedScrollWebView.getDomainSettingsApplied()) {  // The url has custom domain settings.
3731                 // Get a cursor for the current host and move it to the first position.
3732                 Cursor currentDomainSettingsCursor = domainsDatabaseHelper.getCursorForDomainName(domainNameInDatabase);
3733                 currentDomainSettingsCursor.moveToFirst();
3734
3735                 // Get the settings from the cursor.
3736                 nestedScrollWebView.setDomainSettingsDatabaseId(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper._ID)));
3737                 nestedScrollWebView.setDomainSettingsJavaScriptEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1);
3738                 nestedScrollWebView.setAcceptFirstPartyCookies(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FIRST_PARTY_COOKIES)) == 1);
3739                 boolean domainThirdPartyCookiesEnabled = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_THIRD_PARTY_COOKIES)) == 1);
3740                 nestedScrollWebView.getSettings().setDomStorageEnabled(currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1);
3741                 // Form data can be removed once the minimum API >= 26.
3742                 boolean saveFormData = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1);
3743                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST,
3744                         currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1);
3745                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY,
3746                         currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1);
3747                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST,
3748                         currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1);
3749                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST,
3750                         currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1);
3751                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ULTRALIST)) == 1);
3752                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY,
3753                         currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1);
3754                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS,
3755                         currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1);
3756                 String userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.USER_AGENT));
3757                 int fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.FONT_SIZE));
3758                 int swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SWIPE_TO_REFRESH));
3759                 int nightModeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.NIGHT_MODE));
3760                 int wideViewportInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.WIDE_VIEWPORT));
3761                 int displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.DISPLAY_IMAGES));
3762                 boolean pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1);
3763                 String pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME));
3764                 String pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION));
3765                 String pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT));
3766                 String pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME));
3767                 String pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION));
3768                 String pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT));
3769                 boolean pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1);
3770                 String pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.IP_ADDRESSES));
3771
3772                 // Create the pinned SSL date variables.
3773                 Date pinnedSslStartDate;
3774                 Date pinnedSslEndDate;
3775
3776                 // 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.
3777                 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)) == 0) {
3778                     pinnedSslStartDate = null;
3779                 } else {
3780                     pinnedSslStartDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_START_DATE)));
3781                 }
3782
3783                 // 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.
3784                 if (currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)) == 0) {
3785                     pinnedSslEndDate = null;
3786                 } else {
3787                     pinnedSslEndDate = new Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndex(DomainsDatabaseHelper.SSL_END_DATE)));
3788                 }
3789
3790                 // If there is a pinned SSL certificate, store it in the WebView.
3791                 if (pinnedSslCertificate) {
3792                     nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3793                             pinnedSslStartDate, pinnedSslEndDate);
3794                 }
3795
3796                 // If there is a pinned IP address, store it in the WebView.
3797                 if (pinnedIpAddresses) {
3798                     nestedScrollWebView.setPinnedIpAddresses(pinnedHostIpAddresses);
3799                 }
3800
3801                 // Set night mode according to the night mode int.
3802                 switch (nightModeInt) {
3803                     case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3804                         // Set night mode according to the current default.
3805                         nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3806                         break;
3807
3808                     case DomainsDatabaseHelper.ENABLED:
3809                         // Enable night mode.
3810                         nestedScrollWebView.setNightMode(true);
3811                         break;
3812
3813                     case DomainsDatabaseHelper.DISABLED:
3814                         // Disable night mode.
3815                         nestedScrollWebView.setNightMode(false);
3816                         break;
3817                 }
3818
3819                 // Enable JavaScript if night mode is enabled.
3820                 if (nestedScrollWebView.getNightMode()) {
3821                     // Enable JavaScript.
3822                     nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3823                 } else {
3824                     // Set JavaScript according to the domain settings.
3825                     nestedScrollWebView.getSettings().setJavaScriptEnabled(nestedScrollWebView.getDomainSettingsJavaScriptEnabled());
3826                 }
3827
3828                 // Close the current host domain settings cursor.
3829                 currentDomainSettingsCursor.close();
3830
3831                 // Apply the domain settings.
3832                 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
3833
3834                 // Set third-party cookies status if API >= 21.
3835                 if (Build.VERSION.SDK_INT >= 21) {
3836                     cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, domainThirdPartyCookiesEnabled);
3837                 }
3838
3839                 // Apply the form data setting if the API < 26.
3840                 if (Build.VERSION.SDK_INT < 26) {
3841                     nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3842                 }
3843
3844                 // Apply the font size.
3845                 if (fontSize == 0) {  // Apply the default font size.
3846                     nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3847                 } else {  // Apply the specified font size.
3848                     nestedScrollWebView.getSettings().setTextZoom(fontSize);
3849                 }
3850
3851                 // Set the user agent.
3852                 if (userAgentName.equals(getString(R.string.system_default_user_agent))) {  // Use the system default user agent.
3853                     // Get the array position of the default user agent name.
3854                     int defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
3855
3856                     // Set the user agent according to the system default.
3857                     switch (defaultUserAgentArrayPosition) {
3858                         case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
3859                             // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3860                             nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
3861                             break;
3862
3863                         case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3864                             // Set the user agent to `""`, which uses the default value.
3865                             nestedScrollWebView.getSettings().setUserAgentString("");
3866                             break;
3867
3868                         case SETTINGS_CUSTOM_USER_AGENT:
3869                             // Set the default custom user agent.
3870                             nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
3871                             break;
3872
3873                         default:
3874                             // Get the user agent string from the user agent data array
3875                             nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[defaultUserAgentArrayPosition]);
3876                     }
3877                 } else {  // Set the user agent according to the stored name.
3878                     // Get the array position of the user agent name.
3879                     int userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName);
3880
3881                     switch (userAgentArrayPosition) {
3882                         case UNRECOGNIZED_USER_AGENT:  // The user agent name contains a custom user agent.
3883                             nestedScrollWebView.getSettings().setUserAgentString(userAgentName);
3884                             break;
3885
3886                         case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
3887                             // Set the user agent to `""`, which uses the default value.
3888                             nestedScrollWebView.getSettings().setUserAgentString("");
3889                             break;
3890
3891                         default:
3892                             // Get the user agent string from the user agent data array.
3893                             nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
3894                     }
3895                 }
3896
3897                 // Set swipe to refresh.
3898                 switch (swipeToRefreshInt) {
3899                     case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3900                         // Store the swipe to refresh status in the nested scroll WebView.
3901                         nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3902
3903                         // 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.
3904                         swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3905                         break;
3906
3907                     case DomainsDatabaseHelper.ENABLED:
3908                         // Store the swipe to refresh status in the nested scroll WebView.
3909                         nestedScrollWebView.setSwipeToRefresh(true);
3910
3911                         // Enable swipe to refresh.  This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3912                         swipeRefreshLayout.setEnabled(true);
3913                         break;
3914
3915                     case DomainsDatabaseHelper.DISABLED:
3916                         // Store the swipe to refresh status in the nested scroll WebView.
3917                         nestedScrollWebView.setSwipeToRefresh(false);
3918
3919                         // Disable swipe to refresh.  This can be removed once the minimum API >= 23 because it is continuously set by an on scroll change listener.
3920                         swipeRefreshLayout.setEnabled(false);
3921                 }
3922
3923                 // Set the viewport.
3924                 switch (wideViewportInt) {
3925                     case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3926                         nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
3927                         break;
3928
3929                     case DomainsDatabaseHelper.ENABLED:
3930                         nestedScrollWebView.getSettings().setUseWideViewPort(true);
3931                         break;
3932
3933                     case DomainsDatabaseHelper.DISABLED:
3934                         nestedScrollWebView.getSettings().setUseWideViewPort(false);
3935                         break;
3936                 }
3937
3938                 // Set the loading of webpage images.
3939                 switch (displayWebpageImagesInt) {
3940                     case DomainsDatabaseHelper.SYSTEM_DEFAULT:
3941                         nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
3942                         break;
3943
3944                     case DomainsDatabaseHelper.ENABLED:
3945                         nestedScrollWebView.getSettings().setLoadsImagesAutomatically(true);
3946                         break;
3947
3948                     case DomainsDatabaseHelper.DISABLED:
3949                         nestedScrollWebView.getSettings().setLoadsImagesAutomatically(false);
3950                         break;
3951                 }
3952
3953                 // 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.
3954                 if (darkTheme) {
3955                     urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
3956                 } else {
3957                     urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
3958                 }
3959             } else {  // The new URL does not have custom domain settings.  Load the defaults.
3960                 // Store the values from the shared preferences.
3961                 boolean defaultJavaScriptEnabled = sharedPreferences.getBoolean("javascript", false);
3962                 nestedScrollWebView.setAcceptFirstPartyCookies(sharedPreferences.getBoolean("first_party_cookies", false));
3963                 boolean defaultThirdPartyCookiesEnabled = sharedPreferences.getBoolean("third_party_cookies", false);
3964                 nestedScrollWebView.getSettings().setDomStorageEnabled(sharedPreferences.getBoolean("dom_storage", false));
3965                 boolean saveFormData = sharedPreferences.getBoolean("save_form_data", false);  // Form data can be removed once the minimum API >= 26.
3966                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYLIST, sharedPreferences.getBoolean("easylist", true));
3967                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.EASYPRIVACY, sharedPreferences.getBoolean("easyprivacy", true));
3968                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, sharedPreferences.getBoolean("fanboys_annoyance_list", true));
3969                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, sharedPreferences.getBoolean("fanboys_social_blocking_list", true));
3970                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRALIST, sharedPreferences.getBoolean("ultralist", true));
3971                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.ULTRAPRIVACY, sharedPreferences.getBoolean("ultraprivacy", true));
3972                 nestedScrollWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, sharedPreferences.getBoolean("block_all_third_party_requests", false));
3973                 nestedScrollWebView.setNightMode(sharedPreferences.getBoolean("night_mode", false));
3974
3975                 // Enable JavaScript if night mode is enabled.
3976                 if (nestedScrollWebView.getNightMode()) {
3977                     // Enable JavaScript.
3978                     nestedScrollWebView.getSettings().setJavaScriptEnabled(true);
3979                 } else {
3980                     // Set JavaScript according to the domain settings.
3981                     nestedScrollWebView.getSettings().setJavaScriptEnabled(defaultJavaScriptEnabled);
3982                 }
3983
3984                 // Apply the default settings.
3985                 cookieManager.setAcceptCookie(nestedScrollWebView.getAcceptFirstPartyCookies());
3986                 nestedScrollWebView.getSettings().setTextZoom(Integer.valueOf(defaultFontSizeString));
3987
3988                 // Apply the form data setting if the API < 26.
3989                 if (Build.VERSION.SDK_INT < 26) {
3990                     nestedScrollWebView.getSettings().setSaveFormData(saveFormData);
3991                 }
3992
3993                 // Store the swipe to refresh status in the nested scroll WebView.
3994                 nestedScrollWebView.setSwipeToRefresh(defaultSwipeToRefresh);
3995
3996                 // Apply swipe to refresh according to the default.
3997                 swipeRefreshLayout.setEnabled(defaultSwipeToRefresh);
3998
3999                 // Reset the pinned variables.
4000                 nestedScrollWebView.setDomainSettingsDatabaseId(-1);
4001
4002                 // Set third-party cookies status if API >= 21.
4003                 if (Build.VERSION.SDK_INT >= 21) {
4004                     cookieManager.setAcceptThirdPartyCookies(nestedScrollWebView, defaultThirdPartyCookiesEnabled);
4005                 }
4006
4007                 // Get the array position of the user agent name.
4008                 int userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName);
4009
4010                 // Set the user agent.
4011                 switch (userAgentArrayPosition) {
4012                     case UNRECOGNIZED_USER_AGENT:  // The default user agent name is not on the canonical list.
4013                         // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
4014                         nestedScrollWebView.getSettings().setUserAgentString(defaultUserAgentName);
4015                         break;
4016
4017                     case SETTINGS_WEBVIEW_DEFAULT_USER_AGENT:
4018                         // Set the user agent to `""`, which uses the default value.
4019                         nestedScrollWebView.getSettings().setUserAgentString("");
4020                         break;
4021
4022                     case SETTINGS_CUSTOM_USER_AGENT:
4023                         // Set the default custom user agent.
4024                         nestedScrollWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
4025                         break;
4026
4027                     default:
4028                         // Get the user agent string from the user agent data array
4029                         nestedScrollWebView.getSettings().setUserAgentString(userAgentDataArray[userAgentArrayPosition]);
4030                 }
4031
4032                 // Set the viewport.
4033                 nestedScrollWebView.getSettings().setUseWideViewPort(wideViewport);
4034
4035                 // Set the loading of webpage images.
4036                 nestedScrollWebView.getSettings().setLoadsImagesAutomatically(displayWebpageImages);
4037
4038                 // Set a transparent background on URL edit text.  The deprecated `getResources().getDrawable()` must be used until the minimum API >= 21.
4039                 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
4040             }
4041
4042             // Close the domains database helper.
4043             domainsDatabaseHelper.close();
4044
4045             // Update the privacy icons.
4046             updatePrivacyIcons(true);
4047         }
4048
4049         // Reload the website if returning from the Domains activity.
4050         if (reloadWebsite) {
4051             nestedScrollWebView.reload();
4052         }
4053
4054         // Return the user agent changed status.
4055         return !nestedScrollWebView.getSettings().getUserAgentString().equals(initialUserAgent);
4056     }
4057
4058     private void applyProxyThroughOrbot(boolean reloadWebsite) {
4059         // Get a handle for the shared preferences.
4060         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4061
4062         // Get the search and theme preferences.
4063         String torSearchString = sharedPreferences.getString("tor_search", getString(R.string.tor_search_default_value));
4064         String torSearchCustomUrlString = sharedPreferences.getString("tor_search_custom_url", getString(R.string.tor_search_custom_url_default_value));
4065         String searchString = sharedPreferences.getString("search", getString(R.string.search_default_value));
4066         String searchCustomUrlString = sharedPreferences.getString("search_custom_url", getString(R.string.search_custom_url_default_value));
4067         boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4068
4069         // Get a handle for the app bar layout.
4070         AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
4071
4072         // Set the homepage, search, and proxy options.
4073         if (proxyThroughOrbot) {  // Set the Tor options.
4074             // Set the search URL.
4075             if (torSearchString.equals("Custom URL")) {  // Get the custom URL string.
4076                 searchURL = torSearchCustomUrlString;
4077             } else {  // Use the string from the pre-built list.
4078                 searchURL = torSearchString;
4079             }
4080
4081             // Set the proxy.  `this` refers to the current activity where an `AlertDialog` might be displayed.
4082             OrbotProxyHelper.setProxy(getApplicationContext(), this, "localhost", "8118");
4083
4084             // Set the app bar background to indicate proxying through Orbot is enabled.
4085             if (darkTheme) {
4086                 appBarLayout.setBackgroundResource(R.color.dark_blue_30);
4087             } else {
4088                 appBarLayout.setBackgroundResource(R.color.blue_50);
4089             }
4090
4091             // Check to see if Orbot is ready.
4092             if (!orbotStatus.equals("ON")) {  // Orbot is not ready.
4093                 // Set `waitingForOrbot`.
4094                 waitingForOrbot = true;
4095
4096                 // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
4097                 currentWebView.getSettings().setUseWideViewPort(false);
4098
4099                 // Load a waiting page.  `null` specifies no encoding, which defaults to ASCII.
4100                 currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
4101             } else if (reloadWebsite) {  // Orbot is ready and the website should be reloaded.
4102                 // Reload the website.
4103                 currentWebView.reload();
4104             }
4105         } else {  // Set the non-Tor options.
4106             // Set the search URL.
4107             if (searchString.equals("Custom URL")) {  // Get the custom URL string.
4108                 searchURL = searchCustomUrlString;
4109             } else {  // Use the string from the pre-built list.
4110                 searchURL = searchString;
4111             }
4112
4113             // Reset the proxy to default.  The host is `""` and the port is `"0"`.
4114             OrbotProxyHelper.setProxy(getApplicationContext(), this, "", "0");
4115
4116             // Set the default app bar layout background.
4117             if (darkTheme) {
4118                 appBarLayout.setBackgroundResource(R.color.gray_900);
4119             } else {
4120                 appBarLayout.setBackgroundResource(R.color.gray_100);
4121             }
4122
4123             // Reset `waitingForOrbot.
4124             waitingForOrbot = false;
4125
4126             // Reload the WebViews if requested.
4127             if (reloadWebsite) {
4128                 // Reload the WebViews.
4129                 for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4130                     // Get the WebView tab fragment.
4131                     WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4132
4133                     // Get the fragment view.
4134                     View fragmentView = webViewTabFragment.getView();
4135
4136                     // Only reload the WebViews if they exist.
4137                     if (fragmentView != null) {
4138                         // Get the nested scroll WebView from the tab fragment.
4139                         NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4140
4141                         // Reload the WebView.
4142                         nestedScrollWebView.reload();
4143                     }
4144                 }
4145             }
4146         }
4147     }
4148
4149     private void updatePrivacyIcons(boolean runInvalidateOptionsMenu) {
4150         // Only update the privacy icons if the options menu and the current WebView have already been populated.
4151         if ((optionsMenu != null) && (currentWebView != null)) {
4152             // Get a handle for the shared preferences.
4153             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4154
4155             // Get the theme and screenshot preferences.
4156             boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4157
4158             // Get handles for the menu items.
4159             MenuItem privacyMenuItem = optionsMenu.findItem(R.id.toggle_javascript);
4160             MenuItem firstPartyCookiesMenuItem = optionsMenu.findItem(R.id.toggle_first_party_cookies);
4161             MenuItem domStorageMenuItem = optionsMenu.findItem(R.id.toggle_dom_storage);
4162             MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
4163
4164             // Update the privacy icon.
4165             if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is enabled.
4166                 privacyMenuItem.setIcon(R.drawable.javascript_enabled);
4167             } else if (currentWebView.getAcceptFirstPartyCookies()) {  // JavaScript is disabled but cookies are enabled.
4168                 privacyMenuItem.setIcon(R.drawable.warning);
4169             } else {  // All the dangerous features are disabled.
4170                 privacyMenuItem.setIcon(R.drawable.privacy_mode);
4171             }
4172
4173             // Update the first-party cookies icon.
4174             if (currentWebView.getAcceptFirstPartyCookies()) {  // First-party cookies are enabled.
4175                 firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_enabled);
4176             } else {  // First-party cookies are disabled.
4177                 if (darkTheme) {
4178                     firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_dark);
4179                 } else {
4180                     firstPartyCookiesMenuItem.setIcon(R.drawable.cookies_disabled_light);
4181                 }
4182             }
4183
4184             // Update the DOM storage icon.
4185             if (currentWebView.getSettings().getJavaScriptEnabled() && currentWebView.getSettings().getDomStorageEnabled()) {  // Both JavaScript and DOM storage are enabled.
4186                 domStorageMenuItem.setIcon(R.drawable.dom_storage_enabled);
4187             } else if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is enabled but DOM storage is disabled.
4188                 if (darkTheme) {
4189                     domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_dark);
4190                 } else {
4191                     domStorageMenuItem.setIcon(R.drawable.dom_storage_disabled_light);
4192                 }
4193             } else {  // JavaScript is disabled, so DOM storage is ghosted.
4194                 if (darkTheme) {
4195                     domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_dark);
4196                 } else {
4197                     domStorageMenuItem.setIcon(R.drawable.dom_storage_ghosted_light);
4198                 }
4199             }
4200
4201             // Update the refresh icon.
4202             if (darkTheme) {
4203                 refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
4204             } else {
4205                 refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
4206             }
4207
4208             // `invalidateOptionsMenu` calls `onPrepareOptionsMenu()` and redraws the icons in the `AppBar`.
4209             if (runInvalidateOptionsMenu) {
4210                 invalidateOptionsMenu();
4211             }
4212         }
4213     }
4214
4215     private void openUrlWithExternalApp(String url) {
4216         // Create a download intent.  Not specifying the action type will display the maximum number of options.
4217         Intent downloadIntent = new Intent();
4218
4219         // Set the URI and the MIME type.  Specifying `text/html` displays a good number of options.
4220         downloadIntent.setDataAndType(Uri.parse(url), "text/html");
4221
4222         // Flag the intent to open in a new task.
4223         downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4224
4225         // Show the chooser.
4226         startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4227     }
4228
4229     private void highlightUrlText() {
4230         // Get a handle for the URL edit text.
4231         EditText urlEditText = findViewById(R.id.url_edittext);
4232
4233         // Only highlight the URL text if the box is not currently selected.
4234         if (!urlEditText.hasFocus()) {
4235             // Get the URL string.
4236             String urlString = urlEditText.getText().toString();
4237
4238             // Highlight the URL according to the protocol.
4239             if (urlString.startsWith("file://")) {  // This is a file URL.
4240                 // De-emphasize only the protocol.
4241                 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4242             } else if (urlString.startsWith("content://")) {
4243                 // De-emphasize only the protocol.
4244                 urlEditText.getText().setSpan(initialGrayColorSpan, 0, 10, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4245             } else {  // This is a web URL.
4246                 // Get the index of the `/` immediately after the domain name.
4247                 int endOfDomainName = urlString.indexOf("/", (urlString.indexOf("//") + 2));
4248
4249                 // Create a base URL string.
4250                 String baseUrl;
4251
4252                 // Get the base URL.
4253                 if (endOfDomainName > 0) {  // There is at least one character after the base URL.
4254                     // Get the base URL.
4255                     baseUrl = urlString.substring(0, endOfDomainName);
4256                 } else {  // There are no characters after the base URL.
4257                     // Set the base URL to be the entire URL string.
4258                     baseUrl = urlString;
4259                 }
4260
4261                 // Get the index of the last `.` in the domain.
4262                 int lastDotIndex = baseUrl.lastIndexOf(".");
4263
4264                 // Get the index of the penultimate `.` in the domain.
4265                 int penultimateDotIndex = baseUrl.lastIndexOf(".", lastDotIndex - 1);
4266
4267                 // Markup the beginning of the URL.
4268                 if (urlString.startsWith("http://")) {  // Highlight the protocol of connections that are not encrypted.
4269                     urlEditText.getText().setSpan(redColorSpan, 0, 7, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4270
4271                     // De-emphasize subdomains.
4272                     if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4273                         urlEditText.getText().setSpan(initialGrayColorSpan, 7, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4274                     }
4275                 } else if (urlString.startsWith("https://")) {  // De-emphasize the protocol of connections that are encrypted.
4276                     if (penultimateDotIndex > 0) {  // There is more than one subdomain in the domain name.
4277                         // De-emphasize the protocol and the additional subdomains.
4278                         urlEditText.getText().setSpan(initialGrayColorSpan, 0, penultimateDotIndex + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4279                     } else {  // There is only one subdomain in the domain name.
4280                         // De-emphasize only the protocol.
4281                         urlEditText.getText().setSpan(initialGrayColorSpan, 0, 8, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4282                     }
4283                 }
4284
4285                 // De-emphasize the text after the domain name.
4286                 if (endOfDomainName > 0) {
4287                     urlEditText.getText().setSpan(finalGrayColorSpan, endOfDomainName, urlString.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
4288                 }
4289             }
4290         }
4291     }
4292
4293     private void loadBookmarksFolder() {
4294         // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4295         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
4296
4297         // Populate the bookmarks cursor adapter.  `this` specifies the `Context`.  `false` disables `autoRequery`.
4298         bookmarksCursorAdapter = new CursorAdapter(this, bookmarksCursor, false) {
4299             @Override
4300             public View newView(Context context, Cursor cursor, ViewGroup parent) {
4301                 // Inflate the individual item layout.  `false` does not attach it to the root.
4302                 return getLayoutInflater().inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false);
4303             }
4304
4305             @Override
4306             public void bindView(View view, Context context, Cursor cursor) {
4307                 // Get handles for the views.
4308                 ImageView bookmarkFavoriteIcon = view.findViewById(R.id.bookmark_favorite_icon);
4309                 TextView bookmarkNameTextView = view.findViewById(R.id.bookmark_name);
4310
4311                 // Get the favorite icon byte array from the cursor.
4312                 byte[] favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON));
4313
4314                 // Convert the byte array to a `Bitmap` beginning at the first byte and ending at the last.
4315                 Bitmap favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.length);
4316
4317                 // Display the bitmap in `bookmarkFavoriteIcon`.
4318                 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap);
4319
4320                 // Get the bookmark name from the cursor and display it in `bookmarkNameTextView`.
4321                 String bookmarkNameString = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME));
4322                 bookmarkNameTextView.setText(bookmarkNameString);
4323
4324                 // Make the font bold for folders.
4325                 if (cursor.getInt(cursor.getColumnIndex(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {
4326                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT_BOLD);
4327                 } else {  // Reset the font to default for normal bookmarks.
4328                     bookmarkNameTextView.setTypeface(Typeface.DEFAULT);
4329                 }
4330             }
4331         };
4332
4333         // Get a handle for the bookmarks list view.
4334         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
4335
4336         // Populate the list view with the adapter.
4337         bookmarksListView.setAdapter(bookmarksCursorAdapter);
4338
4339         // Get a handle for the bookmarks title text view.
4340         TextView bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview);
4341
4342         // Set the bookmarks drawer title.
4343         if (currentBookmarksFolder.isEmpty()) {
4344             bookmarksTitleTextView.setText(R.string.bookmarks);
4345         } else {
4346             bookmarksTitleTextView.setText(currentBookmarksFolder);
4347         }
4348     }
4349
4350     private void openWithApp(String url) {
4351         // Create the open with intent with `ACTION_VIEW`.
4352         Intent openWithAppIntent = new Intent(Intent.ACTION_VIEW);
4353
4354         // Set the URI but not the MIME type.  This should open all available apps.
4355         openWithAppIntent.setData(Uri.parse(url));
4356
4357         // Flag the intent to open in a new task.
4358         openWithAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4359
4360         try {
4361             // Show the chooser.
4362             startActivity(openWithAppIntent);
4363         } catch (ActivityNotFoundException exception) {
4364             // Show a snackbar with the error.
4365             Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
4366         }
4367     }
4368
4369     private void openWithBrowser(String url) {
4370         // Create the open with intent with `ACTION_VIEW`.
4371         Intent openWithBrowserIntent = new Intent(Intent.ACTION_VIEW);
4372
4373         // Set the URI and the MIME type.  `"text/html"` should load browser options.
4374         openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html");
4375
4376         // Flag the intent to open in a new task.
4377         openWithBrowserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4378
4379         try {
4380             // Show the chooser.
4381             startActivity(openWithBrowserIntent);
4382         } catch (ActivityNotFoundException exception) {
4383             // Show a snackbar with the error.
4384             Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
4385         }
4386     }
4387
4388     private String sanitizeUrl(String url) {
4389         // Sanitize Google Analytics.
4390         if (sanitizeGoogleAnalytics) {
4391             // Remove `?utm_`.
4392             if (url.contains("?utm_")) {
4393                 url = url.substring(0, url.indexOf("?utm_"));
4394             }
4395
4396             // Remove `&utm_`.
4397             if (url.contains("&utm_")) {
4398                 url = url.substring(0, url.indexOf("&utm_"));
4399             }
4400         }
4401
4402         // Sanitize Facebook Click IDs.
4403         if (sanitizeFacebookClickIds) {
4404             // Remove `?fbclid=`.
4405             if (url.contains("?fbclid=")) {
4406                 url = url.substring(0, url.indexOf("?fbclid="));
4407             }
4408
4409             // Remove `&fbclid=`.
4410             if (url.contains("&fbclid=")) {
4411                 url = url.substring(0, url.indexOf("&fbclid="));
4412             }
4413         }
4414
4415         // Sanitize Twitter AMP redirects.
4416         if (sanitizeTwitterAmpRedirects) {
4417             // Remove `?amp=1`.
4418             if (url.contains("?amp=1")) {
4419                 url = url.substring(0, url.indexOf("?amp=1"));
4420             }
4421         }
4422
4423         // Return the sanitized URL.
4424         return url;
4425     }
4426
4427     public void finishedPopulatingBlocklists(ArrayList<ArrayList<List<String[]>>> combinedBlocklists) {
4428         // Store the blocklists.
4429         easyList = combinedBlocklists.get(0);
4430         easyPrivacy = combinedBlocklists.get(1);
4431         fanboysAnnoyanceList = combinedBlocklists.get(2);
4432         fanboysSocialList = combinedBlocklists.get(3);
4433         ultraList = combinedBlocklists.get(4);
4434         ultraPrivacy = combinedBlocklists.get(5);
4435
4436         // Add the first tab.
4437         addNewTab("");
4438     }
4439
4440     public void addTab(View view) {
4441         // Add a new tab with a blank URL.
4442         addNewTab("");
4443     }
4444
4445     private void addNewTab(String url) {
4446         // Sanitize the URL.
4447         url = sanitizeUrl(url);
4448
4449         // Get a handle for the tab layout and the view pager.
4450         TabLayout tabLayout = findViewById(R.id.tablayout);
4451         ViewPager webViewPager = findViewById(R.id.webviewpager);
4452
4453         // Get the new page number.  The page numbers are 0 indexed, so the new page number will match the current count.
4454         int newTabNumber = tabLayout.getTabCount();
4455
4456         // Add a new tab.
4457         tabLayout.addTab(tabLayout.newTab());
4458
4459         // Get the new tab.
4460         TabLayout.Tab newTab = tabLayout.getTabAt(newTabNumber);
4461
4462         // Remove the lint warning below that the current tab might be null.
4463         assert newTab != null;
4464
4465         // Set a custom view on the new tab.
4466         newTab.setCustomView(R.layout.tab_custom_view);
4467
4468         // Add the new WebView page.
4469         webViewPagerAdapter.addPage(newTabNumber, webViewPager, url);
4470     }
4471
4472     public void closeTab(View view) {
4473         // Get a handle for the tab layout.
4474         TabLayout tabLayout = findViewById(R.id.tablayout);
4475
4476         // Run the command according to the number of tabs.
4477         if (tabLayout.getTabCount() > 1) {  // There is more than one tab open.
4478             // Close the current tab.
4479             closeCurrentTab();
4480         } else {  // There is only one tab open.
4481             clearAndExit();
4482         }
4483     }
4484
4485     private void closeCurrentTab() {
4486         // Get handles for the views.
4487         AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
4488         TabLayout tabLayout = findViewById(R.id.tablayout);
4489         ViewPager webViewPager = findViewById(R.id.webviewpager);
4490
4491         // Get the current tab number.
4492         int currentTabNumber = tabLayout.getSelectedTabPosition();
4493
4494         // Delete the current tab.
4495         tabLayout.removeTabAt(currentTabNumber);
4496
4497         // 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.
4498         if (webViewPagerAdapter.deletePage(currentTabNumber, webViewPager)) {
4499             setCurrentWebView(currentTabNumber);
4500         }
4501
4502         // Expand the app bar if it is currently collapsed.
4503         appBarLayout.setExpanded(true);
4504     }
4505
4506     private void clearAndExit() {
4507         // Get a handle for the shared preferences.
4508         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4509
4510         // Close the bookmarks cursor and database.
4511         bookmarksCursor.close();
4512         bookmarksDatabaseHelper.close();
4513
4514         // Get the status of the clear everything preference.
4515         boolean clearEverything = sharedPreferences.getBoolean("clear_everything", true);
4516
4517         // Get a handle for the runtime.
4518         Runtime runtime = Runtime.getRuntime();
4519
4520         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
4521         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
4522         String privateDataDirectoryString = getApplicationInfo().dataDir;
4523
4524         // Clear cookies.
4525         if (clearEverything || sharedPreferences.getBoolean("clear_cookies", true)) {
4526             // The command to remove cookies changed slightly in API 21.
4527             if (Build.VERSION.SDK_INT >= 21) {
4528                 CookieManager.getInstance().removeAllCookies(null);
4529             } else {
4530                 CookieManager.getInstance().removeAllCookie();
4531             }
4532
4533             // Manually delete the cookies database, as `CookieManager` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4534             try {
4535                 // Two commands must be used because `Runtime.exec()` does not like `*`.
4536                 Process deleteCookiesProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies");
4537                 Process deleteCookiesJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/Cookies-journal");
4538
4539                 // Wait until the processes have finished.
4540                 deleteCookiesProcess.waitFor();
4541                 deleteCookiesJournalProcess.waitFor();
4542             } catch (Exception exception) {
4543                 // Do nothing if an error is thrown.
4544             }
4545         }
4546
4547         // Clear DOM storage.
4548         if (clearEverything || sharedPreferences.getBoolean("clear_dom_storage", true)) {
4549             // Ask `WebStorage` to clear the DOM storage.
4550             WebStorage webStorage = WebStorage.getInstance();
4551             webStorage.deleteAllData();
4552
4553             // Manually delete the DOM storage files and directories, as `WebStorage` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4554             try {
4555                 // A `String[]` must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4556                 Process deleteLocalStorageProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
4557
4558                 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
4559                 Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
4560                 Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
4561                 Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
4562                 Process deleteDatabaseProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
4563
4564                 // Wait until the processes have finished.
4565                 deleteLocalStorageProcess.waitFor();
4566                 deleteIndexProcess.waitFor();
4567                 deleteQuotaManagerProcess.waitFor();
4568                 deleteQuotaManagerJournalProcess.waitFor();
4569                 deleteDatabaseProcess.waitFor();
4570             } catch (Exception exception) {
4571                 // Do nothing if an error is thrown.
4572             }
4573         }
4574
4575         // Clear form data if the API < 26.
4576         if ((Build.VERSION.SDK_INT < 26) && (clearEverything || sharedPreferences.getBoolean("clear_form_data", true))) {
4577             WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
4578             webViewDatabase.clearFormData();
4579
4580             // Manually delete the form data database, as `WebViewDatabase` sometimes will not flush its changes to disk before `System.exit(0)` is run.
4581             try {
4582                 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
4583                 Process deleteWebDataProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data"});
4584                 Process deleteWebDataJournalProcess = runtime.exec(new String[] {"rm", "-f", privateDataDirectoryString + "/app_webview/Web Data-journal"});
4585
4586                 // Wait until the processes have finished.
4587                 deleteWebDataProcess.waitFor();
4588                 deleteWebDataJournalProcess.waitFor();
4589             } catch (Exception exception) {
4590                 // Do nothing if an error is thrown.
4591             }
4592         }
4593
4594         // Clear the cache.
4595         if (clearEverything || sharedPreferences.getBoolean("clear_cache", true)) {
4596             // Clear the cache from each WebView.
4597             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4598                 // Get the WebView tab fragment.
4599                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4600
4601                 // Get the fragment view.
4602                 View fragmentView = webViewTabFragment.getView();
4603
4604                 // Only clear the cache if the WebView exists.
4605                 if (fragmentView != null) {
4606                     // Get the nested scroll WebView from the tab fragment.
4607                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4608
4609                     // Clear the cache for this WebView.
4610                     nestedScrollWebView.clearCache(true);
4611                 }
4612             }
4613
4614             // Manually delete the cache directories.
4615             try {
4616                 // Delete the main cache directory.
4617                 Process deleteCacheProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/cache");
4618
4619                 // Delete the secondary `Service Worker` cache directory.
4620                 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4621                 Process deleteServiceWorkerProcess = runtime.exec(new String[] {"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
4622
4623                 // Wait until the processes have finished.
4624                 deleteCacheProcess.waitFor();
4625                 deleteServiceWorkerProcess.waitFor();
4626             } catch (Exception exception) {
4627                 // Do nothing if an error is thrown.
4628             }
4629         }
4630
4631         // Wipe out each WebView.
4632         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
4633             // Get the WebView tab fragment.
4634             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
4635
4636             // Get the fragment view.
4637             View fragmentView = webViewTabFragment.getView();
4638
4639             // Only wipe out the WebView if it exists.
4640             if (fragmentView != null) {
4641                 // Get the nested scroll WebView from the tab fragment.
4642                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4643
4644                 // Clear SSL certificate preferences for this WebView.
4645                 nestedScrollWebView.clearSslPreferences();
4646
4647                 // Clear the back/forward history for this WebView.
4648                 nestedScrollWebView.clearHistory();
4649
4650                 // Destroy the internal state of `mainWebView`.
4651                 nestedScrollWebView.destroy();
4652             }
4653         }
4654
4655         // Clear the custom headers.
4656         customHeaders.clear();
4657
4658         // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
4659         // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
4660         if (clearEverything) {
4661             try {
4662                 // Delete the folder.
4663                 Process deleteAppWebviewProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview");
4664
4665                 // Wait until the process has finished.
4666                 deleteAppWebviewProcess.waitFor();
4667             } catch (Exception exception) {
4668                 // Do nothing if an error is thrown.
4669             }
4670         }
4671
4672         // Close Privacy Browser.  `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
4673         if (Build.VERSION.SDK_INT >= 21) {
4674             finishAndRemoveTask();
4675         } else {
4676             finish();
4677         }
4678
4679         // Remove the terminated program from RAM.  The status code is `0`.
4680         System.exit(0);
4681     }
4682
4683     private void setCurrentWebView(int pageNumber) {
4684         // Get a handle for the shared preferences.
4685         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4686
4687         // Get the theme preference.
4688         boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
4689
4690         // Get handles for the URL views.
4691         RelativeLayout urlRelativeLayout = findViewById(R.id.url_relativelayout);
4692         EditText urlEditText = findViewById(R.id.url_edittext);
4693         SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4694
4695         // Stop the swipe to refresh indicator if it is running
4696         swipeRefreshLayout.setRefreshing(false);
4697
4698         // Get the WebView tab fragment.
4699         WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(pageNumber);
4700
4701         // Get the fragment view.
4702         View fragmentView = webViewTabFragment.getView();
4703
4704         // Set the current WebView if the fragment view is not null.
4705         if (fragmentView != null) {  // The fragment has been populated.
4706             // Store the current WebView.
4707             currentWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
4708
4709             // Update the status of swipe to refresh.
4710             if (currentWebView.getSwipeToRefresh()) {  // Swipe to refresh is enabled.
4711                 // Enable the swipe refresh layout if the WebView is scrolled all the way to the top.  It is updated every time the scroll changes.
4712                 swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
4713             } else {  // Swipe to refresh is disabled.
4714                 // Disable the swipe refresh layout.
4715                 swipeRefreshLayout.setEnabled(false);
4716             }
4717
4718             // Get a handle for the cookie manager.
4719             CookieManager cookieManager = CookieManager.getInstance();
4720
4721             // Set the first-party cookie status.
4722             cookieManager.setAcceptCookie(currentWebView.getAcceptFirstPartyCookies());
4723
4724             // Update the privacy icons.  `true` redraws the icons in the app bar.
4725             updatePrivacyIcons(true);
4726
4727             // Get a handle for the input method manager.
4728             InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
4729
4730             // Remove the lint warning below that the input method manager might be null.
4731             assert inputMethodManager != null;
4732
4733             // Get the current URL.
4734             String url = currentWebView.getUrl();
4735
4736             // Update the URL edit text if not loading a new intent.  Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`.
4737             if (!loadingNewIntent) {  // A new intent is not being loaded.
4738                 if ((url == null) || url.equals("about:blank")) {  // The WebView is blank.
4739                     // Display the hint in the URL edit text.
4740                     urlEditText.setText("");
4741
4742                     // Request focus for the URL text box.
4743                     urlEditText.requestFocus();
4744
4745                     // Display the keyboard.
4746                     inputMethodManager.showSoftInput(urlEditText, 0);
4747                 } else {  // The WebView has a loaded URL.
4748                     // Clear the focus from the URL text box.
4749                     urlEditText.clearFocus();
4750
4751                     // Hide the soft keyboard.
4752                     inputMethodManager.hideSoftInputFromWindow(currentWebView.getWindowToken(), 0);
4753
4754                     // Display the current URL in the URL text box.
4755                     urlEditText.setText(url);
4756
4757                     // Highlight the URL text.
4758                     highlightUrlText();
4759                 }
4760             } else {  // A new intent is being loaded.
4761                 // Reset the loading new intent tracker.
4762                 loadingNewIntent = false;
4763             }
4764
4765             // Set the background to indicate the domain settings status.
4766             if (currentWebView.getDomainSettingsApplied()) {
4767                 // 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.
4768                 if (darkTheme) {
4769                     urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_dark_blue));
4770                 } else {
4771                     urlRelativeLayout.setBackground(getResources().getDrawable(R.drawable.url_bar_background_light_green));
4772                 }
4773             } else {
4774                 urlRelativeLayout.setBackground(getResources().getDrawable(R.color.transparent));
4775             }
4776         } else {  // The fragment has not been populated.  Try again in 100 milliseconds.
4777             // Create a handler to set the current WebView.
4778             Handler setCurrentWebViewHandler = new Handler();
4779
4780             // Create a runnable to set the current WebView.
4781             Runnable setCurrentWebWebRunnable = () -> {
4782                 // Set the current WebView.
4783                 setCurrentWebView(pageNumber);
4784             };
4785
4786             // Try setting the current WebView again after 100 milliseconds.
4787             setCurrentWebViewHandler.postDelayed(setCurrentWebWebRunnable, 100);
4788         }
4789     }
4790
4791     @Override
4792     public void initializeWebView(NestedScrollWebView nestedScrollWebView, int pageNumber, ProgressBar progressBar, String url) {
4793         // Get handles for the activity views.
4794         FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
4795         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
4796         RelativeLayout mainContentRelativeLayout = findViewById(R.id.main_content_relativelayout);
4797         ActionBar actionBar = getSupportActionBar();
4798         LinearLayout tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
4799         EditText urlEditText = findViewById(R.id.url_edittext);
4800         TabLayout tabLayout = findViewById(R.id.tablayout);
4801         SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
4802
4803         // Remove the incorrect lint warning below that the action bar might be null.
4804         assert actionBar != null;
4805
4806         // Get a handle for the activity
4807         Activity activity = this;
4808
4809         // Get a handle for the input method manager.
4810         InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
4811
4812         // Instantiate the blocklist helper.
4813         BlocklistHelper blocklistHelper = new BlocklistHelper();
4814
4815         // Remove the lint warning below that the input method manager might be null.
4816         assert inputMethodManager != null;
4817
4818         // Get a handle for the shared preferences.
4819         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
4820
4821         // Initialize the favorite icon.
4822         nestedScrollWebView.initializeFavoriteIcon();
4823
4824         // Set the app bar scrolling.
4825         nestedScrollWebView.setNestedScrollingEnabled(sharedPreferences.getBoolean("scroll_app_bar", true));
4826
4827         // Allow pinch to zoom.
4828         nestedScrollWebView.getSettings().setBuiltInZoomControls(true);
4829
4830         // Hide zoom controls.
4831         nestedScrollWebView.getSettings().setDisplayZoomControls(false);
4832
4833         // Don't allow mixed content (HTTP and HTTPS) on the same website.
4834         if (Build.VERSION.SDK_INT >= 21) {
4835             nestedScrollWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
4836         }
4837
4838         // Set the WebView to load in overview mode (zoomed out to the maximum width).
4839         nestedScrollWebView.getSettings().setLoadWithOverviewMode(true);
4840
4841         // Explicitly disable geolocation.
4842         nestedScrollWebView.getSettings().setGeolocationEnabled(false);
4843
4844         // Create a double-tap gesture detector to toggle full-screen mode.
4845         GestureDetector doubleTapGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener() {
4846             // Override `onDoubleTap()`.  All other events are handled using the default settings.
4847             @Override
4848             public boolean onDoubleTap(MotionEvent event) {
4849                 if (fullScreenBrowsingModeEnabled) {  // Only process the double-tap if full screen browsing mode is enabled.
4850                     // Toggle the full screen browsing mode tracker.
4851                     inFullScreenBrowsingMode = !inFullScreenBrowsingMode;
4852
4853                     // Toggle the full screen browsing mode.
4854                     if (inFullScreenBrowsingMode) {  // Switch to full screen mode.
4855                         // Store the swipe refresh layout top padding.
4856                         swipeRefreshLayoutPaddingTop = swipeRefreshLayout.getPaddingTop();
4857
4858                         // Hide the app bar if specified.
4859                         if (hideAppBar) {
4860                             // Close the find on page bar if it is visible.
4861                             closeFindOnPage(null);
4862
4863                             // Hide the tab linear layout.
4864                             tabsLinearLayout.setVisibility(View.GONE);
4865
4866                             // Hide the action bar.
4867                             actionBar.hide();
4868
4869                             // Check to see if app bar scrolling is disabled.
4870                             if (!scrollAppBar) {
4871                                 // Remove the padding from the top of the swipe refresh layout.
4872                                 swipeRefreshLayout.setPadding(0, 0, 0, 0);
4873                             }
4874                         }
4875
4876                         // Hide the banner ad in the free flavor.
4877                         if (BuildConfig.FLAVOR.contentEquals("free")) {
4878                             AdHelper.hideAd(findViewById(R.id.adview));
4879                         }
4880
4881                         // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
4882                         getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4883
4884                         /* Hide the system bars.
4885                          * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4886                          * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4887                          * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4888                          * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4889                          */
4890                         rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
4891                                 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
4892                     } else {  // Switch to normal viewing mode.
4893                         // Show the tab linear layout.
4894                         tabsLinearLayout.setVisibility(View.VISIBLE);
4895
4896                         // Show the action bar.
4897                         actionBar.show();
4898
4899                         // Check to see if app bar scrolling is disabled.
4900                         if (!scrollAppBar) {
4901                             // Add the padding from the top of the swipe refresh layout.
4902                             swipeRefreshLayout.setPadding(0, swipeRefreshLayoutPaddingTop, 0, 0);
4903                         }
4904
4905                         // Show the banner ad in the free flavor.
4906                         if (BuildConfig.FLAVOR.contentEquals("free")) {
4907                             // Reload the ad.
4908                             AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
4909                         }
4910
4911                         // Remove the `SYSTEM_UI` flags from the root frame layout.
4912                         rootFrameLayout.setSystemUiVisibility(0);
4913
4914                         // Add the translucent status flag.
4915                         getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
4916                     }
4917
4918                     // Consume the double-tap.
4919                     return true;
4920                 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4921                     return false;
4922                 }
4923             }
4924         });
4925
4926         // Pass all touch events on the WebView through the double-tap gesture detector.
4927         nestedScrollWebView.setOnTouchListener((View view, MotionEvent event) -> {
4928             // Call `performClick()` on the view, which is required for accessibility.
4929             view.performClick();
4930
4931             // Send the event to the gesture detector.
4932             return doubleTapGestureDetector.onTouchEvent(event);
4933         });
4934
4935         // Register the WebView for a context menu.  This is used to see link targets and download images.
4936         registerForContextMenu(nestedScrollWebView);
4937
4938         // Allow the downloading of files.
4939         nestedScrollWebView.setDownloadListener((String downloadUrl, String userAgent, String contentDisposition, String mimetype, long contentLength) -> {
4940             // Check if the download should be processed by an external app.
4941             if (sharedPreferences.getBoolean("download_with_external_app", false)) {  // Download with an external app.
4942                 // Create a download intent.  Not specifying the action type will display the maximum number of options.
4943                 Intent downloadIntent = new Intent();
4944
4945                 // Set the URI and the MIME type.  Specifying `text/html` displays a good number of options.
4946                 downloadIntent.setDataAndType(Uri.parse(downloadUrl), "text/html");
4947
4948                 // Flag the intent to open in a new task.
4949                 downloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4950
4951                 // Show the chooser.
4952                 startActivity(Intent.createChooser(downloadIntent, getString(R.string.open_with)));
4953             } else {  // Download with Android's download manager.
4954                 // Check to see if the WRITE_EXTERNAL_STORAGE permission has already been granted.
4955                 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission has not been granted.
4956                     // The WRITE_EXTERNAL_STORAGE permission needs to be requested.
4957
4958                     // Store the variables for future use by `onRequestPermissionsResult()`.
4959                     this.downloadUrl = downloadUrl;
4960                     downloadContentDisposition = contentDisposition;
4961                     downloadContentLength = contentLength;
4962
4963                     // Show a dialog if the user has previously denied the permission.
4964                     if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
4965                         // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
4966                         DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
4967
4968                         // Show the download location permission alert dialog.  The permission will be requested when the the dialog is closed.
4969                         downloadLocationPermissionDialogFragment.show(getSupportFragmentManager(), getString(R.string.download_location));
4970                     } else {  // Show the permission request directly.
4971                         // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
4972                         ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
4973                     }
4974                 } else {  // The storage permission has already been granted.
4975                     // Get a handle for the download file alert dialog.
4976                     DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, contentDisposition, contentLength);
4977
4978                     // Show the download file alert dialog.
4979                     downloadFileDialogFragment.show(getSupportFragmentManager(), getString(R.string.download));
4980                 }
4981             }
4982         });
4983
4984         // Update the find on page count.
4985         nestedScrollWebView.setFindListener(new WebView.FindListener() {
4986             // Get a handle for `findOnPageCountTextView`.
4987             final TextView findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview);
4988
4989             @Override
4990             public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
4991                 if ((isDoneCounting) && (numberOfMatches == 0)) {  // There are no matches.
4992                     // Set `findOnPageCountTextView` to `0/0`.
4993                     findOnPageCountTextView.setText(R.string.zero_of_zero);
4994                 } else if (isDoneCounting) {  // There are matches.
4995                     // `activeMatchOrdinal` is zero-based.
4996                     int activeMatch = activeMatchOrdinal + 1;
4997
4998                     // Build the match string.
4999                     String matchString = activeMatch + "/" + numberOfMatches;
5000
5001                     // Set `findOnPageCountTextView`.
5002                     findOnPageCountTextView.setText(matchString);
5003                 }
5004             }
5005         });
5006
5007         // Update the status of swipe to refresh based on the scroll position of the nested scroll WebView.  Also reinforce full screen browsing mode.
5008         // Once the minimum API >= 23 this can be replaced with `nestedScrollWebView.setOnScrollChangeListener()`.
5009         nestedScrollWebView.getViewTreeObserver().addOnScrollChangedListener(() -> {
5010             if (nestedScrollWebView.getSwipeToRefresh()) {
5011                 // Only enable swipe to refresh if the WebView is scrolled to the top.
5012                 swipeRefreshLayout.setEnabled(nestedScrollWebView.getScrollY() == 0);
5013             }
5014
5015             // Reinforce the system UI visibility flags if in full screen browsing mode.
5016             // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard.
5017             if (inFullScreenBrowsingMode) {
5018                 /* Hide the system bars.
5019                  * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5020                  * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5021                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5022                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5023                  */
5024                 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5025                         View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5026             }
5027         });
5028
5029         // Set the web chrome client.
5030         nestedScrollWebView.setWebChromeClient(new WebChromeClient() {
5031             // Update the progress bar when a page is loading.
5032             @Override
5033             public void onProgressChanged(WebView view, int progress) {
5034                 // Inject the night mode CSS if night mode is enabled.
5035                 if (nestedScrollWebView.getNightMode()) {  // Night mode is enabled.
5036                     // `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
5037                     // used by WordPress.  `text-decoration: none` removes all text underlines.  `text-shadow: none` removes text shadows, which usually have a hard coded color.
5038                     // `border: none` removes all borders, which can also be used to underline text.  `a {color: #1565C0}` sets links to be a dark blue.
5039                     // `::selection {background: #0D47A1}' sets the text selection highlight color to be a dark blue. `!important` takes precedent over any existing sub-settings.
5040                     nestedScrollWebView.evaluateJavascript("(function() {var parent = document.getElementsByTagName('head').item(0); var style = document.createElement('style'); style.type = 'text/css'; " +
5041                             "style.innerHTML = '* {background-color: #212121 !important; color: #BDBDBD !important; box-shadow: none !important; text-decoration: none !important;" +
5042                             "text-shadow: none !important; border: none !important;} a {color: #1565C0 !important;} ::selection {background: #0D47A1 !important;}'; parent.appendChild(style)})()", value -> {
5043                         // Initialize a handler to display `mainWebView`.
5044                         Handler displayWebViewHandler = new Handler();
5045
5046                         // Setup a runnable to display `mainWebView` after a delay to allow the CSS to be applied.
5047                         Runnable displayWebViewRunnable = () -> {
5048                             // Only display `mainWebView` if the progress bar is gone.  This prevents the display of the `WebView` while it is still loading.
5049                             if (progressBar.getVisibility() == View.GONE) {
5050                                 nestedScrollWebView.setVisibility(View.VISIBLE);
5051                             }
5052                         };
5053
5054                         // Display the WebView after 500 milliseconds.
5055                         displayWebViewHandler.postDelayed(displayWebViewRunnable, 500);
5056                     });
5057                 } else {  // Night mode is disabled.
5058                     // Display the nested scroll WebView if night mode is disabled.
5059                     // Because of a race condition between `applyDomainSettings` and `onPageStarted`,
5060                     // when night mode is set by domain settings the WebView may be hidden even if night mode is not currently enabled.
5061                     nestedScrollWebView.setVisibility(View.VISIBLE);
5062                 }
5063
5064                 // Update the progress bar.
5065                 progressBar.setProgress(progress);
5066
5067                 // Set the visibility of the progress bar.
5068                 if (progress < 100) {
5069                     // Show the progress bar.
5070                     progressBar.setVisibility(View.VISIBLE);
5071                 } else {
5072                     // Hide the progress bar.
5073                     progressBar.setVisibility(View.GONE);
5074
5075                     //Stop the swipe to refresh indicator if it is running
5076                     swipeRefreshLayout.setRefreshing(false);
5077                 }
5078             }
5079
5080             // Set the favorite icon when it changes.
5081             @Override
5082             public void onReceivedIcon(WebView view, Bitmap icon) {
5083                 // Only update the favorite icon if the website has finished loading.
5084                 if (progressBar.getVisibility() == View.GONE) {
5085                     // Store the new favorite icon.
5086                     nestedScrollWebView.setFavoriteOrDefaultIcon(icon);
5087
5088                     // Get the current page position.
5089                     int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5090
5091                     // Get the current tab.
5092                     TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5093
5094                     // Check to see if the tab has been populated.
5095                     if (tab != null) {
5096                         // Get the custom view from the tab.
5097                         View tabView = tab.getCustomView();
5098
5099                         // Check to see if the custom tab view has been populated.
5100                         if (tabView != null) {
5101                             // Get the favorite icon image view from the tab.
5102                             ImageView tabFavoriteIconImageView = tabView.findViewById(R.id.favorite_icon_imageview);
5103
5104                             // Display the favorite icon in the tab.
5105                             tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true));
5106                         }
5107                     }
5108                 }
5109             }
5110
5111             // Save a copy of the title when it changes.
5112             @Override
5113             public void onReceivedTitle(WebView view, String title) {
5114                 // Get the current page position.
5115                 int currentPosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5116
5117                 // Get the current tab.
5118                 TabLayout.Tab tab = tabLayout.getTabAt(currentPosition);
5119
5120                 // Only populate the title text view if the tab has been fully created.
5121                 if (tab != null) {
5122                     // Get the custom view from the tab.
5123                     View tabView = tab.getCustomView();
5124
5125                     // Remove the incorrect warning below that the current tab view might be null.
5126                     assert tabView != null;
5127
5128                     // Get the title text view from the tab.
5129                     TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5130
5131                     // Set the title according to the URL.
5132                     if (title.equals("about:blank")) {
5133                         // Set the title to indicate a new tab.
5134                         tabTitleTextView.setText(R.string.new_tab);
5135                     } else {
5136                         // Set the title as the tab text.
5137                         tabTitleTextView.setText(title);
5138                     }
5139                 }
5140             }
5141
5142             // Enter full screen video.
5143             @Override
5144             public void onShowCustomView(View video, CustomViewCallback callback) {
5145                 // Get a handle for the full screen video frame layout.
5146                 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
5147
5148                 // Set the full screen video flag.
5149                 displayingFullScreenVideo = true;
5150
5151                 // Pause the ad if this is the free flavor.
5152                 if (BuildConfig.FLAVOR.contentEquals("free")) {
5153                     // The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
5154                     AdHelper.pauseAd(findViewById(R.id.adview));
5155                 }
5156
5157                 // Hide the keyboard.
5158                 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5159
5160                 // Hide the main content relative layout.
5161                 mainContentRelativeLayout.setVisibility(View.GONE);
5162
5163                 // Remove the translucent status bar overlay on the `Drawer Layout`, which is special and needs its own command.
5164                 drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
5165
5166                 // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
5167                 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
5168
5169                 /* Hide the system bars.
5170                  * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5171                  * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5172                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5173                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5174                  */
5175                 rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5176                         View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5177
5178                 // Disable the sliding drawers.
5179                 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
5180
5181                 // Add the video view to the full screen video frame layout.
5182                 fullScreenVideoFrameLayout.addView(video);
5183
5184                 // Show the full screen video frame layout.
5185                 fullScreenVideoFrameLayout.setVisibility(View.VISIBLE);
5186             }
5187
5188             // Exit full screen video.
5189             @Override
5190             public void onHideCustomView() {
5191                 // Get a handle for the full screen video frame layout.
5192                 FrameLayout fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
5193
5194                 // Unset the full screen video flag.
5195                 displayingFullScreenVideo = false;
5196
5197                 // Remove all the views from the full screen video frame layout.
5198                 fullScreenVideoFrameLayout.removeAllViews();
5199
5200                 // Hide the full screen video frame layout.
5201                 fullScreenVideoFrameLayout.setVisibility(View.GONE);
5202
5203                 // Enable the sliding drawers.
5204                 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
5205
5206                 // Show the main content relative layout.
5207                 mainContentRelativeLayout.setVisibility(View.VISIBLE);
5208
5209                 // Apply the appropriate full screen mode flags.
5210                 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
5211                     // Hide the app bar if specified.
5212                     if (hideAppBar) {
5213                         // Hide the tab linear layout.
5214                         tabsLinearLayout.setVisibility(View.GONE);
5215
5216                         // Hide the action bar.
5217                         actionBar.hide();
5218                     }
5219
5220                     // Hide the banner ad in the free flavor.
5221                     if (BuildConfig.FLAVOR.contentEquals("free")) {
5222                         AdHelper.hideAd(findViewById(R.id.adview));
5223                     }
5224
5225                     // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
5226                     getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
5227
5228                     /* Hide the system bars.
5229                      * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5230                      * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5231                      * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5232                      * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5233                      */
5234                     rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
5235                             View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
5236                 } else {  // Switch to normal viewing mode.
5237                     // Remove the `SYSTEM_UI` flags from the root frame layout.
5238                     rootFrameLayout.setSystemUiVisibility(0);
5239
5240                     // Add the translucent status flag.
5241                     getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
5242                 }
5243
5244                 // Reload the ad for the free flavor if not in full screen mode.
5245                 if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
5246                     // Reload the ad.
5247                     AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
5248                 }
5249             }
5250
5251             // Upload files.
5252             @Override
5253             public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
5254                 // Show the file chooser if the device is running API >= 21.
5255                 if (Build.VERSION.SDK_INT >= 21) {
5256                     // Store the file path callback.
5257                     fileChooserCallback = filePathCallback;
5258
5259                     // Create an intent to open a chooser based ont the file chooser parameters.
5260                     Intent fileChooserIntent = fileChooserParams.createIntent();
5261
5262                     // Open the file chooser.
5263                     startActivityForResult(fileChooserIntent, FILE_UPLOAD_REQUEST_CODE);
5264                 }
5265                 return true;
5266             }
5267         });
5268
5269         nestedScrollWebView.setWebViewClient(new WebViewClient() {
5270             // `shouldOverrideUrlLoading` makes this `WebView` the default handler for URLs inside the app, so that links are not kicked out to other apps.
5271             // The deprecated `shouldOverrideUrlLoading` must be used until API >= 24.
5272             @Override
5273             public boolean shouldOverrideUrlLoading(WebView view, String url) {
5274                 // Sanitize the url.
5275                 url = sanitizeUrl(url);
5276
5277                 if (url.startsWith("http")) {  // Load the URL in Privacy Browser.
5278                     // Apply the domain settings for the new URL.  This doesn't do anything if the domain has not changed.
5279                     boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
5280
5281                     // Check if the user agent has changed.
5282                     if (userAgentChanged) {
5283                         // Manually load the URL.  The changing of the user agent will cause WebView to reload the previous URL.
5284                         nestedScrollWebView.loadUrl(url, customHeaders);
5285
5286                         // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
5287                         return true;
5288                     } else {
5289                         // Returning false causes the current WebView to handle the URL and prevents it from adding redirects to the history list.
5290                         return false;
5291                     }
5292                 } else if (url.startsWith("mailto:")) {  // Load the email address in an external email program.
5293                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
5294                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
5295
5296                     // Parse the url and set it as the data for the intent.
5297                     emailIntent.setData(Uri.parse(url));
5298
5299                     // Open the email program in a new task instead of as part of Privacy Browser.
5300                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5301
5302                     // Make it so.
5303                     startActivity(emailIntent);
5304
5305                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5306                     return true;
5307                 } else if (url.startsWith("tel:")) {  // Load the phone number in the dialer.
5308                     // Open the dialer and load the phone number, but wait for the user to place the call.
5309                     Intent dialIntent = new Intent(Intent.ACTION_DIAL);
5310
5311                     // Add the phone number to the intent.
5312                     dialIntent.setData(Uri.parse(url));
5313
5314                     // Open the dialer in a new task instead of as part of Privacy Browser.
5315                     dialIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5316
5317                     // Make it so.
5318                     startActivity(dialIntent);
5319
5320                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5321                     return true;
5322                 } else {  // Load a system chooser to select an app that can handle the URL.
5323                     // Open an app that can handle the URL.
5324                     Intent genericIntent = new Intent(Intent.ACTION_VIEW);
5325
5326                     // Add the URL to the intent.
5327                     genericIntent.setData(Uri.parse(url));
5328
5329                     // List all apps that can handle the URL instead of just opening the first one.
5330                     genericIntent.addCategory(Intent.CATEGORY_BROWSABLE);
5331
5332                     // Open the app in a new task instead of as part of Privacy Browser.
5333                     genericIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
5334
5335                     // Start the app or display a snackbar if no app is available to handle the URL.
5336                     try {
5337                         startActivity(genericIntent);
5338                     } catch (ActivityNotFoundException exception) {
5339                         Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url) + "  " + url, Snackbar.LENGTH_SHORT).show();
5340                     }
5341
5342                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5343                     return true;
5344                 }
5345             }
5346
5347             // Check requests against the block lists.  The deprecated `shouldInterceptRequest()` must be used until minimum API >= 21.
5348             @Override
5349             public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
5350                 // Check to see if the resource request is for the main URL.
5351                 if (url.equals(nestedScrollWebView.getCurrentUrl())) {
5352                     // `return null` loads the resource request, which should never be blocked if it is the main URL.
5353                     return null;
5354                 }
5355
5356                 // 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.
5357                 while (ultraPrivacy == null) {
5358                     // The wait must be synchronized, which only lets one thread run on it at a time, or `java.lang.IllegalMonitorStateException` is thrown.
5359                     synchronized (this) {
5360                         try {
5361                             // Check to see if the blocklists have been populated after 100 ms.
5362                             wait(100);
5363                         } catch (InterruptedException exception) {
5364                             // Do nothing.
5365                         }
5366                     }
5367                 }
5368
5369                 // Sanitize the URL.
5370                 url = sanitizeUrl(url);
5371
5372                 // Get a handle for the navigation view.
5373                 NavigationView navigationView = findViewById(R.id.navigationview);
5374
5375                 // Get a handle for the navigation menu.
5376                 Menu navigationMenu = navigationView.getMenu();
5377
5378                 // Get a handle for the navigation requests menu item.  The menu is 0 based.
5379                 MenuItem navigationRequestsMenuItem = navigationMenu.getItem(5);
5380
5381                 // Create an empty web resource response to be used if the resource request is blocked.
5382                 WebResourceResponse emptyWebResourceResponse = new WebResourceResponse("text/plain", "utf8", new ByteArrayInputStream("".getBytes()));
5383
5384                 // Reset the whitelist results tracker.
5385                 String[] whitelistResultStringArray = null;
5386
5387                 // Initialize the third party request tracker.
5388                 boolean isThirdPartyRequest = false;
5389
5390                 // Get the current URL.  `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
5391                 String currentBaseDomain = nestedScrollWebView.getCurrentDomainName();
5392
5393                 // Store a copy of the current domain for use in later requests.
5394                 String currentDomain = currentBaseDomain;
5395
5396                 // Nobody is happy when comparing null strings.
5397                 if ((currentBaseDomain != null) && (url != null)) {
5398                     // Convert the request URL to a URI.
5399                     Uri requestUri = Uri.parse(url);
5400
5401                     // Get the request host name.
5402                     String requestBaseDomain = requestUri.getHost();
5403
5404                     // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
5405                     if (!currentBaseDomain.isEmpty() && (requestBaseDomain != null)) {
5406                         // Determine the current base domain.
5407                         while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) {  // There is at least one subdomain.
5408                             // Remove the first subdomain.
5409                             currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1);
5410                         }
5411
5412                         // Determine the request base domain.
5413                         while (requestBaseDomain.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) {  // There is at least one subdomain.
5414                             // Remove the first subdomain.
5415                             requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1);
5416                         }
5417
5418                         // Update the third party request tracker.
5419                         isThirdPartyRequest = !currentBaseDomain.equals(requestBaseDomain);
5420                     }
5421                 }
5422
5423                 // Get the current WebView page position.
5424                 int webViewPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5425
5426                 // Determine if the WebView is currently displayed.
5427                 boolean webViewDisplayed = (webViewPagePosition == tabLayout.getSelectedTabPosition());
5428
5429                 // Block third-party requests if enabled.
5430                 if (isThirdPartyRequest && nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS)) {
5431                     // Add the result to the resource requests.
5432                     nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_THIRD_PARTY, url});
5433
5434                     // Increment the blocked requests counters.
5435                     nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5436                     nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS);
5437
5438                     // Update the titles of the blocklist menu items if the WebView is currently displayed.
5439                     if (webViewDisplayed) {
5440                         // Updating the UI must be run from the UI thread.
5441                         activity.runOnUiThread(() -> {
5442                             // Update the menu item titles.
5443                             navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5444
5445                             // Update the options menu if it has been populated.
5446                             if (optionsMenu != null) {
5447                                 optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5448                                 optionsMenu.findItem(R.id.block_all_third_party_requests).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " +
5449                                         getString(R.string.block_all_third_party_requests));
5450                             }
5451                         });
5452                     }
5453
5454                     // Return an empty web resource response.
5455                     return emptyWebResourceResponse;
5456                 }
5457
5458                 // Check UltraList if it is enabled.
5459                 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRALIST)) {
5460                     // Check the URL against UltraList.
5461                     String[] ultraListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraList);
5462
5463                     // Process the UltraList results.
5464                     if (ultraListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched UltraLists's blacklist.
5465                         // Add the result to the resource requests.
5466                         nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5467
5468                         // Increment the blocked requests counters.
5469                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5470                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRALIST);
5471
5472                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5473                         if (webViewDisplayed) {
5474                             // Updating the UI must be run from the UI thread.
5475                             activity.runOnUiThread(() -> {
5476                                 // Update the menu item titles.
5477                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5478
5479                                 // Update the options menu if it has been populated.
5480                                 if (optionsMenu != null) {
5481                                     optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5482                                     optionsMenu.findItem(R.id.ultralist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
5483                                 }
5484                             });
5485                         }
5486
5487                         // The resource request was blocked.  Return an empty web resource response.
5488                         return emptyWebResourceResponse;
5489                     } else if (ultraListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched UltraList's whitelist.
5490                         // Add a whitelist entry to the resource requests array.
5491                         nestedScrollWebView.addResourceRequest(new String[] {ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]});
5492
5493                         // The resource request has been allowed by UltraPrivacy.  `return null` loads the requested resource.
5494                         return null;
5495                     }
5496                 }
5497
5498                 // Check UltraPrivacy if it is enabled.
5499                 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.ULTRAPRIVACY)) {
5500                     // Check the URL against UltraPrivacy.
5501                     String[] ultraPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, ultraPrivacy);
5502
5503                     // Process the UltraPrivacy results.
5504                     if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched UltraPrivacy's blacklist.
5505                         // Add the result to the resource requests.
5506                         nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5507                                 ultraPrivacyResults[5]});
5508
5509                         // Increment the blocked requests counters.
5510                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5511                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.ULTRAPRIVACY);
5512
5513                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5514                         if (webViewDisplayed) {
5515                             // Updating the UI must be run from the UI thread.
5516                             activity.runOnUiThread(() -> {
5517                                 // Update the menu item titles.
5518                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5519
5520                                 // Update the options menu if it has been populated.
5521                                 if (optionsMenu != null) {
5522                                     optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5523                                     optionsMenu.findItem(R.id.ultraprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
5524                                 }
5525                             });
5526                         }
5527
5528                         // The resource request was blocked.  Return an empty web resource response.
5529                         return emptyWebResourceResponse;
5530                     } else if (ultraPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched UltraPrivacy's whitelist.
5531                         // Add a whitelist entry to the resource requests array.
5532                         nestedScrollWebView.addResourceRequest(new String[] {ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5533                                 ultraPrivacyResults[5]});
5534
5535                         // The resource request has been allowed by UltraPrivacy.  `return null` loads the requested resource.
5536                         return null;
5537                     }
5538                 }
5539
5540                 // Check EasyList if it is enabled.
5541                 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASYLIST)) {
5542                     // Check the URL against EasyList.
5543                     String[] easyListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyList);
5544
5545                     // Process the EasyList results.
5546                     if (easyListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched EasyList's blacklist.
5547                         // Add the result to the resource requests.
5548                         nestedScrollWebView.addResourceRequest(new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]});
5549
5550                         // Increment the blocked requests counters.
5551                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5552                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYLIST);
5553
5554                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5555                         if (webViewDisplayed) {
5556                             // Updating the UI must be run from the UI thread.
5557                             activity.runOnUiThread(() -> {
5558                                 // Update the menu item titles.
5559                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5560
5561                                 // Update the options menu if it has been populated.
5562                                 if (optionsMenu != null) {
5563                                     optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5564                                     optionsMenu.findItem(R.id.easylist).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
5565                                 }
5566                             });
5567                         }
5568
5569                         // The resource request was blocked.  Return an empty web resource response.
5570                         return emptyWebResourceResponse;
5571                     } else if (easyListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched EasyList's whitelist.
5572                         // Update the whitelist result string array tracker.
5573                         whitelistResultStringArray = new String[] {easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]};
5574                     }
5575                 }
5576
5577                 // Check EasyPrivacy if it is enabled.
5578                 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.EASYPRIVACY)) {
5579                     // Check the URL against EasyPrivacy.
5580                     String[] easyPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, easyPrivacy);
5581
5582                     // Process the EasyPrivacy results.
5583                     if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched EasyPrivacy's blacklist.
5584                         // Add the result to the resource requests.
5585                         nestedScrollWebView.addResourceRequest(new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4],
5586                                 easyPrivacyResults[5]});
5587
5588                         // Increment the blocked requests counters.
5589                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5590                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.EASYPRIVACY);
5591
5592                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5593                         if (webViewDisplayed) {
5594                             // Updating the UI must be run from the UI thread.
5595                             activity.runOnUiThread(() -> {
5596                                 // Update the menu item titles.
5597                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5598
5599                                 // Update the options menu if it has been populated.
5600                                 if (optionsMenu != null) {
5601                                     optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5602                                     optionsMenu.findItem(R.id.easyprivacy).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
5603                                 }
5604                             });
5605                         }
5606
5607                         // The resource request was blocked.  Return an empty web resource response.
5608                         return emptyWebResourceResponse;
5609                     } else if (easyPrivacyResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched EasyPrivacy's whitelist.
5610                         // Update the whitelist result string array tracker.
5611                         whitelistResultStringArray = new String[] {easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]};
5612                     }
5613                 }
5614
5615                 // Check Fanboy’s Annoyance List if it is enabled.
5616                 if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST)) {
5617                     // Check the URL against Fanboy's Annoyance List.
5618                     String[] fanboysAnnoyanceListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysAnnoyanceList);
5619
5620                     // Process the Fanboy's Annoyance List results.
5621                     if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched Fanboy's Annoyance List's blacklist.
5622                         // Add the result to the resource requests.
5623                         nestedScrollWebView.addResourceRequest(new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5624                                 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]});
5625
5626                         // Increment the blocked requests counters.
5627                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5628                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST);
5629
5630                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5631                         if (webViewDisplayed) {
5632                             // Updating the UI must be run from the UI thread.
5633                             activity.runOnUiThread(() -> {
5634                                 // Update the menu item titles.
5635                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5636
5637                                 // Update the options menu if it has been populated.
5638                                 if (optionsMenu != null) {
5639                                     optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5640                                     optionsMenu.findItem(R.id.fanboys_annoyance_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " +
5641                                             getString(R.string.fanboys_annoyance_list));
5642                                 }
5643                             });
5644                         }
5645
5646                         // The resource request was blocked.  Return an empty web resource response.
5647                         return emptyWebResourceResponse;
5648                     } else if (fanboysAnnoyanceListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)){  // The resource request matched Fanboy's Annoyance List's whitelist.
5649                         // Update the whitelist result string array tracker.
5650                         whitelistResultStringArray = new String[] {fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5651                                 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]};
5652                     }
5653                 } else if (nestedScrollWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST)) {  // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5654                     // Check the URL against Fanboy's Annoyance List.
5655                     String[] fanboysSocialListResults = blocklistHelper.checkBlocklist(currentDomain, url, isThirdPartyRequest, fanboysSocialList);
5656
5657                     // Process the Fanboy's Social Blocking List results.
5658                     if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_BLOCKED)) {  // The resource request matched Fanboy's Social Blocking List's blacklist.
5659                         // Add the result to the resource requests.
5660                         nestedScrollWebView.addResourceRequest(new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5661                                 fanboysSocialListResults[4], fanboysSocialListResults[5]});
5662
5663                         // Increment the blocked requests counters.
5664                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS);
5665                         nestedScrollWebView.incrementRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST);
5666
5667                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5668                         if (webViewDisplayed) {
5669                             // Updating the UI must be run from the UI thread.
5670                             activity.runOnUiThread(() -> {
5671                                 // Update the menu item titles.
5672                                 navigationRequestsMenuItem.setTitle(getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5673
5674                                 // Update the options menu if it has been populated.
5675                                 if (optionsMenu != null) {
5676                                     optionsMenu.findItem(R.id.blocklists).setTitle(getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
5677                                     optionsMenu.findItem(R.id.fanboys_social_blocking_list).setTitle(nestedScrollWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " +
5678                                             getString(R.string.fanboys_social_blocking_list));
5679                                 }
5680                             });
5681                         }
5682
5683                         // The resource request was blocked.  Return an empty web resource response.
5684                         return emptyWebResourceResponse;
5685                     } else if (fanboysSocialListResults[0].equals(BlocklistHelper.REQUEST_ALLOWED)) {  // The resource request matched Fanboy's Social Blocking List's whitelist.
5686                         // Update the whitelist result string array tracker.
5687                         whitelistResultStringArray = new String[] {fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5688                                 fanboysSocialListResults[4], fanboysSocialListResults[5]};
5689                     }
5690                 }
5691
5692                 // Add the request to the log because it hasn't been processed by any of the previous checks.
5693                 if (whitelistResultStringArray != null) {  // The request was processed by a whitelist.
5694                     nestedScrollWebView.addResourceRequest(whitelistResultStringArray);
5695                 } else {  // The request didn't match any blocklist entry.  Log it as a default request.
5696                     nestedScrollWebView.addResourceRequest(new String[]{BlocklistHelper.REQUEST_DEFAULT, url});
5697                 }
5698
5699                 // The resource request has not been blocked.  `return null` loads the requested resource.
5700                 return null;
5701             }
5702
5703             // Handle HTTP authentication requests.
5704             @Override
5705             public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
5706                 // Store the handler.
5707                 nestedScrollWebView.setHttpAuthHandler(handler);
5708
5709                 // Instantiate an HTTP authentication dialog.
5710                 DialogFragment httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.getWebViewFragmentId());
5711
5712                 // Show the HTTP authentication dialog.
5713                 httpAuthenticationDialogFragment.show(getSupportFragmentManager(), getString(R.string.http_authentication));
5714             }
5715
5716             @Override
5717             public void onPageStarted(WebView view, String url, Bitmap favicon) {
5718                 // Get the preferences.
5719                 boolean scrollAppBar = sharedPreferences.getBoolean("scroll_app_bar", true);
5720                 boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
5721
5722                 // Get a handler for the app bar layout.
5723                 AppBarLayout appBarLayout = findViewById(R.id.appbar_layout);
5724
5725                 // Set the top padding of the swipe refresh layout according to the app bar scrolling preference.
5726                 if (scrollAppBar) {
5727                     // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior.
5728                     swipeRefreshLayout.setPadding(0, 0, 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, defaultProgressViewEndOffset);
5732                 } else {
5733                     // Get the app bar layout height.  This can't be done in `applyAppSettings()` because the app bar is not yet populated.
5734                     int appBarHeight = appBarLayout.getHeight();
5735
5736                     // The swipe refresh layout must be manually moved below the app bar layout.
5737                     swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0);
5738
5739                     // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5740                     swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight);
5741                 }
5742
5743                 // Reset the list of resource requests.
5744                 nestedScrollWebView.clearResourceRequests();
5745
5746                 // Reset the requests counters.
5747                 nestedScrollWebView.resetRequestsCounters();
5748
5749                 // If night mode is enabled, hide `mainWebView` until after the night mode CSS is applied.
5750                 if (nestedScrollWebView.getNightMode()) {
5751                     nestedScrollWebView.setVisibility(View.INVISIBLE);
5752                 } else {
5753                     nestedScrollWebView.setVisibility(View.VISIBLE);
5754                 }
5755
5756                 // Hide the keyboard.
5757                 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.getWindowToken(), 0);
5758
5759                 // Check to see if Privacy Browser is waiting on Orbot.
5760                 if (!waitingForOrbot) {  // Process the URL.
5761                     // Get the current page position.
5762                     int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5763
5764                     // Update the URL text bar if the page is currently selected.
5765                     if (tabLayout.getSelectedTabPosition() == currentPagePosition) {
5766                         // Clear the focus from the URL edit text.
5767                         urlEditText.clearFocus();
5768
5769                         // Display the formatted URL text.
5770                         urlEditText.setText(url);
5771
5772                         // Apply text highlighting to `urlTextBox`.
5773                         highlightUrlText();
5774                     }
5775
5776                     // Reset the list of host IP addresses.
5777                     nestedScrollWebView.clearCurrentIpAddresses();
5778
5779                     // Get a URI for the current URL.
5780                     Uri currentUri = Uri.parse(url);
5781
5782                     // Get the IP addresses for the host.
5783                     new GetHostIpAddresses(activity, getSupportFragmentManager(), nestedScrollWebView).execute(currentUri.getHost());
5784
5785                     // Apply any custom domain settings if the URL was loaded by navigating history.
5786                     if (nestedScrollWebView.getNavigatingHistory()) {
5787                         // Reset navigating history.
5788                         nestedScrollWebView.setNavigatingHistory(false);
5789
5790                         // Apply the domain settings.
5791                         boolean userAgentChanged = applyDomainSettings(nestedScrollWebView, url, true, false);
5792
5793                         // Manually load the URL if the user agent has changed, which will have caused the previous URL to be reloaded.
5794                         if (userAgentChanged) {
5795                             loadUrl(url);
5796                         }
5797                     }
5798
5799                     // Replace Refresh with Stop if the options menu has been created.  (The first WebView typically begins loading before the menu items are instantiated.)
5800                     if (optionsMenu != null) {
5801                         // Get a handle for the refresh menu item.
5802                         MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5803
5804                         // Set the title.
5805                         refreshMenuItem.setTitle(R.string.stop);
5806
5807                         // Get the app bar and theme preferences.
5808                         boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
5809
5810                         // If the icon is displayed in the AppBar, set it according to the theme.
5811                         if (displayAdditionalAppBarIcons) {
5812                             if (darkTheme) {
5813                                 refreshMenuItem.setIcon(R.drawable.close_dark);
5814                             } else {
5815                                 refreshMenuItem.setIcon(R.drawable.close_light);
5816                             }
5817                         }
5818                     }
5819                 }
5820             }
5821
5822             @Override
5823             public void onPageFinished(WebView view, String url) {
5824                 // Flush any cookies to persistent storage.  The cookie manager has become very lazy about flushing cookies in recent versions.
5825                 if (nestedScrollWebView.getAcceptFirstPartyCookies() && Build.VERSION.SDK_INT >= 21) {
5826                     CookieManager.getInstance().flush();
5827                 }
5828
5829                 // Update the Refresh menu item if the options menu has been created.
5830                 if (optionsMenu != null) {
5831                     // Get a handle for the refresh menu item.
5832                     MenuItem refreshMenuItem = optionsMenu.findItem(R.id.refresh);
5833
5834                     // Reset the Refresh title.
5835                     refreshMenuItem.setTitle(R.string.refresh);
5836
5837                     // Get the app bar and theme preferences.
5838                     boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
5839                     boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
5840
5841                     // If the icon is displayed in the AppBar, reset it according to the theme.
5842                     if (displayAdditionalAppBarIcons) {
5843                         if (darkTheme) {
5844                             refreshMenuItem.setIcon(R.drawable.refresh_enabled_dark);
5845                         } else {
5846                             refreshMenuItem.setIcon(R.drawable.refresh_enabled_light);
5847                         }
5848                     }
5849                 }
5850
5851                 // Clear the cache and history if Incognito Mode is enabled.
5852                 if (incognitoModeEnabled) {
5853                     // Clear the cache.  `true` includes disk files.
5854                     nestedScrollWebView.clearCache(true);
5855
5856                     // Clear the back/forward history.
5857                     nestedScrollWebView.clearHistory();
5858
5859                     // Manually delete cache folders.
5860                     try {
5861                         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
5862                         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
5863                         String privateDataDirectoryString = getApplicationInfo().dataDir;
5864
5865                         // Delete the main cache directory.
5866                         Runtime.getRuntime().exec("rm -rf " + privateDataDirectoryString + "/cache");
5867
5868                         // Delete the secondary `Service Worker` cache directory.
5869                         // A `String[]` must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5870                         Runtime.getRuntime().exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Service Worker/"});
5871                     } catch (IOException e) {
5872                         // Do nothing if an error is thrown.
5873                     }
5874                 }
5875
5876                 // Update the URL text box and apply domain settings if not waiting on Orbot.
5877                 if (!waitingForOrbot) {
5878                     // Get the current page position.
5879                     int currentPagePosition = webViewPagerAdapter.getPositionForId(nestedScrollWebView.getWebViewFragmentId());
5880
5881                     // Check the current website information against any pinned domain information if the current IP addresses have been loaded.
5882                     if ((nestedScrollWebView.hasPinnedSslCertificate() || nestedScrollWebView.hasPinnedIpAddresses()) && nestedScrollWebView.hasCurrentIpAddresses() &&
5883                             !nestedScrollWebView.ignorePinnedDomainInformation()) {
5884                         CheckPinnedMismatchHelper.checkPinnedMismatch(getSupportFragmentManager(), nestedScrollWebView);
5885                     }
5886
5887                     // 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.
5888                     String currentUrl = nestedScrollWebView.getUrl();
5889
5890                     // Get the current tab.
5891                     TabLayout.Tab tab = tabLayout.getTabAt(currentPagePosition);
5892
5893                     // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text.
5894                     // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point.
5895                     // Probably some sort of race condition when Privacy Browser is being resumed.
5896                     if ((tabLayout.getSelectedTabPosition() == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) {
5897                         // Check to see if the URL is `about:blank`.
5898                         if (currentUrl.equals("about:blank")) {  // The WebView is blank.
5899                             // Display the hint in the URL edit text.
5900                             urlEditText.setText("");
5901
5902                             // Request focus for the URL text box.
5903                             urlEditText.requestFocus();
5904
5905                             // Display the keyboard.
5906                             inputMethodManager.showSoftInput(urlEditText, 0);
5907
5908                             // Apply the domain settings.  This clears any settings from the previous domain.
5909                             applyDomainSettings(nestedScrollWebView, "", true, false);
5910
5911                             // Only populate the title text view if the tab has been fully created.
5912                             if (tab != null) {
5913                                 // Get the custom view from the tab.
5914                                 View tabView = tab.getCustomView();
5915
5916                                 // Remove the incorrect warning below that the current tab view might be null.
5917                                 assert tabView != null;
5918
5919                                 // Get the title text view from the tab.
5920                                 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5921
5922                                 // Set the title as the tab text.
5923                                 tabTitleTextView.setText(R.string.new_tab);
5924                             }
5925                         } else {  // The WebView has loaded a webpage.
5926                             // Display the final URL.  Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
5927                             urlEditText.setText(currentUrl);
5928
5929                             // Apply text highlighting to the URL.
5930                             highlightUrlText();
5931
5932                             // Only populate the title text view if the tab has been fully created.
5933                             if (tab != null) {
5934                                 // Get the custom view from the tab.
5935                                 View tabView = tab.getCustomView();
5936
5937                                 // Remove the incorrect warning below that the current tab view might be null.
5938                                 assert tabView != null;
5939
5940                                 // Get the title text view from the tab.
5941                                 TextView tabTitleTextView = tabView.findViewById(R.id.title_textview);
5942
5943                                 // Set the title as the tab text.  Sometimes `onReceivedTitle()` is not called, especially when navigating history.
5944                                 tabTitleTextView.setText(nestedScrollWebView.getTitle());
5945                             }
5946                         }
5947                     }
5948                 }
5949             }
5950
5951             // Handle SSL Certificate errors.
5952             @Override
5953             public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
5954                 // Get the current website SSL certificate.
5955                 SslCertificate currentWebsiteSslCertificate = error.getCertificate();
5956
5957                 // Extract the individual pieces of information from the current website SSL certificate.
5958                 String currentWebsiteIssuedToCName = currentWebsiteSslCertificate.getIssuedTo().getCName();
5959                 String currentWebsiteIssuedToOName = currentWebsiteSslCertificate.getIssuedTo().getOName();
5960                 String currentWebsiteIssuedToUName = currentWebsiteSslCertificate.getIssuedTo().getUName();
5961                 String currentWebsiteIssuedByCName = currentWebsiteSslCertificate.getIssuedBy().getCName();
5962                 String currentWebsiteIssuedByOName = currentWebsiteSslCertificate.getIssuedBy().getOName();
5963                 String currentWebsiteIssuedByUName = currentWebsiteSslCertificate.getIssuedBy().getUName();
5964                 Date currentWebsiteSslStartDate = currentWebsiteSslCertificate.getValidNotBeforeDate();
5965                 Date currentWebsiteSslEndDate = currentWebsiteSslCertificate.getValidNotAfterDate();
5966
5967                 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5968                 if (nestedScrollWebView.hasPinnedSslCertificate()) {
5969                     // Get the pinned SSL certificate.
5970                     ArrayList<Object> pinnedSslCertificateArrayList = nestedScrollWebView.getPinnedSslCertificate();
5971
5972                     // Extract the arrays from the array list.
5973                     String[] pinnedSslCertificateStringArray = (String[]) pinnedSslCertificateArrayList.get(0);
5974                     Date[] pinnedSslCertificateDateArray = (Date[]) pinnedSslCertificateArrayList.get(1);
5975
5976                     // Check if the current SSL certificate matches the pinned certificate.
5977                     if (currentWebsiteIssuedToCName.equals(pinnedSslCertificateStringArray[0]) && currentWebsiteIssuedToOName.equals(pinnedSslCertificateStringArray[1]) &&
5978                         currentWebsiteIssuedToUName.equals(pinnedSslCertificateStringArray[2]) && currentWebsiteIssuedByCName.equals(pinnedSslCertificateStringArray[3]) &&
5979                         currentWebsiteIssuedByOName.equals(pinnedSslCertificateStringArray[4]) && currentWebsiteIssuedByUName.equals(pinnedSslCertificateStringArray[5]) &&
5980                         currentWebsiteSslStartDate.equals(pinnedSslCertificateDateArray[0]) && currentWebsiteSslEndDate.equals(pinnedSslCertificateDateArray[1])) {
5981
5982                         // An SSL certificate is pinned and matches the current domain certificate.  Proceed to the website without displaying an error.
5983                         handler.proceed();
5984                     }
5985                 } else {  // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5986                     // Store the SSL error handler.
5987                     nestedScrollWebView.setSslErrorHandler(handler);
5988
5989                     // Instantiate an SSL certificate error alert dialog.
5990                     DialogFragment sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.getWebViewFragmentId());
5991
5992                     // Show the SSL certificate error dialog.
5993                     sslCertificateErrorDialogFragment.show(getSupportFragmentManager(), getString(R.string.ssl_certificate_error));
5994                 }
5995             }
5996         });
5997
5998         // Check to see if this is the first page.
5999         if (pageNumber == 0) {
6000             // Set this nested scroll WebView as the current WebView.
6001             currentWebView = nestedScrollWebView;
6002
6003             // Apply the app settings from the shared preferences.
6004             applyAppSettings();
6005
6006             // Load the website if not waiting for Orbot to connect.
6007             if (!waitingForOrbot) {
6008                 // Get the intent that started the app.
6009                 Intent launchingIntent = getIntent();
6010
6011                 // Get the information from the intent.
6012                 String launchingIntentAction = launchingIntent.getAction();
6013                 Uri launchingIntentUriData = launchingIntent.getData();
6014
6015                 // If the intent action is a web search, perform the search.
6016                 if ((launchingIntentAction != null) && launchingIntentAction.equals(Intent.ACTION_WEB_SEARCH)) {
6017                     // Create an encoded URL string.
6018                     String encodedUrlString;
6019
6020                     // Sanitize the search input and convert it to a search.
6021                     try {
6022                         encodedUrlString = URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8");
6023                     } catch (UnsupportedEncodingException exception) {
6024                         encodedUrlString = "";
6025                     }
6026
6027                     // Load the completed search URL.
6028                     loadUrl(searchURL + encodedUrlString);
6029                 } else if (launchingIntentUriData != null){  // Check to see if the intent contains a new URL.
6030                     // Load the URL from the intent.
6031                     loadUrl(launchingIntentUriData.toString());
6032                 } else {  // The is no URL in the intent.
6033                     // Select the homepage based on the proxy through Orbot status.
6034                     if (proxyThroughOrbot) {
6035                         // Load the Tor homepage.
6036                         loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
6037                     } else {
6038                         // Load the normal homepage.
6039                         loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
6040                     }
6041                 }
6042             }
6043         } else {  // This is not the first tab.
6044             // Apply the domain settings.
6045             applyDomainSettings(nestedScrollWebView, url, false, false);
6046
6047             // Load the URL.
6048             nestedScrollWebView.loadUrl(url, customHeaders);
6049
6050             // Set the focus and display the keyboard if the URL is blank.
6051             if (url.equals("")) {
6052                 // Request focus for the URL text box.
6053                 urlEditText.requestFocus();
6054
6055                 // Create a display keyboard handler.
6056                 Handler displayKeyboardHandler = new Handler();
6057
6058                 // Create a display keyboard runnable.
6059                 Runnable displayKeyboardRunnable = () -> {
6060                     // Display the keyboard.
6061                     inputMethodManager.showSoftInput(urlEditText, 0);
6062                 };
6063
6064                 // Display the keyboard after 100 milliseconds, which leaves enough time for the tab to transition.
6065                 displayKeyboardHandler.postDelayed(displayKeyboardRunnable, 100);
6066             }
6067         }
6068     }
6069 }