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