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