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