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