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