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