d44636432e2057a9d321fb4facf72b95e9e89fd7
[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.animation.ObjectAnimator;
25 import android.annotation.SuppressLint;
26 import android.app.Activity;
27 import android.app.Dialog;
28 import android.app.DownloadManager;
29 import android.app.SearchManager;
30 import android.content.ActivityNotFoundException;
31 import android.content.BroadcastReceiver;
32 import android.content.ClipData;
33 import android.content.ClipboardManager;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.IntentFilter;
37 import android.content.SharedPreferences;
38 import android.content.pm.PackageManager;
39 import android.content.res.Configuration;
40 import android.database.Cursor;
41 import android.graphics.Bitmap;
42 import android.graphics.BitmapFactory;
43 import android.graphics.Typeface;
44 import android.graphics.drawable.BitmapDrawable;
45 import android.graphics.drawable.Drawable;
46 import android.net.Uri;
47 import android.net.http.SslCertificate;
48 import android.net.http.SslError;
49 import android.os.AsyncTask;
50 import android.os.Build;
51 import android.os.Bundle;
52 import android.os.Environment;
53 import android.os.Handler;
54 import android.os.Message;
55 import android.preference.PreferenceManager;
56 import android.print.PrintDocumentAdapter;
57 import android.print.PrintManager;
58 import android.provider.DocumentsContract;
59 import android.provider.OpenableColumns;
60 import android.text.Editable;
61 import android.text.Spanned;
62 import android.text.TextWatcher;
63 import android.text.style.ForegroundColorSpan;
64 import android.util.Patterns;
65 import android.util.TypedValue;
66 import android.view.ContextMenu;
67 import android.view.GestureDetector;
68 import android.view.KeyEvent;
69 import android.view.Menu;
70 import android.view.MenuItem;
71 import android.view.MotionEvent;
72 import android.view.View;
73 import android.view.ViewGroup;
74 import android.view.WindowManager;
75 import android.view.inputmethod.InputMethodManager;
76 import android.webkit.CookieManager;
77 import android.webkit.HttpAuthHandler;
78 import android.webkit.SslErrorHandler;
79 import android.webkit.ValueCallback;
80 import android.webkit.WebBackForwardList;
81 import android.webkit.WebChromeClient;
82 import android.webkit.WebResourceResponse;
83 import android.webkit.WebSettings;
84 import android.webkit.WebStorage;
85 import android.webkit.WebView;
86 import android.webkit.WebViewClient;
87 import android.webkit.WebViewDatabase;
88 import android.widget.ArrayAdapter;
89 import android.widget.CheckBox;
90 import android.widget.CursorAdapter;
91 import android.widget.EditText;
92 import android.widget.FrameLayout;
93 import android.widget.ImageView;
94 import android.widget.LinearLayout;
95 import android.widget.ListView;
96 import android.widget.ProgressBar;
97 import android.widget.RadioButton;
98 import android.widget.RelativeLayout;
99 import android.widget.TextView;
100
101 import androidx.activity.result.ActivityResultCallback;
102 import androidx.activity.result.ActivityResultLauncher;
103 import androidx.activity.result.contract.ActivityResultContracts;
104 import androidx.annotation.NonNull;
105 import androidx.appcompat.app.ActionBar;
106 import androidx.appcompat.app.ActionBarDrawerToggle;
107 import androidx.appcompat.app.AppCompatActivity;
108 import androidx.appcompat.app.AppCompatDelegate;
109 import androidx.appcompat.widget.Toolbar;
110 import androidx.coordinatorlayout.widget.CoordinatorLayout;
111 import androidx.core.content.res.ResourcesCompat;
112 import androidx.core.view.GravityCompat;
113 import androidx.drawerlayout.widget.DrawerLayout;
114 import androidx.fragment.app.DialogFragment;
115 import androidx.fragment.app.Fragment;
116 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
117 import androidx.viewpager.widget.ViewPager;
118 import androidx.webkit.WebSettingsCompat;
119 import androidx.webkit.WebViewFeature;
120
121 import com.google.android.material.appbar.AppBarLayout;
122 import com.google.android.material.floatingactionbutton.FloatingActionButton;
123 import com.google.android.material.navigation.NavigationView;
124 import com.google.android.material.snackbar.Snackbar;
125 import com.google.android.material.tabs.TabLayout;
126
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.CreateBookmarkDialog;
136 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
137 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
138 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
139 import com.stoutner.privacybrowser.dialogs.FontSizeDialog;
140 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
141 import com.stoutner.privacybrowser.dialogs.OpenDialog;
142 import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog;
143 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog;
144 import com.stoutner.privacybrowser.dialogs.SaveDialog;
145 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
146 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
147 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
148 import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog;
149 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
150 import com.stoutner.privacybrowser.helpers.BlocklistHelper;
151 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
152 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
153 import com.stoutner.privacybrowser.helpers.ProxyHelper;
154 import com.stoutner.privacybrowser.views.NestedScrollWebView;
155
156 import java.io.ByteArrayInputStream;
157 import java.io.ByteArrayOutputStream;
158 import java.io.File;
159 import java.io.FileInputStream;
160 import java.io.FileOutputStream;
161 import java.io.IOException;
162 import java.io.InputStream;
163 import java.io.OutputStream;
164 import java.io.UnsupportedEncodingException;
165
166 import java.net.MalformedURLException;
167 import java.net.URL;
168 import java.net.URLDecoder;
169 import java.net.URLEncoder;
170
171 import java.text.NumberFormat;
172
173 import java.util.ArrayList;
174 import java.util.Date;
175 import java.util.HashMap;
176 import java.util.HashSet;
177 import java.util.List;
178 import java.util.Map;
179 import java.util.Objects;
180 import java.util.Set;
181 import java.util.concurrent.ExecutorService;
182 import java.util.concurrent.Executors;
183
184 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
185         EditBookmarkFolderDialog.EditBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener, NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener,
186         PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklists.PopulateBlocklistsListener, SaveDialog.SaveListener, UrlHistoryDialog.NavigateHistoryListener,
187         WebViewTabFragment.NewTabListener {
188
189     // Define the public static variables.
190     public static ExecutorService executorService = Executors.newFixedThreadPool(4);
191     public static String orbotStatus = "unknown";
192     public static ArrayList<PendingDialog> pendingDialogsArrayList =  new ArrayList<>();
193     public static String proxyMode = ProxyHelper.NONE;
194
195     // Declare the public static variables.
196     public static String currentBookmarksFolder;
197     public static boolean restartFromBookmarksActivity;
198     public static WebViewPagerAdapter webViewPagerAdapter;
199
200     // Declare the public static views.
201     public static AppBarLayout appBarLayout;
202
203     // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
204     public final static int UNRECOGNIZED_USER_AGENT = -1;
205     public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
206     public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
207     public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
208     public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
209     public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
210
211     // Define the start activity for result request codes.  The public static entry is accessed from `OpenDialog()`.
212     private final int BROWSE_FILE_UPLOAD_REQUEST_CODE = 0;
213     public final static int BROWSE_OPEN_REQUEST_CODE = 1;
214
215     // Define the saved instance state constants.
216     private final String SAVED_STATE_ARRAY_LIST = "saved_state_array_list";
217     private final String SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list";
218     private final String SAVED_TAB_POSITION = "saved_tab_position";
219     private final String PROXY_MODE = "proxy_mode";
220
221     // Define the saved instance state variables.
222     private ArrayList<Bundle> savedStateArrayList;
223     private ArrayList<Bundle> savedNestedScrollWebViewStateArrayList;
224     private int savedTabPosition;
225     private String savedProxyMode;
226
227     // Define the class variables.
228     @SuppressWarnings("rawtypes")
229     AsyncTask populateBlocklists;
230
231     // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
232     // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxy()`, and `applyDomainSettings()`.
233     private NestedScrollWebView currentWebView;
234
235     // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
236     private final Map<String, String> customHeaders = new HashMap<>();
237
238     // The search URL is set in `applyAppSettings()` and used in `onNewIntent()`, `loadUrlFromTextBox()`, `initializeApp()`, and `initializeWebView()`.
239     private String searchURL;
240
241     // The blocklists are populated in `finishedPopulatingBlocklists()` and accessed from `initializeWebView()`.
242     private ArrayList<List<String[]>> easyList;
243     private ArrayList<List<String[]>> easyPrivacy;
244     private ArrayList<List<String[]>> fanboysAnnoyanceList;
245     private ArrayList<List<String[]>> fanboysSocialList;
246     private ArrayList<List<String[]>> ultraList;
247     private ArrayList<List<String[]>> ultraPrivacy;
248
249     // The action bar drawer toggle is initialized in `onCreate()` and used in `onResume()`.
250     private ActionBarDrawerToggle actionBarDrawerToggle;
251
252     // The color spans are used in `onCreate()` and `highlightUrlText()`.
253     private ForegroundColorSpan redColorSpan;
254     private ForegroundColorSpan initialGrayColorSpan;
255     private ForegroundColorSpan finalGrayColorSpan;
256
257     // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
258     private Cursor bookmarksCursor;
259
260     // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
261     private CursorAdapter bookmarksCursorAdapter;
262
263     // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
264     private String oldFolderNameString;
265
266     // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
267     private ValueCallback<Uri[]> fileChooserCallback;
268
269     // The default progress view offsets are set in `onCreate()` and used in `initializeWebView()`.
270     private int appBarHeight;
271     private int defaultProgressViewStartOffset;
272     private int defaultProgressViewEndOffset;
273
274     // The URL sanitizers are set in `applyAppSettings()` and used in `sanitizeUrl()`.
275     private boolean sanitizeGoogleAnalytics;
276     private boolean sanitizeFacebookClickIds;
277     private boolean sanitizeTwitterAmpRedirects;
278
279     // Declare the class variables
280     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
281     private boolean bottomAppBar;
282     private boolean displayingFullScreenVideo;
283     private boolean downloadWithExternalApp;
284     private boolean fullScreenBrowsingModeEnabled;
285     private boolean hideAppBar;
286     private boolean incognitoModeEnabled;
287     private boolean inFullScreenBrowsingMode;
288     private boolean loadingNewIntent;
289     private BroadcastReceiver orbotStatusBroadcastReceiver;
290     private ProxyHelper proxyHelper;
291     private boolean reapplyAppSettingsOnRestart;
292     private boolean reapplyDomainSettingsOnRestart;
293     private boolean scrollAppBar;
294     private boolean waitingForProxy;
295     private String webViewDefaultUserAgent;
296
297     // Define the class variables.
298     private ObjectAnimator objectAnimator = new ObjectAnimator();
299     private String saveUrlString = "";
300
301     // Declare the class views.
302     private FrameLayout rootFrameLayout;
303     private DrawerLayout drawerLayout;
304     private CoordinatorLayout coordinatorLayout;
305     private Toolbar toolbar;
306     private RelativeLayout urlRelativeLayout;
307     private EditText urlEditText;
308     private ActionBar actionBar;
309     private LinearLayout findOnPageLinearLayout;
310     private LinearLayout tabsLinearLayout;
311     private TabLayout tabLayout;
312     private SwipeRefreshLayout swipeRefreshLayout;
313     private ViewPager webViewPager;
314     private FrameLayout fullScreenVideoFrameLayout;
315
316     // Declare the class menus.
317     private Menu optionsMenu;
318
319     // Declare the class menu items.
320     private MenuItem navigationBackMenuItem;
321     private MenuItem navigationForwardMenuItem;
322     private MenuItem navigationHistoryMenuItem;
323     private MenuItem navigationRequestsMenuItem;
324     private MenuItem optionsPrivacyMenuItem;
325     private MenuItem optionsRefreshMenuItem;
326     private MenuItem optionsCookiesMenuItem;
327     private MenuItem optionsDomStorageMenuItem;
328     private MenuItem optionsSaveFormDataMenuItem;
329     private MenuItem optionsClearDataMenuItem;
330     private MenuItem optionsClearCookiesMenuItem;
331     private MenuItem optionsClearDomStorageMenuItem;
332     private MenuItem optionsClearFormDataMenuItem;
333     private MenuItem optionsBlocklistsMenuItem;
334     private MenuItem optionsEasyListMenuItem;
335     private MenuItem optionsEasyPrivacyMenuItem;
336     private MenuItem optionsFanboysAnnoyanceListMenuItem;
337     private MenuItem optionsFanboysSocialBlockingListMenuItem;
338     private MenuItem optionsUltraListMenuItem;
339     private MenuItem optionsUltraPrivacyMenuItem;
340     private MenuItem optionsBlockAllThirdPartyRequestsMenuItem;
341     private MenuItem optionsProxyMenuItem;
342     private MenuItem optionsProxyNoneMenuItem;
343     private MenuItem optionsProxyTorMenuItem;
344     private MenuItem optionsProxyI2pMenuItem;
345     private MenuItem optionsProxyCustomMenuItem;
346     private MenuItem optionsUserAgentMenuItem;
347     private MenuItem optionsUserAgentPrivacyBrowserMenuItem;
348     private MenuItem optionsUserAgentWebViewDefaultMenuItem;
349     private MenuItem optionsUserAgentFirefoxOnAndroidMenuItem;
350     private MenuItem optionsUserAgentChromeOnAndroidMenuItem;
351     private MenuItem optionsUserAgentSafariOnIosMenuItem;
352     private MenuItem optionsUserAgentFirefoxOnLinuxMenuItem;
353     private MenuItem optionsUserAgentChromiumOnLinuxMenuItem;
354     private MenuItem optionsUserAgentFirefoxOnWindowsMenuItem;
355     private MenuItem optionsUserAgentChromeOnWindowsMenuItem;
356     private MenuItem optionsUserAgentEdgeOnWindowsMenuItem;
357     private MenuItem optionsUserAgentInternetExplorerOnWindowsMenuItem;
358     private MenuItem optionsUserAgentSafariOnMacosMenuItem;
359     private MenuItem optionsUserAgentCustomMenuItem;
360     private MenuItem optionsSwipeToRefreshMenuItem;
361     private MenuItem optionsWideViewportMenuItem;
362     private MenuItem optionsDisplayImagesMenuItem;
363     private MenuItem optionsDarkWebViewMenuItem;
364     private MenuItem optionsFontSizeMenuItem;
365     private MenuItem optionsAddOrEditDomainMenuItem;
366
367     // This variable won't be needed once the class is migrated to Kotlin, as can be seen in LogcatActivity or AboutVersionFragment.
368     private Activity resultLauncherActivityHandle;
369
370     // Define the save URL activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
371     private final ActivityResultLauncher<String> saveUrlActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument(),
372             new ActivityResultCallback<Uri>() {
373                 @Override
374                 public void onActivityResult(Uri fileUri) {
375                     // Only save the URL if the file URI is not null, which happens if the user exited the file picker by pressing back.
376                     if (fileUri != null) {
377                         new SaveUrl(getApplicationContext(), resultLauncherActivityHandle, fileUri, currentWebView.getSettings().getUserAgentString(), currentWebView.getAcceptCookies()).execute(saveUrlString);
378                     }
379
380                     // Reset the save URL string.
381                     saveUrlString = "";
382                 }
383             });
384
385     // Define the save webpage archive activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
386     private final ActivityResultLauncher<String> saveWebpageArchiveActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument(),
387             new ActivityResultCallback<Uri>() {
388                 @Override
389                 public void onActivityResult(Uri fileUri) {
390                     // Only save the webpage archive if the file URI is not null, which happens if the user exited the file picker by pressing back.
391                     if (fileUri != null) {
392                         try {
393                             // Create a temporary MHT file.
394                             File temporaryMhtFile = File.createTempFile("temporary_mht_file", ".mht", getCacheDir());
395
396                             // Save the temporary MHT file.
397                             currentWebView.saveWebArchive(temporaryMhtFile.toString(), false, callbackValue -> {
398                                 if (callbackValue != null) {  // The temporary MHT file was saved successfully.
399                                     try {
400                                         // Create a temporary MHT file input stream.
401                                         FileInputStream temporaryMhtFileInputStream = new FileInputStream(temporaryMhtFile);
402
403                                         // Get an output stream for the save webpage file path.
404                                         OutputStream mhtOutputStream = getContentResolver().openOutputStream(fileUri);
405
406                                         // Create a transfer byte array.
407                                         byte[] transferByteArray = new byte[1024];
408
409                                         // Create an integer to track the number of bytes read.
410                                         int bytesRead;
411
412                                         // Copy the temporary MHT file input stream to the MHT output stream.
413                                         while ((bytesRead = temporaryMhtFileInputStream.read(transferByteArray)) > 0) {
414                                             mhtOutputStream.write(transferByteArray, 0, bytesRead);
415                                         }
416
417                                         // Close the streams.
418                                         mhtOutputStream.close();
419                                         temporaryMhtFileInputStream.close();
420
421                                         // Initialize the file name string from the file URI last path segment.
422                                         String fileNameString = fileUri.getLastPathSegment();
423
424                                         // Query the exact file name if the API >= 26.
425                                         if (Build.VERSION.SDK_INT >= 26) {
426                                             // Get a cursor from the content resolver.
427                                             Cursor contentResolverCursor = resultLauncherActivityHandle.getContentResolver().query(fileUri, null, null, null);
428
429                                             // Move to the fist row.
430                                             contentResolverCursor.moveToFirst();
431
432                                             // Get the file name from the cursor.
433                                             fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
434
435                                             // Close the cursor.
436                                             contentResolverCursor.close();
437                                         }
438
439                                         // Display a snackbar.
440                                         Snackbar.make(currentWebView, getString(R.string.file_saved) + "  " + fileNameString, Snackbar.LENGTH_SHORT).show();
441                                     } catch (Exception exception) {
442                                         // Display a snackbar with the exception.
443                                         Snackbar.make(currentWebView, getString(R.string.error_saving_file) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
444                                     } finally {
445                                         // Delete the temporary MHT file.
446                                         //noinspection ResultOfMethodCallIgnored
447                                         temporaryMhtFile.delete();
448                                     }
449                                 } else {  // There was an unspecified error while saving the temporary MHT file.
450                                     // Display an error snackbar.
451                                     Snackbar.make(currentWebView, getString(R.string.error_saving_file), Snackbar.LENGTH_INDEFINITE).show();
452                                 }
453                             });
454                         } catch (IOException ioException) {
455                             // Display a snackbar with the IO exception.
456                             Snackbar.make(currentWebView, getString(R.string.error_saving_file) + "  " + ioException.toString(), Snackbar.LENGTH_INDEFINITE).show();
457                         }
458                     }
459                 }
460             });
461
462     // Define the save webpage image activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
463     private final ActivityResultLauncher<String> saveWebpageImageActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument(),
464             new ActivityResultCallback<Uri>() {
465                 @Override
466                 public void onActivityResult(Uri fileUri) {
467                     // Only save the webpage image if the file URI is not null, which happens if the user exited the file picker by pressing back.
468                     if (fileUri != null) {
469                         // Save the webpage image.
470                         new SaveWebpageImage(resultLauncherActivityHandle, fileUri, currentWebView).execute();
471                     }
472                 }
473             });
474
475     // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with WebView.
476     @SuppressLint("ClickableViewAccessibility")
477     @Override
478     protected void onCreate(Bundle savedInstanceState) {
479         // Run the default commands.
480         super.onCreate(savedInstanceState);
481
482         // Populate the result launcher activity.  This will no longer be needed once the activity has transitioned to Kotlin.
483         resultLauncherActivityHandle = this;
484
485         // Check to see if the activity has been restarted.
486         if (savedInstanceState != null) {
487             // Store the saved instance state variables.
488             savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST);
489             savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST);
490             savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION);
491             savedProxyMode = savedInstanceState.getString(PROXY_MODE);
492         }
493
494         // Initialize the default preference values the first time the program is run.  `false` keeps this command from resetting any current preferences back to default.
495         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
496
497         // Get a handle for the shared preferences.
498         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
499
500         // Get the preferences.
501         String appTheme = sharedPreferences.getString("app_theme", getString(R.string.app_theme_default_value));
502         boolean allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false);
503         bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false);
504
505         // Get the theme entry values string array.
506         String[] appThemeEntryValuesStringArray = getResources().getStringArray(R.array.app_theme_entry_values);
507
508         // 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.
509         if (appTheme.equals(appThemeEntryValuesStringArray[1])) {  // The light theme is selected.
510             // Apply the light theme.
511             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
512         } else if (appTheme.equals(appThemeEntryValuesStringArray[2])) {  // The dark theme is selected.
513             // Apply the dark theme.
514             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
515         } else {  // The system default theme is selected.
516             if (Build.VERSION.SDK_INT >= 28) {  // The system default theme is supported.
517                 // Follow the system default theme.
518                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
519             } else {  // The system default theme is not supported.
520                 // Follow the battery saver mode.
521                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
522             }
523         }
524
525         // Disable screenshots if not allowed.
526         if (!allowScreenshots) {
527             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
528         }
529
530         // 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.
531         if (Build.VERSION.SDK_INT >= 21) {
532             WebView.enableSlowWholeDocumentDraw();
533         }
534
535         // Set the theme.
536         setTheme(R.style.PrivacyBrowser);
537
538         // Set the content view.
539         if (bottomAppBar) {
540             setContentView(R.layout.main_framelayout_bottom_appbar);
541         } else {
542             setContentView(R.layout.main_framelayout_top_appbar);
543         }
544
545         // Get handles for the views.
546         rootFrameLayout = findViewById(R.id.root_framelayout);
547         drawerLayout = findViewById(R.id.drawerlayout);
548         coordinatorLayout = findViewById(R.id.coordinatorlayout);
549         appBarLayout = findViewById(R.id.appbar_layout);
550         toolbar = findViewById(R.id.toolbar);
551         findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
552         tabsLinearLayout = findViewById(R.id.tabs_linearlayout);
553         tabLayout = findViewById(R.id.tablayout);
554         swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
555         webViewPager = findViewById(R.id.webviewpager);
556         NavigationView navigationView = findViewById(R.id.navigationview);
557         fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout);
558
559         // Get a handle for the navigation menu.
560         Menu navigationMenu = navigationView.getMenu();
561
562         // Get handles for the navigation menu items.
563         navigationBackMenuItem = navigationMenu.findItem(R.id.back);
564         navigationForwardMenuItem = navigationMenu.findItem(R.id.forward);
565         navigationHistoryMenuItem = navigationMenu.findItem(R.id.history);
566         navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests);
567
568         // Listen for touches on the navigation menu.
569         navigationView.setNavigationItemSelectedListener(this);
570
571         // Get a handle for the app compat delegate.
572         AppCompatDelegate appCompatDelegate = getDelegate();
573
574         // Set the support action bar.
575         appCompatDelegate.setSupportActionBar(toolbar);
576
577         // Get a handle for the action bar.
578         actionBar = appCompatDelegate.getSupportActionBar();
579
580         // Remove the incorrect lint warning below that the action bar might be null.
581         assert actionBar != null;
582
583         // Add the custom layout, which shows the URL text bar.
584         actionBar.setCustomView(R.layout.url_app_bar);
585         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
586
587         // Get handles for the views in the URL app bar.
588         urlRelativeLayout = findViewById(R.id.url_relativelayout);
589         urlEditText = findViewById(R.id.url_edittext);
590
591         // Create the hamburger icon at the start of the AppBar.
592         actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
593
594         // Initially disable the sliding drawers.  They will be enabled once the blocklists are loaded.
595         drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
596
597         // Initialize the web view pager adapter.
598         webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
599
600         // Set the pager adapter on the web view pager.
601         webViewPager.setAdapter(webViewPagerAdapter);
602
603         // Store up to 100 tabs in memory.
604         webViewPager.setOffscreenPageLimit(100);
605
606         // Instantiate the proxy helper.
607         proxyHelper = new ProxyHelper();
608
609         // Initialize the app.
610         initializeApp();
611
612         // Apply the app settings from the shared preferences.
613         applyAppSettings();
614
615         // Populate the blocklists.
616         populateBlocklists = new PopulateBlocklists(this, this).execute();
617     }
618
619     @Override
620     protected void onNewIntent(Intent intent) {
621         // Run the default commands.
622         super.onNewIntent(intent);
623
624         // Replace the intent that started the app with this one.
625         setIntent(intent);
626
627         // Check to see if the app is being restarted from a saved state.
628         if (savedStateArrayList == null || savedStateArrayList.size() == 0) {  // The activity is not being restarted from a saved state.
629             // Get the information from the intent.
630             String intentAction = intent.getAction();
631             Uri intentUriData = intent.getData();
632             String intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
633
634             // Determine if this is a web search.
635             boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH));
636
637             // 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.
638             if (intentUriData != null || intentStringExtra != null || isWebSearch) {
639                 // Exit the full screen video if it is displayed.
640                 if (displayingFullScreenVideo) {
641                     // Exit full screen video mode.
642                     exitFullScreenVideo();
643
644                     // Reload the current WebView.  Otherwise, it can display entirely black.
645                     currentWebView.reload();
646                 }
647
648                 // Get the shared preferences.
649                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
650
651                 // Create a URL string.
652                 String url;
653
654                 // If the intent action is a web search, perform the search.
655                 if (isWebSearch) {  // The intent is a web search.
656                     // Create an encoded URL string.
657                     String encodedUrlString;
658
659                     // Sanitize the search input and convert it to a search.
660                     try {
661                         encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
662                     } catch (UnsupportedEncodingException exception) {
663                         encodedUrlString = "";
664                     }
665
666                     // Add the base search URL.
667                     url = searchURL + encodedUrlString;
668                 } else if (intentUriData != null) {  // The intent contains a URL formatted as a URI.
669                     // Set the intent data as the URL.
670                     url = intentUriData.toString();
671                 } else {  // The intent contains a string, which might be a URL.
672                     // Set the intent string as the URL.
673                     url = intentStringExtra;
674                 }
675
676                 // Add a new tab if specified in the preferences.
677                 if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) {  // Load the URL in a new tab.
678                     // Set the loading new intent flag.
679                     loadingNewIntent = true;
680
681                     // Add a new tab.
682                     addNewTab(url, true);
683                 } else {  // Load the URL in the current tab.
684                     // Make it so.
685                     loadUrl(currentWebView, url);
686                 }
687
688                 // Close the navigation drawer if it is open.
689                 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
690                     drawerLayout.closeDrawer(GravityCompat.START);
691                 }
692
693                 // Close the bookmarks drawer if it is open.
694                 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
695                     drawerLayout.closeDrawer(GravityCompat.END);
696                 }
697             }
698         }
699     }
700
701     @Override
702     public void onRestart() {
703         // Run the default commands.
704         super.onRestart();
705
706         // Apply the app settings if returning from the Settings activity.
707         if (reapplyAppSettingsOnRestart) {
708             // Reset the reapply app settings on restart tracker.
709             reapplyAppSettingsOnRestart = false;
710
711             // Apply the app settings.
712             applyAppSettings();
713         }
714
715         // Apply the domain settings if returning from the settings or domains activity.
716         if (reapplyDomainSettingsOnRestart) {
717             // Reset the reapply domain settings on restart tracker.
718             reapplyDomainSettingsOnRestart = false;
719
720             // Reapply the domain settings for each tab.
721             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
722                 // Get the WebView tab fragment.
723                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
724
725                 // Get the fragment view.
726                 View fragmentView = webViewTabFragment.getView();
727
728                 // Only reload the WebViews if they exist.
729                 if (fragmentView != null) {
730                     // Get the nested scroll WebView from the tab fragment.
731                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
732
733                     // Reset the current domain name so the domain settings will be reapplied.
734                     nestedScrollWebView.setCurrentDomainName("");
735
736                     // Reapply the domain settings if the URL is not null, which can happen if an empty tab is active when returning from settings.
737                     if (nestedScrollWebView.getUrl() != null) {
738                         applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true, false);
739                     }
740                 }
741             }
742         }
743
744         // Update the bookmarks drawer if returning from the Bookmarks activity.
745         if (restartFromBookmarksActivity) {
746             // Close the bookmarks drawer.
747             drawerLayout.closeDrawer(GravityCompat.END);
748
749             // Reload the bookmarks drawer.
750             loadBookmarksFolder();
751
752             // Reset `restartFromBookmarksActivity`.
753             restartFromBookmarksActivity = false;
754         }
755
756         // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.  This can be important if the screen was rotated.
757         updatePrivacyIcons(true);
758     }
759
760     // `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.
761     @Override
762     public void onStart() {
763         // Run the default commands.
764         super.onStart();
765
766         // Resume any WebViews.
767         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
768             // Get the WebView tab fragment.
769             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
770
771             // Get the fragment view.
772             View fragmentView = webViewTabFragment.getView();
773
774             // Only resume the WebViews if they exist (they won't when the app is first created).
775             if (fragmentView != null) {
776                 // Get the nested scroll WebView from the tab fragment.
777                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
778
779                 // Resume the nested scroll WebView.
780                 nestedScrollWebView.onResume();
781             }
782         }
783
784         // Resume the nested scroll WebView JavaScript timers.  This is a global command that resumes JavaScript timers on all WebViews.
785         if (currentWebView != null) {
786             currentWebView.resumeTimers();
787         }
788
789         // Reapply the proxy settings if the system is using a proxy.  This redisplays the appropriate alert dialog.
790         if (!proxyMode.equals(ProxyHelper.NONE)) {
791             applyProxy(false);
792         }
793
794         // Reapply any system UI flags.
795         if (displayingFullScreenVideo || inFullScreenBrowsingMode) {  // The system is displaying a website or a video in full screen mode.
796             /* Hide the system bars.
797              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
798              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
799              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
800              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
801              */
802             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
803                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
804         }
805
806         // Show any pending dialogs.
807         for (int i = 0; i < pendingDialogsArrayList.size(); i++) {
808             // Get the pending dialog from the array list.
809             PendingDialog pendingDialog = pendingDialogsArrayList.get(i);
810
811             // Show the pending dialog.
812             pendingDialog.dialogFragment.show(getSupportFragmentManager(), pendingDialog.tag);
813         }
814
815         // Clear the pending dialogs array list.
816         pendingDialogsArrayList.clear();
817     }
818
819     // `onStop()` runs after `onPause()`.  It is used instead of `onPause()` so the commands are not called every time the screen is partially hidden.
820     @Override
821     public void onStop() {
822         // Run the default commands.
823         super.onStop();
824
825         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
826             // Get the WebView tab fragment.
827             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
828
829             // Get the fragment view.
830             View fragmentView = webViewTabFragment.getView();
831
832             // Only pause the WebViews if they exist (they won't when the app is first created).
833             if (fragmentView != null) {
834                 // Get the nested scroll WebView from the tab fragment.
835                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
836
837                 // Pause the nested scroll WebView.
838                 nestedScrollWebView.onPause();
839             }
840         }
841
842         // Pause the WebView JavaScript timers.  This is a global command that pauses JavaScript on all WebViews.
843         if (currentWebView != null) {
844             currentWebView.pauseTimers();
845         }
846     }
847
848     @Override
849     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
850         // Run the default commands.
851         super.onSaveInstanceState(savedInstanceState);
852
853         // Create the saved state array lists.
854         ArrayList<Bundle> savedStateArrayList = new ArrayList<>();
855         ArrayList<Bundle> savedNestedScrollWebViewStateArrayList = new ArrayList<>();
856
857         // Get the URLs from each tab.
858         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
859             // Get the WebView tab fragment.
860             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
861
862             // Get the fragment view.
863             View fragmentView = webViewTabFragment.getView();
864
865             if (fragmentView != null) {
866                 // Get the nested scroll WebView from the tab fragment.
867                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
868
869                 // Create saved state bundle.
870                 Bundle savedStateBundle = new Bundle();
871
872                 // Get the current states.
873                 nestedScrollWebView.saveState(savedStateBundle);
874                 Bundle savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState();
875
876                 // Store the saved states in the array lists.
877                 savedStateArrayList.add(savedStateBundle);
878                 savedNestedScrollWebViewStateArrayList.add(savedNestedScrollWebViewStateBundle);
879             }
880         }
881
882         // Get the current tab position.
883         int currentTabPosition = tabLayout.getSelectedTabPosition();
884
885         // Store the saved states in the bundle.
886         savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList);
887         savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList);
888         savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition);
889         savedInstanceState.putString(PROXY_MODE, proxyMode);
890     }
891
892     @Override
893     public void onDestroy() {
894         // Unregister the orbot status broadcast receiver if it exists.
895         if (orbotStatusBroadcastReceiver != null) {
896             this.unregisterReceiver(orbotStatusBroadcastReceiver);
897         }
898
899         // Close the bookmarks cursor if it exists.
900         if (bookmarksCursor != null) {
901             bookmarksCursor.close();
902         }
903
904         // Close the bookmarks database if it exists.
905         if (bookmarksDatabaseHelper != null) {
906             bookmarksDatabaseHelper.close();
907         }
908
909         // Stop populating the blocklists if the AsyncTask is running in the background.
910         if (populateBlocklists != null) {
911             populateBlocklists.cancel(true);
912         }
913
914         // Run the default commands.
915         super.onDestroy();
916     }
917
918     @Override
919     public boolean onCreateOptionsMenu(Menu menu) {
920         // Inflate the menu.  This adds items to the action bar if it is present.
921         getMenuInflater().inflate(R.menu.webview_options_menu, menu);
922
923         // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
924         optionsMenu = menu;
925
926         // Get handles for the menu items.
927         optionsPrivacyMenuItem = menu.findItem(R.id.javascript);
928         optionsRefreshMenuItem = menu.findItem(R.id.refresh);
929         MenuItem bookmarksMenuItem = menu.findItem(R.id.bookmarks);
930         optionsCookiesMenuItem = menu.findItem(R.id.cookies);
931         optionsDomStorageMenuItem = menu.findItem(R.id.dom_storage);
932         optionsSaveFormDataMenuItem = menu.findItem(R.id.save_form_data);  // Form data can be removed once the minimum API >= 26.
933         optionsClearDataMenuItem = menu.findItem(R.id.clear_data);
934         optionsClearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
935         optionsClearDomStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
936         optionsClearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
937         optionsBlocklistsMenuItem = menu.findItem(R.id.blocklists);
938         optionsEasyListMenuItem = menu.findItem(R.id.easylist);
939         optionsEasyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
940         optionsFanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
941         optionsFanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
942         optionsUltraListMenuItem = menu.findItem(R.id.ultralist);
943         optionsUltraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
944         optionsBlockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
945         optionsProxyMenuItem = menu.findItem(R.id.proxy);
946         optionsProxyNoneMenuItem = menu.findItem(R.id.proxy_none);
947         optionsProxyTorMenuItem = menu.findItem(R.id.proxy_tor);
948         optionsProxyI2pMenuItem = menu.findItem(R.id.proxy_i2p);
949         optionsProxyCustomMenuItem = menu.findItem(R.id.proxy_custom);
950         optionsUserAgentMenuItem = menu.findItem(R.id.user_agent);
951         optionsUserAgentPrivacyBrowserMenuItem = menu.findItem(R.id.user_agent_privacy_browser);
952         optionsUserAgentWebViewDefaultMenuItem = menu.findItem(R.id.user_agent_webview_default);
953         optionsUserAgentFirefoxOnAndroidMenuItem = menu.findItem(R.id.user_agent_firefox_on_android);
954         optionsUserAgentChromeOnAndroidMenuItem = menu.findItem(R.id.user_agent_chrome_on_android);
955         optionsUserAgentSafariOnIosMenuItem = menu.findItem(R.id.user_agent_safari_on_ios);
956         optionsUserAgentFirefoxOnLinuxMenuItem = menu.findItem(R.id.user_agent_firefox_on_linux);
957         optionsUserAgentChromiumOnLinuxMenuItem = menu.findItem(R.id.user_agent_chromium_on_linux);
958         optionsUserAgentFirefoxOnWindowsMenuItem = menu.findItem(R.id.user_agent_firefox_on_windows);
959         optionsUserAgentChromeOnWindowsMenuItem = menu.findItem(R.id.user_agent_chrome_on_windows);
960         optionsUserAgentEdgeOnWindowsMenuItem = menu.findItem(R.id.user_agent_edge_on_windows);
961         optionsUserAgentInternetExplorerOnWindowsMenuItem = menu.findItem(R.id.user_agent_internet_explorer_on_windows);
962         optionsUserAgentSafariOnMacosMenuItem = menu.findItem(R.id.user_agent_safari_on_macos);
963         optionsUserAgentCustomMenuItem = menu.findItem(R.id.user_agent_custom);
964         optionsSwipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
965         optionsWideViewportMenuItem = menu.findItem(R.id.wide_viewport);
966         optionsDisplayImagesMenuItem = menu.findItem(R.id.display_images);
967         optionsDarkWebViewMenuItem = menu.findItem(R.id.dark_webview);
968         optionsFontSizeMenuItem = menu.findItem(R.id.font_size);
969         optionsAddOrEditDomainMenuItem = menu.findItem(R.id.add_or_edit_domain);
970
971         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
972         updatePrivacyIcons(false);
973
974         // Only display the form data menu items if the API < 26.
975         optionsSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
976         optionsClearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
977
978         // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
979         optionsClearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
980
981         // Only display the dark WebView menu item if API >= 21.
982         optionsDarkWebViewMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
983
984         // Get the shared preferences.
985         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
986
987         // Get the dark theme and app bar preferences.
988         boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false);
989
990         // 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.
991         if (displayAdditionalAppBarIcons) {
992             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
993             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
994             optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
995         } else { //Do not display the additional icons.
996             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
997             bookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
998             optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
999         }
1000
1001         // Replace Refresh with Stop if a URL is already loading.
1002         if (currentWebView != null && currentWebView.getProgress() != 100) {
1003             // Set the title.
1004             optionsRefreshMenuItem.setTitle(R.string.stop);
1005
1006             // Set the icon if it is displayed in the app bar.
1007             if (displayAdditionalAppBarIcons) {
1008                 // Get the current theme status.
1009                 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
1010
1011                 // Set the icon according to the current theme status.
1012                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
1013                     optionsRefreshMenuItem.setIcon(R.drawable.close_blue_day);
1014                 } else {
1015                     optionsRefreshMenuItem.setIcon(R.drawable.close_blue_night);
1016                 }
1017             }
1018         }
1019
1020         // Done.
1021         return true;
1022     }
1023
1024     @Override
1025     public boolean onPrepareOptionsMenu(Menu menu) {
1026         // Get a handle for the cookie manager.
1027         CookieManager cookieManager = CookieManager.getInstance();
1028
1029         // Initialize the current user agent string and the font size.
1030         String currentUserAgent = getString(R.string.user_agent_privacy_browser);
1031         int fontSize = 100;
1032
1033         // 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.
1034         if (currentWebView != null) {
1035             // Set the add or edit domain text.
1036             if (currentWebView.getDomainSettingsApplied()) {
1037                 optionsAddOrEditDomainMenuItem.setTitle(R.string.edit_domain_settings);
1038             } else {
1039                 optionsAddOrEditDomainMenuItem.setTitle(R.string.add_domain_settings);
1040             }
1041
1042             // Get the current user agent from the WebView.
1043             currentUserAgent = currentWebView.getSettings().getUserAgentString();
1044
1045             // Get the current font size from the
1046             fontSize = currentWebView.getSettings().getTextZoom();
1047
1048             // Set the status of the menu item checkboxes.
1049             optionsDomStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1050             optionsSaveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData());  // Form data can be removed once the minimum API >= 26.
1051             optionsEasyListMenuItem.setChecked(currentWebView.getEasyListEnabled());
1052             optionsEasyPrivacyMenuItem.setChecked(currentWebView.getEasyPrivacyEnabled());
1053             optionsFanboysAnnoyanceListMenuItem.setChecked(currentWebView.getFanboysAnnoyanceListEnabled());
1054             optionsFanboysSocialBlockingListMenuItem.setChecked(currentWebView.getFanboysSocialBlockingListEnabled());
1055             optionsUltraListMenuItem.setChecked(currentWebView.getUltraListEnabled());
1056             optionsUltraPrivacyMenuItem.setChecked(currentWebView.getUltraPrivacyEnabled());
1057             optionsBlockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.getBlockAllThirdPartyRequests());
1058             optionsSwipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
1059             optionsWideViewportMenuItem.setChecked(currentWebView.getSettings().getUseWideViewPort());
1060             optionsDisplayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
1061
1062             // Initialize the display names for the blocklists with the number of blocked requests.
1063             optionsBlocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
1064             optionsEasyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYLIST) + " - " + getString(R.string.easylist));
1065             optionsEasyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASYPRIVACY) + " - " + getString(R.string.easyprivacy));
1066             optionsFanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
1067             optionsFanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
1068             optionsUltraListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRALIST) + " - " + getString(R.string.ultralist));
1069             optionsUltraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRAPRIVACY) + " - " + getString(R.string.ultraprivacy));
1070             optionsBlockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
1071
1072             // Enable DOM Storage if JavaScript is enabled.
1073             optionsDomStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
1074
1075             // Set the checkbox status for dark WebView if the WebView supports it.
1076             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
1077                 optionsDarkWebViewMenuItem.setChecked(WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON);
1078             }
1079         }
1080
1081         // Set the cookies menu item checked status.
1082         optionsCookiesMenuItem.setChecked(cookieManager.acceptCookie());
1083
1084         // Enable Clear Cookies if there are any.
1085         optionsClearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
1086
1087         // 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`.
1088         String privateDataDirectoryString = getApplicationInfo().dataDir;
1089
1090         // Get a count of the number of files in the Local Storage directory.
1091         File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
1092         int localStorageDirectoryNumberOfFiles = 0;
1093         if (localStorageDirectory.exists()) {
1094             // `Objects.requireNonNull` removes a lint warning that `localStorageDirectory.list` might produce a null pointed exception if it is dereferenced.
1095             localStorageDirectoryNumberOfFiles = Objects.requireNonNull(localStorageDirectory.list()).length;
1096         }
1097
1098         // Get a count of the number of files in the IndexedDB directory.
1099         File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
1100         int indexedDBDirectoryNumberOfFiles = 0;
1101         if (indexedDBDirectory.exists()) {
1102             // `Objects.requireNonNull` removes a lint warning that `indexedDBDirectory.list` might produce a null pointed exception if it is dereferenced.
1103             indexedDBDirectoryNumberOfFiles = Objects.requireNonNull(indexedDBDirectory.list()).length;
1104         }
1105
1106         // Enable Clear DOM Storage if there is any.
1107         optionsClearDomStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
1108
1109         // Enable Clear Form Data is there is any.  This can be removed once the minimum API >= 26.
1110         if (Build.VERSION.SDK_INT < 26) {
1111             // Get the WebView database.
1112             WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
1113
1114             // Enable the clear form data menu item if there is anything to clear.
1115             optionsClearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
1116         }
1117
1118         // Enable Clear Data if any of the submenu items are enabled.
1119         optionsClearDataMenuItem.setEnabled(optionsClearCookiesMenuItem.isEnabled() || optionsClearDomStorageMenuItem.isEnabled() || optionsClearFormDataMenuItem.isEnabled());
1120
1121         // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1122         optionsFanboysSocialBlockingListMenuItem.setEnabled(!optionsFanboysAnnoyanceListMenuItem.isChecked());
1123
1124         // Set the proxy title and check the applied proxy.
1125         switch (proxyMode) {
1126             case ProxyHelper.NONE:
1127                 // Set the proxy title.
1128                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_none));
1129
1130                 // Check the proxy None radio button.
1131                 optionsProxyNoneMenuItem.setChecked(true);
1132                 break;
1133
1134             case ProxyHelper.TOR:
1135                 // Set the proxy title.
1136                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_tor));
1137
1138                 // Check the proxy Tor radio button.
1139                 optionsProxyTorMenuItem.setChecked(true);
1140                 break;
1141
1142             case ProxyHelper.I2P:
1143                 // Set the proxy title.
1144                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_i2p));
1145
1146                 // Check the proxy I2P radio button.
1147                 optionsProxyI2pMenuItem.setChecked(true);
1148                 break;
1149
1150             case ProxyHelper.CUSTOM:
1151                 // Set the proxy title.
1152                 optionsProxyMenuItem.setTitle(getString(R.string.proxy) + " - " + getString(R.string.proxy_custom));
1153
1154                 // Check the proxy Custom radio button.
1155                 optionsProxyCustomMenuItem.setChecked(true);
1156                 break;
1157         }
1158
1159         // Select the current user agent menu item.  A switch statement cannot be used because the user agents are not compile time constants.
1160         if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) {  // Privacy Browser.
1161             // Update the user agent menu item title.
1162             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_privacy_browser));
1163
1164             // Select the Privacy Browser radio box.
1165             optionsUserAgentPrivacyBrowserMenuItem.setChecked(true);
1166         } else if (currentUserAgent.equals(webViewDefaultUserAgent)) {  // WebView Default.
1167             // Update the user agent menu item title.
1168             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_webview_default));
1169
1170             // Select the WebView Default radio box.
1171             optionsUserAgentWebViewDefaultMenuItem.setChecked(true);
1172         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) {  // Firefox on Android.
1173             // Update the user agent menu item title.
1174             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_android));
1175
1176             // Select the Firefox on Android radio box.
1177             optionsUserAgentFirefoxOnAndroidMenuItem.setChecked(true);
1178         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) {  // Chrome on Android.
1179             // Update the user agent menu item title.
1180             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_android));
1181
1182             // Select the Chrome on Android radio box.
1183             optionsUserAgentChromeOnAndroidMenuItem.setChecked(true);
1184         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) {  // Safari on iOS.
1185             // Update the user agent menu item title.
1186             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_ios));
1187
1188             // Select the Safari on iOS radio box.
1189             optionsUserAgentSafariOnIosMenuItem.setChecked(true);
1190         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) {  // Firefox on Linux.
1191             // Update the user agent menu item title.
1192             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_linux));
1193
1194             // Select the Firefox on Linux radio box.
1195             optionsUserAgentFirefoxOnLinuxMenuItem.setChecked(true);
1196         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) {  // Chromium on Linux.
1197             // Update the user agent menu item title.
1198             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chromium_on_linux));
1199
1200             // Select the Chromium on Linux radio box.
1201             optionsUserAgentChromiumOnLinuxMenuItem.setChecked(true);
1202         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) {  // Firefox on Windows.
1203             // Update the user agent menu item title.
1204             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_windows));
1205
1206             // Select the Firefox on Windows radio box.
1207             optionsUserAgentFirefoxOnWindowsMenuItem.setChecked(true);
1208         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) {  // Chrome on Windows.
1209             // Update the user agent menu item title.
1210             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_windows));
1211
1212             // Select the Chrome on Windows radio box.
1213             optionsUserAgentChromeOnWindowsMenuItem.setChecked(true);
1214         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) {  // Edge on Windows.
1215             // Update the user agent menu item title.
1216             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_edge_on_windows));
1217
1218             // Select the Edge on Windows radio box.
1219             optionsUserAgentEdgeOnWindowsMenuItem.setChecked(true);
1220         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) {  // Internet Explorer on Windows.
1221             // Update the user agent menu item title.
1222             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_internet_explorer_on_windows));
1223
1224             // Select the Internet on Windows radio box.
1225             optionsUserAgentInternetExplorerOnWindowsMenuItem.setChecked(true);
1226         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) {  // Safari on macOS.
1227             // Update the user agent menu item title.
1228             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_macos));
1229
1230             // Select the Safari on macOS radio box.
1231             optionsUserAgentSafariOnMacosMenuItem.setChecked(true);
1232         } else {  // Custom user agent.
1233             // Update the user agent menu item title.
1234             optionsUserAgentMenuItem.setTitle(getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_custom));
1235
1236             // Select the Custom radio box.
1237             optionsUserAgentCustomMenuItem.setChecked(true);
1238         }
1239
1240         // Set the font size title.
1241         optionsFontSizeMenuItem.setTitle(getString(R.string.font_size) + " - " + fontSize + "%");
1242
1243         // Run all the other default commands.
1244         super.onPrepareOptionsMenu(menu);
1245
1246         // Display the menu.
1247         return true;
1248     }
1249
1250     @Override
1251     public boolean onOptionsItemSelected(MenuItem menuItem) {
1252         // Get a handle for the shared preferences.
1253         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1254
1255         // Get a handle for the cookie manager.
1256         CookieManager cookieManager = CookieManager.getInstance();
1257
1258         // Get the selected menu item ID.
1259         int menuItemId = menuItem.getItemId();
1260
1261         // Run the commands that correlate to the selected menu item.
1262         if (menuItemId == R.id.javascript) {  // JavaScript.
1263             // Toggle the JavaScript status.
1264             currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
1265
1266             // Update the privacy icon.
1267             updatePrivacyIcons(true);
1268
1269             // Display a `Snackbar`.
1270             if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScrip is enabled.
1271                 Snackbar.make(webViewPager, R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
1272             } else if (cookieManager.acceptCookie()) {  // JavaScript is disabled, but first-party cookies are enabled.
1273                 Snackbar.make(webViewPager, R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
1274             } else {  // Privacy mode.
1275                 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1276             }
1277
1278             // Reload the current WebView.
1279             currentWebView.reload();
1280
1281             // Consume the event.
1282             return true;
1283         } else if (menuItemId == R.id.refresh) {  // Refresh.
1284             // Run the command that correlates to the current status of the menu item.
1285             if (menuItem.getTitle().equals(getString(R.string.refresh))) {  // The refresh button was pushed.
1286                 // Reload the current WebView.
1287                 currentWebView.reload();
1288             } else {  // The stop button was pushed.
1289                 // Stop the loading of the WebView.
1290                 currentWebView.stopLoading();
1291             }
1292
1293             // Consume the event.
1294             return true;
1295         } else if (menuItemId == R.id.bookmarks) {  // Bookmarks.
1296             // Open the bookmarks drawer.
1297             drawerLayout.openDrawer(GravityCompat.END);
1298
1299             // Consume the event.
1300             return true;
1301         } else if (menuItemId == R.id.cookies) {  // Cookies.
1302             // Switch the first-party cookie status.
1303             cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
1304
1305             // Store the cookie status.
1306             currentWebView.setAcceptCookies(cookieManager.acceptCookie());
1307
1308             // Update the menu checkbox.
1309             menuItem.setChecked(cookieManager.acceptCookie());
1310
1311             // Update the privacy icon.
1312             updatePrivacyIcons(true);
1313
1314             // Display a snackbar.
1315             if (cookieManager.acceptCookie()) {  // Cookies are enabled.
1316                 Snackbar.make(webViewPager, R.string.cookies_enabled, Snackbar.LENGTH_SHORT).show();
1317             } else if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is still enabled.
1318                 Snackbar.make(webViewPager, R.string.cookies_disabled, Snackbar.LENGTH_SHORT).show();
1319             } else {  // Privacy mode.
1320                 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1321             }
1322
1323             // Reload the current WebView.
1324             currentWebView.reload();
1325
1326             // Consume the event.
1327             return true;
1328         } else if (menuItemId == R.id.dom_storage) {  // DOM storage.
1329             // Toggle the status of domStorageEnabled.
1330             currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1331
1332             // Update the menu checkbox.
1333             menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1334
1335             // Update the privacy icon.
1336             updatePrivacyIcons(true);
1337
1338             // Display a snackbar.
1339             if (currentWebView.getSettings().getDomStorageEnabled()) {
1340                 Snackbar.make(webViewPager, R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1341             } else {
1342                 Snackbar.make(webViewPager, R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1343             }
1344
1345             // Reload the current WebView.
1346             currentWebView.reload();
1347
1348             // Consume the event.
1349             return true;
1350         } else if (menuItemId == R.id.save_form_data) {  // Form data.  This can be removed once the minimum API >= 26.
1351             // Switch the status of saveFormDataEnabled.
1352             currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
1353
1354             // Update the menu checkbox.
1355             menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
1356
1357             // Display a snackbar.
1358             if (currentWebView.getSettings().getSaveFormData()) {
1359                 Snackbar.make(webViewPager, R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1360             } else {
1361                 Snackbar.make(webViewPager, R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1362             }
1363
1364             // Update the privacy icon.
1365             updatePrivacyIcons(true);
1366
1367             // Reload the current WebView.
1368             currentWebView.reload();
1369
1370             // Consume the event.
1371             return true;
1372         } else if (menuItemId == R.id.clear_cookies) {  // Clear cookies.
1373             // Create a snackbar.
1374             Snackbar.make(webViewPager, R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1375                     .setAction(R.string.undo, v -> {
1376                         // Do nothing because everything will be handled by `onDismissed()` below.
1377                     })
1378                     .addCallback(new Snackbar.Callback() {
1379                         @Override
1380                         public void onDismissed(Snackbar snackbar, int event) {
1381                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1382                                 // Delete the cookies, which command varies by SDK.
1383                                 if (Build.VERSION.SDK_INT < 21) {
1384                                     cookieManager.removeAllCookie();
1385                                 } else {
1386                                     cookieManager.removeAllCookies(null);
1387                                 }
1388                             }
1389                         }
1390                     })
1391                     .show();
1392
1393             // Consume the event.
1394             return true;
1395         } else if (menuItemId == R.id.clear_dom_storage) {  // Clear DOM storage.
1396             // Create a snackbar.
1397             Snackbar.make(webViewPager, R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1398                     .setAction(R.string.undo, v -> {
1399                         // Do nothing because everything will be handled by `onDismissed()` below.
1400                     })
1401                     .addCallback(new Snackbar.Callback() {
1402                         @Override
1403                         public void onDismissed(Snackbar snackbar, int event) {
1404                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1405                                 // Delete the DOM Storage.
1406                                 WebStorage webStorage = WebStorage.getInstance();
1407                                 webStorage.deleteAllData();
1408
1409                                 // Initialize a handler to manually delete the DOM storage files and directories.
1410                                 Handler deleteDomStorageHandler = new Handler();
1411
1412                                 // Setup a runnable to manually delete the DOM storage files and directories.
1413                                 Runnable deleteDomStorageRunnable = () -> {
1414                                     try {
1415                                         // Get a handle for the runtime.
1416                                         Runtime runtime = Runtime.getRuntime();
1417
1418                                         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1419                                         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1420                                         String privateDataDirectoryString = getApplicationInfo().dataDir;
1421
1422                                         // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1423                                         Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1424
1425                                         // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1426                                         Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1427                                         Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1428                                         Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1429                                         Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1430
1431                                         // Wait for the processes to finish.
1432                                         deleteLocalStorageProcess.waitFor();
1433                                         deleteIndexProcess.waitFor();
1434                                         deleteQuotaManagerProcess.waitFor();
1435                                         deleteQuotaManagerJournalProcess.waitFor();
1436                                         deleteDatabasesProcess.waitFor();
1437                                     } catch (Exception exception) {
1438                                         // Do nothing if an error is thrown.
1439                                     }
1440                                 };
1441
1442                                 // Manually delete the DOM storage files after 200 milliseconds.
1443                                 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1444                             }
1445                         }
1446                     })
1447                     .show();
1448
1449             // Consume the event.
1450             return true;
1451         } else if (menuItemId == R.id.clear_form_data) {  // Clear form data.  This can be remove once the minimum API >= 26.
1452             // Create a snackbar.
1453             Snackbar.make(webViewPager, R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1454                     .setAction(R.string.undo, v -> {
1455                         // Do nothing because everything will be handled by `onDismissed()` below.
1456                     })
1457                     .addCallback(new Snackbar.Callback() {
1458                         @Override
1459                         public void onDismissed(Snackbar snackbar, int event) {
1460                             if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1461                                 // Get a handle for the webView database.
1462                                 WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1463
1464                                 // Delete the form data.
1465                                 webViewDatabase.clearFormData();
1466                             }
1467                         }
1468                     })
1469                     .show();
1470
1471             // Consume the event.
1472             return true;
1473         } else if (menuItemId == R.id.easylist) {  // EasyList.
1474             // Toggle the EasyList status.
1475             currentWebView.setEasyListEnabled(!currentWebView.getEasyListEnabled());
1476
1477             // Update the menu checkbox.
1478             menuItem.setChecked(currentWebView.getEasyListEnabled());
1479
1480             // Reload the current WebView.
1481             currentWebView.reload();
1482
1483             // Consume the event.
1484             return true;
1485         } else if (menuItemId == R.id.easyprivacy) {  // EasyPrivacy.
1486             // Toggle the EasyPrivacy status.
1487             currentWebView.setEasyPrivacyEnabled(!currentWebView.getEasyPrivacyEnabled());
1488
1489             // Update the menu checkbox.
1490             menuItem.setChecked(currentWebView.getEasyPrivacyEnabled());
1491
1492             // Reload the current WebView.
1493             currentWebView.reload();
1494
1495             // Consume the event.
1496             return true;
1497         } else if (menuItemId == R.id.fanboys_annoyance_list) {  // Fanboy's Annoyance List.
1498             // Toggle Fanboy's Annoyance List status.
1499             currentWebView.setFanboysAnnoyanceListEnabled(!currentWebView.getFanboysAnnoyanceListEnabled());
1500
1501             // Update the menu checkbox.
1502             menuItem.setChecked(currentWebView.getFanboysAnnoyanceListEnabled());
1503
1504             // Update the status of Fanboy's Social Blocking List.
1505             optionsFanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.getFanboysAnnoyanceListEnabled());
1506
1507             // Reload the current WebView.
1508             currentWebView.reload();
1509
1510             // Consume the event.
1511             return true;
1512         } else if (menuItemId == R.id.fanboys_social_blocking_list) {  // Fanboy's Social Blocking List.
1513             // Toggle Fanboy's Social Blocking List status.
1514             currentWebView.setFanboysSocialBlockingListEnabled(!currentWebView.getFanboysSocialBlockingListEnabled());
1515
1516             // Update the menu checkbox.
1517             menuItem.setChecked(currentWebView.getFanboysSocialBlockingListEnabled());
1518
1519             // Reload the current WebView.
1520             currentWebView.reload();
1521
1522             // Consume the event.
1523             return true;
1524         } else if (menuItemId == R.id.ultralist) {  // UltraList.
1525             // Toggle the UltraList status.
1526             currentWebView.setUltraListEnabled(!currentWebView.getUltraListEnabled());
1527
1528             // Update the menu checkbox.
1529             menuItem.setChecked(currentWebView.getUltraListEnabled());
1530
1531             // Reload the current WebView.
1532             currentWebView.reload();
1533
1534             // Consume the event.
1535             return true;
1536         } else if (menuItemId == R.id.ultraprivacy) {  // UltraPrivacy.
1537             // Toggle the UltraPrivacy status.
1538             currentWebView.setUltraPrivacyEnabled(!currentWebView.getUltraPrivacyEnabled());
1539
1540             // Update the menu checkbox.
1541             menuItem.setChecked(currentWebView.getUltraPrivacyEnabled());
1542
1543             // Reload the current WebView.
1544             currentWebView.reload();
1545
1546             // Consume the event.
1547             return true;
1548         } else if (menuItemId == R.id.block_all_third_party_requests) {  // Block all third-party requests.
1549             //Toggle the third-party requests blocker status.
1550             currentWebView.setBlockAllThirdPartyRequests(!currentWebView.getBlockAllThirdPartyRequests());
1551
1552             // Update the menu checkbox.
1553             menuItem.setChecked(currentWebView.getBlockAllThirdPartyRequests());
1554
1555             // Reload the current WebView.
1556             currentWebView.reload();
1557
1558             // Consume the event.
1559             return true;
1560         } else if (menuItemId == R.id.proxy_none) {  // Proxy - None.
1561             // Update the proxy mode.
1562             proxyMode = ProxyHelper.NONE;
1563
1564             // Apply the proxy mode.
1565             applyProxy(true);
1566
1567             // Consume the event.
1568             return true;
1569         } else if (menuItemId == R.id.proxy_tor) {  // Proxy - Tor.
1570             // Update the proxy mode.
1571             proxyMode = ProxyHelper.TOR;
1572
1573             // Apply the proxy mode.
1574             applyProxy(true);
1575
1576             // Consume the event.
1577             return true;
1578         } else if (menuItemId == R.id.proxy_i2p) {  // Proxy - I2P.
1579             // Update the proxy mode.
1580             proxyMode = ProxyHelper.I2P;
1581
1582             // Apply the proxy mode.
1583             applyProxy(true);
1584
1585             // Consume the event.
1586             return true;
1587         } else if (menuItemId == R.id.proxy_custom) {  // Proxy - Custom.
1588             // Update the proxy mode.
1589             proxyMode = ProxyHelper.CUSTOM;
1590
1591             // Apply the proxy mode.
1592             applyProxy(true);
1593
1594             // Consume the event.
1595             return true;
1596         } else if (menuItemId == R.id.user_agent_privacy_browser) {  // User Agent - Privacy Browser.
1597             // Update the user agent.
1598             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1599
1600             // Reload the current WebView.
1601             currentWebView.reload();
1602
1603             // Consume the event.
1604             return true;
1605         } else if (menuItemId == R.id.user_agent_webview_default) {  // User Agent - WebView Default.
1606             // Update the user agent.
1607             currentWebView.getSettings().setUserAgentString("");
1608
1609             // Reload the current WebView.
1610             currentWebView.reload();
1611
1612             // Consume the event.
1613             return true;
1614         } else if (menuItemId == R.id.user_agent_firefox_on_android) {  // User Agent - Firefox on Android.
1615             // Update the user agent.
1616             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1617
1618             // Reload the current WebView.
1619             currentWebView.reload();
1620
1621             // Consume the event.
1622             return true;
1623         } else if (menuItemId == R.id.user_agent_chrome_on_android) {  // User Agent - Chrome on Android.
1624             // Update the user agent.
1625             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1626
1627             // Reload the current WebView.
1628             currentWebView.reload();
1629
1630             // Consume the event.
1631             return true;
1632         } else if (menuItemId == R.id.user_agent_safari_on_ios) {  // User Agent - Safari on iOS.
1633             // Update the user agent.
1634             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1635
1636             // Reload the current WebView.
1637             currentWebView.reload();
1638
1639             // Consume the event.
1640             return true;
1641         } else if (menuItemId == R.id.user_agent_firefox_on_linux) {  // User Agent - Firefox on Linux.
1642             // Update the user agent.
1643             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1644
1645             // Reload the current WebView.
1646             currentWebView.reload();
1647
1648             // Consume the event.
1649             return true;
1650         } else if (menuItemId == R.id.user_agent_chromium_on_linux) {  // User Agent - Chromium on Linux.
1651             // Update the user agent.
1652             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1653
1654             // Reload the current WebView.
1655             currentWebView.reload();
1656
1657             // Consume the event.
1658             return true;
1659         } else if (menuItemId == R.id.user_agent_firefox_on_windows) {  // User Agent - Firefox on Windows.
1660             // Update the user agent.
1661             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1662
1663             // Reload the current WebView.
1664             currentWebView.reload();
1665
1666             // Consume the event.
1667             return true;
1668         } else if (menuItemId == R.id.user_agent_chrome_on_windows) {  // User Agent - Chrome on Windows.
1669             // Update the user agent.
1670             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1671
1672             // Reload the current WebView.
1673             currentWebView.reload();
1674
1675             // Consume the event.
1676             return true;
1677         } else if (menuItemId == R.id.user_agent_edge_on_windows) {  // User Agent - Edge on Windows.
1678             // Update the user agent.
1679             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1680
1681             // Reload the current WebView.
1682             currentWebView.reload();
1683
1684             // Consume the event.
1685             return true;
1686         } else if (menuItemId == R.id.user_agent_internet_explorer_on_windows) {  // User Agent - Internet Explorer on Windows.
1687             // Update the user agent.
1688             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1689
1690             // Reload the current WebView.
1691             currentWebView.reload();
1692
1693             // Consume the event.
1694             return true;
1695         } else if (menuItemId == R.id.user_agent_safari_on_macos) {  // User Agent - Safari on macOS.
1696             // Update the user agent.
1697             currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1698
1699             // Reload the current WebView.
1700             currentWebView.reload();
1701
1702             // Consume the event.
1703             return true;
1704         } else if (menuItemId == R.id.user_agent_custom) {  // User Agent - Custom.
1705             // Update the user agent.
1706             currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1707
1708             // Reload the current WebView.
1709             currentWebView.reload();
1710
1711             // Consume the event.
1712             return true;
1713         } else if (menuItemId == R.id.font_size) {  // Font size.
1714             // Instantiate the font size dialog.
1715             DialogFragment fontSizeDialogFragment = FontSizeDialog.displayDialog(currentWebView.getSettings().getTextZoom());
1716
1717             // Show the font size dialog.
1718             fontSizeDialogFragment.show(getSupportFragmentManager(), getString(R.string.font_size));
1719
1720             // Consume the event.
1721             return true;
1722         } else if (menuItemId == R.id.swipe_to_refresh) {  // Swipe to refresh.
1723             // Toggle the stored status of swipe to refresh.
1724             currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
1725
1726             // Update the swipe refresh layout.
1727             if (currentWebView.getSwipeToRefresh()) {  // Swipe to refresh is enabled.
1728                 // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
1729                 swipeRefreshLayout.setEnabled(currentWebView.getScrollY() == 0);
1730             } else {  // Swipe to refresh is disabled.
1731                 // Disable the swipe refresh layout.
1732                 swipeRefreshLayout.setEnabled(false);
1733             }
1734
1735             // Consume the event.
1736             return true;
1737         } else if (menuItemId == R.id.wide_viewport) {  // Wide viewport.
1738             // Toggle the viewport.
1739             currentWebView.getSettings().setUseWideViewPort(!currentWebView.getSettings().getUseWideViewPort());
1740
1741             // Consume the event.
1742             return true;
1743         } else if (menuItemId == R.id.display_images) {  // Display images.
1744             // Toggle the displaying of images.
1745             if (currentWebView.getSettings().getLoadsImagesAutomatically()) {  // Images are currently loaded automatically.
1746                 // Disable loading of images.
1747                 currentWebView.getSettings().setLoadsImagesAutomatically(false);
1748
1749                 // Reload the website to remove existing images.
1750                 currentWebView.reload();
1751             } else {  // Images are not currently loaded automatically.
1752                 // Enable loading of images.  Missing images will be loaded without the need for a reload.
1753                 currentWebView.getSettings().setLoadsImagesAutomatically(true);
1754             }
1755
1756             // Consume the event.
1757             return true;
1758         } else if (menuItemId == R.id.dark_webview) {  // Dark WebView.
1759             // Check to see if dark WebView is supported by this WebView.
1760             if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
1761                 // Toggle the dark WebView setting.
1762                 if (WebSettingsCompat.getForceDark(currentWebView.getSettings()) == WebSettingsCompat.FORCE_DARK_ON) {  // Dark WebView is currently enabled.
1763                     // Turn off dark WebView.
1764                     WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
1765                 } else {  // Dark WebView is currently disabled.
1766                     // Turn on dark WebView.
1767                     WebSettingsCompat.setForceDark(currentWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
1768                 }
1769             }
1770
1771             // Consume the event.
1772             return true;
1773         } else if (menuItemId == R.id.find_on_page) {  // Find on page.
1774             // Get a handle for the views.
1775             Toolbar toolbar = findViewById(R.id.toolbar);
1776             LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1777             EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
1778
1779             // Set the minimum height of the find on page linear layout to match the toolbar.
1780             findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight());
1781
1782             // Hide the toolbar.
1783             toolbar.setVisibility(View.GONE);
1784
1785             // Show the find on page linear layout.
1786             findOnPageLinearLayout.setVisibility(View.VISIBLE);
1787
1788             // Display the keyboard.  The app must wait 200 ms before running the command to work around a bug in Android.
1789             // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1790             findOnPageEditText.postDelayed(() -> {
1791                 // Set the focus on the find on page edit text.
1792                 findOnPageEditText.requestFocus();
1793
1794                 // Get a handle for the input method manager.
1795                 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
1796
1797                 // Remove the lint warning below that the input method manager might be null.
1798                 assert inputMethodManager != null;
1799
1800                 // Display the keyboard.  `0` sets no input flags.
1801                 inputMethodManager.showSoftInput(findOnPageEditText, 0);
1802             }, 200);
1803
1804             // Consume the event.
1805             return true;
1806         } else if (menuItemId == R.id.print) {  // Print.
1807             // Get a print manager instance.
1808             PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
1809
1810             // Remove the lint error below that print manager might be null.
1811             assert printManager != null;
1812
1813             // Create a print document adapter from the current WebView.
1814             PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
1815
1816             // Print the document.
1817             printManager.print(getString(R.string.privacy_browser_webpage), printDocumentAdapter, null);
1818
1819             // Consume the event.
1820             return true;
1821         } else if (menuItemId == R.id.save_url) {  // Save URL.
1822             // Check the download preference.
1823             if (downloadWithExternalApp) {  // Download with an external app.
1824                 downloadUrlWithExternalApp(currentWebView.getCurrentUrl());
1825             } else {  // Handle the download inside of Privacy Browser.
1826                 // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
1827                 new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
1828                         currentWebView.getAcceptCookies()).execute(currentWebView.getCurrentUrl());
1829             }
1830
1831             // Consume the event.
1832             return true;
1833         } else if (menuItemId == R.id.save_archive) {
1834             // Open the file picker with a default file name built from the current domain name.
1835             saveWebpageArchiveActivityResultLauncher.launch(currentWebView.getCurrentDomainName() + ".mht");
1836
1837             // Consume the event.
1838             return true;
1839         } else if (menuItemId == R.id.save_image) {  // Save image.
1840             // Open the file picker with a default file name built from the current domain name.
1841             saveWebpageImageActivityResultLauncher.launch(currentWebView.getCurrentDomainName() + ".png");
1842
1843             // Consume the event.
1844             return true;
1845         } else if (menuItemId == R.id.add_to_homescreen) {  // Add to homescreen.
1846             // Instantiate the create home screen shortcut dialog.
1847             DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
1848                     currentWebView.getFavoriteOrDefaultIcon());
1849
1850             // Show the create home screen shortcut dialog.
1851             createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
1852
1853             // Consume the event.
1854             return true;
1855         } else if (menuItemId == R.id.view_source) {  // View source.
1856             // Create an intent to launch the view source activity.
1857             Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
1858
1859             // Add the variables to the intent.
1860             viewSourceIntent.putExtra(ViewSourceActivityKt.CURRENT_URL, currentWebView.getUrl());
1861             viewSourceIntent.putExtra(ViewSourceActivityKt.USER_AGENT, currentWebView.getSettings().getUserAgentString());
1862
1863             // Make it so.
1864             startActivity(viewSourceIntent);
1865
1866             // Consume the event.
1867             return true;
1868         } else if (menuItemId == R.id.share_url) {  // Share URL.
1869             // Setup the share string.
1870             String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl();
1871
1872             // Create the share intent.
1873             Intent shareIntent = new Intent(Intent.ACTION_SEND);
1874
1875             // Add the share string to the intent.
1876             shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
1877
1878             // Set the MIME type.
1879             shareIntent.setType("text/plain");
1880
1881             // Set the intent to open in a new task.
1882             shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1883
1884             // Make it so.
1885             startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
1886
1887             // Consume the event.
1888             return true;
1889         } else if (menuItemId == R.id.open_with_app) {  // Open with app.
1890             // Open the URL with an outside app.
1891             openWithApp(currentWebView.getUrl());
1892
1893             // Consume the event.
1894             return true;
1895         } else if (menuItemId == R.id.open_with_browser) {  // Open with browser.
1896             // Open the URL with an outside browser.
1897             openWithBrowser(currentWebView.getUrl());
1898
1899             // Consume the event.
1900             return true;
1901         } else if (menuItemId == R.id.add_or_edit_domain) {  // Add or edit domain.
1902             // Check if domain settings currently exist.
1903             if (currentWebView.getDomainSettingsApplied()) {  // Edit the current domain settings.
1904                 // Reapply the domain settings on returning to `MainWebViewActivity`.
1905                 reapplyDomainSettingsOnRestart = true;
1906
1907                 // Create an intent to launch the domains activity.
1908                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1909
1910                 // Add the extra information to the intent.
1911                 domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
1912                 domainsIntent.putExtra("close_on_back", true);
1913                 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1914                 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1915
1916                 // Get the current certificate.
1917                 SslCertificate sslCertificate = currentWebView.getCertificate();
1918
1919                 // Check to see if the SSL certificate is populated.
1920                 if (sslCertificate != null) {
1921                     // Extract the certificate to strings.
1922                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
1923                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
1924                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
1925                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
1926                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
1927                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
1928                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1929                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1930
1931                     // Add the certificate to the intent.
1932                     domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1933                     domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1934                     domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1935                     domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1936                     domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1937                     domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1938                     domainsIntent.putExtra("ssl_start_date", startDateLong);
1939                     domainsIntent.putExtra("ssl_end_date", endDateLong);
1940                 }
1941
1942                 // Make it so.
1943                 startActivity(domainsIntent);
1944             } else {  // Add a new domain.
1945                 // Apply the new domain settings on returning to `MainWebViewActivity`.
1946                 reapplyDomainSettingsOnRestart = true;
1947
1948                 // Get the current domain
1949                 Uri currentUri = Uri.parse(currentWebView.getUrl());
1950                 String currentDomain = currentUri.getHost();
1951
1952                 // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1953                 DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1954
1955                 // Create the domain and store the database ID.
1956                 int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1957
1958                 // Create an intent to launch the domains activity.
1959                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1960
1961                 // Add the extra information to the intent.
1962                 domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1963                 domainsIntent.putExtra("close_on_back", true);
1964                 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1965                 domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1966
1967                 // Get the current certificate.
1968                 SslCertificate sslCertificate = currentWebView.getCertificate();
1969
1970                 // Check to see if the SSL certificate is populated.
1971                 if (sslCertificate != null) {
1972                     // Extract the certificate to strings.
1973                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
1974                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
1975                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
1976                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
1977                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
1978                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
1979                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1980                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1981
1982                     // Add the certificate to the intent.
1983                     domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1984                     domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1985                     domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1986                     domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1987                     domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1988                     domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1989                     domainsIntent.putExtra("ssl_start_date", startDateLong);
1990                     domainsIntent.putExtra("ssl_end_date", endDateLong);
1991                 }
1992
1993                 // Make it so.
1994                 startActivity(domainsIntent);
1995             }
1996
1997             // Consume the event.
1998             return true;
1999         } else {  // There is no match with the options menu.  Pass the event up to the parent method.
2000             // Don't consume the event.
2001             return super.onOptionsItemSelected(menuItem);
2002         }
2003     }
2004
2005     // removeAllCookies is deprecated, but it is required for API < 21.
2006     @Override
2007     public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
2008         // Get a handle for the shared preferences.
2009         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
2010
2011         // Get the menu item ID.
2012         int menuItemId = menuItem.getItemId();
2013
2014         // Run the commands that correspond to the selected menu item.
2015         if (menuItemId == R.id.clear_and_exit) {  // Clear and exit.
2016             // Clear and exit Privacy Browser.
2017             clearAndExit();
2018         } else if (menuItemId == R.id.home) {  // Home.
2019             // Load the homepage.
2020             loadUrl(currentWebView, sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
2021         } else if (menuItemId == R.id.back) {  // Back.
2022             // Check if the WebView can go back.
2023             if (currentWebView.canGoBack()) {
2024                 // Get the current web back forward list.
2025                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2026
2027                 // Get the previous entry URL.
2028                 String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
2029
2030                 // Apply the domain settings.
2031                 applyDomainSettings(currentWebView, previousUrl, false, false, false);
2032
2033                 // Load the previous website in the history.
2034                 currentWebView.goBack();
2035             }
2036         } else if (menuItemId == R.id.forward) {  // Forward.
2037             // Check if the WebView can go forward.
2038             if (currentWebView.canGoForward()) {
2039                 // Get the current web back forward list.
2040                 WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2041
2042                 // Get the next entry URL.
2043                 String nextUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() + 1).getUrl();
2044
2045                 // Apply the domain settings.
2046                 applyDomainSettings(currentWebView, nextUrl, false, false, false);
2047
2048                 // Load the next website in the history.
2049                 currentWebView.goForward();
2050             }
2051         } else if (menuItemId == R.id.history) {  // History.
2052             // Instantiate the URL history dialog.
2053             DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId());
2054
2055             // Show the URL history dialog.
2056             urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
2057         } else if (menuItemId == R.id.open) {  // Open.
2058             // Instantiate the open file dialog.
2059             DialogFragment openDialogFragment = new OpenDialog();
2060
2061             // Show the open file dialog.
2062             openDialogFragment.show(getSupportFragmentManager(), getString(R.string.open));
2063         } else if (menuItemId == R.id.requests) {  // Requests.
2064             // Populate the resource requests.
2065             RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
2066
2067             // Create an intent to launch the Requests activity.
2068             Intent requestsIntent = new Intent(this, RequestsActivity.class);
2069
2070             // Add the block third-party requests status to the intent.
2071             requestsIntent.putExtra("block_all_third_party_requests", currentWebView.getBlockAllThirdPartyRequests());
2072
2073             // Make it so.
2074             startActivity(requestsIntent);
2075         } else if (menuItemId == R.id.downloads) {  // Downloads.
2076             // Try the default system download manager.
2077             try {
2078                 // Launch the default system Download Manager.
2079                 Intent defaultDownloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
2080
2081                 // Launch as a new task so that the download manager and Privacy Browser show as separate windows in the recent tasks list.
2082                 defaultDownloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2083
2084                 // Make it so.
2085                 startActivity(defaultDownloadManagerIntent);
2086             } catch (Exception defaultDownloadManagerException) {
2087                 // Try a generic file manager.
2088                 try {
2089                     // Create a generic file manager intent.
2090                     Intent genericFileManagerIntent = new Intent(Intent.ACTION_VIEW);
2091
2092                     // Open the download directory.
2093                     genericFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), DocumentsContract.Document.MIME_TYPE_DIR);
2094
2095                     // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2096                     genericFileManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2097
2098                     // Make it so.
2099                     startActivity(genericFileManagerIntent);
2100                 } catch (Exception genericFileManagerException) {
2101                     // Try an alternate file manager.
2102                     try {
2103                         // Create an alternate file manager intent.
2104                         Intent alternateFileManagerIntent = new Intent(Intent.ACTION_VIEW);
2105
2106                         // Open the download directory.
2107                         alternateFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), "resource/folder");
2108
2109                         // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2110                         alternateFileManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2111
2112                         // Open the alternate file manager.
2113                         startActivity(alternateFileManagerIntent);
2114                     } catch (Exception alternateFileManagerException) {
2115                         // Display a snackbar.
2116                         Snackbar.make(currentWebView, R.string.no_file_manager_detected, Snackbar.LENGTH_INDEFINITE).show();
2117                     }
2118                 }
2119             }
2120         } else if (menuItemId == R.id.domains) {  // Domains.
2121             // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2122             reapplyDomainSettingsOnRestart = true;
2123
2124             // Launch the domains activity.
2125             Intent domainsIntent = new Intent(this, DomainsActivity.class);
2126
2127             // Add the extra information to the intent.
2128             domainsIntent.putExtra("current_url", currentWebView.getUrl());
2129             domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
2130
2131             // Get the current certificate.
2132             SslCertificate sslCertificate = currentWebView.getCertificate();
2133
2134             // Check to see if the SSL certificate is populated.
2135             if (sslCertificate != null) {
2136                 // Extract the certificate to strings.
2137                 String issuedToCName = sslCertificate.getIssuedTo().getCName();
2138                 String issuedToOName = sslCertificate.getIssuedTo().getOName();
2139                 String issuedToUName = sslCertificate.getIssuedTo().getUName();
2140                 String issuedByCName = sslCertificate.getIssuedBy().getCName();
2141                 String issuedByOName = sslCertificate.getIssuedBy().getOName();
2142                 String issuedByUName = sslCertificate.getIssuedBy().getUName();
2143                 long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
2144                 long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
2145
2146                 // Add the certificate to the intent.
2147                 domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
2148                 domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
2149                 domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
2150                 domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
2151                 domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
2152                 domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
2153                 domainsIntent.putExtra("ssl_start_date", startDateLong);
2154                 domainsIntent.putExtra("ssl_end_date", endDateLong);
2155             }
2156
2157             // Make it so.
2158             startActivity(domainsIntent);
2159         } else if (menuItemId == R.id.settings) {  // Settings.
2160             // Set the flag to reapply app settings on restart when returning from Settings.
2161             reapplyAppSettingsOnRestart = true;
2162
2163             // Set the flag to reapply the domain settings on restart when returning from Settings.
2164             reapplyDomainSettingsOnRestart = true;
2165
2166             // Launch the settings activity.
2167             Intent settingsIntent = new Intent(this, SettingsActivity.class);
2168             startActivity(settingsIntent);
2169         } else if (menuItemId == R.id.import_export) { // Import/Export.
2170             // Create an intent to launch the import/export activity.
2171             Intent importExportIntent = new Intent(this, ImportExportActivity.class);
2172
2173             // Make it so.
2174             startActivity(importExportIntent);
2175         } else if (menuItemId == R.id.logcat) {  // Logcat.
2176             // Create an intent to launch the logcat activity.
2177             Intent logcatIntent = new Intent(this, LogcatActivity.class);
2178
2179             // Make it so.
2180             startActivity(logcatIntent);
2181         } else if (menuItemId == R.id.guide) {  // Guide.
2182             // Create an intent to launch the guide activity.
2183             Intent guideIntent = new Intent(this, GuideActivity.class);
2184
2185             // Make it so.
2186             startActivity(guideIntent);
2187         } else if (menuItemId == R.id.about) {  // About
2188             // Create an intent to launch the about activity.
2189             Intent aboutIntent = new Intent(this, AboutActivity.class);
2190
2191             // Create a string array for the blocklist versions.
2192             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],
2193                     ultraList.get(0).get(0)[0], ultraPrivacy.get(0).get(0)[0]};
2194
2195             // Add the blocklist versions to the intent.
2196             aboutIntent.putExtra(AboutActivity.BLOCKLIST_VERSIONS, blocklistVersions);
2197
2198             // Make it so.
2199             startActivity(aboutIntent);
2200         }
2201
2202         // Close the navigation drawer.
2203         drawerLayout.closeDrawer(GravityCompat.START);
2204         return true;
2205     }
2206
2207     @Override
2208     public void onPostCreate(Bundle savedInstanceState) {
2209         // Run the default commands.
2210         super.onPostCreate(savedInstanceState);
2211
2212         // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished.  This creates the navigation drawer icon.
2213         actionBarDrawerToggle.syncState();
2214     }
2215
2216     @Override
2217     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
2218         // Get the hit test result.
2219         final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
2220
2221         // Define the URL strings.
2222         final String imageUrl;
2223         final String linkUrl;
2224
2225         // Get handles for the system managers.
2226         final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
2227
2228         // Remove the lint errors below that the clipboard manager might be null.
2229         assert clipboardManager != null;
2230
2231         // Process the link according to the type.
2232         switch (hitTestResult.getType()) {
2233             // `SRC_ANCHOR_TYPE` is a link.
2234             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
2235                 // Get the target URL.
2236                 linkUrl = hitTestResult.getExtra();
2237
2238                 // Set the target URL as the title of the `ContextMenu`.
2239                 menu.setHeaderTitle(linkUrl);
2240
2241                 // Add an Open in New Tab entry.
2242                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2243                     // Load the link URL in a new tab and move to it.
2244                     addNewTab(linkUrl, true);
2245
2246                     // Consume the event.
2247                     return true;
2248                 });
2249
2250                 // Add an Open in Background entry.
2251                 menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
2252                     // Load the link URL in a new tab but do not move to it.
2253                     addNewTab(linkUrl, false);
2254
2255                     // Consume the event.
2256                     return true;
2257                 });
2258
2259                 // Add an Open with App entry.
2260                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2261                     openWithApp(linkUrl);
2262
2263                     // Consume the event.
2264                     return true;
2265                 });
2266
2267                 // Add an Open with Browser entry.
2268                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2269                     openWithBrowser(linkUrl);
2270
2271                     // Consume the event.
2272                     return true;
2273                 });
2274
2275                 // Add a Copy URL entry.
2276                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2277                     // Save the link URL in a `ClipData`.
2278                     ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2279
2280                     // Set the `ClipData` as the clipboard's primary clip.
2281                     clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
2282
2283                     // Consume the event.
2284                     return true;
2285                 });
2286
2287                 // Add a Save URL entry.
2288                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
2289                     // Check the download preference.
2290                     if (downloadWithExternalApp) {  // Download with an external app.
2291                         downloadUrlWithExternalApp(linkUrl);
2292                     } else {  // Handle the download inside of Privacy Browser.
2293                         // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2294                         new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
2295                                 currentWebView.getAcceptCookies()).execute(linkUrl);
2296                     }
2297
2298                     // Consume the event.
2299                     return true;
2300                 });
2301
2302                 // Add an empty Cancel entry, which by default closes the context menu.
2303                 menu.add(R.string.cancel);
2304                 break;
2305
2306             // `IMAGE_TYPE` is an image.
2307             case WebView.HitTestResult.IMAGE_TYPE:
2308                 // Get the image URL.
2309                 imageUrl = hitTestResult.getExtra();
2310
2311                 // Remove the incorrect lint warning below that the image URL might be null.
2312                 assert imageUrl != null;
2313
2314                 // Set the context menu title.
2315                 if (imageUrl.startsWith("data:")) {  // The image data is contained in within the URL, making it exceedingly long.
2316                     // Truncate the image URL before making it the title.
2317                     menu.setHeaderTitle(imageUrl.substring(0, 100));
2318                 } else {  // The image URL does not contain the full image data.
2319                     // Set the image URL as the title of the context menu.
2320                     menu.setHeaderTitle(imageUrl);
2321                 }
2322
2323                 // Add an Open in New Tab entry.
2324                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2325                     // Load the image in a new tab.
2326                     addNewTab(imageUrl, true);
2327
2328                     // Consume the event.
2329                     return true;
2330                 });
2331
2332                 // Add an Open with App entry.
2333                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2334                     // Open the image URL with an external app.
2335                     openWithApp(imageUrl);
2336
2337                     // Consume the event.
2338                     return true;
2339                 });
2340
2341                 // Add an Open with Browser entry.
2342                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2343                     // Open the image URL with an external browser.
2344                     openWithBrowser(imageUrl);
2345
2346                     // Consume the event.
2347                     return true;
2348                 });
2349
2350                 // Add a View Image entry.
2351                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2352                     // Load the image in the current tab.
2353                     loadUrl(currentWebView, imageUrl);
2354
2355                     // Consume the event.
2356                     return true;
2357                 });
2358
2359                 // Add a Save Image entry.
2360                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
2361                     // Check the download preference.
2362                     if (downloadWithExternalApp) {  // Download with an external app.
2363                         downloadUrlWithExternalApp(imageUrl);
2364                     } else {  // Handle the download inside of Privacy Browser.
2365                         // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2366                         new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
2367                                 currentWebView.getAcceptCookies()).execute(imageUrl);
2368                     }
2369
2370                     // Consume the event.
2371                     return true;
2372                 });
2373
2374                 // Add a Copy URL entry.
2375                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2376                     // Save the image URL in a clip data.
2377                     ClipData imageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2378
2379                     // Set the clip data as the clipboard's primary clip.
2380                     clipboardManager.setPrimaryClip(imageTypeClipData);
2381
2382                     // Consume the event.
2383                     return true;
2384                 });
2385
2386                 // Add an empty Cancel entry, which by default closes the context menu.
2387                 menu.add(R.string.cancel);
2388                 break;
2389
2390             // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2391             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2392                 // Get the image URL.
2393                 imageUrl = hitTestResult.getExtra();
2394
2395                 // Instantiate a handler.
2396                 Handler handler = new Handler();
2397
2398                 // Get a message from the handler.
2399                 Message message = handler.obtainMessage();
2400
2401                 // Request the image details from the last touched node be returned in the message.
2402                 currentWebView.requestFocusNodeHref(message);
2403
2404                 // Get the link URL from the message data.
2405                 linkUrl = message.getData().getString("url");
2406
2407                 // Set the link URL as the title of the context menu.
2408                 menu.setHeaderTitle(linkUrl);
2409
2410                 // Add an Open in New Tab entry.
2411                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2412                     // Load the link URL in a new tab and move to it.
2413                     addNewTab(linkUrl, true);
2414
2415                     // Consume the event.
2416                     return true;
2417                 });
2418
2419                 // Add an Open in Background entry.
2420                 menu.add(R.string.open_in_background).setOnMenuItemClickListener((MenuItem item) -> {
2421                     // Lod the link URL in a new tab but do not move to it.
2422                     addNewTab(linkUrl, false);
2423
2424                     // Consume the event.
2425                     return true;
2426                 });
2427
2428                 // Add an Open Image in New Tab entry.
2429                 menu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2430                     // Load the image in a new tab and move to it.
2431                     addNewTab(imageUrl, true);
2432
2433                     // Consume the event.
2434                     return true;
2435                 });
2436
2437                 // Add an Open with App entry.
2438                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2439                     // Open the link URL with an external app.
2440                     openWithApp(linkUrl);
2441
2442                     // Consume the event.
2443                     return true;
2444                 });
2445
2446                 // Add an Open with Browser entry.
2447                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2448                     // Open the link URL with an external browser.
2449                     openWithBrowser(linkUrl);
2450
2451                     // Consume the event.
2452                     return true;
2453                 });
2454
2455                 // Add a View Image entry.
2456                 menu.add(R.string.view_image).setOnMenuItemClickListener((MenuItem item) -> {
2457                    // View the image in the current tab.
2458                    loadUrl(currentWebView, imageUrl);
2459
2460                    // Consume the event.
2461                    return true;
2462                 });
2463
2464                 // Add a Save Image entry.
2465                 menu.add(R.string.save_image).setOnMenuItemClickListener((MenuItem item) -> {
2466                     // Check the download preference.
2467                     if (downloadWithExternalApp) {  // Download with an external app.
2468                         downloadUrlWithExternalApp(imageUrl);
2469                     } else {  // Handle the download inside of Privacy Browser.
2470                         // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2471                         new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
2472                                 currentWebView.getAcceptCookies()).execute(imageUrl);
2473                     }
2474
2475                     // Consume the event.
2476                     return true;
2477                 });
2478
2479                 // Add a Copy URL entry.
2480                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
2481                     // Save the link URL in a clip data.
2482                     ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
2483
2484                     // Set the clip data as the clipboard's primary clip.
2485                     clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2486
2487                     // Consume the event.
2488                     return true;
2489                 });
2490
2491                 // Add a Save URL entry.
2492                 menu.add(R.string.save_url).setOnMenuItemClickListener((MenuItem item) -> {
2493                     // Check the download preference.
2494                     if (downloadWithExternalApp) {  // Download with an external app.
2495                         downloadUrlWithExternalApp(linkUrl);
2496                     } else {  // Handle the download inside of Privacy Browser.
2497                         // Prepare the save dialog.  The dialog will be displayed once the file size and the content disposition have been acquired.
2498                         new PrepareSaveDialog(this, this, getSupportFragmentManager(), currentWebView.getSettings().getUserAgentString(),
2499                                 currentWebView.getAcceptCookies()).execute(linkUrl);
2500                     }
2501
2502                     // Consume the event.
2503                     return true;
2504                 });
2505
2506                 // Add an empty Cancel entry, which by default closes the context menu.
2507                 menu.add(R.string.cancel);
2508                 break;
2509
2510             case WebView.HitTestResult.EMAIL_TYPE:
2511                 // Get the target URL.
2512                 linkUrl = hitTestResult.getExtra();
2513
2514                 // Set the target URL as the title of the `ContextMenu`.
2515                 menu.setHeaderTitle(linkUrl);
2516
2517                 // Add a Write Email entry.
2518                 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2519                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2520                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2521
2522                     // Parse the url and set it as the data for the `Intent`.
2523                     emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2524
2525                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2526                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2527
2528                     try {
2529                         // Make it so.
2530                         startActivity(emailIntent);
2531                     } catch (ActivityNotFoundException exception) {
2532                         // Display a snackbar.
2533                         Snackbar.make(currentWebView, getString(R.string.error) + "  " + exception, Snackbar.LENGTH_INDEFINITE).show();
2534                     }
2535
2536                     // Consume the event.
2537                     return true;
2538                 });
2539
2540                 // Add a Copy Email Address entry.
2541                 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2542                     // Save the email address in a `ClipData`.
2543                     ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2544
2545                     // Set the `ClipData` as the clipboard's primary clip.
2546                     clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2547
2548                     // Consume the event.
2549                     return true;
2550                 });
2551
2552                 // Add an empty Cancel entry, which by default closes the context menu.
2553                 menu.add(R.string.cancel);
2554                 break;
2555         }
2556     }
2557
2558     @Override
2559     public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2560         // Get a handle for the bookmarks list view.
2561         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2562
2563         // Get the dialog.
2564         Dialog dialog = dialogFragment.getDialog();
2565
2566         // Remove the incorrect lint warning below that the dialog might be null.
2567         assert dialog != null;
2568
2569         // Get the views from the dialog fragment.
2570         EditText createBookmarkNameEditText = dialog.findViewById(R.id.create_bookmark_name_edittext);
2571         EditText createBookmarkUrlEditText = dialog.findViewById(R.id.create_bookmark_url_edittext);
2572
2573         // Extract the strings from the edit texts.
2574         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2575         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2576
2577         // Create a favorite icon byte array output stream.
2578         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2579
2580         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2581         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2582
2583         // Convert the favorite icon byte array stream to a byte array.
2584         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2585
2586         // Display the new bookmark below the current items in the (0 indexed) list.
2587         int newBookmarkDisplayOrder = bookmarksListView.getCount();
2588
2589         // Create the bookmark.
2590         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2591
2592         // Update the bookmarks cursor with the current contents of this folder.
2593         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2594
2595         // Update the list view.
2596         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2597
2598         // Scroll to the new bookmark.
2599         bookmarksListView.setSelection(newBookmarkDisplayOrder);
2600     }
2601
2602     @Override
2603     public void onCreateBookmarkFolder(DialogFragment dialogFragment, @NonNull Bitmap favoriteIconBitmap) {
2604         // Get a handle for the bookmarks list view.
2605         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2606
2607         // Get the dialog.
2608         Dialog dialog = dialogFragment.getDialog();
2609
2610         // Remove the incorrect lint warning below that the dialog might be null.
2611         assert dialog != null;
2612
2613         // Get handles for the views in the dialog fragment.
2614         EditText folderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
2615         RadioButton defaultIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
2616         ImageView defaultIconImageView = dialog.findViewById(R.id.default_icon_imageview);
2617
2618         // Get new folder name string.
2619         String folderNameString = folderNameEditText.getText().toString();
2620
2621         // Create a folder icon bitmap.
2622         Bitmap folderIconBitmap;
2623
2624         // Set the folder icon bitmap according to the dialog.
2625         if (defaultIconRadioButton.isChecked()) {  // Use the default folder icon.
2626             // Get the default folder icon drawable.
2627             Drawable folderIconDrawable = defaultIconImageView.getDrawable();
2628
2629             // Convert the folder icon drawable to a bitmap drawable.
2630             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2631
2632             // Convert the folder icon bitmap drawable to a bitmap.
2633             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2634         } else {  // Use the WebView favorite icon.
2635             // Copy the favorite icon bitmap to the folder icon bitmap.
2636             folderIconBitmap = favoriteIconBitmap;
2637         }
2638
2639         // Create a folder icon byte array output stream.
2640         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2641
2642         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2643         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2644
2645         // Convert the folder icon byte array stream to a byte array.
2646         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2647
2648         // Move all the bookmarks down one in the display order.
2649         for (int i = 0; i < bookmarksListView.getCount(); i++) {
2650             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2651             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2652         }
2653
2654         // Create the folder, which will be placed at the top of the `ListView`.
2655         bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2656
2657         // Update the bookmarks cursor with the current contents of this folder.
2658         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2659
2660         // Update the `ListView`.
2661         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2662
2663         // Scroll to the new folder.
2664         bookmarksListView.setSelection(0);
2665     }
2666
2667     @Override
2668     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, @NonNull Bitmap favoriteIconBitmap) {
2669         // Remove the incorrect lint warning below that the dialog fragment might be null.
2670         assert dialogFragment != null;
2671
2672         // Get the dialog.
2673         Dialog dialog = dialogFragment.getDialog();
2674
2675         // Remove the incorrect lint warning below that the dialog might be null.
2676         assert dialog != null;
2677
2678         // Get handles for the views from the dialog.
2679         RadioButton currentFolderIconRadioButton = dialog.findViewById(R.id.current_icon_radiobutton);
2680         RadioButton defaultFolderIconRadioButton = dialog.findViewById(R.id.default_icon_radiobutton);
2681         ImageView defaultFolderIconImageView = dialog.findViewById(R.id.default_icon_imageview);
2682         EditText editFolderNameEditText = dialog.findViewById(R.id.folder_name_edittext);
2683
2684         // Get the new folder name.
2685         String newFolderNameString = editFolderNameEditText.getText().toString();
2686
2687         // Check if the favorite icon has changed.
2688         if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
2689             // Update the name in the database.
2690             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2691         } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
2692             // Create the new folder icon Bitmap.
2693             Bitmap folderIconBitmap;
2694
2695             // Populate the new folder icon bitmap.
2696             if (defaultFolderIconRadioButton.isChecked()) {
2697                 // Get the default folder icon drawable.
2698                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2699
2700                 // Convert the folder icon drawable to a bitmap drawable.
2701                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2702
2703                 // Convert the folder icon bitmap drawable to a bitmap.
2704                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2705             } else {  // Use the `WebView` favorite icon.
2706                 // Copy the favorite icon bitmap to the folder icon bitmap.
2707                 folderIconBitmap = favoriteIconBitmap;
2708             }
2709
2710             // Create a folder icon byte array output stream.
2711             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2712
2713             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2714             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2715
2716             // Convert the folder icon byte array stream to a byte array.
2717             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2718
2719             // Update the folder icon in the database.
2720             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2721         } else {  // The folder icon and the name have changed.
2722             // Get the new folder icon bitmap.
2723             Bitmap folderIconBitmap;
2724             if (defaultFolderIconRadioButton.isChecked()) {
2725                 // Get the default folder icon drawable.
2726                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2727
2728                 // Convert the folder icon drawable to a bitmap drawable.
2729                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2730
2731                 // Convert the folder icon bitmap drawable to a bitmap.
2732                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2733             } else {  // Use the `WebView` favorite icon.
2734                 // Copy the favorite icon bitmap to the folder icon bitmap.
2735                 folderIconBitmap = favoriteIconBitmap;
2736             }
2737
2738             // Create a folder icon byte array output stream.
2739             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2740
2741             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2742             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2743
2744             // Convert the folder icon byte array stream to a byte array.
2745             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2746
2747             // Update the folder name and icon in the database.
2748             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
2749         }
2750
2751         // Update the bookmarks cursor with the current contents of this folder.
2752         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2753
2754         // Update the `ListView`.
2755         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2756     }
2757
2758     // Override `onBackPressed()` to handle the navigation drawer and and the WebViews.
2759     @Override
2760     public void onBackPressed() {
2761         // Check the different options for processing `back`.
2762         if (drawerLayout.isDrawerVisible(GravityCompat.START)) {  // The navigation drawer is open.
2763             // Close the navigation drawer.
2764             drawerLayout.closeDrawer(GravityCompat.START);
2765         } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){  // The bookmarks drawer is open.
2766             // close the bookmarks drawer.
2767             drawerLayout.closeDrawer(GravityCompat.END);
2768         } else if (displayingFullScreenVideo) {  // A full screen video is shown.
2769             // Exit the full screen video.
2770             exitFullScreenVideo();
2771         } else if (currentWebView.canGoBack()) {  // There is at least one item in the current WebView history.
2772             // Get the current web back forward list.
2773             WebBackForwardList webBackForwardList = currentWebView.copyBackForwardList();
2774
2775             // Get the previous entry URL.
2776             String previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.getCurrentIndex() - 1).getUrl();
2777
2778             // Apply the domain settings.
2779             applyDomainSettings(currentWebView, previousUrl, false, false, false);
2780
2781             // Go back.
2782             currentWebView.goBack();
2783         } else if (tabLayout.getTabCount() > 1) {  // There are at least two tabs.
2784             // Close the current tab.
2785             closeCurrentTab();
2786         } else {  // There isn't anything to do in Privacy Browser.
2787             // Close Privacy Browser.  `finishAndRemoveTask()` also removes Privacy Browser from the recent app list.
2788             if (Build.VERSION.SDK_INT >= 21) {
2789                 finishAndRemoveTask();
2790             } else {
2791                 finish();
2792             }
2793
2794             // Manually kill Privacy Browser.  Otherwise, it is glitchy when restarted.
2795             System.exit(0);
2796         }
2797     }
2798
2799     // Process the results of a file browse.
2800     @Override
2801     public void onActivityResult(int requestCode, int resultCode, Intent returnedIntent) {
2802         // Run the default commands.
2803         super.onActivityResult(requestCode, resultCode, returnedIntent);
2804
2805         // Run the commands that correlate to the specified request code.
2806         switch (requestCode) {
2807             case BROWSE_FILE_UPLOAD_REQUEST_CODE:
2808                 // File uploads only work on API >= 21.
2809                 if (Build.VERSION.SDK_INT >= 21) {
2810                     // Pass the file to the WebView.
2811                     fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, returnedIntent));
2812                 }
2813                 break;
2814
2815             case BROWSE_OPEN_REQUEST_CODE:
2816                 // Don't do anything if the user pressed back from the file picker.
2817                 if (resultCode == Activity.RESULT_OK) {
2818                     // Get a handle for the open dialog fragment.
2819                     DialogFragment openDialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(getString(R.string.open));
2820
2821                     // Only update the file name if the dialog still exists.
2822                     if (openDialogFragment != null) {
2823                         // Get a handle for the open dialog.
2824                         Dialog openDialog = openDialogFragment.getDialog();
2825
2826                         // Remove the incorrect lint warning below that the dialog might be null.
2827                         assert openDialog != null;
2828
2829                         // Get a handle for the file name edit text.
2830                         EditText fileNameEditText = openDialog.findViewById(R.id.file_name_edittext);
2831
2832                         // Get the file name URI from the intent.
2833                         Uri fileNameUri = returnedIntent.getData();
2834
2835                         // Get the file name string from the URI.
2836                         String fileNameString = fileNameUri.toString();
2837
2838                         // Set the file name text.
2839                         fileNameEditText.setText(fileNameString);
2840
2841                         // Move the cursor to the end of the file name edit text.
2842                         fileNameEditText.setSelection(fileNameString.length());
2843                     }
2844                 }
2845                 break;
2846         }
2847     }
2848
2849     private void loadUrlFromTextBox() {
2850         // Get the text from urlTextBox and convert it to a string.  trim() removes white spaces from the beginning and end of the string.
2851         String unformattedUrlString = urlEditText.getText().toString().trim();
2852
2853         // Initialize the formatted URL string.
2854         String url = "";
2855
2856         // Check to see if `unformattedUrlString` is a valid URL.  Otherwise, convert it into a search.
2857         if (unformattedUrlString.startsWith("content://")) {  // This is a Content URL.
2858             // Load the entire content URL.
2859             url = unformattedUrlString;
2860         } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
2861                 unformattedUrlString.startsWith("file://")) {  // This is a standard URL.
2862             // Add `https://` at the beginning if there is no protocol.  Otherwise the app will segfault.
2863             if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
2864                 unformattedUrlString = "https://" + unformattedUrlString;
2865             }
2866
2867             // Initialize `unformattedUrl`.
2868             URL unformattedUrl = null;
2869
2870             // 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.
2871             try {
2872                 unformattedUrl = new URL(unformattedUrlString);
2873             } catch (MalformedURLException e) {
2874                 e.printStackTrace();
2875             }
2876
2877             // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
2878             String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
2879             String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
2880             String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
2881             String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
2882             String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
2883
2884             // Build the URI.
2885             Uri.Builder uri = new Uri.Builder();
2886             uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
2887
2888             // Decode the URI as a UTF-8 string in.
2889             try {
2890                 url = URLDecoder.decode(uri.build().toString(), "UTF-8");
2891             } catch (UnsupportedEncodingException exception) {
2892                 // Do nothing.  The formatted URL string will remain blank.
2893             }
2894         } else if (!unformattedUrlString.isEmpty()){  // This is not a URL, but rather a search string.
2895             // Create an encoded URL String.
2896             String encodedUrlString;
2897
2898             // Sanitize the search input.
2899             try {
2900                 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
2901             } catch (UnsupportedEncodingException exception) {
2902                 encodedUrlString = "";
2903             }
2904
2905             // Add the base search URL.
2906             url = searchURL + encodedUrlString;
2907         }
2908
2909         // 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.
2910         urlEditText.clearFocus();
2911
2912         // Make it so.
2913         loadUrl(currentWebView, url);
2914     }
2915
2916     private void loadUrl(NestedScrollWebView nestedScrollWebView, String url) {
2917         // Sanitize the URL.
2918         url = sanitizeUrl(url);
2919
2920         // Apply the domain settings and load the URL.
2921         applyDomainSettings(nestedScrollWebView, url, true, false, true);
2922     }
2923
2924     public void findPreviousOnPage(View view) {
2925         // Go to the previous highlighted phrase on the page.  `false` goes backwards instead of forwards.
2926         currentWebView.findNext(false);
2927     }
2928
2929     public void findNextOnPage(View view) {
2930         // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
2931         currentWebView.findNext(true);
2932     }
2933
2934     public void closeFindOnPage(View view) {
2935         // Get a handle for the views.
2936         Toolbar toolbar = findViewById(R.id.toolbar);