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