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