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