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