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