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