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