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