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