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