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