Updated Turkisk translation.
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.java
1 /*
2  * Copyright © 2015-2019 Soren Stoutner <soren@stoutner.com>.
3  *
4  * Download cookie code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
5  *
6  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
7  *
8  * Privacy Browser is free software: you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation, either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * Privacy Browser is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
20  */
21
22 package com.stoutner.privacybrowser.activities;
23
24 import android.Manifest;
25 import android.annotation.SuppressLint;
26 import android.app.Activity;
27 import android.app.DownloadManager;
28 import android.app.SearchManager;
29 import android.content.ActivityNotFoundException;
30 import android.content.BroadcastReceiver;
31 import android.content.ClipData;
32 import android.content.ClipboardManager;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.IntentFilter;
36 import android.content.SharedPreferences;
37 import android.content.pm.PackageManager;
38 import android.content.res.Configuration;
39 import android.database.Cursor;
40 import android.graphics.Bitmap;
41 import android.graphics.BitmapFactory;
42 import android.graphics.Canvas;
43 import android.graphics.Typeface;
44 import android.graphics.drawable.BitmapDrawable;
45 import android.graphics.drawable.Drawable;
46 import android.net.Uri;
47 import android.net.http.SslCertificate;
48 import android.net.http.SslError;
49 import android.os.Build;
50 import android.os.Bundle;
51 import android.os.Environment;
52 import android.os.Handler;
53 import android.preference.PreferenceManager;
54 import android.print.PrintDocumentAdapter;
55 import android.print.PrintManager;
56 import android.text.Editable;
57 import android.text.Spanned;
58 import android.text.TextWatcher;
59 import android.text.style.ForegroundColorSpan;
60 import android.util.Patterns;
61 import android.view.ContextMenu;
62 import android.view.GestureDetector;
63 import android.view.KeyEvent;
64 import android.view.Menu;
65 import android.view.MenuItem;
66 import android.view.MotionEvent;
67 import android.view.View;
68 import android.view.ViewGroup;
69 import android.view.WindowManager;
70 import android.view.inputmethod.InputMethodManager;
71 import android.webkit.CookieManager;
72 import android.webkit.HttpAuthHandler;
73 import android.webkit.SslErrorHandler;
74 import android.webkit.ValueCallback;
75 import android.webkit.WebChromeClient;
76 import android.webkit.WebResourceResponse;
77 import android.webkit.WebSettings;
78 import android.webkit.WebStorage;
79 import android.webkit.WebView;
80 import android.webkit.WebViewClient;
81 import android.webkit.WebViewDatabase;
82 import android.widget.ArrayAdapter;
83 import android.widget.CursorAdapter;
84 import android.widget.EditText;
85 import android.widget.FrameLayout;
86 import android.widget.ImageView;
87 import android.widget.LinearLayout;
88 import android.widget.ListView;
89 import android.widget.ProgressBar;
90 import android.widget.RadioButton;
91 import android.widget.RelativeLayout;
92 import android.widget.TextView;
93
94 import androidx.annotation.NonNull;
95 import androidx.appcompat.app.ActionBar;
96 import androidx.appcompat.app.ActionBarDrawerToggle;
97 import androidx.appcompat.app.AppCompatActivity;
98 import androidx.appcompat.widget.Toolbar;
99 import androidx.coordinatorlayout.widget.CoordinatorLayout;
100 import androidx.core.app.ActivityCompat;
101 import androidx.core.content.ContextCompat;
102 import androidx.core.view.GravityCompat;
103 import androidx.drawerlayout.widget.DrawerLayout;
104 import androidx.fragment.app.DialogFragment;
105 import androidx.fragment.app.FragmentManager;
106 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
107 import androidx.viewpager.widget.ViewPager;
108
109 import com.google.android.material.appbar.AppBarLayout;
110 import com.google.android.material.floatingactionbutton.FloatingActionButton;
111 import com.google.android.material.navigation.NavigationView;
112 import com.google.android.material.snackbar.Snackbar;
113 import com.google.android.material.tabs.TabLayout;
114
115 import com.stoutner.privacybrowser.BuildConfig;
116 import com.stoutner.privacybrowser.R;
117 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter;
118 import com.stoutner.privacybrowser.asynctasks.GetHostIpAddresses;
119 import com.stoutner.privacybrowser.asynctasks.PopulateBlocklists;
120 import com.stoutner.privacybrowser.dialogs.AdConsentDialog;
121 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog;
122 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog;
123 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog;
124 import com.stoutner.privacybrowser.dialogs.DownloadFileDialog;
125 import com.stoutner.privacybrowser.dialogs.DownloadImageDialog;
126 import com.stoutner.privacybrowser.dialogs.DownloadLocationPermissionDialog;
127 import com.stoutner.privacybrowser.dialogs.EditBookmarkDialog;
128 import com.stoutner.privacybrowser.dialogs.EditBookmarkFolderDialog;
129 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog;
130 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog;
131 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog;
132 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog;
133 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
134 import com.stoutner.privacybrowser.helpers.AdHelper;
135 import com.stoutner.privacybrowser.helpers.BlocklistHelper;
136 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper;
137 import com.stoutner.privacybrowser.helpers.CheckPinnedMismatchHelper;
138 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper;
139 import com.stoutner.privacybrowser.helpers.OrbotProxyHelper;
140 import com.stoutner.privacybrowser.views.NestedScrollWebView;
141
142 import java.io.ByteArrayInputStream;
143 import java.io.ByteArrayOutputStream;
144 import java.io.File;
145 import java.io.FileOutputStream;
146 import java.io.IOException;
147 import java.io.UnsupportedEncodingException;
148 import java.net.MalformedURLException;
149 import java.net.URL;
150 import java.net.URLDecoder;
151 import java.net.URLEncoder;
152 import java.util.ArrayList;
153 import java.util.Date;
154 import java.util.HashMap;
155 import java.util.HashSet;
156 import java.util.List;
157 import java.util.Map;
158 import java.util.Set;
159
160 // AppCompatActivity from android.support.v7.app.AppCompatActivity must be used to have access to the SupportActionBar until the minimum API is >= 21.
161 public class MainWebViewActivity extends AppCompatActivity implements CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener,
162         DownloadFileDialog.DownloadFileListener, DownloadImageDialog.DownloadImageListener, DownloadLocationPermissionDialog.DownloadLocationPermissionDialogListener, EditBookmarkDialog.EditBookmarkListener,
163         EditBookmarkFolderDialog.EditBookmarkFolderListener, NavigationView.OnNavigationItemSelectedListener, PopulateBlocklists.PopulateBlocklistsListener, WebViewTabFragment.NewTabListener {
164
165     // `orbotStatus` is public static so it can be accessed from `OrbotProxyHelper`.  It is also used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
166     public static String orbotStatus;
167
168     // The WebView pager adapter is accessed from `HttpAuthenticationDialog`, `PinnedMismatchDialog`, and `SslCertificateErrorDialog`.  It is also used in `onCreate()`, `onResume()`, and `addTab()`.
169     public static WebViewPagerAdapter webViewPagerAdapter;
170
171     // The load URL on restart variables are public static so they can be accessed from `BookmarksActivity`.  They are used in `onRestart()`.
172     public static boolean loadUrlOnRestart;
173     public static String urlToLoadOnRestart;
174
175     // `restartFromBookmarksActivity` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onRestart()`.
176     public static boolean restartFromBookmarksActivity;
177
178     // `currentBookmarksFolder` is public static so it can be accessed from `BookmarksActivity`.  It is also used in `onCreate()`, `onBackPressed()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`,
179     // `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
180     public static String currentBookmarksFolder;
181
182     // The user agent constants are public static so they can be accessed from `SettingsFragment`, `DomainsActivity`, and `DomainSettingsFragment`.
183     public final static int UNRECOGNIZED_USER_AGENT = -1;
184     public final static int SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1;
185     public final static int SETTINGS_CUSTOM_USER_AGENT = 12;
186     public final static int DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0;
187     public final static int DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2;
188     public final static int DOMAINS_CUSTOM_USER_AGENT = 13;
189
190
191
192     // The current WebView is used in `onCreate()`, `onPrepareOptionsMenu()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, `onCreateContextMenu()`, `findPreviousOnPage()`,
193     // `findNextOnPage()`, `closeFindOnPage()`, `loadUrlFromTextBox()`, `onSslMismatchBack()`, `applyProxyThroughOrbot()`, and `applyDomainSettings()`.
194     private NestedScrollWebView currentWebView;
195
196     // `customHeader` is used in `onCreate()`, `onOptionsItemSelected()`, `onCreateContextMenu()`, and `loadUrl()`.
197     private final Map<String, String> customHeaders = new HashMap<>();
198
199     // The search URL is set in `applyProxyThroughOrbot()` and used in `onCreate()`, `onNewIntent()`, `loadURLFromTextBox()`, and `initializeWebView()`.
200     private String searchURL;
201
202     // The options menu is set in `onCreateOptionsMenu()` and used in `onOptionsItemSelected()`, `updatePrivacyIcons()`, and `initializeWebView()`.
203     private Menu optionsMenu;
204
205     // The blocklists are populated in `finishedPopulatingBlocklists()` and accessed from `initializeWebView()`.
206     private ArrayList<List<String[]>> easyList;
207     private ArrayList<List<String[]>> easyPrivacy;
208     private ArrayList<List<String[]>> fanboysAnnoyanceList;
209     private ArrayList<List<String[]>> fanboysSocialList;
210     private ArrayList<List<String[]>> ultraPrivacy;
211
212     // `webViewDefaultUserAgent` is used in `onCreate()` and `onPrepareOptionsMenu()`.
213     private String webViewDefaultUserAgent;
214
215     // `proxyThroughOrbot` is used in `onRestart()`, `onOptionsItemSelected()`, `applyAppSettings()`, and `applyProxyThroughOrbot()`.
216     private boolean proxyThroughOrbot;
217
218     // The incognito mode is set in `applyAppSettings()` and used in `initializeWebView()`.
219     private boolean incognitoModeEnabled;
220
221     // The full screen browsing mode tracker is set it `applyAppSettings()` and used in `initializeWebView()`.
222     private boolean fullScreenBrowsingModeEnabled;
223
224     // `inFullScreenBrowsingMode` is used in `onCreate()`, `onConfigurationChanged()`, and `applyAppSettings()`.
225     private boolean inFullScreenBrowsingMode;
226
227     // The app bar trackers are set in `applyAppSettings()` and used in `initializeWebView()`.
228     private boolean hideAppBar;
229     private boolean scrollAppBar;
230
231     // The loading new intent tracker is set in `onNewIntent()` and used in `setCurrentWebView()`.
232     private boolean loadingNewIntent;
233
234     // `reapplyDomainSettingsOnRestart` is used in `onCreate()`, `onOptionsItemSelected()`, `onNavigationItemSelected()`, `onRestart()`, and `onAddDomain()`, .
235     private boolean reapplyDomainSettingsOnRestart;
236
237     // `reapplyAppSettingsOnRestart` is used in `onNavigationItemSelected()` and `onRestart()`.
238     private boolean reapplyAppSettingsOnRestart;
239
240     // `displayingFullScreenVideo` is used in `onCreate()` and `onResume()`.
241     private boolean displayingFullScreenVideo;
242
243     // `orbotStatusBroadcastReceiver` is used in `onCreate()` and `onDestroy()`.
244     private BroadcastReceiver orbotStatusBroadcastReceiver;
245
246     // `waitingForOrbot` is used in `onCreate()`, `onResume()`, and `applyProxyThroughOrbot()`.
247     private boolean waitingForOrbot;
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     // The drawer header padding variables are used in `onCreate()` and `onConfigurationChanged()`.
258     private int drawerHeaderPaddingLeftAndRight;
259     private int drawerHeaderPaddingTop;
260     private int drawerHeaderPaddingBottom;
261
262     // `bookmarksDatabaseHelper` is used in `onCreate()`, `onDestroy`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`,
263     // and `loadBookmarksFolder()`.
264     private BookmarksDatabaseHelper bookmarksDatabaseHelper;
265
266     // `bookmarksCursor` is used in `onDestroy()`, `onOptionsItemSelected()`, `onCreateBookmark()`, `onCreateBookmarkFolder()`, `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
267     private Cursor bookmarksCursor;
268
269     // `bookmarksCursorAdapter` is used in `onCreateBookmark()`, `onCreateBookmarkFolder()` `onSaveEditBookmark()`, `onSaveEditBookmarkFolder()`, and `loadBookmarksFolder()`.
270     private CursorAdapter bookmarksCursorAdapter;
271
272     // `oldFolderNameString` is used in `onCreate()` and `onSaveEditBookmarkFolder()`.
273     private String oldFolderNameString;
274
275     // `fileChooserCallback` is used in `onCreate()` and `onActivityResult()`.
276     private ValueCallback<Uri[]> fileChooserCallback;
277
278     // The default progress view offsets are set in `onCreate()` and used in `initializeWebView()`.
279     private int defaultProgressViewStartOffset;
280     private int defaultProgressViewEndOffset;
281
282     // The swipe refresh layout top padding is used when exiting full screen browsing mode.  It is used in an inner class in `initializeWebView()`.
283     private int swipeRefreshLayoutPaddingTop;
284
285     // The URL sanitizers are set in `applyAppSettings()` and used in `sanitizeUrl()`.
286     private boolean sanitizeGoogleAnalytics;
287     private boolean sanitizeFacebookClickIds;
288     private boolean sanitizeTwitterAmpRedirects;
289
290     // The download strings are used in `onCreate()`, `onRequestPermissionResult()` and `initializeWebView()`.
291     private String downloadUrl;
292     private String downloadContentDisposition;
293     private long downloadContentLength;
294
295     // `downloadImageUrl` is used in `onCreateContextMenu()` and `onRequestPermissionResult()`.
296     private String downloadImageUrl;
297
298     // The request codes are used in `onCreate()`, `onCreateContextMenu()`, `onCloseDownloadLocationPermissionDialog()`, `onRequestPermissionResult()`, and `initializeWebView()`.
299     private final int DOWNLOAD_FILE_REQUEST_CODE = 1;
300     private final int DOWNLOAD_IMAGE_REQUEST_CODE = 2;
301
302     @Override
303     // Remove the warning about needing to override `performClick()` when using an `OnTouchListener` with `WebView`.
304     @SuppressLint("ClickableViewAccessibility")
305     protected void onCreate(Bundle savedInstanceState) {
306         if (Build.VERSION.SDK_INT >= 21) {
307             WebView.enableSlowWholeDocumentDraw();
308         }
309
310         // Initialize the default preference values the first time the program is run.  `false` keeps this command from resetting any current preferences back to default.
311         PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
312
313         // Get a handle for the shared preferences.
314         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
315
316         // Get the theme and screenshot preferences.
317         boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
318         boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
319
320         // Disable screenshots if not allowed.
321         if (!allowScreenshots) {
322             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
323         }
324
325         // Set the activity theme.
326         if (darkTheme) {
327             setTheme(R.style.PrivacyBrowserDark);
328         } else {
329             setTheme(R.style.PrivacyBrowserLight);
330         }
331
332         // Run the default commands.
333         super.onCreate(savedInstanceState);
334
335         // Set the content view.
336         setContentView(R.layout.main_framelayout);
337
338         // Get handles for the views that need to be modified.
339         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
340         Toolbar toolbar = findViewById(R.id.toolbar);
341         ViewPager webViewPager = findViewById(R.id.webviewpager);
342
343         // Set the action bar.  `SupportActionBar` must be used until the minimum API is >= 21.
344         setSupportActionBar(toolbar);
345
346         // Get a handle for the action bar.
347         ActionBar actionBar = getSupportActionBar();
348
349         // This is needed to get rid of the Android Studio warning that the action bar might be null.
350         assert actionBar != null;
351
352         // Add the custom layout, which shows the URL text bar.
353         actionBar.setCustomView(R.layout.url_app_bar);
354         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
355
356         // Initially disable the sliding drawers.  They will be enabled once the blocklists are loaded.
357         drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
358
359         // Create the hamburger icon at the start of the AppBar.
360         actionBarDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer);
361
362         // Initialize the web view pager adapter.
363         webViewPagerAdapter = new WebViewPagerAdapter(getSupportFragmentManager());
364
365         // Set the pager adapter on the web view pager.
366         webViewPager.setAdapter(webViewPagerAdapter);
367
368         // Store up to 100 tabs in memory.
369         webViewPager.setOffscreenPageLimit(100);
370
371         // Populate the blocklists.
372         new PopulateBlocklists(this, this).execute();
373     }
374
375     @Override
376     protected void onNewIntent(Intent intent) {
377         // Replace the intent that started the app with this one.
378         setIntent(intent);
379
380         // Process the intent here if Privacy Browser is fully initialized.  If the process has been killed by the system while sitting in the background, this will be handled in `initializeWebView()`.
381         if (ultraPrivacy != null) {
382             // Get the information from the intent.
383             String intentAction = intent.getAction();
384             Uri intentUriData = intent.getData();
385
386             // Determine if this is a web search.
387             boolean isWebSearch = ((intentAction != null) && intentAction.equals(Intent.ACTION_WEB_SEARCH));
388
389             // 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.
390             if (intentUriData != null || isWebSearch) {
391                 // Get the shared preferences.
392                 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
393
394                 // Create a URL string.
395                 String url;
396
397                 // If the intent action is a web search, perform the search.
398                 if (isWebSearch) {
399                     // Create an encoded URL string.
400                     String encodedUrlString;
401
402                     // Sanitize the search input and convert it to a search.
403                     try {
404                         encodedUrlString = URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8");
405                     } catch (UnsupportedEncodingException exception) {
406                         encodedUrlString = "";
407                     }
408
409                     // Add the base search URL.
410                     url = searchURL + encodedUrlString;
411                 } else {  // The intent should contain a URL.
412                     // Set the intent data as the URL.
413                     url = intentUriData.toString();
414                 }
415
416                 // Add a new tab if specified in the preferences.
417                 if (sharedPreferences.getBoolean("open_intents_in_new_tab", true)) {  // Load the URL in a new tab.
418                     // Set the loading new intent flag.
419                     loadingNewIntent = true;
420
421                     // Add a new tab.
422                     addNewTab(url);
423                 } else {  // Load the URL in the current tab.
424                     // Make it so.
425                     loadUrl(url);
426                 }
427
428                 // Get a handle for the drawer layout.
429                 DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
430
431                 // Close the navigation drawer if it is open.
432                 if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
433                     drawerLayout.closeDrawer(GravityCompat.START);
434                 }
435
436                 // Close the bookmarks drawer if it is open.
437                 if (drawerLayout.isDrawerVisible(GravityCompat.END)) {
438                     drawerLayout.closeDrawer(GravityCompat.END);
439                 }
440             }
441         }
442     }
443
444     @Override
445     public void onRestart() {
446         // Run the default commands.
447         super.onRestart();
448
449         // Make sure Orbot is running if Privacy Browser is proxying through Orbot.
450         if (proxyThroughOrbot) {
451             // Request Orbot to start.  If Orbot is already running no hard will be caused by this request.
452             Intent orbotIntent = new Intent("org.torproject.android.intent.action.START");
453
454             // Send the intent to the Orbot package.
455             orbotIntent.setPackage("org.torproject.android");
456
457             // Make it so.
458             sendBroadcast(orbotIntent);
459         }
460
461         // Apply the app settings if returning from the Settings activity.
462         if (reapplyAppSettingsOnRestart) {
463             // Reset the reapply app settings on restart tracker.
464             reapplyAppSettingsOnRestart = false;
465
466             // Apply the app settings.
467             applyAppSettings();
468         }
469
470         // Apply the domain settings if returning from the settings or domains activity.
471         if (reapplyDomainSettingsOnRestart) {
472             // Reset the reapply domain settings on restart tracker.
473             reapplyDomainSettingsOnRestart = false;
474
475             // Reapply the domain settings for each tab.
476             for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
477                 // Get the WebView tab fragment.
478                 WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
479
480                 // Get the fragment view.
481                 View fragmentView = webViewTabFragment.getView();
482
483                 // Only reload the WebViews if they exist.
484                 if (fragmentView != null) {
485                     // Get the nested scroll WebView from the tab fragment.
486                     NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
487
488                     // Reset the current domain name so the domain settings will be reapplied.
489                     nestedScrollWebView.resetCurrentDomainName();
490
491                     // Reapply the domain settings if the URL is not null, which can happen if an empty tab is active when returning from settings.
492                     if (nestedScrollWebView.getUrl() != null) {
493                         applyDomainSettings(nestedScrollWebView, nestedScrollWebView.getUrl(), false, true);
494                     }
495                 }
496             }
497         }
498
499         // Load the URL on restart (used when loading a bookmark).
500         if (loadUrlOnRestart) {
501             // Load the specified URL.
502             loadUrl(urlToLoadOnRestart);
503
504             // Reset the load on restart tracker.
505             loadUrlOnRestart = false;
506         }
507
508         // Update the bookmarks drawer if returning from the Bookmarks activity.
509         if (restartFromBookmarksActivity) {
510             // Get a handle for the drawer layout.
511             DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
512
513             // Close the bookmarks drawer.
514             drawerLayout.closeDrawer(GravityCompat.END);
515
516             // Reload the bookmarks drawer.
517             loadBookmarksFolder();
518
519             // Reset `restartFromBookmarksActivity`.
520             restartFromBookmarksActivity = false;
521         }
522
523         // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.  This can be important if the screen was rotated.
524         updatePrivacyIcons(true);
525     }
526
527     // `onResume()` runs after `onStart()`, which runs after `onCreate()` and `onRestart()`.
528     @Override
529     public void onResume() {
530         // Run the default commands.
531         super.onResume();
532
533         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
534             // Get the WebView tab fragment.
535             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
536
537             // Get the fragment view.
538             View fragmentView = webViewTabFragment.getView();
539
540             // Only resume the WebViews if they exist (they won't when the app is first created).
541             if (fragmentView != null) {
542                 // Get the nested scroll WebView from the tab fragment.
543                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
544
545                 // Resume the nested scroll WebView JavaScript timers.
546                 nestedScrollWebView.resumeTimers();
547
548                 // Resume the nested scroll WebView.
549                 nestedScrollWebView.onResume();
550             }
551         }
552
553         // Display a message to the user if waiting for Orbot.
554         if (waitingForOrbot && !orbotStatus.equals("ON")) {
555             // Disable the wide view port so that the waiting for Orbot text is displayed correctly.
556             currentWebView.getSettings().setUseWideViewPort(false);
557
558             // Load a waiting page.  `null` specifies no encoding, which defaults to ASCII.
559             currentWebView.loadData("<html><body><br/><center><h1>" + getString(R.string.waiting_for_orbot) + "</h1></center></body></html>", "text/html", null);
560         }
561
562         if (displayingFullScreenVideo || inFullScreenBrowsingMode) {
563             // Get a handle for the root frame layouts.
564             FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
565
566             // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
567             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
568
569             /* Hide the system bars.
570              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
571              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
572              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
573              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
574              */
575             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
576                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
577         } else if (BuildConfig.FLAVOR.contentEquals("free")) {  // Resume the adView for the free flavor.
578             // Resume the ad.
579             AdHelper.resumeAd(findViewById(R.id.adview));
580         }
581     }
582
583     @Override
584     public void onPause() {
585         // Run the default commands.
586         super.onPause();
587
588         for (int i = 0; i < webViewPagerAdapter.getCount(); i++) {
589             // Get the WebView tab fragment.
590             WebViewTabFragment webViewTabFragment = webViewPagerAdapter.getPageFragment(i);
591
592             // Get the fragment view.
593             View fragmentView = webViewTabFragment.getView();
594
595             // Only pause the WebViews if they exist (they won't when the app is first created).
596             if (fragmentView != null) {
597                 // Get the nested scroll WebView from the tab fragment.
598                 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
599
600                 // Pause the nested scroll WebView.
601                 nestedScrollWebView.onPause();
602
603                 // Pause the nested scroll WebView JavaScript timers.
604                 nestedScrollWebView.pauseTimers();
605             }
606         }
607
608         // Pause the ad or it will continue to consume resources in the background on the free flavor.
609         if (BuildConfig.FLAVOR.contentEquals("free")) {
610             // Pause the ad.
611             AdHelper.pauseAd(findViewById(R.id.adview));
612         }
613     }
614
615     @Override
616     public void onDestroy() {
617         // Unregister the Orbot status broadcast receiver.
618         this.unregisterReceiver(orbotStatusBroadcastReceiver);
619
620         // Close the bookmarks cursor and database.
621         bookmarksCursor.close();
622         bookmarksDatabaseHelper.close();
623
624         // Run the default commands.
625         super.onDestroy();
626     }
627
628     @Override
629     public boolean onCreateOptionsMenu(Menu menu) {
630         // Inflate the menu.  This adds items to the action bar if it is present.
631         getMenuInflater().inflate(R.menu.webview_options_menu, menu);
632
633         // Store a handle for the options menu so it can be used by `onOptionsItemSelected()` and `updatePrivacyIcons()`.
634         optionsMenu = menu;
635
636         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
637         updatePrivacyIcons(false);
638
639         // Get handles for the menu items.
640         MenuItem toggleFirstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
641         MenuItem toggleThirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
642         MenuItem toggleDomStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
643         MenuItem toggleSaveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data);  // Form data can be removed once the minimum API >= 26.
644         MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
645         MenuItem refreshMenuItem = menu.findItem(R.id.refresh);
646         MenuItem adConsentMenuItem = menu.findItem(R.id.ad_consent);
647
648         // Only display third-party cookies if API >= 21
649         toggleThirdPartyCookiesMenuItem.setVisible(Build.VERSION.SDK_INT >= 21);
650
651         // Only display the form data menu items if the API < 26.
652         toggleSaveFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
653         clearFormDataMenuItem.setVisible(Build.VERSION.SDK_INT < 26);
654
655         // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
656         clearFormDataMenuItem.setEnabled(Build.VERSION.SDK_INT < 26);
657
658         // Only show Ad Consent if this is the free flavor.
659         adConsentMenuItem.setVisible(BuildConfig.FLAVOR.contentEquals("free"));
660
661         // Get the shared preferences.
662         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
663
664         // Get the dark theme and app bar preferences..
665         boolean displayAdditionalAppBarIcons = sharedPreferences.getBoolean("display_additional_app_bar_icons", false);
666         boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
667
668         // 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.
669         if (displayAdditionalAppBarIcons) {
670             toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
671             toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
672             refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
673         } else { //Do not display the additional icons.
674             toggleFirstPartyCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
675             toggleDomStorageMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
676             refreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
677         }
678
679         // Replace Refresh with Stop if a URL is already loading.
680         if (currentWebView != null && currentWebView.getProgress() != 100) {
681             // Set the title.
682             refreshMenuItem.setTitle(R.string.stop);
683
684             // If the icon is displayed in the AppBar, set it according to the theme.
685             if (displayAdditionalAppBarIcons) {
686                 if (darkTheme) {
687                     refreshMenuItem.setIcon(R.drawable.close_dark);
688                 } else {
689                     refreshMenuItem.setIcon(R.drawable.close_light);
690                 }
691             }
692         }
693
694         // Done.
695         return true;
696     }
697
698     @Override
699     public boolean onPrepareOptionsMenu(Menu menu) {
700         // Get handles for the menu items.
701         MenuItem addOrEditDomain = menu.findItem(R.id.add_or_edit_domain);
702         MenuItem firstPartyCookiesMenuItem = menu.findItem(R.id.toggle_first_party_cookies);
703         MenuItem thirdPartyCookiesMenuItem = menu.findItem(R.id.toggle_third_party_cookies);
704         MenuItem domStorageMenuItem = menu.findItem(R.id.toggle_dom_storage);
705         MenuItem saveFormDataMenuItem = menu.findItem(R.id.toggle_save_form_data);  // Form data can be removed once the minimum API >= 26.
706         MenuItem clearDataMenuItem = menu.findItem(R.id.clear_data);
707         MenuItem clearCookiesMenuItem = menu.findItem(R.id.clear_cookies);
708         MenuItem clearDOMStorageMenuItem = menu.findItem(R.id.clear_dom_storage);
709         MenuItem clearFormDataMenuItem = menu.findItem(R.id.clear_form_data);  // Form data can be removed once the minimum API >= 26.
710         MenuItem blocklistsMenuItem = menu.findItem(R.id.blocklists);
711         MenuItem easyListMenuItem = menu.findItem(R.id.easylist);
712         MenuItem easyPrivacyMenuItem = menu.findItem(R.id.easyprivacy);
713         MenuItem fanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list);
714         MenuItem fanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list);
715         MenuItem ultraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy);
716         MenuItem blockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests);
717         MenuItem fontSizeMenuItem = menu.findItem(R.id.font_size);
718         MenuItem swipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh);
719         MenuItem wideViewportMenuItem = menu.findItem(R.id.wide_viewport);
720         MenuItem displayImagesMenuItem = menu.findItem(R.id.display_images);
721         MenuItem nightModeMenuItem = menu.findItem(R.id.night_mode);
722         MenuItem proxyThroughOrbotMenuItem = menu.findItem(R.id.proxy_through_orbot);
723
724         // Get a handle for the cookie manager.
725         CookieManager cookieManager = CookieManager.getInstance();
726
727         // Initialize the current user agent string and the font size.
728         String currentUserAgent = getString(R.string.user_agent_privacy_browser);
729         int fontSize = 100;
730
731         // 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.
732         if (currentWebView != null) {
733             // Set the add or edit domain text.
734             if (currentWebView.getDomainSettingsApplied()) {
735                 addOrEditDomain.setTitle(R.string.edit_domain_settings);
736             } else {
737                 addOrEditDomain.setTitle(R.string.add_domain_settings);
738             }
739
740             // Get the current user agent from the WebView.
741             currentUserAgent = currentWebView.getSettings().getUserAgentString();
742
743             // Get the current font size from the
744             fontSize = currentWebView.getSettings().getTextZoom();
745
746             // Set the status of the menu item checkboxes.
747             domStorageMenuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
748             saveFormDataMenuItem.setChecked(currentWebView.getSettings().getSaveFormData());  // Form data can be removed once the minimum API >= 26.
749             easyListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
750             easyPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
751             fanboysAnnoyanceListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
752             fanboysSocialBlockingListMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
753             ultraPrivacyMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
754             blockAllThirdPartyRequestsMenuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
755             swipeToRefreshMenuItem.setChecked(currentWebView.getSwipeToRefresh());
756             wideViewportMenuItem.setChecked(currentWebView.getSettings().getUseWideViewPort());
757             displayImagesMenuItem.setChecked(currentWebView.getSettings().getLoadsImagesAutomatically());
758             nightModeMenuItem.setChecked(currentWebView.getNightMode());
759
760             // Initialize the display names for the blocklists with the number of blocked requests.
761             blocklistsMenuItem.setTitle(getString(R.string.blocklists) + " - " + currentWebView.getRequestsCount(NestedScrollWebView.BLOCKED_REQUESTS));
762             easyListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_LIST) + " - " + getString(R.string.easylist));
763             easyPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.EASY_PRIVACY) + " - " + getString(R.string.easyprivacy));
764             fanboysAnnoyanceListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST) + " - " + getString(R.string.fanboys_annoyance_list));
765             fanboysSocialBlockingListMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST) + " - " + getString(R.string.fanboys_social_blocking_list));
766             ultraPrivacyMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.ULTRA_PRIVACY) + " - " + getString(R.string.ultraprivacy));
767             blockAllThirdPartyRequestsMenuItem.setTitle(currentWebView.getRequestsCount(NestedScrollWebView.THIRD_PARTY_REQUESTS) + " - " + getString(R.string.block_all_third_party_requests));
768
769             // Only modify third-party cookies if the API >= 21.
770             if (Build.VERSION.SDK_INT >= 21) {
771                 // Set the status of the third-party cookies checkbox.
772                 thirdPartyCookiesMenuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
773
774                 // Enable third-party cookies if first-party cookies are enabled.
775                 thirdPartyCookiesMenuItem.setEnabled(cookieManager.acceptCookie());
776             }
777
778             // Enable DOM Storage if JavaScript is enabled.
779             domStorageMenuItem.setEnabled(currentWebView.getSettings().getJavaScriptEnabled());
780         }
781
782         // Set the status of the menu item checkboxes.
783         firstPartyCookiesMenuItem.setChecked(cookieManager.acceptCookie());
784         proxyThroughOrbotMenuItem.setChecked(proxyThroughOrbot);
785
786         // Enable Clear Cookies if there are any.
787         clearCookiesMenuItem.setEnabled(cookieManager.hasCookies());
788
789         // 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`.
790         String privateDataDirectoryString = getApplicationInfo().dataDir;
791
792         // Get a count of the number of files in the Local Storage directory.
793         File localStorageDirectory = new File (privateDataDirectoryString + "/app_webview/Local Storage/");
794         int localStorageDirectoryNumberOfFiles = 0;
795         if (localStorageDirectory.exists()) {
796             localStorageDirectoryNumberOfFiles = localStorageDirectory.list().length;
797         }
798
799         // Get a count of the number of files in the IndexedDB directory.
800         File indexedDBDirectory = new File (privateDataDirectoryString + "/app_webview/IndexedDB");
801         int indexedDBDirectoryNumberOfFiles = 0;
802         if (indexedDBDirectory.exists()) {
803             indexedDBDirectoryNumberOfFiles = indexedDBDirectory.list().length;
804         }
805
806         // Enable Clear DOM Storage if there is any.
807         clearDOMStorageMenuItem.setEnabled(localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0);
808
809         // Enable Clear Form Data is there is any.  This can be removed once the minimum API >= 26.
810         if (Build.VERSION.SDK_INT < 26) {
811             // Get the WebView database.
812             WebViewDatabase webViewDatabase = WebViewDatabase.getInstance(this);
813
814             // Enable the clear form data menu item if there is anything to clear.
815             clearFormDataMenuItem.setEnabled(webViewDatabase.hasFormData());
816         }
817
818         // Enable Clear Data if any of the submenu items are enabled.
819         clearDataMenuItem.setEnabled(clearCookiesMenuItem.isEnabled() || clearDOMStorageMenuItem.isEnabled() || clearFormDataMenuItem.isEnabled());
820
821         // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
822         fanboysSocialBlockingListMenuItem.setEnabled(!fanboysAnnoyanceListMenuItem.isChecked());
823
824         // Select the current user agent menu item.  A switch statement cannot be used because the user agents are not compile time constants.
825         if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[0])) {  // Privacy Browser.
826             menu.findItem(R.id.user_agent_privacy_browser).setChecked(true);
827         } else if (currentUserAgent.equals(webViewDefaultUserAgent)) {  // WebView Default.
828             menu.findItem(R.id.user_agent_webview_default).setChecked(true);
829         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[2])) {  // Firefox on Android.
830             menu.findItem(R.id.user_agent_firefox_on_android).setChecked(true);
831         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[3])) {  // Chrome on Android.
832             menu.findItem(R.id.user_agent_chrome_on_android).setChecked(true);
833         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[4])) {  // Safari on iOS.
834             menu.findItem(R.id.user_agent_safari_on_ios).setChecked(true);
835         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[5])) {  // Firefox on Linux.
836             menu.findItem(R.id.user_agent_firefox_on_linux).setChecked(true);
837         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[6])) {  // Chromium on Linux.
838             menu.findItem(R.id.user_agent_chromium_on_linux).setChecked(true);
839         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[7])) {  // Firefox on Windows.
840             menu.findItem(R.id.user_agent_firefox_on_windows).setChecked(true);
841         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[8])) {  // Chrome on Windows.
842             menu.findItem(R.id.user_agent_chrome_on_windows).setChecked(true);
843         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[9])) {  // Edge on Windows.
844             menu.findItem(R.id.user_agent_edge_on_windows).setChecked(true);
845         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[10])) {  // Internet Explorer on Windows.
846             menu.findItem(R.id.user_agent_internet_explorer_on_windows).setChecked(true);
847         } else if (currentUserAgent.equals(getResources().getStringArray(R.array.user_agent_data)[11])) {  // Safari on macOS.
848             menu.findItem(R.id.user_agent_safari_on_macos).setChecked(true);
849         } else {  // Custom user agent.
850             menu.findItem(R.id.user_agent_custom).setChecked(true);
851         }
852
853         // Instantiate the font size title and the selected font size menu item.
854         String fontSizeTitle;
855         MenuItem selectedFontSizeMenuItem;
856
857         // Prepare the font size title and current size menu item.
858         switch (fontSize) {
859             case 25:
860                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.twenty_five_percent);
861                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_twenty_five_percent);
862                 break;
863
864             case 50:
865                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.fifty_percent);
866                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_fifty_percent);
867                 break;
868
869             case 75:
870                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.seventy_five_percent);
871                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_seventy_five_percent);
872                 break;
873
874             case 100:
875                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
876                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
877                 break;
878
879             case 125:
880                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_twenty_five_percent);
881                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_twenty_five_percent);
882                 break;
883
884             case 150:
885                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_fifty_percent);
886                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_fifty_percent);
887                 break;
888
889             case 175:
890                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_seventy_five_percent);
891                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_seventy_five_percent);
892                 break;
893
894             case 200:
895                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.two_hundred_percent);
896                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_two_hundred_percent);
897                 break;
898
899             default:
900                 fontSizeTitle = getString(R.string.font_size) + " - " + getString(R.string.one_hundred_percent);
901                 selectedFontSizeMenuItem = menu.findItem(R.id.font_size_one_hundred_percent);
902                 break;
903         }
904
905         // Set the font size title and select the current size menu item.
906         fontSizeMenuItem.setTitle(fontSizeTitle);
907         selectedFontSizeMenuItem.setChecked(true);
908
909         // Run all the other default commands.
910         super.onPrepareOptionsMenu(menu);
911
912         // Display the menu.
913         return true;
914     }
915
916     @Override
917     // Remove Android Studio's warning about the dangers of using SetJavaScriptEnabled.
918     @SuppressLint("SetJavaScriptEnabled")
919     public boolean onOptionsItemSelected(MenuItem menuItem) {
920         // Reenter full screen browsing mode if it was interrupted by the options menu.  <https://redmine.stoutner.com/issues/389>
921         if (inFullScreenBrowsingMode) {
922             // Remove the translucent status flag.  This is necessary so the root frame layout can fill the entire screen.
923             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
924
925             FrameLayout rootFrameLayout = findViewById(R.id.root_framelayout);
926
927             /* Hide the system bars.
928              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
929              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
930              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
931              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
932              */
933             rootFrameLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
934                     View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
935         }
936
937         // Get the selected menu item ID.
938         int menuItemId = menuItem.getItemId();
939
940         // Get a handle for the shared preferences.
941         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
942
943         // Get a handle for the cookie manager.
944         CookieManager cookieManager = CookieManager.getInstance();
945
946         // Run the commands that correlate to the selected menu item.
947         switch (menuItemId) {
948             case R.id.toggle_javascript:
949                 // Toggle the JavaScript status.
950                 currentWebView.getSettings().setJavaScriptEnabled(!currentWebView.getSettings().getJavaScriptEnabled());
951
952                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
953                 updatePrivacyIcons(true);
954
955                 // Display a `Snackbar`.
956                 if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScrip is enabled.
957                     Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show();
958                 } else if (cookieManager.acceptCookie()) {  // JavaScript is disabled, but first-party cookies are enabled.
959                     Snackbar.make(findViewById(R.id.webviewpager), R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show();
960                 } else {  // Privacy mode.
961                     Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
962                 }
963
964                 // Reload the current WebView.
965                 currentWebView.reload();
966                 return true;
967
968             case R.id.add_or_edit_domain:
969                 if (currentWebView.getDomainSettingsApplied()) {  // Edit the current domain settings.
970                     // Reapply the domain settings on returning to `MainWebViewActivity`.
971                     reapplyDomainSettingsOnRestart = true;
972
973                     // Create an intent to launch the domains activity.
974                     Intent domainsIntent = new Intent(this, DomainsActivity.class);
975
976                     // Add the extra information to the intent.
977                     domainsIntent.putExtra("load_domain", currentWebView.getDomainSettingsDatabaseId());
978                     domainsIntent.putExtra("close_on_back", true);
979                     domainsIntent.putExtra("current_url", currentWebView.getUrl());
980
981                     // Get the current certificate.
982                     SslCertificate sslCertificate = currentWebView.getCertificate();
983
984                     // Check to see if the SSL certificate is populated.
985                     if (sslCertificate != null) {
986                         // Extract the certificate to strings.
987                         String issuedToCName = sslCertificate.getIssuedTo().getCName();
988                         String issuedToOName = sslCertificate.getIssuedTo().getOName();
989                         String issuedToUName = sslCertificate.getIssuedTo().getUName();
990                         String issuedByCName = sslCertificate.getIssuedBy().getCName();
991                         String issuedByOName = sslCertificate.getIssuedBy().getOName();
992                         String issuedByUName = sslCertificate.getIssuedBy().getUName();
993                         long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
994                         long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
995
996                         // Add the certificate to the intent.
997                         domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
998                         domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
999                         domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1000                         domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1001                         domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1002                         domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1003                         domainsIntent.putExtra("ssl_start_date", startDateLong);
1004                         domainsIntent.putExtra("ssl_end_date", endDateLong);
1005                     }
1006
1007                     // Check to see if the current IP addresses have been received.
1008                     if (currentWebView.hasCurrentIpAddresses()) {
1009                         // Add the current IP addresses to the intent.
1010                         domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1011                     }
1012
1013                     // Make it so.
1014                     startActivity(domainsIntent);
1015                 } else {  // Add a new domain.
1016                     // Apply the new domain settings on returning to `MainWebViewActivity`.
1017                     reapplyDomainSettingsOnRestart = true;
1018
1019                     // Get the current domain
1020                     Uri currentUri = Uri.parse(currentWebView.getUrl());
1021                     String currentDomain = currentUri.getHost();
1022
1023                     // Initialize the database handler.  The `0` specifies the database version, but that is ignored and set instead using a constant in `DomainsDatabaseHelper`.
1024                     DomainsDatabaseHelper domainsDatabaseHelper = new DomainsDatabaseHelper(this, null, null, 0);
1025
1026                     // Create the domain and store the database ID.
1027                     int newDomainDatabaseId = domainsDatabaseHelper.addDomain(currentDomain);
1028
1029                     // Create an intent to launch the domains activity.
1030                     Intent domainsIntent = new Intent(this, DomainsActivity.class);
1031
1032                     // Add the extra information to the intent.
1033                     domainsIntent.putExtra("load_domain", newDomainDatabaseId);
1034                     domainsIntent.putExtra("close_on_back", true);
1035                     domainsIntent.putExtra("current_url", currentWebView.getUrl());
1036
1037                     // Get the current certificate.
1038                     SslCertificate sslCertificate = currentWebView.getCertificate();
1039
1040                     // Check to see if the SSL certificate is populated.
1041                     if (sslCertificate != null) {
1042                         // Extract the certificate to strings.
1043                         String issuedToCName = sslCertificate.getIssuedTo().getCName();
1044                         String issuedToOName = sslCertificate.getIssuedTo().getOName();
1045                         String issuedToUName = sslCertificate.getIssuedTo().getUName();
1046                         String issuedByCName = sslCertificate.getIssuedBy().getCName();
1047                         String issuedByOName = sslCertificate.getIssuedBy().getOName();
1048                         String issuedByUName = sslCertificate.getIssuedBy().getUName();
1049                         long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1050                         long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1051
1052                         // Add the certificate to the intent.
1053                         domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1054                         domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1055                         domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1056                         domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1057                         domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1058                         domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1059                         domainsIntent.putExtra("ssl_start_date", startDateLong);
1060                         domainsIntent.putExtra("ssl_end_date", endDateLong);
1061                     }
1062
1063                     // Check to see if the current IP addresses have been received.
1064                     if (currentWebView.hasCurrentIpAddresses()) {
1065                         // Add the current IP addresses to the intent.
1066                         domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1067                     }
1068
1069                     // Make it so.
1070                     startActivity(domainsIntent);
1071                 }
1072                 return true;
1073
1074             case R.id.toggle_first_party_cookies:
1075                 // Switch the first-party cookie status.
1076                 cookieManager.setAcceptCookie(!cookieManager.acceptCookie());
1077
1078                 // Store the first-party cookie status.
1079                 currentWebView.setAcceptFirstPartyCookies(cookieManager.acceptCookie());
1080
1081                 // Update the menu checkbox.
1082                 menuItem.setChecked(cookieManager.acceptCookie());
1083
1084                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1085                 updatePrivacyIcons(true);
1086
1087                 // Display a snackbar.
1088                 if (cookieManager.acceptCookie()) {  // First-party cookies are enabled.
1089                     Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1090                 } else if (currentWebView.getSettings().getJavaScriptEnabled()) {  // JavaScript is still enabled.
1091                     Snackbar.make(findViewById(R.id.webviewpager), R.string.first_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1092                 } else {  // Privacy mode.
1093                     Snackbar.make(findViewById(R.id.webviewpager), R.string.privacy_mode, Snackbar.LENGTH_SHORT).show();
1094                 }
1095
1096                 // Reload the current WebView.
1097                 currentWebView.reload();
1098                 return true;
1099
1100             case R.id.toggle_third_party_cookies:
1101                 if (Build.VERSION.SDK_INT >= 21) {
1102                     // Switch the status of thirdPartyCookiesEnabled.
1103                     cookieManager.setAcceptThirdPartyCookies(currentWebView, !cookieManager.acceptThirdPartyCookies(currentWebView));
1104
1105                     // Update the menu checkbox.
1106                     menuItem.setChecked(cookieManager.acceptThirdPartyCookies(currentWebView));
1107
1108                     // Display a snackbar.
1109                     if (cookieManager.acceptThirdPartyCookies(currentWebView)) {
1110                         Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_enabled, Snackbar.LENGTH_SHORT).show();
1111                     } else {
1112                         Snackbar.make(findViewById(R.id.webviewpager), R.string.third_party_cookies_disabled, Snackbar.LENGTH_SHORT).show();
1113                     }
1114
1115                     // Reload the current WebView.
1116                     currentWebView.reload();
1117                 } // Else do nothing because SDK < 21.
1118                 return true;
1119
1120             case R.id.toggle_dom_storage:
1121                 // Toggle the status of domStorageEnabled.
1122                 currentWebView.getSettings().setDomStorageEnabled(!currentWebView.getSettings().getDomStorageEnabled());
1123
1124                 // Update the menu checkbox.
1125                 menuItem.setChecked(currentWebView.getSettings().getDomStorageEnabled());
1126
1127                 // Update the privacy icon.  `true` refreshes the app bar icons.
1128                 updatePrivacyIcons(true);
1129
1130                 // Display a snackbar.
1131                 if (currentWebView.getSettings().getDomStorageEnabled()) {
1132                     Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show();
1133                 } else {
1134                     Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show();
1135                 }
1136
1137                 // Reload the current WebView.
1138                 currentWebView.reload();
1139                 return true;
1140
1141             // Form data can be removed once the minimum API >= 26.
1142             case R.id.toggle_save_form_data:
1143                 // Switch the status of saveFormDataEnabled.
1144                 currentWebView.getSettings().setSaveFormData(!currentWebView.getSettings().getSaveFormData());
1145
1146                 // Update the menu checkbox.
1147                 menuItem.setChecked(currentWebView.getSettings().getSaveFormData());
1148
1149                 // Display a snackbar.
1150                 if (currentWebView.getSettings().getSaveFormData()) {
1151                     Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show();
1152                 } else {
1153                     Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show();
1154                 }
1155
1156                 // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.
1157                 updatePrivacyIcons(true);
1158
1159                 // Reload the current WebView.
1160                 currentWebView.reload();
1161                 return true;
1162
1163             case R.id.clear_cookies:
1164                 Snackbar.make(findViewById(R.id.webviewpager), R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1165                         .setAction(R.string.undo, v -> {
1166                             // Do nothing because everything will be handled by `onDismissed()` below.
1167                         })
1168                         .addCallback(new Snackbar.Callback() {
1169                             @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1170                             @Override
1171                             public void onDismissed(Snackbar snackbar, int event) {
1172                                 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1173                                     // Delete the cookies, which command varies by SDK.
1174                                     if (Build.VERSION.SDK_INT < 21) {
1175                                         cookieManager.removeAllCookie();
1176                                     } else {
1177                                         cookieManager.removeAllCookies(null);
1178                                     }
1179                                 }
1180                             }
1181                         })
1182                         .show();
1183                 return true;
1184
1185             case R.id.clear_dom_storage:
1186                 Snackbar.make(findViewById(R.id.webviewpager), R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1187                         .setAction(R.string.undo, v -> {
1188                             // Do nothing because everything will be handled by `onDismissed()` below.
1189                         })
1190                         .addCallback(new Snackbar.Callback() {
1191                             @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1192                             @Override
1193                             public void onDismissed(Snackbar snackbar, int event) {
1194                                 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1195                                     // Delete the DOM Storage.
1196                                     WebStorage webStorage = WebStorage.getInstance();
1197                                     webStorage.deleteAllData();
1198
1199                                     // Initialize a handler to manually delete the DOM storage files and directories.
1200                                     Handler deleteDomStorageHandler = new Handler();
1201
1202                                     // Setup a runnable to manually delete the DOM storage files and directories.
1203                                     Runnable deleteDomStorageRunnable = () -> {
1204                                         try {
1205                                             // Get a handle for the runtime.
1206                                             Runtime runtime = Runtime.getRuntime();
1207
1208                                             // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1209                                             // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1210                                             String privateDataDirectoryString = getApplicationInfo().dataDir;
1211
1212                                             // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1213                                             Process deleteLocalStorageProcess = runtime.exec(new String[]{"rm", "-rf", privateDataDirectoryString + "/app_webview/Local Storage/"});
1214
1215                                             // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1216                                             Process deleteIndexProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/IndexedDB");
1217                                             Process deleteQuotaManagerProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager");
1218                                             Process deleteQuotaManagerJournalProcess = runtime.exec("rm -f " + privateDataDirectoryString + "/app_webview/QuotaManager-journal");
1219                                             Process deleteDatabasesProcess = runtime.exec("rm -rf " + privateDataDirectoryString + "/app_webview/databases");
1220
1221                                             // Wait for the processes to finish.
1222                                             deleteLocalStorageProcess.waitFor();
1223                                             deleteIndexProcess.waitFor();
1224                                             deleteQuotaManagerProcess.waitFor();
1225                                             deleteQuotaManagerJournalProcess.waitFor();
1226                                             deleteDatabasesProcess.waitFor();
1227                                         } catch (Exception exception) {
1228                                             // Do nothing if an error is thrown.
1229                                         }
1230                                     };
1231
1232                                     // Manually delete the DOM storage files after 200 milliseconds.
1233                                     deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200);
1234                                 }
1235                             }
1236                         })
1237                         .show();
1238                 return true;
1239
1240             // Form data can be remove once the minimum API >= 26.
1241             case R.id.clear_form_data:
1242                 Snackbar.make(findViewById(R.id.webviewpager), R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1243                         .setAction(R.string.undo, v -> {
1244                             // Do nothing because everything will be handled by `onDismissed()` below.
1245                         })
1246                         .addCallback(new Snackbar.Callback() {
1247                             @SuppressLint("SwitchIntDef")  // Ignore the lint warning about not handling the other possible events as they are covered by `default:`.
1248                             @Override
1249                             public void onDismissed(Snackbar snackbar, int event) {
1250                                 if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1251                                     // Delete the form data.
1252                                     WebViewDatabase mainWebViewDatabase = WebViewDatabase.getInstance(getApplicationContext());
1253                                     mainWebViewDatabase.clearFormData();
1254                                 }
1255                             }
1256                         })
1257                         .show();
1258                 return true;
1259
1260             case R.id.easylist:
1261                 // Toggle the EasyList status.
1262                 currentWebView.enableBlocklist(NestedScrollWebView.EASY_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1263
1264                 // Update the menu checkbox.
1265                 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_LIST));
1266
1267                 // Reload the current WebView.
1268                 currentWebView.reload();
1269                 return true;
1270
1271             case R.id.easyprivacy:
1272                 // Toggle the EasyPrivacy status.
1273                 currentWebView.enableBlocklist(NestedScrollWebView.EASY_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1274
1275                 // Update the menu checkbox.
1276                 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.EASY_PRIVACY));
1277
1278                 // Reload the current WebView.
1279                 currentWebView.reload();
1280                 return true;
1281
1282             case R.id.fanboys_annoyance_list:
1283                 // Toggle Fanboy's Annoyance List status.
1284                 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1285
1286                 // Update the menu checkbox.
1287                 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1288
1289                 // Update the staus of Fanboy's Social Blocking List.
1290                 MenuItem fanboysSocialBlockingListMenuItem = optionsMenu.findItem(R.id.fanboys_social_blocking_list);
1291                 fanboysSocialBlockingListMenuItem.setEnabled(!currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_ANNOYANCE_LIST));
1292
1293                 // Reload the current WebView.
1294                 currentWebView.reload();
1295                 return true;
1296
1297             case R.id.fanboys_social_blocking_list:
1298                 // Toggle Fanboy's Social Blocking List status.
1299                 currentWebView.enableBlocklist(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST, !currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1300
1301                 // Update the menu checkbox.
1302                 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.FANBOYS_SOCIAL_BLOCKING_LIST));
1303
1304                 // Reload the current WebView.
1305                 currentWebView.reload();
1306                 return true;
1307
1308             case R.id.ultraprivacy:
1309                 // Toggle the UltraPrivacy status.
1310                 currentWebView.enableBlocklist(NestedScrollWebView.ULTRA_PRIVACY, !currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1311
1312                 // Update the menu checkbox.
1313                 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.ULTRA_PRIVACY));
1314
1315                 // Reload the current WebView.
1316                 currentWebView.reload();
1317                 return true;
1318
1319             case R.id.block_all_third_party_requests:
1320                 //Toggle the third-party requests blocker status.
1321                 currentWebView.enableBlocklist(NestedScrollWebView.THIRD_PARTY_REQUESTS, !currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1322
1323                 // Update the menu checkbox.
1324                 menuItem.setChecked(currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1325
1326                 // Reload the current WebView.
1327                 currentWebView.reload();
1328                 return true;
1329
1330             case R.id.user_agent_privacy_browser:
1331                 // Update the user agent.
1332                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[0]);
1333
1334                 // Reload the current WebView.
1335                 currentWebView.reload();
1336                 return true;
1337
1338             case R.id.user_agent_webview_default:
1339                 // Update the user agent.
1340                 currentWebView.getSettings().setUserAgentString("");
1341
1342                 // Reload the current WebView.
1343                 currentWebView.reload();
1344                 return true;
1345
1346             case R.id.user_agent_firefox_on_android:
1347                 // Update the user agent.
1348                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[2]);
1349
1350                 // Reload the current WebView.
1351                 currentWebView.reload();
1352                 return true;
1353
1354             case R.id.user_agent_chrome_on_android:
1355                 // Update the user agent.
1356                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[3]);
1357
1358                 // Reload the current WebView.
1359                 currentWebView.reload();
1360                 return true;
1361
1362             case R.id.user_agent_safari_on_ios:
1363                 // Update the user agent.
1364                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[4]);
1365
1366                 // Reload the current WebView.
1367                 currentWebView.reload();
1368                 return true;
1369
1370             case R.id.user_agent_firefox_on_linux:
1371                 // Update the user agent.
1372                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[5]);
1373
1374                 // Reload the current WebView.
1375                 currentWebView.reload();
1376                 return true;
1377
1378             case R.id.user_agent_chromium_on_linux:
1379                 // Update the user agent.
1380                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[6]);
1381
1382                 // Reload the current WebView.
1383                 currentWebView.reload();
1384                 return true;
1385
1386             case R.id.user_agent_firefox_on_windows:
1387                 // Update the user agent.
1388                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[7]);
1389
1390                 // Reload the current WebView.
1391                 currentWebView.reload();
1392                 return true;
1393
1394             case R.id.user_agent_chrome_on_windows:
1395                 // Update the user agent.
1396                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[8]);
1397
1398                 // Reload the current WebView.
1399                 currentWebView.reload();
1400                 return true;
1401
1402             case R.id.user_agent_edge_on_windows:
1403                 // Update the user agent.
1404                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[9]);
1405
1406                 // Reload the current WebView.
1407                 currentWebView.reload();
1408                 return true;
1409
1410             case R.id.user_agent_internet_explorer_on_windows:
1411                 // Update the user agent.
1412                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[10]);
1413
1414                 // Reload the current WebView.
1415                 currentWebView.reload();
1416                 return true;
1417
1418             case R.id.user_agent_safari_on_macos:
1419                 // Update the user agent.
1420                 currentWebView.getSettings().setUserAgentString(getResources().getStringArray(R.array.user_agent_data)[11]);
1421
1422                 // Reload the current WebView.
1423                 currentWebView.reload();
1424                 return true;
1425
1426             case R.id.user_agent_custom:
1427                 // Update the user agent.
1428                 currentWebView.getSettings().setUserAgentString(sharedPreferences.getString("custom_user_agent", getString(R.string.custom_user_agent_default_value)));
1429
1430                 // Reload the current WebView.
1431                 currentWebView.reload();
1432                 return true;
1433
1434             case R.id.font_size_twenty_five_percent:
1435                 currentWebView.getSettings().setTextZoom(25);
1436                 return true;
1437
1438             case R.id.font_size_fifty_percent:
1439                 currentWebView.getSettings().setTextZoom(50);
1440                 return true;
1441
1442             case R.id.font_size_seventy_five_percent:
1443                 currentWebView.getSettings().setTextZoom(75);
1444                 return true;
1445
1446             case R.id.font_size_one_hundred_percent:
1447                 currentWebView.getSettings().setTextZoom(100);
1448                 return true;
1449
1450             case R.id.font_size_one_hundred_twenty_five_percent:
1451                 currentWebView.getSettings().setTextZoom(125);
1452                 return true;
1453
1454             case R.id.font_size_one_hundred_fifty_percent:
1455                 currentWebView.getSettings().setTextZoom(150);
1456                 return true;
1457
1458             case R.id.font_size_one_hundred_seventy_five_percent:
1459                 currentWebView.getSettings().setTextZoom(175);
1460                 return true;
1461
1462             case R.id.font_size_two_hundred_percent:
1463                 currentWebView.getSettings().setTextZoom(200);
1464                 return true;
1465
1466             case R.id.swipe_to_refresh:
1467                 // Toggle the stored status of swipe to refresh.
1468                 currentWebView.setSwipeToRefresh(!currentWebView.getSwipeToRefresh());
1469
1470                 // Get a handle for the swipe refresh layout.
1471                 SwipeRefreshLayout swipeRefreshLayout = findViewById(R.id.swiperefreshlayout);
1472
1473                 // Update the swipe refresh layout.
1474                 if (currentWebView.getSwipeToRefresh()) {  // Swipe to refresh is enabled.
1475                     // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
1476                     swipeRefreshLayout.setEnabled(currentWebView.getY() == 0);
1477                 } else {  // Swipe to refresh is disabled.
1478                     // Disable the swipe refresh layout.
1479                     swipeRefreshLayout.setEnabled(false);
1480                 }
1481                 return true;
1482
1483             case R.id.wide_viewport:
1484                 // Toggle the viewport.
1485                 currentWebView.getSettings().setUseWideViewPort(!currentWebView.getSettings().getUseWideViewPort());
1486                 return true;
1487
1488             case R.id.display_images:
1489                 if (currentWebView.getSettings().getLoadsImagesAutomatically()) {  // Images are currently loaded automatically.
1490                     // Disable loading of images.
1491                     currentWebView.getSettings().setLoadsImagesAutomatically(false);
1492
1493                     // Reload the website to remove existing images.
1494                     currentWebView.reload();
1495                 } else {  // Images are not currently loaded automatically.
1496                     // Enable loading of images.  Missing images will be loaded without the need for a reload.
1497                     currentWebView.getSettings().setLoadsImagesAutomatically(true);
1498                 }
1499                 return true;
1500
1501             case R.id.night_mode:
1502                 // Toggle night mode.
1503                 currentWebView.setNightMode(!currentWebView.getNightMode());
1504
1505                 // Enable or disable JavaScript according to night mode, the global preference, and any domain settings.
1506                 if (currentWebView.getNightMode()) {  // Night mode is enabled, which requires JavaScript.
1507                     // Enable JavaScript.
1508                     currentWebView.getSettings().setJavaScriptEnabled(true);
1509                 } else if (currentWebView.getDomainSettingsApplied()) {  // Night mode is disabled and domain settings are applied.  Set JavaScript according to the domain settings.
1510                     // Apply the JavaScript preference that was stored the last time domain settings were loaded.
1511                     currentWebView.getSettings().setJavaScriptEnabled(currentWebView.getDomainSettingsJavaScriptEnabled());
1512                 } else {  // Night mode is disabled and domain settings are not applied.  Set JavaScript according to the global preference.
1513                     // Apply the JavaScript preference.
1514                     currentWebView.getSettings().setJavaScriptEnabled(sharedPreferences.getBoolean("javascript", false));
1515                 }
1516
1517                 // Update the privacy icons.
1518                 updatePrivacyIcons(false);
1519
1520                 // Reload the website.
1521                 currentWebView.reload();
1522                 return true;
1523
1524             case R.id.find_on_page:
1525                 // Get a handle for the views.
1526                 Toolbar toolbar = findViewById(R.id.toolbar);
1527                 LinearLayout findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout);
1528                 EditText findOnPageEditText = findViewById(R.id.find_on_page_edittext);
1529
1530                 // Set the minimum height of the find on page linear layout to match the toolbar.
1531                 findOnPageLinearLayout.setMinimumHeight(toolbar.getHeight());
1532
1533                 // Hide the toolbar.
1534                 toolbar.setVisibility(View.GONE);
1535
1536                 // Show the find on page linear layout.
1537                 findOnPageLinearLayout.setVisibility(View.VISIBLE);
1538
1539                 // Display the keyboard.  The app must wait 200 ms before running the command to work around a bug in Android.
1540                 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1541                 findOnPageEditText.postDelayed(() -> {
1542                     // Set the focus on `findOnPageEditText`.
1543                     findOnPageEditText.requestFocus();
1544
1545                     // Get a handle for the input method manager.
1546                     InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
1547
1548                     // Remove the lint warning below that the input method manager might be null.
1549                     assert inputMethodManager != null;
1550
1551                     // Display the keyboard.  `0` sets no input flags.
1552                     inputMethodManager.showSoftInput(findOnPageEditText, 0);
1553                 }, 200);
1554                 return true;
1555
1556             case R.id.print:
1557                 // Get a print manager instance.
1558                 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
1559
1560                 // Remove the lint error below that print manager might be null.
1561                 assert printManager != null;
1562
1563                 // Create a print document adapter from the current WebView.
1564                 PrintDocumentAdapter printDocumentAdapter = currentWebView.createPrintDocumentAdapter();
1565
1566                 // Print the document.
1567                 printManager.print(getString(R.string.privacy_browser_web_page), printDocumentAdapter, null);
1568                 return true;
1569
1570             case R.id.save_as_image:
1571                 // Create a webpage bitmap.  Once the Minimum API >= 26 Bitmap.Config.RBGA_F16 can be used instead of ARGB_8888.
1572                 Bitmap webpageBitmap = Bitmap.createBitmap(currentWebView.getHorizontalScrollRange(), currentWebView.getVerticalScrollRange(), Bitmap.Config.ARGB_8888);
1573
1574                 // Create a canvas.
1575                 Canvas webpageCanvas = new Canvas(webpageBitmap);
1576
1577                 // Draw the current webpage onto the bitmap.
1578                 currentWebView.draw(webpageCanvas);
1579
1580                 // Create a webpage PNG byte array output stream.
1581                 ByteArrayOutputStream webpageByteArrayOutputStream = new ByteArrayOutputStream();
1582
1583                 // Convert the bitmap to a PNG.  `0` is for lossless compression (the only option for a PNG).
1584                 webpageBitmap.compress(Bitmap.CompressFormat.PNG, 0, webpageByteArrayOutputStream);
1585
1586                 // Get a file for the image.
1587                 File imageFile = new File("/storage/emulated/0/webpage.png");
1588
1589                 // Delete the current file if it exists.
1590                 if (imageFile.exists()) {
1591                     //noinspection ResultOfMethodCallIgnored
1592                     imageFile.delete();
1593                 }
1594
1595                 try {
1596                     // Create an image file output stream.
1597                     FileOutputStream imageFileOutputStream = new FileOutputStream(imageFile);
1598
1599                     // Write the webpage image to the image file.
1600                     webpageByteArrayOutputStream.writeTo(imageFileOutputStream);
1601                 } catch (Exception exception) {
1602                     // Add a snackbar.
1603                 }
1604                 return true;
1605
1606             case R.id.add_to_homescreen:
1607                 // Instantiate the create home screen shortcut dialog.
1608                 DialogFragment createHomeScreenShortcutDialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView.getTitle(), currentWebView.getUrl(),
1609                         currentWebView.getFavoriteOrDefaultIcon());
1610
1611                 // Show the create home screen shortcut dialog.
1612                 createHomeScreenShortcutDialogFragment.show(getSupportFragmentManager(), getString(R.string.create_shortcut));
1613                 return true;
1614
1615             case R.id.view_source:
1616                 // Create an intent to launch the view source activity.
1617                 Intent viewSourceIntent = new Intent(this, ViewSourceActivity.class);
1618
1619                 // Add the variables to the intent.
1620                 viewSourceIntent.putExtra("user_agent", currentWebView.getSettings().getUserAgentString());
1621                 viewSourceIntent.putExtra("current_url", currentWebView.getUrl());
1622
1623                 // Make it so.
1624                 startActivity(viewSourceIntent);
1625                 return true;
1626
1627             case R.id.share_url:
1628                 // Setup the share string.
1629                 String shareString = currentWebView.getTitle() + " – " + currentWebView.getUrl();
1630
1631                 // Create the share intent.
1632                 Intent shareIntent = new Intent(Intent.ACTION_SEND);
1633                 shareIntent.putExtra(Intent.EXTRA_TEXT, shareString);
1634                 shareIntent.setType("text/plain");
1635
1636                 // Make it so.
1637                 startActivity(Intent.createChooser(shareIntent, getString(R.string.share_url)));
1638                 return true;
1639
1640             case R.id.open_with_app:
1641                 openWithApp(currentWebView.getUrl());
1642                 return true;
1643
1644             case R.id.open_with_browser:
1645                 openWithBrowser(currentWebView.getUrl());
1646                 return true;
1647
1648             case R.id.proxy_through_orbot:
1649                 // Toggle the proxy through Orbot variable.
1650                 proxyThroughOrbot = !proxyThroughOrbot;
1651
1652                 // Apply the proxy through Orbot settings.
1653                 applyProxyThroughOrbot(true);
1654                 return true;
1655
1656             case R.id.refresh:
1657                 if (menuItem.getTitle().equals(getString(R.string.refresh))) {  // The refresh button was pushed.
1658                     // Reload the current WebView.
1659                     currentWebView.reload();
1660                 } else {  // The stop button was pushed.
1661                     // Stop the loading of the WebView.
1662                     currentWebView.stopLoading();
1663                 }
1664                 return true;
1665
1666             case R.id.ad_consent:
1667                 // Display the ad consent dialog.
1668                 DialogFragment adConsentDialogFragment = new AdConsentDialog();
1669                 adConsentDialogFragment.show(getSupportFragmentManager(), getString(R.string.ad_consent));
1670                 return true;
1671
1672             default:
1673                 // Don't consume the event.
1674                 return super.onOptionsItemSelected(menuItem);
1675         }
1676     }
1677
1678     // removeAllCookies is deprecated, but it is required for API < 21.
1679     @Override
1680     public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
1681         // Get the menu item ID.
1682         int menuItemId = menuItem.getItemId();
1683
1684         // Get a handle for the shared preferences.
1685         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1686
1687         // Run the commands that correspond to the selected menu item.
1688         switch (menuItemId) {
1689             case R.id.clear_and_exit:
1690                 // Clear and exit Privacy Browser.
1691                 clearAndExit();
1692                 break;
1693
1694             case R.id.home:
1695                 // Select the homepage based on the proxy through Orbot status.
1696                 if (proxyThroughOrbot) {
1697                     // Load the Tor homepage.
1698                     loadUrl(sharedPreferences.getString("tor_homepage", getString(R.string.tor_homepage_default_value)));
1699                 } else {
1700                     // Load the normal homepage.
1701                     loadUrl(sharedPreferences.getString("homepage", getString(R.string.homepage_default_value)));
1702                 }
1703                 break;
1704
1705             case R.id.back:
1706                 if (currentWebView.canGoBack()) {
1707                     // Reset the current domain name so that navigation works if third-party requests are blocked.
1708                     currentWebView.resetCurrentDomainName();
1709
1710                     // Set navigating history so that the domain settings are applied when the new URL is loaded.
1711                     currentWebView.setNavigatingHistory(true);
1712
1713                     // Load the previous website in the history.
1714                     currentWebView.goBack();
1715                 }
1716                 break;
1717
1718             case R.id.forward:
1719                 if (currentWebView.canGoForward()) {
1720                     // Reset the current domain name so that navigation works if third-party requests are blocked.
1721                     currentWebView.resetCurrentDomainName();
1722
1723                     // Set navigating history so that the domain settings are applied when the new URL is loaded.
1724                     currentWebView.setNavigatingHistory(true);
1725
1726                     // Load the next website in the history.
1727                     currentWebView.goForward();
1728                 }
1729                 break;
1730
1731             case R.id.history:
1732                 // Instantiate the URL history dialog.
1733                 DialogFragment urlHistoryDialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView.getWebViewFragmentId());
1734
1735                 // Show the URL history dialog.
1736                 urlHistoryDialogFragment.show(getSupportFragmentManager(), getString(R.string.history));
1737                 break;
1738
1739             case R.id.requests:
1740                 // Populate the resource requests.
1741                 RequestsActivity.resourceRequests = currentWebView.getResourceRequests();
1742
1743                 // Create an intent to launch the Requests activity.
1744                 Intent requestsIntent = new Intent(this, RequestsActivity.class);
1745
1746                 // Add the block third-party requests status to the intent.
1747                 requestsIntent.putExtra("block_all_third_party_requests", currentWebView.isBlocklistEnabled(NestedScrollWebView.THIRD_PARTY_REQUESTS));
1748
1749                 // Make it so.
1750                 startActivity(requestsIntent);
1751                 break;
1752
1753             case R.id.downloads:
1754                 // Launch the system Download Manager.
1755                 Intent downloadManagerIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
1756
1757                 // Launch as a new task so that Download Manager and Privacy Browser show as separate windows in the recent tasks list.
1758                 downloadManagerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1759
1760                 startActivity(downloadManagerIntent);
1761                 break;
1762
1763             case R.id.domains:
1764                 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
1765                 reapplyDomainSettingsOnRestart = true;
1766
1767                 // Launch the domains activity.
1768                 Intent domainsIntent = new Intent(this, DomainsActivity.class);
1769
1770                 // Add the extra information to the intent.
1771                 domainsIntent.putExtra("current_url", currentWebView.getUrl());
1772
1773                 // Get the current certificate.
1774                 SslCertificate sslCertificate = currentWebView.getCertificate();
1775
1776                 // Check to see if the SSL certificate is populated.
1777                 if (sslCertificate != null) {
1778                     // Extract the certificate to strings.
1779                     String issuedToCName = sslCertificate.getIssuedTo().getCName();
1780                     String issuedToOName = sslCertificate.getIssuedTo().getOName();
1781                     String issuedToUName = sslCertificate.getIssuedTo().getUName();
1782                     String issuedByCName = sslCertificate.getIssuedBy().getCName();
1783                     String issuedByOName = sslCertificate.getIssuedBy().getOName();
1784                     String issuedByUName = sslCertificate.getIssuedBy().getUName();
1785                     long startDateLong = sslCertificate.getValidNotBeforeDate().getTime();
1786                     long endDateLong = sslCertificate.getValidNotAfterDate().getTime();
1787
1788                     // Add the certificate to the intent.
1789                     domainsIntent.putExtra("ssl_issued_to_cname", issuedToCName);
1790                     domainsIntent.putExtra("ssl_issued_to_oname", issuedToOName);
1791                     domainsIntent.putExtra("ssl_issued_to_uname", issuedToUName);
1792                     domainsIntent.putExtra("ssl_issued_by_cname", issuedByCName);
1793                     domainsIntent.putExtra("ssl_issued_by_oname", issuedByOName);
1794                     domainsIntent.putExtra("ssl_issued_by_uname", issuedByUName);
1795                     domainsIntent.putExtra("ssl_start_date", startDateLong);
1796                     domainsIntent.putExtra("ssl_end_date", endDateLong);
1797                 }
1798
1799                 // Check to see if the current IP addresses have been received.
1800                 if (currentWebView.hasCurrentIpAddresses()) {
1801                     // Add the current IP addresses to the intent.
1802                     domainsIntent.putExtra("current_ip_addresses", currentWebView.getCurrentIpAddresses());
1803                 }
1804
1805                 // Make it so.
1806                 startActivity(domainsIntent);
1807                 break;
1808
1809             case R.id.settings:
1810                 // Set the flag to reapply app settings on restart when returning from Settings.
1811                 reapplyAppSettingsOnRestart = true;
1812
1813                 // Set the flag to reapply the domain settings on restart when returning from Settings.
1814                 reapplyDomainSettingsOnRestart = true;
1815
1816                 // Launch the settings activity.
1817                 Intent settingsIntent = new Intent(this, SettingsActivity.class);
1818                 startActivity(settingsIntent);
1819                 break;
1820
1821             case R.id.import_export:
1822                 // Launch the import/export activity.
1823                 Intent importExportIntent = new Intent (this, ImportExportActivity.class);
1824                 startActivity(importExportIntent);
1825                 break;
1826
1827             case R.id.logcat:
1828                 // Launch the logcat activity.
1829                 Intent logcatIntent = new Intent(this, LogcatActivity.class);
1830                 startActivity(logcatIntent);
1831                 break;
1832
1833             case R.id.guide:
1834                 // Launch `GuideActivity`.
1835                 Intent guideIntent = new Intent(this, GuideActivity.class);
1836                 startActivity(guideIntent);
1837                 break;
1838
1839             case R.id.about:
1840                 // Create an intent to launch the about activity.
1841                 Intent aboutIntent = new Intent(this, AboutActivity.class);
1842
1843                 // Create a string array for the blocklist versions.
1844                 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],
1845                         ultraPrivacy.get(0).get(0)[0]};
1846
1847                 // Add the blocklist versions to the intent.
1848                 aboutIntent.putExtra("blocklist_versions", blocklistVersions);
1849
1850                 // Make it so.
1851                 startActivity(aboutIntent);
1852                 break;
1853         }
1854
1855         // Get a handle for the drawer layout.
1856         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
1857
1858         // Close the navigation drawer.
1859         drawerLayout.closeDrawer(GravityCompat.START);
1860         return true;
1861     }
1862
1863     @Override
1864     public void onPostCreate(Bundle savedInstanceState) {
1865         // Run the default commands.
1866         super.onPostCreate(savedInstanceState);
1867
1868         // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished.  This creates the navigation drawer icon.
1869         actionBarDrawerToggle.syncState();
1870     }
1871
1872     @Override
1873     public void onConfigurationChanged(Configuration newConfig) {
1874         // Run the default commands.
1875         super.onConfigurationChanged(newConfig);
1876
1877         // Get the status bar pixel size.
1878         int statusBarResourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
1879         int statusBarPixelSize = getResources().getDimensionPixelSize(statusBarResourceId);
1880
1881         // Get the resource density.
1882         float screenDensity = getResources().getDisplayMetrics().density;
1883
1884         // Recalculate the drawer header padding.
1885         drawerHeaderPaddingLeftAndRight = (int) (15 * screenDensity);
1886         drawerHeaderPaddingTop = statusBarPixelSize + (int) (4 * screenDensity);
1887         drawerHeaderPaddingBottom = (int) (8 * screenDensity);
1888
1889         // Reload the ad for the free flavor if not in full screen mode.
1890         if (BuildConfig.FLAVOR.contentEquals("free") && !inFullScreenBrowsingMode) {
1891             // Reload the ad.  The AdView is destroyed and recreated, which changes the ID, every time it is reloaded to handle possible rotations.
1892             AdHelper.loadAd(findViewById(R.id.adview), getApplicationContext(), getString(R.string.ad_unit_id));
1893         }
1894
1895         // `invalidateOptionsMenu` should recalculate the number of action buttons from the menu to display on the app bar, but it doesn't because of the this bug:
1896         // https://code.google.com/p/android/issues/detail?id=20493#c8
1897         // ActivityCompat.invalidateOptionsMenu(this);
1898     }
1899
1900     @Override
1901     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
1902         // Store the hit test result.
1903         final WebView.HitTestResult hitTestResult = currentWebView.getHitTestResult();
1904
1905         // Create the URL strings.
1906         final String imageUrl;
1907         final String linkUrl;
1908
1909         // Get handles for the system managers.
1910         final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
1911         FragmentManager fragmentManager = getSupportFragmentManager();
1912         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
1913
1914         // Remove the lint errors below that the clipboard manager might be null.
1915         assert clipboardManager != null;
1916
1917         // Process the link according to the type.
1918         switch (hitTestResult.getType()) {
1919             // `SRC_ANCHOR_TYPE` is a link.
1920             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
1921                 // Get the target URL.
1922                 linkUrl = hitTestResult.getExtra();
1923
1924                 // Set the target URL as the title of the `ContextMenu`.
1925                 menu.setHeaderTitle(linkUrl);
1926
1927                 // Add an Open in New Tab entry.
1928                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
1929                     // Load the link URL in a new tab.
1930                     addNewTab(linkUrl);
1931                     return false;
1932                 });
1933
1934                 // Add an Open with App entry.
1935                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
1936                     openWithApp(linkUrl);
1937                     return false;
1938                 });
1939
1940                 // Add an Open with Browser entry.
1941                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
1942                     openWithBrowser(linkUrl);
1943                     return false;
1944                 });
1945
1946                 // Add a Copy URL entry.
1947                 menu.add(R.string.copy_url).setOnMenuItemClickListener((MenuItem item) -> {
1948                     // Save the link URL in a `ClipData`.
1949                     ClipData srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl);
1950
1951                     // Set the `ClipData` as the clipboard's primary clip.
1952                     clipboardManager.setPrimaryClip(srcAnchorTypeClipData);
1953                     return false;
1954                 });
1955
1956                 // Add a Download URL entry.
1957                 menu.add(R.string.download_url).setOnMenuItemClickListener((MenuItem item) -> {
1958                     // Check if the download should be processed by an external app.
1959                     if (sharedPreferences.getBoolean("download_with_external_app", false)) {  // Download with an external app.
1960                         openUrlWithExternalApp(linkUrl);
1961                     } else {  // Download with Android's download manager.
1962                         // Check to see if the storage permission has already been granted.
1963                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
1964                             // Store the variables for future use by `onRequestPermissionsResult()`.
1965                             downloadUrl = linkUrl;
1966                             downloadContentDisposition = "none";
1967                             downloadContentLength = -1;
1968
1969                             // Show a dialog if the user has previously denied the permission.
1970                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
1971                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_FILE.
1972                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_FILE);
1973
1974                                 // Show the download location permission alert dialog.  The permission will be requested when the the dialog is closed.
1975                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
1976                             } else {  // Show the permission request directly.
1977                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult()`.
1978                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
1979                             }
1980                         } else {  // The storage permission has already been granted.
1981                             // Get a handle for the download file alert dialog.
1982                             DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(linkUrl, "none", -1);
1983
1984                             // Show the download file alert dialog.
1985                             downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
1986                         }
1987                     }
1988                     return false;
1989                 });
1990
1991                 // Add a Cancel entry, which by default closes the context menu.
1992                 menu.add(R.string.cancel);
1993                 break;
1994
1995             case WebView.HitTestResult.EMAIL_TYPE:
1996                 // Get the target URL.
1997                 linkUrl = hitTestResult.getExtra();
1998
1999                 // Set the target URL as the title of the `ContextMenu`.
2000                 menu.setHeaderTitle(linkUrl);
2001
2002                 // Add a Write Email entry.
2003                 menu.add(R.string.write_email).setOnMenuItemClickListener(item -> {
2004                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2005                     Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
2006
2007                     // Parse the url and set it as the data for the `Intent`.
2008                     emailIntent.setData(Uri.parse("mailto:" + linkUrl));
2009
2010                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2011                     emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2012
2013                     // Make it so.
2014                     startActivity(emailIntent);
2015                     return false;
2016                 });
2017
2018                 // Add a Copy Email Address entry.
2019                 menu.add(R.string.copy_email_address).setOnMenuItemClickListener(item -> {
2020                     // Save the email address in a `ClipData`.
2021                     ClipData srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl);
2022
2023                     // Set the `ClipData` as the clipboard's primary clip.
2024                     clipboardManager.setPrimaryClip(srcEmailTypeClipData);
2025                     return false;
2026                 });
2027
2028                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2029                 menu.add(R.string.cancel);
2030                 break;
2031
2032             // `IMAGE_TYPE` is an image. `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.  Privacy Browser processes them the same.
2033             case WebView.HitTestResult.IMAGE_TYPE:
2034             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
2035                 // Get the image URL.
2036                 imageUrl = hitTestResult.getExtra();
2037
2038                 // Set the image URL as the title of the context menu.
2039                 menu.setHeaderTitle(imageUrl);
2040
2041                 // Add an Open in New Tab entry.
2042                 menu.add(R.string.open_in_new_tab).setOnMenuItemClickListener((MenuItem item) -> {
2043                     // Load the image URL in a new tab.
2044                     addNewTab(imageUrl);
2045                     return false;
2046                 });
2047
2048                 // Add a View Image entry.
2049                 menu.add(R.string.view_image).setOnMenuItemClickListener(item -> {
2050                     loadUrl(imageUrl);
2051                     return false;
2052                 });
2053
2054                 // Add a `Download Image` entry.
2055                 menu.add(R.string.download_image).setOnMenuItemClickListener((MenuItem item) -> {
2056                     // Check if the download should be processed by an external app.
2057                     if (sharedPreferences.getBoolean("download_with_external_app", false)) {  // Download with an external app.
2058                         openUrlWithExternalApp(imageUrl);
2059                     } else {  // Download with Android's download manager.
2060                         // Check to see if the storage permission has already been granted.
2061                         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {  // The storage permission needs to be requested.
2062                             // Store the image URL for use by `onRequestPermissionResult()`.
2063                             downloadImageUrl = imageUrl;
2064
2065                             // Show a dialog if the user has previously denied the permission.
2066                             if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
2067                                 // Instantiate the download location permission alert dialog and set the download type to DOWNLOAD_IMAGE.
2068                                 DialogFragment downloadLocationPermissionDialogFragment = DownloadLocationPermissionDialog.downloadType(DownloadLocationPermissionDialog.DOWNLOAD_IMAGE);
2069
2070                                 // Show the download location permission alert dialog.  The permission will be requested when the dialog is closed.
2071                                 downloadLocationPermissionDialogFragment.show(fragmentManager, getString(R.string.download_location));
2072                             } else {  // Show the permission request directly.
2073                                 // Request the permission.  The download dialog will be launched by `onRequestPermissionResult().
2074                                 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2075                             }
2076                         } else {  // The storage permission has already been granted.
2077                             // Get a handle for the download image alert dialog.
2078                             DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(imageUrl);
2079
2080                             // Show the download image alert dialog.
2081                             downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2082                         }
2083                     }
2084                     return false;
2085                 });
2086
2087                 // Add a `Copy URL` entry.
2088                 menu.add(R.string.copy_url).setOnMenuItemClickListener(item -> {
2089                     // Save the image URL in a `ClipData`.
2090                     ClipData srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl);
2091
2092                     // Set the `ClipData` as the clipboard's primary clip.
2093                     clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData);
2094                     return false;
2095                 });
2096
2097                 // Add an Open with App entry.
2098                 menu.add(R.string.open_with_app).setOnMenuItemClickListener((MenuItem item) -> {
2099                     openWithApp(imageUrl);
2100                     return false;
2101                 });
2102
2103                 // Add an Open with Browser entry.
2104                 menu.add(R.string.open_with_browser).setOnMenuItemClickListener((MenuItem item) -> {
2105                     openWithBrowser(imageUrl);
2106                     return false;
2107                 });
2108
2109                 // Add a `Cancel` entry, which by default closes the `ContextMenu`.
2110                 menu.add(R.string.cancel);
2111                 break;
2112         }
2113     }
2114
2115     @Override
2116     public void onCreateBookmark(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2117         // Get a handle for the bookmarks list view.
2118         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2119
2120         // Get the views from the dialog fragment.
2121         EditText createBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_name_edittext);
2122         EditText createBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.create_bookmark_url_edittext);
2123
2124         // Extract the strings from the edit texts.
2125         String bookmarkNameString = createBookmarkNameEditText.getText().toString();
2126         String bookmarkUrlString = createBookmarkUrlEditText.getText().toString();
2127
2128         // Create a favorite icon byte array output stream.
2129         ByteArrayOutputStream favoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2130
2131         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2132         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream);
2133
2134         // Convert the favorite icon byte array stream to a byte array.
2135         byte[] favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray();
2136
2137         // Display the new bookmark below the current items in the (0 indexed) list.
2138         int newBookmarkDisplayOrder = bookmarksListView.getCount();
2139
2140         // Create the bookmark.
2141         bookmarksDatabaseHelper.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray);
2142
2143         // Update the bookmarks cursor with the current contents of this folder.
2144         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2145
2146         // Update the list view.
2147         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2148
2149         // Scroll to the new bookmark.
2150         bookmarksListView.setSelection(newBookmarkDisplayOrder);
2151     }
2152
2153     @Override
2154     public void onCreateBookmarkFolder(DialogFragment dialogFragment, Bitmap favoriteIconBitmap) {
2155         // Get a handle for the bookmarks list view.
2156         ListView bookmarksListView = findViewById(R.id.bookmarks_drawer_listview);
2157
2158         // Get handles for the views in the dialog fragment.
2159         EditText createFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.create_folder_name_edittext);
2160         RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon_radiobutton);
2161         ImageView folderIconImageView = dialogFragment.getDialog().findViewById(R.id.create_folder_default_icon);
2162
2163         // Get new folder name string.
2164         String folderNameString = createFolderNameEditText.getText().toString();
2165
2166         // Create a folder icon bitmap.
2167         Bitmap folderIconBitmap;
2168
2169         // Set the folder icon bitmap according to the dialog.
2170         if (defaultFolderIconRadioButton.isChecked()) {  // Use the default folder icon.
2171             // Get the default folder icon drawable.
2172             Drawable folderIconDrawable = folderIconImageView.getDrawable();
2173
2174             // Convert the folder icon drawable to a bitmap drawable.
2175             BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2176
2177             // Convert the folder icon bitmap drawable to a bitmap.
2178             folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2179         } else {  // Use the WebView favorite icon.
2180             // Copy the favorite icon bitmap to the folder icon bitmap.
2181             folderIconBitmap = favoriteIconBitmap;
2182         }
2183
2184         // Create a folder icon byte array output stream.
2185         ByteArrayOutputStream folderIconByteArrayOutputStream = new ByteArrayOutputStream();
2186
2187         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2188         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream);
2189
2190         // Convert the folder icon byte array stream to a byte array.
2191         byte[] folderIconByteArray = folderIconByteArrayOutputStream.toByteArray();
2192
2193         // Move all the bookmarks down one in the display order.
2194         for (int i = 0; i < bookmarksListView.getCount(); i++) {
2195             int databaseId = (int) bookmarksListView.getItemIdAtPosition(i);
2196             bookmarksDatabaseHelper.updateDisplayOrder(databaseId, i + 1);
2197         }
2198
2199         // Create the folder, which will be placed at the top of the `ListView`.
2200         bookmarksDatabaseHelper.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray);
2201
2202         // Update the bookmarks cursor with the current contents of this folder.
2203         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2204
2205         // Update the `ListView`.
2206         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2207
2208         // Scroll to the new folder.
2209         bookmarksListView.setSelection(0);
2210     }
2211
2212     @Override
2213     public void onSaveBookmark(DialogFragment dialogFragment, int selectedBookmarkDatabaseId, Bitmap favoriteIconBitmap) {
2214         // Get handles for the views from `dialogFragment`.
2215         EditText editBookmarkNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_name_edittext);
2216         EditText editBookmarkUrlEditText = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_url_edittext);
2217         RadioButton currentBookmarkIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_bookmark_current_icon_radiobutton);
2218
2219         // Store the bookmark strings.
2220         String bookmarkNameString = editBookmarkNameEditText.getText().toString();
2221         String bookmarkUrlString = editBookmarkUrlEditText.getText().toString();
2222
2223         // Update the bookmark.
2224         if (currentBookmarkIconRadioButton.isChecked()) {  // Update the bookmark without changing the favorite icon.
2225             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString);
2226         } else {  // Update the bookmark using the `WebView` favorite icon.
2227             // Create a favorite icon byte array output stream.
2228             ByteArrayOutputStream newFavoriteIconByteArrayOutputStream = new ByteArrayOutputStream();
2229
2230             // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2231             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFavoriteIconByteArrayOutputStream);
2232
2233             // Convert the favorite icon byte array stream to a byte array.
2234             byte[] newFavoriteIconByteArray = newFavoriteIconByteArrayOutputStream.toByteArray();
2235
2236             //  Update the bookmark and the favorite icon.
2237             bookmarksDatabaseHelper.updateBookmark(selectedBookmarkDatabaseId, bookmarkNameString, bookmarkUrlString, newFavoriteIconByteArray);
2238         }
2239
2240         // Update the bookmarks cursor with the current contents of this folder.
2241         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2242
2243         // Update the list view.
2244         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2245     }
2246
2247     @Override
2248     public void onSaveBookmarkFolder(DialogFragment dialogFragment, int selectedFolderDatabaseId, Bitmap favoriteIconBitmap) {
2249         // Get handles for the views from `dialogFragment`.
2250         EditText editFolderNameEditText = dialogFragment.getDialog().findViewById(R.id.edit_folder_name_edittext);
2251         RadioButton currentFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_current_icon_radiobutton);
2252         RadioButton defaultFolderIconRadioButton = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_radiobutton);
2253         ImageView defaultFolderIconImageView = dialogFragment.getDialog().findViewById(R.id.edit_folder_default_icon_imageview);
2254
2255         // Get the new folder name.
2256         String newFolderNameString = editFolderNameEditText.getText().toString();
2257
2258         // Check if the favorite icon has changed.
2259         if (currentFolderIconRadioButton.isChecked()) {  // Only the name has changed.
2260             // Update the name in the database.
2261             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString);
2262         } else if (!currentFolderIconRadioButton.isChecked() && newFolderNameString.equals(oldFolderNameString)) {  // Only the icon has changed.
2263             // Create the new folder icon Bitmap.
2264             Bitmap folderIconBitmap;
2265
2266             // Populate the new folder icon bitmap.
2267             if (defaultFolderIconRadioButton.isChecked()) {
2268                 // Get the default folder icon drawable.
2269                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2270
2271                 // Convert the folder icon drawable to a bitmap drawable.
2272                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2273
2274                 // Convert the folder icon bitmap drawable to a bitmap.
2275                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2276             } else {  // Use the `WebView` favorite icon.
2277                 // Copy the favorite icon bitmap to the folder icon bitmap.
2278                 folderIconBitmap = favoriteIconBitmap;
2279             }
2280
2281             // Create a folder icon byte array output stream.
2282             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2283
2284             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2285             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2286
2287             // Convert the folder icon byte array stream to a byte array.
2288             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2289
2290             // Update the folder icon in the database.
2291             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, newFolderIconByteArray);
2292         } else {  // The folder icon and the name have changed.
2293             // Get the new folder icon `Bitmap`.
2294             Bitmap folderIconBitmap;
2295             if (defaultFolderIconRadioButton.isChecked()) {
2296                 // Get the default folder icon drawable.
2297                 Drawable folderIconDrawable = defaultFolderIconImageView.getDrawable();
2298
2299                 // Convert the folder icon drawable to a bitmap drawable.
2300                 BitmapDrawable folderIconBitmapDrawable = (BitmapDrawable) folderIconDrawable;
2301
2302                 // Convert the folder icon bitmap drawable to a bitmap.
2303                 folderIconBitmap = folderIconBitmapDrawable.getBitmap();
2304             } else {  // Use the `WebView` favorite icon.
2305                 // Copy the favorite icon bitmap to the folder icon bitmap.
2306                 folderIconBitmap = favoriteIconBitmap;
2307             }
2308
2309             // Create a folder icon byte array output stream.
2310             ByteArrayOutputStream newFolderIconByteArrayOutputStream = new ByteArrayOutputStream();
2311
2312             // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2313             folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, newFolderIconByteArrayOutputStream);
2314
2315             // Convert the folder icon byte array stream to a byte array.
2316             byte[] newFolderIconByteArray = newFolderIconByteArrayOutputStream.toByteArray();
2317
2318             // Update the folder name and icon in the database.
2319             bookmarksDatabaseHelper.updateFolder(selectedFolderDatabaseId, oldFolderNameString, newFolderNameString, newFolderIconByteArray);
2320         }
2321
2322         // Update the bookmarks cursor with the current contents of this folder.
2323         bookmarksCursor = bookmarksDatabaseHelper.getBookmarksByDisplayOrder(currentBookmarksFolder);
2324
2325         // Update the `ListView`.
2326         bookmarksCursorAdapter.changeCursor(bookmarksCursor);
2327     }
2328
2329     @Override
2330     public void onCloseDownloadLocationPermissionDialog(int downloadType) {
2331         switch (downloadType) {
2332             case DownloadLocationPermissionDialog.DOWNLOAD_FILE:
2333                 // Request the WRITE_EXTERNAL_STORAGE permission with a file request code.
2334                 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_FILE_REQUEST_CODE);
2335                 break;
2336
2337             case DownloadLocationPermissionDialog.DOWNLOAD_IMAGE:
2338                 // Request the WRITE_EXTERNAL_STORAGE permission with an image request code.
2339                 ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, DOWNLOAD_IMAGE_REQUEST_CODE);
2340                 break;
2341         }
2342     }
2343
2344     @Override
2345     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
2346         // Get a handle for the fragment manager.
2347         FragmentManager fragmentManager = getSupportFragmentManager();
2348
2349         switch (requestCode) {
2350             case DOWNLOAD_FILE_REQUEST_CODE:
2351                 // Show the download file alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
2352                 DialogFragment downloadFileDialogFragment = DownloadFileDialog.fromUrl(downloadUrl, downloadContentDisposition, downloadContentLength);
2353
2354                 // On API 23, displaying the fragment must be delayed or the app will crash.
2355                 if (Build.VERSION.SDK_INT == 23) {
2356                     new Handler().postDelayed(() -> downloadFileDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
2357                 } else {
2358                     downloadFileDialogFragment.show(fragmentManager, getString(R.string.download));
2359                 }
2360
2361                 // Reset the download variables.
2362                 downloadUrl = "";
2363                 downloadContentDisposition = "";
2364                 downloadContentLength = 0;
2365                 break;
2366
2367             case DOWNLOAD_IMAGE_REQUEST_CODE:
2368                 // Show the download image alert dialog.  When the dialog closes, the correct command will be used based on the permission status.
2369                 DialogFragment downloadImageDialogFragment = DownloadImageDialog.imageUrl(downloadImageUrl);
2370
2371                 // On API 23, displaying the fragment must be delayed or the app will crash.
2372                 if (Build.VERSION.SDK_INT == 23) {
2373                     new Handler().postDelayed(() -> downloadImageDialogFragment.show(fragmentManager, getString(R.string.download)), 500);
2374                 } else {
2375                     downloadImageDialogFragment.show(fragmentManager, getString(R.string.download));
2376                 }
2377
2378                 // Reset the image URL variable.
2379                 downloadImageUrl = "";
2380                 break;
2381         }
2382     }
2383
2384     @Override
2385     public void onDownloadImage(DialogFragment dialogFragment, String imageUrl) {
2386         // Download the image if it has an HTTP or HTTPS URI.
2387         if (imageUrl.startsWith("http")) {
2388             // Get a handle for the system `DOWNLOAD_SERVICE`.
2389             DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
2390
2391             // Parse `imageUrl`.
2392             DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(imageUrl));
2393
2394             // Get a handle for the cookie manager.
2395             CookieManager cookieManager = CookieManager.getInstance();
2396
2397             // Pass cookies to download manager if cookies are enabled.  This is required to download images from websites that require a login.
2398             // Code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
2399             if (cookieManager.acceptCookie()) {
2400                 // Get the cookies for `imageUrl`.
2401                 String cookies = cookieManager.getCookie(imageUrl);
2402
2403                 // Add the cookies to `downloadRequest`.  In the HTTP request header, cookies are named `Cookie`.
2404                 downloadRequest.addRequestHeader("Cookie", cookies);
2405             }
2406
2407             // Get the file name from the dialog fragment.
2408             EditText downloadImageNameEditText = dialogFragment.getDialog().findViewById(R.id.download_image_name);
2409             String imageName = downloadImageNameEditText.getText().toString();
2410
2411             // Specify the download location.
2412             if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // External write permission granted.
2413                 // Download to the public download directory.
2414                 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, imageName);
2415             } else {  // External write permission denied.
2416                 // Download to the app's external download directory.
2417                 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, imageName);
2418             }
2419
2420             // Allow `MediaScanner` to index the download if it is a media file.
2421             downloadRequest.allowScanningByMediaScanner();
2422
2423             // Add the URL as the description for the download.
2424             downloadRequest.setDescription(imageUrl);
2425
2426             // Show the download notification after the download is completed.
2427             downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
2428
2429             // Remove the lint warning below that `downloadManager` might be `null`.
2430             assert downloadManager != null;
2431
2432             // Initiate the download.
2433             downloadManager.enqueue(downloadRequest);
2434         } else {  // The image is not an HTTP or HTTPS URI.
2435             Snackbar.make(currentWebView, R.string.cannot_download_image, Snackbar.LENGTH_INDEFINITE).show();
2436         }
2437     }
2438
2439     @Override
2440     public void onDownloadFile(DialogFragment dialogFragment, String downloadUrl) {
2441         // Download the file if it has an HTTP or HTTPS URI.
2442         if (downloadUrl.startsWith("http")) {
2443             // Get a handle for the system `DOWNLOAD_SERVICE`.
2444             DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
2445
2446             // Parse `downloadUrl`.
2447             DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(downloadUrl));
2448
2449             // Get a handle for the cookie manager.
2450             CookieManager cookieManager = CookieManager.getInstance();
2451
2452             // Pass cookies to download manager if cookies are enabled.  This is required to download files from websites that require a login.
2453             // Code contributed 2017 Hendrik Knackstedt.  Copyright assigned to Soren Stoutner <soren@stoutner.com>.
2454             if (cookieManager.acceptCookie()) {
2455                 // Get the cookies for `downloadUrl`.
2456                 String cookies = cookieManager.getCookie(downloadUrl);
2457
2458                 // Add the cookies to `downloadRequest`.  In the HTTP request header, cookies are named `Cookie`.
2459                 downloadRequest.addRequestHeader("Cookie", cookies);
2460             }
2461
2462             // Get the file name from the dialog fragment.
2463             EditText downloadFileNameEditText = dialogFragment.getDialog().findViewById(R.id.download_file_name);
2464             String fileName = downloadFileNameEditText.getText().toString();
2465
2466             // Specify the download location.
2467             if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // External write permission granted.
2468                 // Download to the public download directory.
2469                 downloadRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
2470             } else {  // External write permission denied.
2471                 // Download to the app's external download directory.
2472                 downloadRequest.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, fileName);
2473             }
2474
2475             // Allow `MediaScanner` to index the download if it is a media file.
2476             downloadRequest.allowScanningByMediaScanner();
2477
2478             // Add the URL as the description for the download.
2479             downloadRequest.setDescription(downloadUrl);
2480
2481             // Show the download notification after the download is completed.
2482             downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
2483
2484             // Remove the lint warning below that `downloadManager` might be `null`.
2485             assert downloadManager != null;
2486
2487             // Initiate the download.
2488             downloadManager.enqueue(downloadRequest);
2489         } else {  // The download is not an HTTP or HTTPS URI.
2490             Snackbar.make(currentWebView, R.string.cannot_download_file, Snackbar.LENGTH_INDEFINITE).show();
2491         }
2492     }
2493
2494     // Override `onBackPressed` to handle the navigation drawer and and the WebView.
2495     @Override
2496     public void onBackPressed() {
2497         // Get a handle for the drawer layout and the tab layout.
2498         DrawerLayout drawerLayout = findViewById(R.id.drawerlayout);
2499         TabLayout tabLayout = findViewById(R.id.tablayout);
2500
2501         if (drawerLayout.isDrawerVisible(GravityCompat.START)) {  // The navigation drawer is open.
2502             // Close the navigation drawer.
2503             drawerLayout.closeDrawer(GravityCompat.START);
2504         } else if (drawerLayout.isDrawerVisible(GravityCompat.END)){  // The bookmarks drawer is open.
2505             if (currentBookmarksFolder.isEmpty()) {  // The home folder is displayed.
2506                 // close the bookmarks drawer.
2507                 drawerLayout.closeDrawer(GravityCompat.END);
2508             } else {  // A subfolder is displayed.
2509                 // Place the former parent folder in `currentFolder`.
2510                 currentBookmarksFolder = bookmarksDatabaseHelper.getParentFolderName(currentBookmarksFolder);
2511
2512                 // Load the new folder.
2513                 loadBookmarksFolder();
2514             }
2515         } else if (currentWebView.canGoBack()) {  // There is at least one item in the current WebView history.
2516             // Reset the current domain name so that navigation works if third-party requests are blocked.
2517             currentWebView.resetCurrentDomainName();
2518
2519             // Set navigating history so that the domain settings are applied when the new URL is loaded.
2520             currentWebView.setNavigatingHistory(true);
2521
2522             // Go back.
2523             currentWebView.goBack();
2524         } else if (tabLayout.getTabCount() > 1) {  // There are at least two tabs.
2525             // Close the current tab.
2526             closeCurrentTab();
2527         } else {  // There isn't anything to do in Privacy Browser.
2528             // Run the default commands.
2529             super.onBackPressed();
2530
2531             // Manually kill Privacy Browser.  Otherwise, it is glitchy when restarted.
2532             System.exit(0);
2533         }
2534     }
2535
2536     // Process the results of an upload file chooser.  Currently there is only one `startActivityForResult` in this activity, so the request code, used to differentiate them, is ignored.
2537     @Override
2538     public void onActivityResult(int requestCode, int resultCode, Intent data) {
2539         // File uploads only work on API >= 21.
2540         if (Build.VERSION.SDK_INT >= 21) {
2541             // Pass the file to the WebView.
2542             fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
2543         }
2544     }
2545
2546     private void loadUrlFromTextBox() {
2547         // Get a handle for the URL edit text.
2548         EditText urlEditText = findViewById(R.id.url_edittext);
2549
2550         // Get the text from urlTextBox and convert it to a string.  trim() removes white spaces from the beginning and end of the string.
2551         String unformattedUrlString = urlEditText.getText().toString().trim();
2552
2553         // Initialize the formatted URL string.
2554         String url = "";
2555
2556         // Check to see if `unformattedUrlString` is a valid URL.  Otherwise, convert it into a search.
2557         if (unformattedUrlString.startsWith("content://")) {  // This is a Content URL.
2558             // Load the entire content URL.
2559             url = unformattedUrlString;
2560         } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
2561                 unformattedUrlString.startsWith("file://")) {  // This is a standard URL.
2562             // Add `https://` at the beginning if there is no protocol.  Otherwise the app will segfault.
2563             if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://") && !unformattedUrlString.startsWith("content://")) {
2564                 unformattedUrlString = "https://" + unformattedUrlString;
2565             }
2566
2567             // Initialize `unformattedUrl`.
2568             URL unformattedUrl = null;
2569
2570             // 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.
2571             try {
2572                 unformattedUrl = new URL(unformattedUrlString);
2573             } catch (MalformedURLException e) {
2574                 e.printStackTrace();
2575             }
2576
2577             // The ternary operator (? :) makes sure that a null pointer exception is not thrown, which would happen if `.get` was called on a `null` value.
2578             String scheme = unformattedUrl != null ? unformattedUrl.getProtocol() : null;
2579             String authority = unformattedUrl != null ? unformattedUrl.getAuthority() : null;
2580             String path = unformattedUrl != null ? unformattedUrl.getPath() : null;
2581             String query = unformattedUrl != null ? unformattedUrl.getQuery() : null;
2582             String fragment = unformattedUrl != null ? unformattedUrl.getRef() : null;
2583
2584             // Build the URI.
2585             Uri.Builder uri = new Uri.Builder();
2586             uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment);
2587
2588             // Decode the URI as a UTF-8 string in.
2589             try {
2590                 url = URLDecoder.decode(uri.build().toString(), "UTF-8");
2591             } catch (UnsupportedEncodingException exception) {
2592                 // Do nothing.  The formatted URL string will remain blank.
2593             }
2594         } else if (!unformattedUrlString.isEmpty()){  // This is not a URL, but rather a search string.
2595             // Create an encoded URL String.
2596             String encodedUrlString;
2597
2598             // Sanitize the search input.
2599             try {
2600                 encodedUrlString = URLEncoder.encode(unformattedUrlString, "UTF-8");
2601             } catch (UnsupportedEncodingException exception) {
2602                 encodedUrlString = "";
2603             }
2604
2605             // Add the base search URL.
2606             url = searchURL + encodedUrlString;
2607         }
2608
2609         // 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.
2610         urlEditText.clearFocus();
2611
2612         // Make it so.
2613         loadUrl(url);
2614     }
2615
2616     private void loadUrl(String url) {
2617         // Sanitize the URL.
2618         url = sanitizeUrl(url);
2619
2620         // Apply the domain settings.
2621         applyDomainSettings(currentWebView, url, true, false);
2622
2623         // Load the URL.
2624         currentWebView.loadUrl(url, customHeaders);
2625     }
2626
2627     public void findPreviousOnPage(View view) {
2628         // Go to the previous highlighted phrase on the page.  `false` goes backwards instead of forwards.
2629         currentWebView.findNext(false);