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