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