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