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