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