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