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