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