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