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