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