]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt
Move the blocklist helper to a class instance. https://redmine.stoutner.com/issues/991
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / MainWebViewActivity.kt
1 /*
2  * Copyright 2015-2023 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 Android <https://www.stoutner.com/privacy-browser-android>.
7  *
8  * Privacy Browser Android 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 Android 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 Android.  If not, see <http://www.gnu.org/licenses/>.
20  */
21
22 package com.stoutner.privacybrowser.activities
23
24 import android.animation.ObjectAnimator
25 import android.annotation.SuppressLint
26 import android.app.DownloadManager
27 import android.app.SearchManager
28 import android.content.ActivityNotFoundException
29 import android.content.BroadcastReceiver
30 import android.content.ClipData
31 import android.content.ClipboardManager
32 import android.content.Context
33 import android.content.Intent
34 import android.content.IntentFilter
35 import android.content.SharedPreferences
36 import android.content.pm.PackageManager
37 import android.content.res.Configuration
38 import android.database.Cursor
39 import android.graphics.Bitmap
40 import android.graphics.BitmapFactory
41 import android.graphics.Typeface
42 import android.graphics.drawable.BitmapDrawable
43 import android.net.Uri
44 import android.net.http.SslError
45 import android.os.Build
46 import android.os.Bundle
47 import android.os.Environment
48 import android.os.Handler
49 import android.os.Looper
50 import android.print.PrintManager
51 import android.provider.DocumentsContract
52 import android.provider.OpenableColumns
53 import android.text.Editable
54 import android.text.TextWatcher
55 import android.text.style.ForegroundColorSpan
56 import android.util.Patterns
57 import android.util.TypedValue
58 import android.view.ContextMenu
59 import android.view.GestureDetector
60 import android.view.KeyEvent
61 import android.view.Menu
62 import android.view.MenuItem
63 import android.view.MotionEvent
64 import android.view.View
65 import android.view.ViewGroup
66 import android.view.WindowManager
67 import android.view.inputmethod.InputMethodManager
68 import android.webkit.CookieManager
69 import android.webkit.HttpAuthHandler
70 import android.webkit.ValueCallback
71 import android.webkit.SslErrorHandler
72 import android.webkit.WebChromeClient
73 import android.webkit.WebResourceRequest
74 import android.webkit.WebResourceResponse
75 import android.webkit.WebSettings
76 import android.webkit.WebStorage
77 import android.webkit.WebView
78 import android.webkit.WebViewClient
79 import android.webkit.WebViewDatabase
80 import android.widget.AdapterView
81 import android.widget.ArrayAdapter
82 import android.widget.CheckBox
83 import android.widget.EditText
84 import android.widget.FrameLayout
85 import android.widget.ImageView
86 import android.widget.LinearLayout
87 import android.widget.ListView
88 import android.widget.ProgressBar
89 import android.widget.RadioButton
90 import android.widget.RelativeLayout
91 import android.widget.TextView
92
93 import androidx.activity.OnBackPressedCallback
94 import androidx.activity.result.contract.ActivityResultContracts
95 import androidx.appcompat.app.ActionBar
96 import androidx.appcompat.app.ActionBarDrawerToggle
97 import androidx.appcompat.app.AppCompatActivity
98 import androidx.appcompat.app.AppCompatDelegate
99 import androidx.appcompat.content.res.AppCompatResources
100 import androidx.appcompat.widget.Toolbar
101 import androidx.coordinatorlayout.widget.CoordinatorLayout
102 import androidx.core.view.GravityCompat
103 import androidx.cursoradapter.widget.CursorAdapter
104 import androidx.drawerlayout.widget.DrawerLayout
105 import androidx.fragment.app.DialogFragment
106 import androidx.preference.PreferenceManager
107 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
108 import androidx.viewpager.widget.ViewPager
109 import androidx.webkit.WebSettingsCompat
110 import androidx.webkit.WebViewFeature
111
112 import com.google.android.material.appbar.AppBarLayout
113 import com.google.android.material.floatingactionbutton.FloatingActionButton
114 import com.google.android.material.navigation.NavigationView
115 import com.google.android.material.snackbar.Snackbar
116 import com.google.android.material.tabs.TabLayout
117
118 import com.stoutner.privacybrowser.R
119 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter
120 import com.stoutner.privacybrowser.coroutines.GetHostIpAddressesCoroutine
121 import com.stoutner.privacybrowser.coroutines.PopulateBlocklistsCoroutine
122 import com.stoutner.privacybrowser.coroutines.PrepareSaveDialogCoroutine
123 import com.stoutner.privacybrowser.coroutines.SaveUrlCoroutine
124 import com.stoutner.privacybrowser.coroutines.SaveWebpageImageCoroutine
125 import com.stoutner.privacybrowser.dataclasses.PendingDialogDataClass
126 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog
127 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog
128 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog
129 import com.stoutner.privacybrowser.dialogs.FontSizeDialog
130 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog
131 import com.stoutner.privacybrowser.dialogs.OpenDialog
132 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog
133 import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog
134 import com.stoutner.privacybrowser.dialogs.SaveDialog
135 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog
136 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog
137 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog
138 import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog
139 import com.stoutner.privacybrowser.fragments.WebViewTabFragment
140 import com.stoutner.privacybrowser.helpers.BlocklistHelper
141 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
142 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper
143 import com.stoutner.privacybrowser.helpers.ProxyHelper
144 import com.stoutner.privacybrowser.helpers.SanitizeUrlHelper
145 import com.stoutner.privacybrowser.helpers.UrlHelper
146 import com.stoutner.privacybrowser.views.BLOCKED_REQUESTS
147 import com.stoutner.privacybrowser.views.EASYLIST
148 import com.stoutner.privacybrowser.views.EASYPRIVACY
149 import com.stoutner.privacybrowser.views.FANBOYS_ANNOYANCE_LIST
150 import com.stoutner.privacybrowser.views.FANBOYS_SOCIAL_BLOCKING_LIST
151 import com.stoutner.privacybrowser.views.THIRD_PARTY_REQUESTS
152 import com.stoutner.privacybrowser.views.ULTRALIST
153 import com.stoutner.privacybrowser.views.ULTRAPRIVACY
154 import com.stoutner.privacybrowser.views.NestedScrollWebView
155
156 import kotlinx.coroutines.CoroutineScope
157 import kotlinx.coroutines.Dispatchers
158 import kotlinx.coroutines.launch
159 import kotlinx.coroutines.withContext
160
161 import java.io.ByteArrayInputStream
162 import java.io.ByteArrayOutputStream
163 import java.io.File
164 import java.io.FileInputStream
165 import java.io.FileOutputStream
166 import java.io.IOException
167 import java.io.UnsupportedEncodingException
168
169 import java.net.MalformedURLException
170 import java.net.URL
171 import java.net.URLDecoder
172 import java.net.URLEncoder
173
174 import java.text.NumberFormat
175
176 import java.util.ArrayList
177 import java.util.Date
178 import java.util.concurrent.Executors
179 import kotlin.system.exitProcess
180
181 // Define the public constants
182 const val CURRENT_URL = "current_url"
183 const val DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0
184 const val DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2
185 const val DOMAINS_CUSTOM_USER_AGENT = 12
186 const val SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1
187 const val SETTINGS_CUSTOM_USER_AGENT = 11
188 const val UNRECOGNIZED_USER_AGENT = -1
189
190 // Define the private class constants.
191 private const val BOOKMARKS_DRAWER_PINNED = "bookmarks_drawer_pinned"
192 private const val PROXY_MODE = "proxy_mode"
193 private const val SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list"
194 private const val SAVED_STATE_ARRAY_LIST = "saved_state_array_list"
195 private const val SAVED_TAB_POSITION = "saved_tab_position"
196 private const val TEMPORARY_MHT_FILE = "temporary_mht_file"
197
198 // TODO.  Reorder methods alphabetically.
199
200 class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener,
201     NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener, PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklistsCoroutine.PopulateBlocklistsListener, SaveDialog.SaveListener,
202     UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener {
203
204     companion object {
205         // Define the public static variables.
206         var currentBookmarksFolder = ""
207         val executorService = Executors.newFixedThreadPool(4)!!
208         var orbotStatus = "unknown"
209         val pendingDialogsArrayList = ArrayList<PendingDialogDataClass>()
210         var proxyMode = ProxyHelper.NONE
211         var restartFromBookmarksActivity = false
212         var webViewPagerAdapter: WebViewPagerAdapter? = null
213
214         // Declare the public static variables.
215         lateinit var appBarLayout: AppBarLayout
216     }
217
218     // Declare the class variables.
219     private lateinit var appBar: ActionBar
220     private lateinit var blocklistHelper: BlocklistHelper
221     private lateinit var bookmarksCursorAdapter: CursorAdapter
222     private lateinit var bookmarksListView: ListView
223     private lateinit var bookmarksDrawerPinnedImageView: ImageView
224     private lateinit var bookmarksTitleTextView: TextView
225     private lateinit var coordinatorLayout: CoordinatorLayout
226     private lateinit var cookieManager: CookieManager
227     private lateinit var domainsSettingsSet: MutableSet<String>
228     private lateinit var drawerLayout: DrawerLayout
229     private lateinit var easyList: ArrayList<List<Array<String>>>
230     private lateinit var easyPrivacy: ArrayList<List<Array<String>>>
231     private lateinit var fanboysAnnoyanceList: ArrayList<List<Array<String>>>
232     private lateinit var fanboysSocialList: ArrayList<List<Array<String>>>
233     private lateinit var fileChooserCallback: ValueCallback<Array<Uri>>
234     private lateinit var finalGrayColorSpan: ForegroundColorSpan
235     private lateinit var findOnPageCountTextView: TextView
236     private lateinit var findOnPageEditText: EditText
237     private lateinit var findOnPageLinearLayout: LinearLayout
238     private lateinit var fullScreenVideoFrameLayout: FrameLayout
239     private lateinit var initialGrayColorSpan: ForegroundColorSpan
240     private lateinit var navigationBackMenuItem: MenuItem
241     private lateinit var navigationForwardMenuItem: MenuItem
242     private lateinit var navigationHistoryMenuItem: MenuItem
243     private lateinit var navigationRequestsMenuItem: MenuItem
244     private lateinit var optionsAddOrEditDomainMenuItem: MenuItem
245     private lateinit var optionsBlockAllThirdPartyRequestsMenuItem: MenuItem
246     private lateinit var optionsBlocklistsMenuItem: MenuItem
247     private lateinit var optionsClearCookiesMenuItem: MenuItem
248     private lateinit var optionsClearDataMenuItem: MenuItem
249     private lateinit var optionsClearDomStorageMenuItem: MenuItem
250     private lateinit var optionsClearFormDataMenuItem: MenuItem
251     private lateinit var optionsCookiesMenuItem: MenuItem
252     private lateinit var optionsDarkWebViewMenuItem: MenuItem
253     private lateinit var optionsDisplayImagesMenuItem: MenuItem
254     private lateinit var optionsDomStorageMenuItem: MenuItem
255     private lateinit var optionsEasyListMenuItem: MenuItem
256     private lateinit var optionsEasyPrivacyMenuItem: MenuItem
257     private lateinit var optionsFanboysAnnoyanceListMenuItem: MenuItem
258     private lateinit var optionsFanboysSocialBlockingListMenuItem: MenuItem
259     private lateinit var optionsFontSizeMenuItem: MenuItem
260     private lateinit var optionsPrivacyMenuItem: MenuItem
261     private lateinit var optionsProxyCustomMenuItem: MenuItem
262     private lateinit var optionsProxyI2pMenuItem: MenuItem
263     private lateinit var optionsProxyMenuItem: MenuItem
264     private lateinit var optionsProxyNoneMenuItem: MenuItem
265     private lateinit var optionsProxyTorMenuItem: MenuItem
266     private lateinit var optionsRefreshMenuItem: MenuItem
267     private lateinit var optionsSaveFormDataMenuItem: MenuItem
268     private lateinit var optionsSwipeToRefreshMenuItem: MenuItem
269     private lateinit var optionsUltraListMenuItem: MenuItem
270     private lateinit var optionsUltraPrivacyMenuItem: MenuItem
271     private lateinit var optionsUserAgentChromeOnAndroidMenuItem: MenuItem
272     private lateinit var optionsUserAgentChromeOnWindowsMenuItem: MenuItem
273     private lateinit var optionsUserAgentChromiumOnLinuxMenuItem: MenuItem
274     private lateinit var optionsUserAgentCustomMenuItem: MenuItem
275     private lateinit var optionsUserAgentEdgeOnWindowsMenuItem: MenuItem
276     private lateinit var optionsUserAgentFirefoxOnAndroidMenuItem: MenuItem
277     private lateinit var optionsUserAgentFirefoxOnLinuxMenuItem: MenuItem
278     private lateinit var optionsUserAgentFirefoxOnWindowsMenuItem: MenuItem
279     private lateinit var optionsUserAgentInternetExplorerOnWindowsMenuItem: MenuItem
280     private lateinit var optionsUserAgentMenuItem: MenuItem
281     private lateinit var optionsUserAgentPrivacyBrowserMenuItem: MenuItem
282     private lateinit var optionsUserAgentSafariOnIosMenuItem: MenuItem
283     private lateinit var optionsUserAgentSafariOnMacosMenuItem: MenuItem
284     private lateinit var optionsUserAgentWebViewDefaultMenuItem: MenuItem
285     private lateinit var optionsWideViewportMenuItem: MenuItem
286     private lateinit var proxyHelper: ProxyHelper
287     private lateinit var redColorSpan: ForegroundColorSpan
288     private lateinit var rootFrameLayout: FrameLayout
289     private lateinit var saveUrlString: String
290     private lateinit var searchURL: String
291     private lateinit var sharedPreferences: SharedPreferences
292     private lateinit var swipeRefreshLayout: SwipeRefreshLayout
293     private lateinit var tabLayout: TabLayout
294     private lateinit var tabsLinearLayout: LinearLayout
295     private lateinit var toolbar: Toolbar
296     private lateinit var webViewDefaultUserAgent: String
297     private lateinit var webViewPager: ViewPager
298     private lateinit var ultraList: ArrayList<List<Array<String>>>
299     private lateinit var urlEditText: EditText
300     private lateinit var urlRelativeLayout: RelativeLayout
301
302     // Define the class variables.
303     private var actionBarDrawerToggle: ActionBarDrawerToggle? = null
304     private var appBarHeight = 0
305     private var bookmarksCursor: Cursor? = null
306     private var bookmarksDatabaseHelper: BookmarksDatabaseHelper? = null
307     private var bookmarksDrawerPinned = false
308     private var bottomAppBar = false
309     private var currentWebView: NestedScrollWebView? = null
310     private var defaultProgressViewEndOffset = 0
311     private var defaultProgressViewStartOffset = 0
312     private var displayAdditionalAppBarIcons = false
313     private var displayingFullScreenVideo = false
314     private var domainsDatabaseHelper: DomainsDatabaseHelper? = null
315     private var downloadWithExternalApp = false
316     private var fullScreenBrowsingModeEnabled = false
317     private var hideAppBar = false
318     private var inFullScreenBrowsingMode = false
319     private var incognitoModeEnabled = false
320     private var loadingNewIntent = false
321     private var objectAnimator = ObjectAnimator()
322     private var optionsMenu: Menu? = null
323     private var orbotStatusBroadcastReceiver: BroadcastReceiver? = null
324     private var reapplyAppSettingsOnRestart = false
325     private var reapplyDomainSettingsOnRestart = false
326     private var sanitizeAmpRedirects = false
327     private var sanitizeTrackingQueries = false
328     private var savedProxyMode: String? = null
329     private var savedNestedScrollWebViewStateArrayList: ArrayList<Bundle>? = null
330     private var savedStateArrayList: ArrayList<Bundle>? = null
331     private var savedTabPosition = 0
332     private var scrollAppBar = false
333     private var ultraPrivacy: ArrayList<List<Array<String>>>? = null
334     private var waitingForProxy = false
335
336     // Define the save URL activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
337     private val saveUrlActivityResultLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.CreateDocument("*/*")) { fileUri ->
338         // Only save the URL if the file URI is not null, which happens if the user exited the file picker by pressing back.
339         if (fileUri != null) {
340             // Instantiate the save URL coroutine.
341             val saveUrlCoroutine = SaveUrlCoroutine()
342
343             // Save the URL.
344             saveUrlCoroutine.save(this, this, saveUrlString, fileUri, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
345         }
346
347         // Reset the save URL string.
348         saveUrlString = ""
349     }
350
351     // Define the save webpage archive activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
352     private val saveWebpageArchiveActivityResultLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.CreateDocument("multipart/related")) { fileUri ->
353         // Only save the webpage archive if the file URI is not null, which happens if the user exited the file picker by pressing back.
354         if (fileUri != null) {
355             // Initialize the file name string from the file URI last path segment.
356             var fileNameString = fileUri.lastPathSegment
357
358             // Query the exact file name if the API >= 26.
359             if (Build.VERSION.SDK_INT >= 26) {
360                 // Get a cursor from the content resolver.
361                 val contentResolverCursor = contentResolver.query(fileUri, null, null, null)!!
362
363                 // Move to the fist row.
364                 contentResolverCursor.moveToFirst()
365
366                 // Get the file name from the cursor.
367                 fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
368
369                 // Close the cursor.
370                 contentResolverCursor.close()
371             }
372
373             // Use a coroutine to save the file.
374             CoroutineScope(Dispatchers.Main).launch {
375                 try {
376                     // Create the file on the IO thread.
377                     withContext(Dispatchers.IO) {
378                         // Create a temporary MHT file.
379                         val temporaryMhtFile = File.createTempFile(TEMPORARY_MHT_FILE, ".mht", cacheDir)
380
381                         // The WebView must be accessed from the main thread.
382                         withContext(Dispatchers.Main) {
383                             currentWebView!!.saveWebArchive(temporaryMhtFile.toString(), false) { callbackValue ->
384                                 if (callbackValue != null) {  // The temporary MHT file was saved successfully.
385                                     try {
386                                         // Create a temporary MHT file input stream.
387                                         val temporaryMhtFileInputStream = FileInputStream(temporaryMhtFile)
388
389                                         // Get an output stream for the save webpage file path.
390                                         val mhtOutputStream = contentResolver.openOutputStream(fileUri)!!
391
392                                         // Create a transfer byte array.
393                                         val transferByteArray = ByteArray(1024)
394
395                                         // Create an integer to track the number of bytes read.
396                                         var bytesRead: Int
397
398                                         // Copy the temporary MHT file input stream to the MHT output stream.
399                                         while (temporaryMhtFileInputStream.read(transferByteArray).also { bytesRead = it } > 0)
400                                             mhtOutputStream.write(transferByteArray, 0, bytesRead)
401
402                                         // Close the streams.
403                                         mhtOutputStream.close()
404                                         temporaryMhtFileInputStream.close()
405
406                                         // Display a snackbar.
407                                         Snackbar.make(currentWebView!!, getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
408                                     } catch (exception: Exception) {
409                                         // Display snackbar with the exception.
410                                         Snackbar.make(currentWebView!!, getString(R.string.error_saving_file, fileNameString, exception), Snackbar.LENGTH_INDEFINITE).show()
411                                     } finally {
412                                         // Delete the temporary MHT file.
413                                         temporaryMhtFile.delete()
414                                     }
415                                 } else {  // There was an unspecified error while saving the temporary MHT file.
416                                     // Display a snackbar.
417                                     Snackbar.make(currentWebView!!, getString(R.string.error_saving_file, fileNameString, getString(R.string.unknown_error)), Snackbar.LENGTH_INDEFINITE).show()
418                                 }
419                             }
420                         }
421                     }
422                 } catch (ioException: IOException) {
423                     // Display a snackbar with the IO exception.
424                     Snackbar.make(currentWebView!!, getString(R.string.error_saving_file, fileNameString, ioException), Snackbar.LENGTH_INDEFINITE).show()
425                 }
426             }
427         }
428     }
429
430     // Define the save webpage image activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
431     private val saveWebpageImageActivityResultLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.CreateDocument("image/png")) { fileUri ->
432         // Only save the webpage image if the file URI is not null, which happens if the user exited the file picker by pressing back.
433         if (fileUri != null) {
434             // Instantiate the save webpage image coroutine.
435             val saveWebpageImageCoroutine = SaveWebpageImageCoroutine()
436
437             // Save the webpage image.
438             saveWebpageImageCoroutine.save(this, fileUri, currentWebView!!)
439         }
440     }
441
442     // Define the save webpage image activity result launcher.  It must be defined before `onCreate()` is run or the app will crash.
443     private val browseFileUploadActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
444         // Pass the file to the WebView.
445         fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(activityResult.resultCode, activityResult.data))
446     }
447
448     override fun onCreate(savedInstanceState: Bundle?) {
449         // Run the default commands.
450         super.onCreate(savedInstanceState)
451
452         // Initialize the default preference values the first time the program is run.  `false` keeps this command from resetting any current preferences back to default.
453         PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
454
455         // Get a handle for the shared preferences.
456         sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
457
458         // Get the preferences.
459         val appTheme = sharedPreferences.getString(getString(R.string.app_theme_key), getString(R.string.app_theme_default_value))
460         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
461         bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
462         displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false)
463
464         // Get the theme entry values string array.
465         val appThemeEntryValuesStringArray = resources.getStringArray(R.array.app_theme_entry_values)
466
467         // Get the current theme status.
468         val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
469
470         // Set the app theme according to the preference.  A switch statement cannot be used because the theme entry values string array is not a compile time constant.
471         if (appTheme == appThemeEntryValuesStringArray[1]) {  // The light theme is selected.
472             // Apply the light theme.
473             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
474         } else if (appTheme == appThemeEntryValuesStringArray[2]) {  // The dark theme is selected.
475             // Apply the dark theme.
476             AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
477         } else {  // The system default theme is selected.
478             if (Build.VERSION.SDK_INT >= 28) {  // The system default theme is supported.
479                 // Follow the system default theme.
480                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
481             } else {  // The system default theme is not supported.
482                 // Follow the battery saver mode.
483                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
484             }
485         }
486
487         // Do not continue if the app theme is different than the OS theme.  The app always initially starts in the OS theme.
488         // If the user has specified the opposite theme should be used, the app will restart in that mode after the above `setDefaultNightMode()` code processes.  However, the restart is delayed.
489         // If the blacklist coroutine starts below it will continue to run during the restart, which leads to indeterminate behavior, with the system often not knowing how many tabs exist.
490         // See https://redmine.stoutner.com/issues/952.
491         if ((appTheme == appThemeEntryValuesStringArray[0]) ||  // The system default theme is used.
492             ((appTheme == appThemeEntryValuesStringArray[1]) && (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO)) ||  // The app is running in day theme as desired.
493             ((appTheme == appThemeEntryValuesStringArray[2]) && (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES))) {  // The app is running in night theme as desired.
494
495             // Disable screenshots if not allowed.
496             if (!allowScreenshots) {
497                 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
498             }
499
500             // Check to see if the activity has been restarted.
501             if (savedInstanceState != null) {
502                 // Store the saved instance state variables.  The deprecated `getParcelableArrayList` can be upgraded once the minimum API >= 33.
503                 bookmarksDrawerPinned = savedInstanceState.getBoolean(BOOKMARKS_DRAWER_PINNED)
504                 @Suppress("DEPRECATION")
505                 savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST)
506                 @Suppress("DEPRECATION")
507                 savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST)
508                 savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION)
509                 savedProxyMode = savedInstanceState.getString(PROXY_MODE)
510             }
511
512             // Enable the drawing of the entire webpage.  This makes it possible to save a website image.  This must be done before anything else happens with the WebView.
513             WebView.enableSlowWholeDocumentDraw()
514
515             // Set the content view according to the position of the app bar.
516             if (bottomAppBar)
517                 setContentView(R.layout.main_framelayout_bottom_appbar)
518             else
519                 setContentView(R.layout.main_framelayout_top_appbar)
520
521             // Get handles for the views.
522             rootFrameLayout = findViewById(R.id.root_framelayout)
523             drawerLayout = findViewById(R.id.drawerlayout)
524             coordinatorLayout = findViewById(R.id.coordinatorlayout)
525             appBarLayout = findViewById(R.id.appbar_layout)
526             toolbar = findViewById(R.id.toolbar)
527             findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout)
528             findOnPageEditText = findViewById(R.id.find_on_page_edittext)
529             findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview)
530             tabsLinearLayout = findViewById(R.id.tabs_linearlayout)
531             tabLayout = findViewById(R.id.tablayout)
532             swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
533             webViewPager = findViewById(R.id.webviewpager)
534             val navigationView = findViewById<NavigationView>(R.id.navigationview)
535             bookmarksListView = findViewById(R.id.bookmarks_drawer_listview)
536             bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview)
537             bookmarksDrawerPinnedImageView = findViewById(R.id.bookmarks_drawer_pinned_imageview)
538             fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout)
539
540             // Get a handle for the navigation menu.
541             val navigationMenu = navigationView.menu
542
543             // Get handles for the navigation menu items.
544             navigationBackMenuItem = navigationMenu.findItem(R.id.back)
545             navigationForwardMenuItem = navigationMenu.findItem(R.id.forward)
546             navigationHistoryMenuItem = navigationMenu.findItem(R.id.history)
547             navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests)
548
549             // Listen for touches on the navigation menu.
550             navigationView.setNavigationItemSelectedListener(this)
551
552             // Set the support action bar.
553             setSupportActionBar(toolbar)
554
555             // Get a handle for the app bar.
556             appBar = supportActionBar!!
557
558             // Set the custom app bar layout, which shows the URL text bar.
559             appBar.setCustomView(R.layout.url_app_bar)
560
561             // Display the custom app bar layout.
562             appBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
563
564             // Get handles for the views in the URL app bar.
565             urlRelativeLayout = findViewById(R.id.url_relativelayout)
566             urlEditText = findViewById(R.id.url_edittext)
567
568             // Create the hamburger icon at the start of the AppBar.
569             actionBarDrawerToggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer)
570
571             // Initially disable the sliding drawers.  They will be enabled once the blocklists are loaded.
572             drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
573
574             // Initially hide the user interface so that only the blocklist loading screen is shown (if reloading).
575             drawerLayout.visibility = View.GONE
576
577             // Initialize the WebView pager adapter.
578             webViewPagerAdapter = WebViewPagerAdapter(supportFragmentManager)
579
580             // Set the pager adapter on the web view pager.
581             webViewPager.adapter = webViewPagerAdapter
582
583             // Store up to 100 tabs in memory.
584             webViewPager.offscreenPageLimit = 100
585
586             // Get a handle for the cookie manager.
587             cookieManager = CookieManager.getInstance()
588
589             // Instantiate the helpers.
590             bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
591             domainsDatabaseHelper = DomainsDatabaseHelper(this)
592             proxyHelper = ProxyHelper()
593
594             // Update the bookmarks drawer pinned image view.
595             updateBookmarksDrawerPinnedImageView()
596
597             // Initialize the app.
598             initializeApp()
599
600             // Apply the app settings from the shared preferences.
601             applyAppSettings()
602
603             // Control what the system back command does.
604             val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
605                 override fun handleOnBackPressed() {
606                     // Process the different back options.
607                     if (drawerLayout.isDrawerVisible(GravityCompat.START)) {  // The navigation drawer is open.
608                         // Close the navigation drawer.
609                         drawerLayout.closeDrawer(GravityCompat.START)
610                     } else if (drawerLayout.isDrawerVisible(GravityCompat.END)) {  // The bookmarks drawer is open.
611                         // close the bookmarks drawer.
612                         drawerLayout.closeDrawer(GravityCompat.END)
613                     } else if (displayingFullScreenVideo) {  // A full screen video is shown.
614                         // Exit the full screen video.
615                         exitFullScreenVideo()
616                         // It shouldn't be possible for the currentWebView to be null, but crash logs indicate it sometimes happens.
617                     } else if (currentWebView != null && currentWebView!!.canGoBack()) {  // There is at least one item in the current WebView history.
618                         // Get the current web back forward list.
619                         val webBackForwardList = currentWebView!!.copyBackForwardList()
620
621                         // Get the previous entry URL.
622                         val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url
623
624                         // Apply the domain settings.
625                         applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
626
627                         // Go back.
628                         currentWebView!!.goBack()
629                     } else if (tabLayout.tabCount > 1) {  // There are at least two tabs.
630                         // Close the current tab.
631                         closeCurrentTab()
632                     } else {  // There isn't anything to do in Privacy Browser.
633                         // Run clear and exit.
634                         clearAndExit()
635                     }
636                 }
637             }
638
639             // Register the on back pressed callback.
640             onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
641
642             // Instantiate the populate blocklists coroutine.
643             val populateBlocklistsCoroutine = PopulateBlocklistsCoroutine(this)
644
645             // Populate the blocklists.
646             populateBlocklistsCoroutine.populateBlocklists(this)
647         }
648     }
649
650     override fun onNewIntent(intent: Intent) {
651         // Run the default commands.
652         super.onNewIntent(intent)
653
654         // Get the information from the intent.
655         val intentAction = intent.action
656         val intentUriData = intent.data
657         val intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT)
658
659         // Determine if this is a web search.
660         val isWebSearch = (intentAction != null) && (intentAction == Intent.ACTION_WEB_SEARCH)
661
662         // Check to see if the app is being restarted from a saved state.
663         if (ultraPrivacy != null) {  // The activity is not being restarted from a saved state.
664             // Only process the URI if it contains data or it is a web search.  If the user pressed the desktop icon after the app was already running the URI will be null.
665             if ((intentUriData != null) || (intentStringExtra != null) || isWebSearch) {
666                 // Exit the full screen video if it is displayed.
667                 if (displayingFullScreenVideo) {
668                     // Exit full screen video mode.
669                     exitFullScreenVideo()
670
671                     // Reload the current WebView.  Otherwise, it can display entirely black.
672                     currentWebView!!.reload()
673                 }
674
675                 // Get the URL.
676                 val url = if (isWebSearch) {  // The intent is a web search.
677                     // Sanitize the search input and convert it to a search.
678                     val encodedSearchString = try {
679                         URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8")
680                     } catch (exception: UnsupportedEncodingException) {
681                         ""
682                     }
683
684                     // Add the base search URL.
685                     searchURL + encodedSearchString
686                 } else {  // The intent contains a URL in either the data or an extra.
687                     intentUriData?.toString() ?: intentStringExtra
688                 }
689
690                 // Add a new tab if specified in the preferences.
691                 if (sharedPreferences.getBoolean(getString(R.string.open_intents_in_new_tab_key), true)) {  // Load the URL in a new tab.
692                     // Set the loading new intent flag.
693                     loadingNewIntent = true
694
695                     // Add a new tab.
696                     addNewTab(url!!, true)
697                 } else {  // Load the URL in the current tab.
698                     // Make it so.
699                     loadUrl(currentWebView!!, url!!)
700                 }
701
702                 // Close the navigation drawer if it is open.
703                 if (drawerLayout.isDrawerVisible(GravityCompat.START))
704                     drawerLayout.closeDrawer(GravityCompat.START)
705
706                 // Close the bookmarks drawer if it is open.
707                 if (drawerLayout.isDrawerVisible(GravityCompat.END))
708                     drawerLayout.closeDrawer(GravityCompat.END)
709             }
710         } else {  // The app has been restarted.
711             // If the new intent will open a new tab, set the saved tab position to be the size of the saved state array list.
712             // The tab position is 0 based, meaning the at the new tab will be the tab position that is restored.
713             if ((intentUriData != null) || (intentStringExtra != null) || isWebSearch)
714                 savedTabPosition = savedStateArrayList!!.size
715
716             // Replace the intent that started the app with this one.  This will load the tab after the others have been restored.
717             setIntent(intent)
718         }
719     }
720
721     public override fun onRestart() {
722         // Run the default commands.
723         super.onRestart()
724
725         // Apply the app settings if returning from the Settings activity.
726         if (reapplyAppSettingsOnRestart) {
727             // Reset the reapply app settings on restart flag.
728             reapplyAppSettingsOnRestart = false
729
730             // Apply the app settings.
731             applyAppSettings()
732         }
733
734         // Apply the domain settings if returning from the settings or domains activity.
735         if (reapplyDomainSettingsOnRestart) {
736             // Reset the reapply domain settings on restart flag.
737             reapplyDomainSettingsOnRestart = false
738
739             // Update the domains settings set.
740             updateDomainsSettingsSet()
741
742             // Reapply the domain settings for each tab.
743             for (i in 0 until webViewPagerAdapter!!.count) {
744                 // Get the WebView tab fragment.
745                 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
746
747                 // Get the fragment view.
748                 val fragmentView = webViewTabFragment.view
749
750                 // Only reload the WebViews if they exist.
751                 if (fragmentView != null) {
752                     // Get the nested scroll WebView from the tab fragment.
753                     val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
754
755                     // Reset the current domain name so the domain settings will be reapplied.
756                     nestedScrollWebView.currentDomainName = ""
757
758                     // Reapply the domain settings if the URL is not null, which happens for empty tabs when returning from settings.
759                     if (nestedScrollWebView.url != null)
760                         applyDomainSettings(nestedScrollWebView, nestedScrollWebView.url, resetTab = false, reloadWebsite = true, loadUrl = false)
761                 }
762             }
763         }
764
765         // Update the bookmarks drawer if returning from the Bookmarks activity.
766         if (restartFromBookmarksActivity) {
767             // Reset the restart from bookmarks activity flag.
768             restartFromBookmarksActivity = false
769
770             // Close the bookmarks drawer.
771             drawerLayout.closeDrawer(GravityCompat.END)
772
773             // Reload the bookmarks drawer.
774             loadBookmarksFolder()
775         }
776
777         // Update the privacy icon.  `true` runs `invalidateOptionsMenu` as the last step.  This can be important if the screen was rotated.
778         updatePrivacyIcons(true)
779     }
780
781     // `onStart()` runs after `onCreate()` or `onRestart()`.  This is used instead of `onResume()` so the commands aren't called every time the screen is partially hidden.
782     public override fun onStart() {
783         // Run the default commands.
784         super.onStart()
785
786         // Resume any WebViews if the pager adapter exists.  If the app is restarting to change the initial app theme it won't have been populated yet.
787         if (webViewPagerAdapter != null) {
788             for (i in 0 until webViewPagerAdapter!!.count) {
789                 // Get the WebView tab fragment.
790                 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
791
792                 // Get the fragment view.
793                 val fragmentView = webViewTabFragment.view
794
795                 // Only resume the WebViews if they exist (they won't when the app is first created).
796                 if (fragmentView != null) {
797                     // Get the nested scroll WebView from the tab fragment.
798                     val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
799
800                     // Resume the nested scroll WebView.
801                     nestedScrollWebView.onResume()
802                 }
803             }
804         }
805
806         // Resume the nested scroll WebView JavaScript timers.  This is a global command that resumes JavaScript timers on all WebViews.
807         if (currentWebView != null)
808             currentWebView!!.resumeTimers()
809
810         // Reapply the proxy settings if the system is using a proxy.  This redisplays the appropriate alert dialog.
811         if (proxyMode != ProxyHelper.NONE)
812             applyProxy(false)
813
814         // Reapply any system UI flags.
815         if (displayingFullScreenVideo || inFullScreenBrowsingMode) {  // The system is displaying a website or a video in full screen mode.
816             /* Hide the system bars.
817              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
818              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
819              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
820              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
821              */
822
823             // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
824             @Suppress("DEPRECATION")
825             rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
826         }
827
828         // Show any pending dialogs.
829         for (i in pendingDialogsArrayList.indices) {
830             // Get the pending dialog from the array list.
831             val (dialogFragment, tag) = pendingDialogsArrayList[i]
832
833             // Show the pending dialog.
834             dialogFragment.show(supportFragmentManager, tag)
835         }
836
837         // Clear the pending dialogs array list.
838         pendingDialogsArrayList.clear()
839     }
840
841     // `onStop()` runs after `onPause()`.  It is used instead of `onPause()` so the commands are not called every time the screen is partially hidden.
842     public override fun onStop() {
843         // Run the default commands.
844         super.onStop()
845
846         // Only pause the WebViews if the pager adapter is not null, which is the case if the app is restarting to change the initial app theme.
847         if (webViewPagerAdapter != null) {
848             // Pause each web view.
849             for (i in 0 until webViewPagerAdapter!!.count) {
850                 // Get the WebView tab fragment.
851                 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
852
853                 // Get the fragment view.
854                 val fragmentView = webViewTabFragment.view
855
856                 // Only pause the WebViews if they exist (they won't when the app is first created).
857                 if (fragmentView != null) {
858                     // Get the nested scroll WebView from the tab fragment.
859                     val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
860
861                     // Pause the nested scroll WebView.
862                     nestedScrollWebView.onPause()
863                 }
864             }
865         }
866
867         // Pause the WebView JavaScript timers.  This is a global command that pauses JavaScript on all WebViews.
868         if (currentWebView != null)
869             currentWebView!!.pauseTimers()
870     }
871
872     public override fun onSaveInstanceState(savedInstanceState: Bundle) {
873         // Run the default commands.
874         super.onSaveInstanceState(savedInstanceState)
875
876         // Only save the instance state if the WebView pager adapter is not null, which will be the case if the app is restarting to change the initial app theme.
877         if (webViewPagerAdapter != null) {
878             // Initialize the saved state array lists.
879             savedStateArrayList = ArrayList<Bundle>()
880             savedNestedScrollWebViewStateArrayList = ArrayList<Bundle>()
881
882             // Get the URLs from each tab.
883             for (i in 0 until webViewPagerAdapter!!.count) {
884                 // Get the WebView tab fragment.
885                 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
886
887                 // Get the fragment view.
888                 val fragmentView = webViewTabFragment.view
889
890                 // Save the fragment state if it is not null.
891                 if (fragmentView != null) {
892                     // Get the nested scroll WebView from the tab fragment.
893                     val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
894
895                     // Create the saved state bundle.
896                     val savedStateBundle = Bundle()
897
898                     // Get the current states.
899                     nestedScrollWebView.saveState(savedStateBundle)
900                     val savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState()
901
902                     // Store the saved states in the array lists.
903                     savedStateArrayList!!.add(savedStateBundle)
904                     savedNestedScrollWebViewStateArrayList!!.add(savedNestedScrollWebViewStateBundle)
905                 }
906             }
907
908             // Get the current tab position.
909             val currentTabPosition = tabLayout.selectedTabPosition
910
911             // Store the saved states in the bundle.
912             savedInstanceState.putBoolean(BOOKMARKS_DRAWER_PINNED, bookmarksDrawerPinned)
913             savedInstanceState.putString(PROXY_MODE, proxyMode)
914             savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList)
915             savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList)
916             savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition)
917         }
918     }
919
920     public override fun onDestroy() {
921         // Unregister the orbot status broadcast receiver if it exists.
922         if (orbotStatusBroadcastReceiver != null) {
923             unregisterReceiver(orbotStatusBroadcastReceiver)
924         }
925
926         // Close the bookmarks cursor if it exists.
927         bookmarksCursor?.close()
928
929         // Close the databases if they exist.
930         bookmarksDatabaseHelper?.close()
931         domainsDatabaseHelper?.close()
932
933         // Run the default commands.
934         super.onDestroy()
935     }
936
937     override fun onCreateOptionsMenu(menu: Menu): Boolean {
938         // Inflate the menu.  This adds items to the app bar if it is present.
939         menuInflater.inflate(R.menu.webview_options_menu, menu)
940
941         // Get handles for the menu items.
942         optionsPrivacyMenuItem = menu.findItem(R.id.javascript)
943         optionsRefreshMenuItem = menu.findItem(R.id.refresh)
944         val optionsBookmarksMenuItem = menu.findItem(R.id.bookmarks)
945         optionsCookiesMenuItem = menu.findItem(R.id.cookies)
946         optionsDomStorageMenuItem = menu.findItem(R.id.dom_storage)
947         optionsSaveFormDataMenuItem = menu.findItem(R.id.save_form_data) // Form data can be removed once the minimum API >= 26.
948         optionsClearDataMenuItem = menu.findItem(R.id.clear_data)
949         optionsClearCookiesMenuItem = menu.findItem(R.id.clear_cookies)
950         optionsClearDomStorageMenuItem = menu.findItem(R.id.clear_dom_storage)
951         optionsClearFormDataMenuItem = menu.findItem(R.id.clear_form_data) // Form data can be removed once the minimum API >= 26.
952         optionsBlocklistsMenuItem = menu.findItem(R.id.blocklists)
953         optionsEasyListMenuItem = menu.findItem(R.id.easylist)
954         optionsEasyPrivacyMenuItem = menu.findItem(R.id.easyprivacy)
955         optionsFanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list)
956         optionsFanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list)
957         optionsUltraListMenuItem = menu.findItem(R.id.ultralist)
958         optionsUltraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy)
959         optionsBlockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests)
960         optionsProxyMenuItem = menu.findItem(R.id.proxy)
961         optionsProxyNoneMenuItem = menu.findItem(R.id.proxy_none)
962         optionsProxyTorMenuItem = menu.findItem(R.id.proxy_tor)
963         optionsProxyI2pMenuItem = menu.findItem(R.id.proxy_i2p)
964         optionsProxyCustomMenuItem = menu.findItem(R.id.proxy_custom)
965         optionsUserAgentMenuItem = menu.findItem(R.id.user_agent)
966         optionsUserAgentPrivacyBrowserMenuItem = menu.findItem(R.id.user_agent_privacy_browser)
967         optionsUserAgentWebViewDefaultMenuItem = menu.findItem(R.id.user_agent_webview_default)
968         optionsUserAgentFirefoxOnAndroidMenuItem = menu.findItem(R.id.user_agent_firefox_on_android)
969         optionsUserAgentChromeOnAndroidMenuItem = menu.findItem(R.id.user_agent_chrome_on_android)
970         optionsUserAgentSafariOnIosMenuItem = menu.findItem(R.id.user_agent_safari_on_ios)
971         optionsUserAgentFirefoxOnLinuxMenuItem = menu.findItem(R.id.user_agent_firefox_on_linux)
972         optionsUserAgentChromiumOnLinuxMenuItem = menu.findItem(R.id.user_agent_chromium_on_linux)
973         optionsUserAgentFirefoxOnWindowsMenuItem = menu.findItem(R.id.user_agent_firefox_on_windows)
974         optionsUserAgentChromeOnWindowsMenuItem = menu.findItem(R.id.user_agent_chrome_on_windows)
975         optionsUserAgentEdgeOnWindowsMenuItem = menu.findItem(R.id.user_agent_edge_on_windows)
976         optionsUserAgentInternetExplorerOnWindowsMenuItem = menu.findItem(R.id.user_agent_internet_explorer_on_windows)
977         optionsUserAgentSafariOnMacosMenuItem = menu.findItem(R.id.user_agent_safari_on_macos)
978         optionsUserAgentCustomMenuItem = menu.findItem(R.id.user_agent_custom)
979         optionsSwipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh)
980         optionsWideViewportMenuItem = menu.findItem(R.id.wide_viewport)
981         optionsDisplayImagesMenuItem = menu.findItem(R.id.display_images)
982         optionsDarkWebViewMenuItem = menu.findItem(R.id.dark_webview)
983         optionsFontSizeMenuItem = menu.findItem(R.id.font_size)
984         optionsAddOrEditDomainMenuItem = menu.findItem(R.id.add_or_edit_domain)
985
986         // Set the initial status of the privacy icons.  `false` does not call `invalidateOptionsMenu` as the last step.
987         updatePrivacyIcons(false)
988
989         // Only display the form data menu items if the API < 26.
990         optionsSaveFormDataMenuItem.isVisible = Build.VERSION.SDK_INT < 26
991         optionsClearFormDataMenuItem.isVisible = Build.VERSION.SDK_INT < 26
992
993         // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
994         optionsClearFormDataMenuItem.isEnabled = Build.VERSION.SDK_INT < 26
995
996         // Only display the dark WebView menu item if the API >= 29.
997         optionsDarkWebViewMenuItem.isVisible = Build.VERSION.SDK_INT >= 29
998
999         // 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.
1000         if (displayAdditionalAppBarIcons) {  // Display the additional icons.
1001             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
1002             optionsBookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
1003             optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
1004         } else { //Do not display the additional icons.
1005             optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
1006             optionsBookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
1007             optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
1008         }
1009
1010         // Replace `Refresh` with `Stop` if a URL is already loading.
1011         if ((currentWebView != null) && (currentWebView!!.progress != 100)) {
1012             // Set the title.
1013             optionsRefreshMenuItem.setTitle(R.string.stop)
1014
1015             // Set the icon if it is displayed in the app bar.  Once the minimum API is >= 26, the blue and black icons can be combined with a tint list.
1016             if (displayAdditionalAppBarIcons)
1017                 optionsRefreshMenuItem.setIcon(R.drawable.close_blue)
1018         }
1019
1020         // Store a handle for the options menu.
1021         optionsMenu = menu
1022
1023         // Done.
1024         return true
1025     }
1026
1027     override fun onPrepareOptionsMenu(menu: Menu): Boolean {
1028         // Initialize the current user agent string and the font size.
1029         var currentUserAgent = getString(R.string.user_agent_privacy_browser)
1030         var fontSize = 100
1031
1032         // 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.
1033         if (currentWebView != null) {
1034             // Set the add or edit domain text.
1035             if (currentWebView!!.domainSettingsApplied)
1036                 optionsAddOrEditDomainMenuItem.setTitle(R.string.edit_domain_settings)
1037             else
1038                 optionsAddOrEditDomainMenuItem.setTitle(R.string.add_domain_settings)
1039
1040             // Get the current user agent from the WebView.
1041             currentUserAgent = currentWebView!!.settings.userAgentString
1042
1043             // Get the current font size from the the WebView.
1044             fontSize = currentWebView!!.settings.textZoom
1045
1046             // Set the status of the menu item checkboxes.
1047             optionsDomStorageMenuItem.isChecked = currentWebView!!.settings.domStorageEnabled
1048             @Suppress("DEPRECATION")
1049             optionsSaveFormDataMenuItem.isChecked = currentWebView!!.settings.saveFormData // Form data can be removed once the minimum API >= 26.
1050             optionsEasyListMenuItem.isChecked = currentWebView!!.easyListEnabled
1051             optionsEasyPrivacyMenuItem.isChecked = currentWebView!!.easyPrivacyEnabled
1052             optionsFanboysAnnoyanceListMenuItem.isChecked = currentWebView!!.fanboysAnnoyanceListEnabled
1053             optionsFanboysSocialBlockingListMenuItem.isChecked = currentWebView!!.fanboysSocialBlockingListEnabled
1054             optionsUltraListMenuItem.isChecked = currentWebView!!.ultraListEnabled
1055             optionsUltraPrivacyMenuItem.isChecked = currentWebView!!.ultraPrivacyEnabled
1056             optionsBlockAllThirdPartyRequestsMenuItem.isChecked = currentWebView!!.blockAllThirdPartyRequests
1057             optionsSwipeToRefreshMenuItem.isChecked = currentWebView!!.swipeToRefresh
1058             optionsWideViewportMenuItem.isChecked = currentWebView!!.settings.useWideViewPort
1059             optionsDisplayImagesMenuItem.isChecked = currentWebView!!.settings.loadsImagesAutomatically
1060
1061             // Initialize the display names for the blocklists with the number of blocked requests.
1062             optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + currentWebView!!.getRequestsCount(BLOCKED_REQUESTS)
1063             optionsEasyListMenuItem.title = currentWebView!!.getRequestsCount(EASYLIST).toString() + " - " + getString(R.string.easylist)
1064             optionsEasyPrivacyMenuItem.title = currentWebView!!.getRequestsCount(EASYPRIVACY).toString() + " - " + getString(R.string.easyprivacy)
1065             optionsFanboysAnnoyanceListMenuItem.title = currentWebView!!.getRequestsCount(FANBOYS_ANNOYANCE_LIST).toString() + " - " + getString(R.string.fanboys_annoyance_list)
1066             optionsFanboysSocialBlockingListMenuItem.title = currentWebView!!.getRequestsCount(FANBOYS_SOCIAL_BLOCKING_LIST).toString() + " - " + getString(R.string.fanboys_social_blocking_list)
1067             optionsUltraListMenuItem.title = currentWebView!!.getRequestsCount(ULTRALIST).toString() + " - " + getString(R.string.ultralist)
1068             optionsUltraPrivacyMenuItem.title = currentWebView!!.getRequestsCount(ULTRAPRIVACY).toString() + " - " + getString(R.string.ultraprivacy)
1069             optionsBlockAllThirdPartyRequestsMenuItem.title = currentWebView!!.getRequestsCount(THIRD_PARTY_REQUESTS).toString() + " - " + getString(R.string.block_all_third_party_requests)
1070
1071             // Enable DOM Storage if JavaScript is enabled.
1072             optionsDomStorageMenuItem.isEnabled = currentWebView!!.settings.javaScriptEnabled
1073
1074             // Get the current theme status.
1075             val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
1076
1077             // Enable dark WebView if night mode is enabled.
1078             optionsDarkWebViewMenuItem.isEnabled = (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES)
1079
1080             // Set the checkbox status for dark WebView if the device is running API >= 29 and algorithmic darkening is supported.
1081             if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING))
1082                 optionsDarkWebViewMenuItem.isChecked = WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView!!.settings)
1083         }
1084
1085         // Set the cookies menu item checked status.
1086         optionsCookiesMenuItem.isChecked = cookieManager.acceptCookie()
1087
1088         // Enable Clear Cookies if there are any.
1089         optionsClearCookiesMenuItem.isEnabled = cookieManager.hasCookies()
1090
1091         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`.
1092         val privateDataDirectoryString = applicationInfo.dataDir
1093
1094         // Get the storage directories.
1095         val localStorageDirectory = File("$privateDataDirectoryString/app_webview/Local Storage/")
1096         val indexedDBDirectory = File("$privateDataDirectoryString/app_webview/IndexedDB")
1097
1098         // Initialize the number of files counters.
1099         var localStorageDirectoryNumberOfFiles = 0
1100         var indexedDBDirectoryNumberOfFiles = 0
1101
1102         // Get a count of the number of files in the Local Storage directory.  The list can be null, in which case a `0` is returned.
1103         if (localStorageDirectory.exists())
1104             localStorageDirectoryNumberOfFiles = (localStorageDirectory.list())?.size ?: 0
1105
1106         // Get a count of the number of files in the IndexedDB directory.  The list can be null, in which case a `0` is returned.
1107         if (indexedDBDirectory.exists())
1108             indexedDBDirectoryNumberOfFiles = (indexedDBDirectory.list())?.size ?: 0
1109
1110         // Enable Clear DOM Storage if there is any.
1111         optionsClearDomStorageMenuItem.isEnabled = localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0
1112
1113         // Enable Clear Form Data is there is any.  This can be removed once the minimum API >= 26.
1114         if (Build.VERSION.SDK_INT < 26) {
1115             // Get the WebView database.
1116             val webViewDatabase = WebViewDatabase.getInstance(this)
1117
1118             // Enable the clear form data menu item if there is anything to clear.
1119             @Suppress("DEPRECATION")
1120             optionsClearFormDataMenuItem.isEnabled = webViewDatabase.hasFormData()
1121         }
1122
1123         // Enable Clear Data if any of the submenu items are enabled.
1124         optionsClearDataMenuItem.isEnabled = (optionsClearCookiesMenuItem.isEnabled || optionsClearDomStorageMenuItem.isEnabled || optionsClearFormDataMenuItem.isEnabled)
1125
1126         // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1127         optionsFanboysSocialBlockingListMenuItem.isEnabled = !optionsFanboysAnnoyanceListMenuItem.isChecked
1128
1129         // Set the proxy title and check the applied proxy.
1130         when (proxyMode) {
1131             ProxyHelper.NONE -> {
1132                 // Set the proxy title.
1133                 optionsProxyMenuItem.title = getString(R.string.proxy) + " - " + getString(R.string.proxy_none)
1134
1135                 // Check the proxy None radio button.
1136                 optionsProxyNoneMenuItem.isChecked = true
1137             }
1138
1139             ProxyHelper.TOR -> {
1140                 // Set the proxy title.
1141                 optionsProxyMenuItem.title = getString(R.string.proxy) + " - " + getString(R.string.proxy_tor)
1142
1143                 // Check the proxy Tor radio button.
1144                 optionsProxyTorMenuItem.isChecked = true
1145             }
1146
1147             ProxyHelper.I2P -> {
1148                 // Set the proxy title.
1149                 optionsProxyMenuItem.title = getString(R.string.proxy) + " - " + getString(R.string.proxy_i2p)
1150
1151                 // Check the proxy I2P radio button.
1152                 optionsProxyI2pMenuItem.isChecked = true
1153             }
1154
1155             ProxyHelper.CUSTOM -> {
1156                 // Set the proxy title.
1157                 optionsProxyMenuItem.title = getString(R.string.proxy) + " - " + getString(R.string.proxy_custom)
1158
1159                 // Check the proxy Custom radio button.
1160                 optionsProxyCustomMenuItem.isChecked = true
1161             }
1162         }
1163
1164         // Select the current user agent menu item.
1165         when (currentUserAgent) {
1166             resources.getStringArray(R.array.user_agent_data)[0] -> {  // Privacy Browser.
1167                 // Update the user agent menu item title.
1168                 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_privacy_browser)
1169
1170                 // Select the Privacy Browser radio box.
1171                 optionsUserAgentPrivacyBrowserMenuItem.isChecked = true
1172             }
1173
1174             webViewDefaultUserAgent -> {  // WebView Default.
1175                 // Update the user agent menu item title.
1176                 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_webview_default)
1177
1178                 // Select the WebView Default radio box.
1179                 optionsUserAgentWebViewDefaultMenuItem.isChecked = true
1180             }
1181
1182             resources.getStringArray(R.array.user_agent_data)[2] -> {  // Firefox on Android.
1183                 // Update the user agent menu item title.
1184                 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_android)
1185
1186                 // Select the Firefox on Android radio box.
1187                 optionsUserAgentFirefoxOnAndroidMenuItem.isChecked = true
1188             }
1189
1190             resources.getStringArray(R.array.user_agent_data)[3] -> {  // Chrome on Android.
1191                 // Update the user agent menu item title.
1192                 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_android)
1193
1194                 // Select the Chrome on Android radio box.
1195                 optionsUserAgentChromeOnAndroidMenuItem.isChecked = true
1196             }
1197
1198             resources.getStringArray(R.array.user_agent_data)[4] -> {  // Safari on iOS.
1199                 // Update the user agent menu item title.
1200                 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_ios)
1201
1202                 // Select the Safari on iOS radio box.
1203                 optionsUserAgentSafariOnIosMenuItem.isChecked = true
1204             }
1205
1206             resources.getStringArray(R.array.user_agent_data)[5] -> {  // Firefox on Linux.
1207                 // Update the user agent menu item title.
1208                 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_linux)
1209
1210                 // Select the Firefox on Linux radio box.
1211                 optionsUserAgentFirefoxOnLinuxMenuItem.isChecked = true
1212             }
1213
1214             resources.getStringArray(R.array.user_agent_data)[6] -> {  // Chromium on Linux.
1215                 // Update the user agent menu item title.
1216                 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chromium_on_linux)
1217
1218                 // Select the Chromium on Linux radio box.
1219                 optionsUserAgentChromiumOnLinuxMenuItem.isChecked = true
1220             }
1221
1222             resources.getStringArray(R.array.user_agent_data)[7] -> {  // Firefox on Windows.
1223                 // Update the user agent menu item title.
1224                 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_windows)
1225
1226                 // Select the Firefox on Windows radio box.
1227                 optionsUserAgentFirefoxOnWindowsMenuItem.isChecked = true
1228             }
1229
1230             resources.getStringArray(R.array.user_agent_data)[8] -> {  // Chrome on Windows.
1231                 // Update the user agent menu item title.
1232                 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_windows)
1233
1234                 // Select the Chrome on Windows radio box.
1235                 optionsUserAgentChromeOnWindowsMenuItem.isChecked = true
1236             }
1237
1238             resources.getStringArray(R.array.user_agent_data)[9] -> {  // Edge on Windows.
1239                 // Update the user agent menu item title.
1240                 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_edge_on_windows)
1241
1242                 // Select the Edge on Windows radio box.
1243                 optionsUserAgentEdgeOnWindowsMenuItem.isChecked = true
1244             }
1245
1246             resources.getStringArray(R.array.user_agent_data)[10] -> {  // Internet Explorer on Windows.
1247                 // Update the user agent menu item title.
1248                 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_internet_explorer_on_windows)
1249
1250                 // Select the Internet on Windows radio box.
1251                 optionsUserAgentInternetExplorerOnWindowsMenuItem.isChecked = true
1252             }
1253
1254             resources.getStringArray(R.array.user_agent_data)[11] -> {  // Safari on macOS.
1255                 // Update the user agent menu item title.
1256                 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_macos)
1257
1258                 // Select the Safari on macOS radio box.
1259                 optionsUserAgentSafariOnMacosMenuItem.isChecked = true
1260             }
1261
1262             else -> {  // Custom user agent.
1263                 // Update the user agent menu item title.
1264                 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_custom)
1265
1266                 // Select the Custom radio box.
1267                 optionsUserAgentCustomMenuItem.isChecked = true
1268             }
1269         }
1270
1271         // Set the font size title.
1272         optionsFontSizeMenuItem.title = getString(R.string.font_size) + " - " + fontSize + "%"
1273
1274         // Run all the other default commands.
1275         super.onPrepareOptionsMenu(menu)
1276
1277         // Display the menu.
1278         return true
1279     }
1280
1281     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
1282         // Run the commands that correlate to the selected menu item.
1283         return when (menuItem.itemId) {
1284             R.id.javascript -> {  // JavaScript.
1285                 // Toggle the JavaScript status.
1286                 currentWebView!!.settings.javaScriptEnabled = !currentWebView!!.settings.javaScriptEnabled
1287
1288                 // Update the privacy icon.
1289                 updatePrivacyIcons(true)
1290
1291                 // Display a snackbar.
1292                 if (currentWebView!!.settings.javaScriptEnabled)  // JavaScrip is enabled.
1293                     Snackbar.make(webViewPager, R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show()
1294                 else if (cookieManager.acceptCookie())  // JavaScript is disabled, but cookies are enabled.
1295                     Snackbar.make(webViewPager, R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show()
1296                 else  // Privacy mode.
1297                     Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show()
1298
1299                 // Reload the current WebView.
1300                 currentWebView!!.reload()
1301
1302                 // Consume the event.
1303                 true
1304             }
1305
1306             R.id.refresh -> {  // Refresh.
1307                 // Run the command that correlates to the current status of the menu item.
1308                 if (menuItem.title == getString(R.string.refresh))  // The refresh button was pushed.
1309                     currentWebView!!.reload()
1310                 else  // The stop button was pushed.
1311                     currentWebView!!.stopLoading()
1312
1313                 // Consume the event.
1314                 true
1315             }
1316
1317             R.id.bookmarks -> {  // Bookmarks.
1318                 // Open the bookmarks drawer.
1319                 drawerLayout.openDrawer(GravityCompat.END)
1320
1321                 // Consume the event.
1322                 true
1323             }
1324
1325             R.id.cookies -> {  // Cookies.
1326                 // Toggle the cookie status.
1327                 cookieManager.setAcceptCookie(!cookieManager.acceptCookie())
1328
1329                 // Store the cookie status.
1330                 currentWebView!!.acceptCookies = cookieManager.acceptCookie()
1331
1332                 // Update the menu checkbox.
1333                 menuItem.isChecked = cookieManager.acceptCookie()
1334
1335                 // Update the privacy icon.
1336                 updatePrivacyIcons(true)
1337
1338                 // Display a snackbar.
1339                 if (cookieManager.acceptCookie())  // Cookies are enabled.
1340                     Snackbar.make(webViewPager, R.string.cookies_enabled, Snackbar.LENGTH_SHORT).show()
1341                 else if (currentWebView!!.settings.javaScriptEnabled)  // JavaScript is still enabled.
1342                     Snackbar.make(webViewPager, R.string.cookies_disabled, Snackbar.LENGTH_SHORT).show()
1343                 else  // Privacy mode.
1344                     Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show()
1345
1346                 // Reload the current WebView.
1347                 currentWebView!!.reload()
1348
1349                 // Consume the event.
1350                 true
1351             }
1352
1353             R.id.dom_storage -> {  // DOM storage.
1354                 // Toggle the DOM storage status.
1355                 currentWebView!!.settings.domStorageEnabled = !currentWebView!!.settings.domStorageEnabled
1356
1357                 // Update the menu checkbox.
1358                 menuItem.isChecked = currentWebView!!.settings.domStorageEnabled
1359
1360                 // Update the privacy icon.
1361                 updatePrivacyIcons(true)
1362
1363                 // Display a snackbar.
1364                 if (currentWebView!!.settings.domStorageEnabled)
1365                     Snackbar.make(webViewPager, R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show()
1366                 else
1367                     Snackbar.make(webViewPager, R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show()
1368
1369                 // Reload the current WebView.
1370                 currentWebView!!.reload()
1371
1372                 // Consume the event.
1373                 true
1374             }
1375
1376             R.id.save_form_data -> {  // Form data.  This can be removed once the minimum API >= 26.
1377                 // Switch the status of saveFormDataEnabled.
1378                 @Suppress("DEPRECATION")
1379                 currentWebView!!.settings.saveFormData = !currentWebView!!.settings.saveFormData
1380
1381                 // Update the menu checkbox.
1382                 @Suppress("DEPRECATION")
1383                 menuItem.isChecked = currentWebView!!.settings.saveFormData
1384
1385                 // Display a snackbar.
1386                 @Suppress("DEPRECATION")
1387                 if (currentWebView!!.settings.saveFormData)
1388                     Snackbar.make(webViewPager, R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show()
1389                 else
1390                     Snackbar.make(webViewPager, R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show()
1391
1392                 // Update the privacy icon.
1393                 updatePrivacyIcons(true)
1394
1395                 // Reload the current WebView.
1396                 currentWebView!!.reload()
1397
1398                 // Consume the event.
1399                 true
1400             }
1401
1402             R.id.clear_cookies -> {  // Clear cookies.
1403                 // Create a snackbar.
1404                 Snackbar.make(webViewPager, R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1405                     .setAction(R.string.undo) {}  // Everything will be handled by `onDismissed()` below.
1406                     .addCallback(object : Snackbar.Callback() {
1407                         override fun onDismissed(snackbar: Snackbar, event: Int) {
1408                             if (event != DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1409                                 // Delete the cookies.
1410                                 cookieManager.removeAllCookies(null)
1411                             }
1412                         }
1413                     })
1414                     .show()
1415
1416                 // Consume the event.
1417                 true
1418             }
1419
1420             R.id.clear_dom_storage -> {  // Clear DOM storage.
1421                 // Create a snackbar.
1422                 Snackbar.make(webViewPager, R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1423                     .setAction(R.string.undo) {}  // Everything will be handled by `onDismissed()` below.
1424                     .addCallback(object : Snackbar.Callback() {
1425                         override fun onDismissed(snackbar: Snackbar, event: Int) {
1426                             if (event != DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1427                                 // Get a handle for the web storage.
1428                                 val webStorage = WebStorage.getInstance()
1429
1430                                 // Delete the DOM Storage.
1431                                 webStorage.deleteAllData()
1432
1433                                 // Initialize a handler to manually delete the DOM storage files and directories.
1434                                 val deleteDomStorageHandler = Handler(Looper.getMainLooper())
1435
1436                                 // Setup a runnable to manually delete the DOM storage files and directories.
1437                                 val deleteDomStorageRunnable = Runnable {
1438                                     try {
1439                                         // Get a handle for the runtime.
1440                                         val runtime = Runtime.getRuntime()
1441
1442                                         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1443                                         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1444                                         val privateDataDirectoryString = applicationInfo.dataDir
1445
1446                                         // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1447                                         val deleteLocalStorageProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Local Storage/"))
1448
1449                                         // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1450                                         val deleteIndexProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/IndexedDB")
1451                                         val deleteQuotaManagerProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager")
1452                                         val deleteQuotaManagerJournalProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager-journal")
1453                                         val deleteDatabasesProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/databases")
1454
1455                                         // Wait for the processes to finish.
1456                                         deleteLocalStorageProcess.waitFor()
1457                                         deleteIndexProcess.waitFor()
1458                                         deleteQuotaManagerProcess.waitFor()
1459                                         deleteQuotaManagerJournalProcess.waitFor()
1460                                         deleteDatabasesProcess.waitFor()
1461                                     } catch (exception: Exception) {
1462                                         // Do nothing if an error is thrown.
1463                                     }
1464                                 }
1465
1466                                 // Manually delete the DOM storage files after 200 milliseconds.
1467                                 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200)
1468                             }
1469                         }
1470                     })
1471                     .show()
1472
1473                 // Consume the event.
1474                 true
1475             }
1476
1477             R.id.clear_form_data -> {  // Clear form data.  This can be remove once the minimum API >= 26.
1478                 // Create a snackbar.
1479                 Snackbar.make(webViewPager, R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1480                     .setAction(R.string.undo) {}  // Everything will be handled by `onDismissed()` below.
1481                     .addCallback(object : Snackbar.Callback() {
1482                         override fun onDismissed(snackbar: Snackbar, event: Int) {
1483                             if (event != DISMISS_EVENT_ACTION) {  // The snackbar was dismissed without the undo button being pushed.
1484                                 // Get a handle for the webView database.
1485                                 val webViewDatabase = WebViewDatabase.getInstance(applicationContext)
1486
1487                                 // Delete the form data.
1488                                 @Suppress("DEPRECATION")
1489                                 webViewDatabase.clearFormData()
1490                             }
1491                         }
1492                     })
1493                     .show()
1494
1495                 // Consume the event.
1496                 true
1497             }
1498
1499             R.id.easylist -> {  // EasyList.
1500                 // Toggle the EasyList status.
1501                 currentWebView!!.easyListEnabled = !currentWebView!!.easyListEnabled
1502
1503                 // Update the menu checkbox.
1504                 menuItem.isChecked = currentWebView!!.easyListEnabled
1505
1506                 // Reload the current WebView.
1507                 currentWebView!!.reload()
1508
1509                 // Consume the event.
1510                 true
1511             }
1512
1513             R.id.easyprivacy -> {  // EasyPrivacy.
1514                 // Toggle the EasyPrivacy status.
1515                 currentWebView!!.easyPrivacyEnabled = !currentWebView!!.easyPrivacyEnabled
1516
1517                 // Update the menu checkbox.
1518                 menuItem.isChecked = currentWebView!!.easyPrivacyEnabled
1519
1520                 // Reload the current WebView.
1521                 currentWebView!!.reload()
1522
1523                 // Consume the event.
1524                 true
1525             }
1526
1527             R.id.fanboys_annoyance_list -> {  // Fanboy's Annoyance List.
1528                 // Toggle Fanboy's Annoyance List status.
1529                 currentWebView!!.fanboysAnnoyanceListEnabled = !currentWebView!!.fanboysAnnoyanceListEnabled
1530
1531                 // Update the menu checkbox.
1532                 menuItem.isChecked = currentWebView!!.fanboysAnnoyanceListEnabled
1533
1534                 // Update the status of Fanboy's Social Blocking List.
1535                 optionsFanboysSocialBlockingListMenuItem.isEnabled = !currentWebView!!.fanboysAnnoyanceListEnabled
1536
1537                 // Reload the current WebView.
1538                 currentWebView!!.reload()
1539
1540                 // Consume the event.
1541                 true
1542             }
1543
1544             R.id.fanboys_social_blocking_list -> {  // Fanboy's Social Blocking List.
1545                 // Toggle Fanboy's Social Blocking List status.
1546                 currentWebView!!.fanboysSocialBlockingListEnabled = !currentWebView!!.fanboysSocialBlockingListEnabled
1547
1548                 // Update the menu checkbox.
1549                 menuItem.isChecked = currentWebView!!.fanboysSocialBlockingListEnabled
1550
1551                 // Reload the current WebView.
1552                 currentWebView!!.reload()
1553
1554                 // Consume the event.
1555                 true
1556             }
1557
1558             R.id.ultralist -> {  // UltraList.
1559                 // Toggle the UltraList status.
1560                 currentWebView!!.ultraListEnabled = !currentWebView!!.ultraListEnabled
1561
1562                 // Update the menu checkbox.
1563                 menuItem.isChecked = currentWebView!!.ultraListEnabled
1564
1565                 // Reload the current WebView.
1566                 currentWebView!!.reload()
1567
1568                 // Consume the event.
1569                 true
1570             }
1571
1572             R.id.ultraprivacy -> {  // UltraPrivacy.
1573                 // Toggle the UltraPrivacy status.
1574                 currentWebView!!.ultraPrivacyEnabled = !currentWebView!!.ultraPrivacyEnabled
1575
1576                 // Update the menu checkbox.
1577                 menuItem.isChecked = currentWebView!!.ultraPrivacyEnabled
1578
1579                 // Reload the current WebView.
1580                 currentWebView!!.reload()
1581
1582                 // Consume the event.
1583                 true
1584             }
1585
1586             R.id.block_all_third_party_requests -> {  // Block all third-party requests.
1587                 //Toggle the third-party requests blocker status.
1588                 currentWebView!!.blockAllThirdPartyRequests = !currentWebView!!.blockAllThirdPartyRequests
1589
1590                 // Update the menu checkbox.
1591                 menuItem.isChecked = currentWebView!!.blockAllThirdPartyRequests
1592
1593                 // Reload the current WebView.
1594                 currentWebView!!.reload()
1595
1596                 // Consume the event.
1597                 true
1598             }
1599
1600             R.id.proxy_none -> {  // Proxy - None.
1601                 // Update the proxy mode.
1602                 proxyMode = ProxyHelper.NONE
1603
1604                 // Apply the proxy mode.
1605                 applyProxy(true)
1606
1607                 // Consume the event.
1608                 true
1609             }
1610
1611             R.id.proxy_tor -> {  // Proxy - Tor.
1612                 // Update the proxy mode.
1613                 proxyMode = ProxyHelper.TOR
1614
1615                 // Apply the proxy mode.
1616                 applyProxy(true)
1617
1618                 // Consume the event.
1619                 true
1620             }
1621
1622             R.id.proxy_i2p -> {  // Proxy - I2P.
1623                 // Update the proxy mode.
1624                 proxyMode = ProxyHelper.I2P
1625
1626                 // Apply the proxy mode.
1627                 applyProxy(true)
1628
1629                 // Consume the event.
1630                 true
1631             }
1632
1633             R.id.proxy_custom -> {  // Proxy - Custom.
1634                 // Update the proxy mode.
1635                 proxyMode = ProxyHelper.CUSTOM
1636
1637                 // Apply the proxy mode.
1638                 applyProxy(true)
1639
1640                 // Consume the event.
1641                 true
1642             }
1643
1644             R.id.user_agent_privacy_browser -> {  // User Agent - Privacy Browser.
1645                 // Update the user agent.
1646                 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[0]
1647
1648                 // Reload the current WebView.
1649                 currentWebView!!.reload()
1650
1651                 // Consume the event.
1652                 true
1653             }
1654
1655             R.id.user_agent_webview_default -> {  // User Agent - WebView Default.
1656                 // Update the user agent.
1657                 currentWebView!!.settings.userAgentString = ""
1658
1659                 // Reload the current WebView.
1660                 currentWebView!!.reload()
1661
1662                 // Consume the event.
1663                 true
1664             }
1665
1666             R.id.user_agent_firefox_on_android -> {  // User Agent - Firefox on Android.
1667                 // Update the user agent.
1668                 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[2]
1669
1670                 // Reload the current WebView.
1671                 currentWebView!!.reload()
1672
1673                 // Consume the event.
1674                 true
1675             }
1676
1677             R.id.user_agent_chrome_on_android -> {  // User Agent - Chrome on Android.
1678                 // Update the user agent.
1679                 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[3]
1680
1681                 // Reload the current WebView.
1682                 currentWebView!!.reload()
1683
1684                 // Consume the event.
1685                 true
1686             }
1687
1688             R.id.user_agent_safari_on_ios -> {  // User Agent - Safari on iOS.
1689                 // Update the user agent.
1690                 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[4]
1691
1692                 // Reload the current WebView.
1693                 currentWebView!!.reload()
1694
1695                 // Consume the event.
1696                 true
1697             }
1698
1699             R.id.user_agent_firefox_on_linux -> {  // User Agent - Firefox on Linux.
1700                 // Update the user agent.
1701                 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[5]
1702
1703                 // Reload the current WebView.
1704                 currentWebView!!.reload()
1705
1706                 // Consume the event.
1707                 true
1708             }
1709
1710             R.id.user_agent_chromium_on_linux -> {  // User Agent - Chromium on Linux.
1711                 // Update the user agent.
1712                 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[6]
1713
1714                 // Reload the current WebView.
1715                 currentWebView!!.reload()
1716
1717                 // Consume the event.
1718                 true
1719             }
1720
1721             R.id.user_agent_firefox_on_windows -> {  // User Agent - Firefox on Windows.
1722                 // Update the user agent.
1723                 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[7]
1724
1725                 // Reload the current WebView.
1726                 currentWebView!!.reload()
1727
1728                 // Consume the event.
1729                 true
1730             }
1731
1732             R.id.user_agent_chrome_on_windows -> {  // User Agent - Chrome on Windows.
1733                 // Update the user agent.
1734                 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[8]
1735
1736                 // Reload the current WebView.
1737                 currentWebView!!.reload()
1738
1739                 // Consume the event.
1740                 true
1741             }
1742
1743             R.id.user_agent_edge_on_windows -> {  // User Agent - Edge on Windows.
1744                 // Update the user agent.
1745                 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[9]
1746
1747                 // Reload the current WebView.
1748                 currentWebView!!.reload()
1749
1750                 // Consume the event.
1751                 true
1752             }
1753
1754             R.id.user_agent_internet_explorer_on_windows -> {  // User Agent - Internet Explorer on Windows.
1755                 // Update the user agent.
1756                 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[10]
1757
1758                 // Reload the current WebView.
1759                 currentWebView!!.reload()
1760
1761                 // Consume the event.
1762                 true
1763             }
1764
1765             R.id.user_agent_safari_on_macos -> {  // User Agent - Safari on macOS.
1766                 // Update the user agent.
1767                 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[11]
1768
1769                 // Reload the current WebView.
1770                 currentWebView!!.reload()
1771
1772                 // Consume the event.
1773                 true
1774             }
1775
1776             R.id.user_agent_custom -> {  // User Agent - Custom.
1777                 // Update the user agent.
1778                 currentWebView!!.settings.userAgentString = sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value))
1779
1780                 // Reload the current WebView.
1781                 currentWebView!!.reload()
1782
1783                 // Consume the event.
1784                 true
1785             }
1786
1787             R.id.font_size -> {  // Font size.
1788                 // Instantiate the font size dialog.
1789                 val fontSizeDialogFragment: DialogFragment = FontSizeDialog.displayDialog(currentWebView!!.settings.textZoom)
1790
1791                 // Show the font size dialog.
1792                 fontSizeDialogFragment.show(supportFragmentManager, getString(R.string.font_size))
1793
1794                 // Consume the event.
1795                 true
1796             }
1797
1798             R.id.swipe_to_refresh -> {  // Swipe to refresh.
1799                 // Toggle the stored status of swipe to refresh.
1800                 currentWebView!!.swipeToRefresh = !currentWebView!!.swipeToRefresh
1801
1802                 // Update the swipe refresh layout.
1803                 if (currentWebView!!.swipeToRefresh)  // Swipe to refresh is enabled.  Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
1804                     swipeRefreshLayout.isEnabled = currentWebView!!.scrollY == 0
1805                 else  // Swipe to refresh is disabled.
1806                     swipeRefreshLayout.isEnabled = false
1807
1808                 // Consume the event.
1809                 true
1810             }
1811
1812             R.id.wide_viewport -> {  // Wide viewport.
1813                 // Toggle the viewport.
1814                 currentWebView!!.settings.useWideViewPort = !currentWebView!!.settings.useWideViewPort
1815
1816                 // Consume the event.
1817                 true
1818             }
1819
1820             R.id.display_images -> {  // Display images.
1821                 // Toggle the displaying of images.
1822                 if (currentWebView!!.settings.loadsImagesAutomatically) {  // Images are currently loaded automatically.
1823                     // Disable loading of images.
1824                     currentWebView!!.settings.loadsImagesAutomatically = false
1825
1826                     // Reload the website to remove existing images.
1827                     currentWebView!!.reload()
1828                 } else {  // Images are not currently loaded automatically.
1829                     // Enable loading of images.  Missing images will be loaded without the need for a reload.
1830                     currentWebView!!.settings.loadsImagesAutomatically = true
1831                 }
1832
1833                 // Consume the event.
1834                 true
1835             }
1836
1837             R.id.dark_webview -> {  // Dark WebView.
1838                 // Toggle dark WebView if supported.
1839                 if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING))
1840                     WebSettingsCompat.setAlgorithmicDarkeningAllowed(currentWebView!!.settings, !WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView!!.settings)
1841                 )
1842
1843                 // Consume the event.
1844                 true
1845             }
1846
1847             R.id.find_on_page -> {  // Find on page.
1848                 // Set the minimum height of the find on page linear layout to match the toolbar.
1849                 findOnPageLinearLayout.minimumHeight = toolbar.height
1850
1851                 // Hide the toolbar.
1852                 toolbar.visibility = View.GONE
1853
1854                 // Show the find on page linear layout.
1855                 findOnPageLinearLayout.visibility = View.VISIBLE
1856
1857                 // Display the keyboard.  The app must wait 200 ms before running the command to work around a bug in Android.
1858                 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1859                 findOnPageEditText.postDelayed({
1860                     // Set the focus on the find on page edit text.
1861                     findOnPageEditText.requestFocus()
1862
1863                     // Get a handle for the input method manager.
1864                     val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
1865
1866                     // Display the keyboard.  `0` sets no input flags.
1867                     inputMethodManager.showSoftInput(findOnPageEditText, 0)
1868                 }, 200)
1869
1870                 // Consume the event.
1871                 true
1872             }
1873
1874             R.id.print -> {  // Print.
1875                 // Get a print manager instance.
1876                 val printManager = (getSystemService(PRINT_SERVICE) as PrintManager)
1877
1878                 // Create a print document adapter from the current WebView.
1879                 val printDocumentAdapter = currentWebView!!.createPrintDocumentAdapter(getString(R.string.print))
1880
1881                 // Print the document.
1882                 printManager.print(getString(R.string.privacy_browser_webpage), printDocumentAdapter, null)
1883
1884                 // Consume the event.
1885                 true
1886             }
1887
1888             R.id.save_url -> {  // Save URL.
1889                 // Check the download preference.
1890                 if (downloadWithExternalApp)  // Download with an external app.
1891                     downloadUrlWithExternalApp(currentWebView!!.currentUrl)
1892                 else  // Handle the download inside of Privacy Browser.  The dialog will be displayed once the file size and the content disposition have been acquired.
1893                     PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, currentWebView!!.currentUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
1894
1895                 // Consume the event.
1896                 true
1897             }
1898
1899             R.id.save_archive -> {
1900                 // Open the file picker with a default file name built from the current domain name.
1901                 saveWebpageArchiveActivityResultLauncher.launch(currentWebView!!.currentDomainName + ".mht")
1902
1903                 // Consume the event.
1904                 true
1905             }
1906
1907             R.id.save_image -> {  // Save image.
1908                 // Open the file picker with a default file name built from the current domain name.
1909                 saveWebpageImageActivityResultLauncher.launch(currentWebView!!.currentDomainName + ".png")
1910
1911                 // Consume the event.
1912                 true
1913             }
1914
1915             R.id.add_to_homescreen -> {  // Add to homescreen.
1916                 // Instantiate the create home screen shortcut dialog.
1917                 val createHomeScreenShortcutDialogFragment: DialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView!!.title!!, currentWebView!!.url!!, currentWebView!!.getFavoriteIcon())
1918
1919                 // Show the create home screen shortcut dialog.
1920                 createHomeScreenShortcutDialogFragment.show(supportFragmentManager, getString(R.string.create_shortcut))
1921
1922                 // Consume the event.
1923                 true
1924             }
1925
1926             R.id.view_source -> {  // View source.
1927                 // Create an intent to launch the view source activity.
1928                 val viewSourceIntent = Intent(this, ViewSourceActivity::class.java)
1929
1930                 // Add the variables to the intent.
1931                 viewSourceIntent.putExtra(CURRENT_URL, currentWebView!!.url)
1932                 viewSourceIntent.putExtra(USER_AGENT, currentWebView!!.settings.userAgentString)
1933
1934                 // Make it so.
1935                 startActivity(viewSourceIntent)
1936
1937                 // Consume the event.
1938                 true
1939             }
1940
1941             R.id.share_message -> {  // Share a message.
1942                 // Prepare the share string.
1943                 val shareString = currentWebView!!.title + " â€“ " + currentWebView!!.url
1944
1945                 // Create the share intent.
1946                 val shareMessageIntent = Intent(Intent.ACTION_SEND)
1947
1948                 // Add the share string to the intent.
1949                 shareMessageIntent.putExtra(Intent.EXTRA_TEXT, shareString)
1950
1951                 // Set the MIME type.
1952                 shareMessageIntent.type = "text/plain"
1953
1954                 // Set the intent to open in a new task.
1955                 shareMessageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
1956
1957                 // Make it so.
1958                 startActivity(Intent.createChooser(shareMessageIntent, getString(R.string.share_message)))
1959
1960                 // Consume the event.
1961                 true
1962             }
1963
1964             R.id.share_url -> {  // Share URL.
1965                 // Create the share intent.
1966                 val shareUrlIntent = Intent(Intent.ACTION_SEND)
1967
1968                 // Add the URL to the intent.
1969                 shareUrlIntent.putExtra(Intent.EXTRA_TEXT, currentWebView!!.url)
1970
1971                 // Add the title to the intent.
1972                 shareUrlIntent.putExtra(Intent.EXTRA_SUBJECT, currentWebView!!.title)
1973
1974                 // Set the MIME type.
1975                 shareUrlIntent.type = "text/plain"
1976
1977                 // Set the intent to open in a new task.
1978                 shareUrlIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
1979
1980                 //Make it so.
1981                 startActivity(Intent.createChooser(shareUrlIntent, getString(R.string.share_url)))
1982
1983                 // Consume the event.
1984                 true
1985             }
1986
1987             R.id.open_with_app -> {  // Open with app.
1988                 // Open the URL with an outside app.
1989                 openWithApp(currentWebView!!.url!!)
1990
1991                 // Consume the event.
1992                 true
1993             }
1994
1995             R.id.open_with_browser -> {  // Open with browser.
1996                 // Open the URL with an outside browser.
1997                 openWithBrowser(currentWebView!!.url!!)
1998
1999                 // Consume the event.
2000                 true
2001             }
2002
2003             R.id.add_or_edit_domain -> {  // Add or edit domain.
2004                 // Reapply the domain settings on returning to `MainWebViewActivity`.
2005                 reapplyDomainSettingsOnRestart = true
2006
2007                 // Check if domain settings are currently applied.
2008                 if (currentWebView!!.domainSettingsApplied) {  // Edit the current domain settings.
2009                     // Create an intent to launch the domains activity.
2010                     val domainsIntent = Intent(this, DomainsActivity::class.java)
2011
2012                     // Add the extra information to the intent.
2013                     domainsIntent.putExtra(LOAD_DOMAIN, currentWebView!!.domainSettingsDatabaseId)
2014                     domainsIntent.putExtra(CLOSE_ON_BACK, true)
2015                     domainsIntent.putExtra(CURRENT_URL, currentWebView!!.url)
2016                     domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
2017
2018                     // Get the current certificate.
2019                     val sslCertificate = currentWebView!!.certificate
2020
2021                     // Check to see if the SSL certificate is populated.
2022                     if (sslCertificate != null) {
2023                         // Extract the certificate to strings.
2024                         val issuedToCName = sslCertificate.issuedTo.cName
2025                         val issuedToOName = sslCertificate.issuedTo.oName
2026                         val issuedToUName = sslCertificate.issuedTo.uName
2027                         val issuedByCName = sslCertificate.issuedBy.cName
2028                         val issuedByOName = sslCertificate.issuedBy.oName
2029                         val issuedByUName = sslCertificate.issuedBy.uName
2030                         val startDateLong = sslCertificate.validNotBeforeDate.time
2031                         val endDateLong = sslCertificate.validNotAfterDate.time
2032
2033                         // Add the certificate to the intent.
2034                         domainsIntent.putExtra(SSL_ISSUED_TO_CNAME, issuedToCName)
2035                         domainsIntent.putExtra(SSL_ISSUED_TO_ONAME, issuedToOName)
2036                         domainsIntent.putExtra(SSL_ISSUED_TO_UNAME, issuedToUName)
2037                         domainsIntent.putExtra(SSL_ISSUED_BY_CNAME, issuedByCName)
2038                         domainsIntent.putExtra(SSL_ISSUED_BY_ONAME, issuedByOName)
2039                         domainsIntent.putExtra(SSL_ISSUED_BY_UNAME, issuedByUName)
2040                         domainsIntent.putExtra(SSL_START_DATE, startDateLong)
2041                         domainsIntent.putExtra(SSL_END_DATE, endDateLong)
2042                     }
2043
2044                     // Make it so.
2045                     startActivity(domainsIntent)
2046                 } else {  // Add a new domain.
2047                     // Get the current URI.
2048                     val currentUri = Uri.parse(currentWebView!!.url)
2049
2050                     // Get the current domain from the URI.  Use an empty string if it is null.
2051                     val currentDomain = currentUri.host?: ""
2052
2053                     // Create the domain and store the database ID.
2054                     val newDomainDatabaseId = domainsDatabaseHelper!!.addDomain(currentDomain)
2055
2056                     // Create an intent to launch the domains activity.
2057                     val domainsIntent = Intent(this, DomainsActivity::class.java)
2058
2059                     // Add the extra information to the intent.
2060                     domainsIntent.putExtra(LOAD_DOMAIN, newDomainDatabaseId)
2061                     domainsIntent.putExtra(CLOSE_ON_BACK, true)
2062                     domainsIntent.putExtra(CURRENT_URL, currentWebView!!.url)
2063                     domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
2064
2065                     // Get the current certificate.
2066                     val sslCertificate = currentWebView!!.certificate
2067
2068                     // Check to see if the SSL certificate is populated.
2069                     if (sslCertificate != null) {
2070                         // Extract the certificate to strings.
2071                         val issuedToCName = sslCertificate.issuedTo.cName
2072                         val issuedToOName = sslCertificate.issuedTo.oName
2073                         val issuedToUName = sslCertificate.issuedTo.uName
2074                         val issuedByCName = sslCertificate.issuedBy.cName
2075                         val issuedByOName = sslCertificate.issuedBy.oName
2076                         val issuedByUName = sslCertificate.issuedBy.uName
2077                         val startDateLong = sslCertificate.validNotBeforeDate.time
2078                         val endDateLong = sslCertificate.validNotAfterDate.time
2079
2080                         // Add the certificate to the intent.
2081                         domainsIntent.putExtra(SSL_ISSUED_TO_CNAME, issuedToCName)
2082                         domainsIntent.putExtra(SSL_ISSUED_TO_ONAME, issuedToOName)
2083                         domainsIntent.putExtra(SSL_ISSUED_TO_UNAME, issuedToUName)
2084                         domainsIntent.putExtra(SSL_ISSUED_BY_CNAME, issuedByCName)
2085                         domainsIntent.putExtra(SSL_ISSUED_BY_ONAME, issuedByOName)
2086                         domainsIntent.putExtra(SSL_ISSUED_BY_UNAME, issuedByUName)
2087                         domainsIntent.putExtra(SSL_START_DATE, startDateLong)
2088                         domainsIntent.putExtra(SSL_END_DATE, endDateLong)
2089                     }
2090
2091                     // Make it so.
2092                     startActivity(domainsIntent)
2093                 }
2094
2095                 // Consume the event.
2096                 true
2097             }
2098
2099             else -> {  // There is no match with the options menu.  Pass the event up to the parent method.
2100                 // Don't consume the event.
2101                 super.onOptionsItemSelected(menuItem)
2102             }
2103         }
2104     }
2105
2106     override fun onNavigationItemSelected(menuItem: MenuItem): Boolean {
2107         // Run the commands that correspond to the selected menu item.
2108         when (menuItem.itemId) {
2109             R.id.clear_and_exit -> {  // Clear and exit.
2110                 // Clear and exit Privacy Browser.
2111                 clearAndExit()
2112             }
2113
2114             R.id.home -> {  // Home.
2115                 // Load the homepage.
2116                 loadUrl(currentWebView!!, sharedPreferences.getString(getString(R.string.homepage_key), getString(R.string.homepage_default_value))!!)
2117             }
2118
2119             R.id.back -> {  // Back.
2120                 // Check if the WebView can go back.
2121                 if (currentWebView!!.canGoBack()) {
2122                     // Get the current web back forward list.
2123                     val webBackForwardList = currentWebView!!.copyBackForwardList()
2124
2125                     // Get the previous entry URL.
2126                     val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url
2127
2128                     // Apply the domain settings.
2129                     applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
2130
2131                     // Load the previous website in the history.
2132                     currentWebView!!.goBack()
2133                 }
2134             }
2135
2136             R.id.forward -> {  // Forward.
2137                 // Check if the WebView can go forward.
2138                 if (currentWebView!!.canGoForward()) {
2139                     // Get the current web back forward list.
2140                     val webBackForwardList = currentWebView!!.copyBackForwardList()
2141
2142                     // Get the next entry URL.
2143                     val nextUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex + 1).url
2144
2145                     // Apply the domain settings.
2146                     applyDomainSettings(currentWebView!!, nextUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
2147
2148                     // Load the next website in the history.
2149                     currentWebView!!.goForward()
2150                 }
2151             }
2152
2153             R.id.history -> {  // History.
2154                 // Instantiate the URL history dialog.
2155                 val urlHistoryDialogFragment: DialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView!!.webViewFragmentId)
2156
2157                 // Show the URL history dialog.
2158                 urlHistoryDialogFragment.show(supportFragmentManager, getString(R.string.history))
2159             }
2160
2161             R.id.open -> {  // Open.
2162                 // Instantiate the open file dialog.
2163                 val openDialogFragment: DialogFragment = OpenDialog()
2164
2165                 // Show the open file dialog.
2166                 openDialogFragment.show(supportFragmentManager, getString(R.string.open))
2167             }
2168
2169             R.id.requests -> {  // Requests.
2170                 // Populate the resource requests.
2171                 RequestsActivity.resourceRequests = currentWebView!!.getResourceRequests()
2172
2173                 // Create an intent to launch the Requests activity.
2174                 val requestsIntent = Intent(this, RequestsActivity::class.java)
2175
2176                 // Add the block third-party requests status to the intent.
2177                 requestsIntent.putExtra(BLOCK_ALL_THIRD_PARTY_REQUESTS, currentWebView!!.blockAllThirdPartyRequests)
2178
2179                 // Make it so.
2180                 startActivity(requestsIntent)
2181             }
2182
2183             R.id.downloads -> {  // Downloads.
2184                 // Try the default system download manager.
2185                 try {
2186                     // Launch the default system Download Manager.
2187                     val defaultDownloadManagerIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
2188
2189                     // Launch as a new task so that the download manager and Privacy Browser show as separate windows in the recent tasks list.
2190                     defaultDownloadManagerIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2191
2192                     // Make it so.
2193                     startActivity(defaultDownloadManagerIntent)
2194                 } catch (defaultDownloadManagerException: Exception) {  // The system download manager is not available.
2195                     // Try a generic file manager.
2196                     try {
2197                         // Create a generic file manager intent.
2198                         val genericFileManagerIntent = Intent(Intent.ACTION_VIEW)
2199
2200                         // Open the download directory.
2201                         genericFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), DocumentsContract.Document.MIME_TYPE_DIR)
2202
2203                         // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2204                         genericFileManagerIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2205
2206                         // Make it so.
2207                         startActivity(genericFileManagerIntent)
2208                     } catch (genericFileManagerException: Exception) {  // A generic file manager is not available.
2209                         // Try an alternate file manager.
2210                         try {
2211                             // Create an alternate file manager intent.
2212                             val alternateFileManagerIntent = Intent(Intent.ACTION_VIEW)
2213
2214                             // Open the download directory.
2215                             alternateFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), "resource/folder")
2216
2217                             // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2218                             alternateFileManagerIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2219
2220                             // Open the alternate file manager.
2221                             startActivity(alternateFileManagerIntent)
2222                         } catch (alternateFileManagerException: Exception) {
2223                             // Display a snackbar.
2224                             Snackbar.make(currentWebView!!, R.string.no_file_manager_detected, Snackbar.LENGTH_INDEFINITE).show()
2225                         }
2226                     }
2227                 }
2228             }
2229
2230             R.id.domains -> {  // Domains.
2231                 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2232                 reapplyDomainSettingsOnRestart = true
2233
2234                 // Create a domains activity intent.
2235                 val domainsIntent = Intent(this, DomainsActivity::class.java)
2236
2237                 // Add the extra information to the intent.
2238                 domainsIntent.putExtra(CURRENT_URL, currentWebView!!.url)
2239                 domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
2240
2241                 // Get the current certificate.
2242                 val sslCertificate = currentWebView!!.certificate
2243
2244                 // Check to see if the SSL certificate is populated.
2245                 if (sslCertificate != null) {
2246                     // Extract the certificate to strings.
2247                     val issuedToCName = sslCertificate.issuedTo.cName
2248                     val issuedToOName = sslCertificate.issuedTo.oName
2249                     val issuedToUName = sslCertificate.issuedTo.uName
2250                     val issuedByCName = sslCertificate.issuedBy.cName
2251                     val issuedByOName = sslCertificate.issuedBy.oName
2252                     val issuedByUName = sslCertificate.issuedBy.uName
2253                     val startDateLong = sslCertificate.validNotBeforeDate.time
2254                     val endDateLong = sslCertificate.validNotAfterDate.time
2255
2256                     // Add the certificate to the intent.
2257                     domainsIntent.putExtra(SSL_ISSUED_TO_CNAME, issuedToCName)
2258                     domainsIntent.putExtra(SSL_ISSUED_TO_ONAME, issuedToOName)
2259                     domainsIntent.putExtra(SSL_ISSUED_TO_UNAME, issuedToUName)
2260                     domainsIntent.putExtra(SSL_ISSUED_BY_CNAME, issuedByCName)
2261                     domainsIntent.putExtra(SSL_ISSUED_BY_ONAME, issuedByOName)
2262                     domainsIntent.putExtra(SSL_ISSUED_BY_UNAME, issuedByUName)
2263                     domainsIntent.putExtra(SSL_START_DATE, startDateLong)
2264                     domainsIntent.putExtra(SSL_END_DATE, endDateLong)
2265                 }
2266
2267                 // Make it so.
2268                 startActivity(domainsIntent)
2269             }
2270
2271             R.id.settings -> {  // Settings.
2272                 // Set the reapply on restart flags.
2273                 reapplyAppSettingsOnRestart = true
2274                 reapplyDomainSettingsOnRestart = true
2275
2276                 // Create a settings intent.
2277                 val settingsIntent = Intent(this, SettingsActivity::class.java)
2278
2279                 // Make it so.
2280                 startActivity(settingsIntent)
2281             }
2282
2283             R.id.import_export -> { // Import/Export.
2284                 // Create an intent to launch the import/export activity.
2285                 val importExportIntent = Intent(this, ImportExportActivity::class.java)
2286
2287                 // Make it so.
2288                 startActivity(importExportIntent)
2289             }
2290
2291             R.id.logcat -> {  // Logcat.
2292                 // Create an intent to launch the logcat activity.
2293                 val logcatIntent = Intent(this, LogcatActivity::class.java)
2294
2295                 // Make it so.
2296                 startActivity(logcatIntent)
2297             }
2298
2299             R.id.webview_devtools -> {  // WebView DevTools.
2300                 // Create a WebView DevTools intent.
2301                 val webViewDevToolsIntent = Intent("com.android.webview.SHOW_DEV_UI")
2302
2303                 // Launch as a new task so that the WebView DevTools and Privacy Browser show as a separate windows in the recent tasks list.
2304                 webViewDevToolsIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2305
2306                 // Make it so.
2307                 startActivity(webViewDevToolsIntent)
2308             }
2309
2310             R.id.guide -> {  // Guide.
2311                 // Create an intent to launch the guide activity.
2312                 val guideIntent = Intent(this, GuideActivity::class.java)
2313
2314                 // Make it so.
2315                 startActivity(guideIntent)
2316             }
2317
2318             R.id.about -> {  // About
2319                 // Create an intent to launch the about activity.
2320                 val aboutIntent = Intent(this, AboutActivity::class.java)
2321
2322                 // Create a string array for the blocklist versions.
2323                 val blocklistVersions = arrayOf(easyList[0][0][0], easyPrivacy[0][0][0], fanboysAnnoyanceList[0][0][0], fanboysSocialList[0][0][0], ultraList[0][0][0], ultraPrivacy!![0][0][0])
2324
2325                 // Add the blocklist versions to the intent.
2326                 aboutIntent.putExtra(AboutActivity.BLOCKLIST_VERSIONS, blocklistVersions)
2327
2328                 // Make it so.
2329                 startActivity(aboutIntent)
2330             }
2331         }
2332
2333         // Close the navigation drawer.
2334         drawerLayout.closeDrawer(GravityCompat.START)
2335
2336         // Return true.
2337         return true
2338     }
2339
2340     public override fun onPostCreate(savedInstanceState: Bundle?) {
2341         // Run the default commands.
2342         super.onPostCreate(savedInstanceState)
2343
2344         // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished.  This creates the navigation drawer icon.
2345         // If the app is restarting to change the app theme the action bar drawer toggle will not yet be populated.
2346         actionBarDrawerToggle?.syncState()
2347     }
2348
2349     override fun onCreateContextMenu(contextMenu: ContextMenu, view: View, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
2350         // Get the hit test result.
2351         val hitTestResult = currentWebView!!.hitTestResult
2352
2353         // Define the URL strings.
2354         val imageUrl: String?
2355         val linkUrl: String?
2356
2357         // Get a handle for the clipboard manager.
2358         val clipboardManager = (getSystemService(CLIPBOARD_SERVICE) as ClipboardManager)
2359
2360         // Process the link according to the type.
2361         when (hitTestResult.type) {
2362             // `SRC_ANCHOR_TYPE` is a link.
2363             WebView.HitTestResult.SRC_ANCHOR_TYPE -> {
2364                 // Get the target URL.
2365                 linkUrl = hitTestResult.extra!!
2366
2367                 // Set the target URL as the context menu title.
2368                 contextMenu.setHeaderTitle(linkUrl)
2369
2370                 // Add an open in new tab entry.
2371                 contextMenu.add(R.string.open_in_new_tab).setOnMenuItemClickListener {
2372                     // Load the link URL in a new tab and move to it.
2373                     addNewTab(linkUrl, true)
2374
2375                     // Consume the event.
2376                     true
2377                 }
2378
2379                 // Add an open in background entry.
2380                 contextMenu.add(R.string.open_in_background).setOnMenuItemClickListener {
2381                     // Load the link URL in a new tab but do not move to it.
2382                     addNewTab(linkUrl, false)
2383
2384                     // Consume the event.
2385                     true
2386                 }
2387
2388                 // Add an open with app entry.
2389                 contextMenu.add(R.string.open_with_app).setOnMenuItemClickListener {
2390                     // Open the URL with another app.
2391                     openWithApp(linkUrl)
2392
2393                     // Consume the event.
2394                     true
2395                 }
2396
2397                 // Add an open with browser entry.
2398                 contextMenu.add(R.string.open_with_browser).setOnMenuItemClickListener {
2399                     // Open the URL with another browser.
2400                     openWithBrowser(linkUrl)
2401
2402                     // Consume the event.
2403                     true
2404                 }
2405
2406                 // Add a copy URL entry.
2407                 contextMenu.add(R.string.copy_url).setOnMenuItemClickListener {
2408                     // Save the link URL in a clip data.
2409                     val srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl)
2410
2411                     // Set the clip data as the clipboard's primary clip.
2412                     clipboardManager.setPrimaryClip(srcAnchorTypeClipData)
2413
2414                     // Consume the event.
2415                     true
2416                 }
2417
2418                 // Add a Save URL entry.
2419                 contextMenu.add(R.string.save_url).setOnMenuItemClickListener {
2420                     // Check the download preference.
2421                     if (downloadWithExternalApp)  // Download with an external app.
2422                         downloadUrlWithExternalApp(linkUrl)
2423                     else  // Handle the download inside of Privacy Browser.  The dialog will be displayed once the file size and the content disposition have been acquired.
2424                         PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, linkUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
2425
2426                     // Consume the event.
2427                     true
2428                 }
2429
2430                 // Add an empty cancel entry, which by default closes the context menu.
2431                 contextMenu.add(R.string.cancel)
2432             }
2433
2434             // `IMAGE_TYPE` is an image.
2435             WebView.HitTestResult.IMAGE_TYPE -> {
2436                 // Get the image URL.
2437                 imageUrl = hitTestResult.extra!!
2438
2439                 // Set the context menu title.
2440                 if (imageUrl.startsWith("data:"))  // The image data is contained in within the URL, making it exceedingly long.  Truncate the image URL before making it the title.
2441                     contextMenu.setHeaderTitle(imageUrl.substring(0, 100))
2442                 else  // The image URL does not contain the full image data.  Set the image URL as the title of the context menu.
2443                     contextMenu.setHeaderTitle(imageUrl)
2444
2445                 // Add an open in new tab entry.
2446                 contextMenu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener {
2447                     // Load the image in a new tab.
2448                     addNewTab(imageUrl, true)
2449
2450                     // Consume the event.
2451                     true
2452                 }
2453
2454                 // Add an open with app entry.
2455                 contextMenu.add(R.string.open_with_app).setOnMenuItemClickListener {
2456                     // Open the image URL with an external app.
2457                     openWithApp(imageUrl)
2458
2459                     // Consume the event.
2460                     true
2461                 }
2462
2463                 // Add an open with browser entry.
2464                 contextMenu.add(R.string.open_with_browser).setOnMenuItemClickListener {
2465                     // Open the image URL with an external browser.
2466                     openWithBrowser(imageUrl)
2467
2468                     // Consume the event.
2469                     true
2470                 }
2471
2472                 // Add a view image entry.
2473                 contextMenu.add(R.string.view_image).setOnMenuItemClickListener {
2474                     // Load the image in the current tab.
2475                     loadUrl(currentWebView!!, imageUrl)
2476
2477                     // Consume the event.
2478                     true
2479                 }
2480
2481                 // Add a save image entry.
2482                 contextMenu.add(R.string.save_image).setOnMenuItemClickListener {
2483                     // Check the download preference.
2484                     if (downloadWithExternalApp) {  // Download with an external app.
2485                         downloadUrlWithExternalApp(imageUrl)
2486                     } else {  // Handle the download inside of Privacy Browser.  The dialog will be displayed once the file size and the content disposition have been acquired.
2487                         PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, imageUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
2488                     }
2489
2490                     // Consume the event.
2491                     true
2492                 }
2493
2494                 // Add a copy URL entry.
2495                 contextMenu.add(R.string.copy_url).setOnMenuItemClickListener {
2496                     // Save the image URL in a clip data.
2497                     val imageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl)
2498
2499                     // Set the clip data as the clipboard's primary clip.
2500                     clipboardManager.setPrimaryClip(imageTypeClipData)
2501
2502                     // Consume the event.
2503                     true
2504                 }
2505
2506                 // Add an empty cancel entry, which by default closes the context menu.
2507                 contextMenu.add(R.string.cancel)
2508             }
2509
2510             // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2511             WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE -> {
2512                 // Get the image URL.
2513                 imageUrl = hitTestResult.extra!!
2514
2515                 // Instantiate a handler.
2516                 val handler = Handler(Looper.getMainLooper())
2517
2518                 // Get a handle for the handler message.
2519                 val message = handler.obtainMessage()
2520
2521                 // Request the image details from the last touched node be returned in the message.
2522                 currentWebView!!.requestFocusNodeHref(message)
2523
2524                 // Get the link URL from the message data.
2525                 linkUrl = message.data.getString("url")!!
2526
2527                 // Set the link URL as the title of the context menu.
2528                 contextMenu.setHeaderTitle(linkUrl)
2529
2530                 // Add an open in new tab entry.
2531                 contextMenu.add(R.string.open_in_new_tab).setOnMenuItemClickListener {
2532                     // Load the link URL in a new tab and move to it.
2533                     addNewTab(linkUrl, true)
2534
2535                     // Consume the event.
2536                     true
2537                 }
2538
2539                 // Add an open in background entry.
2540                 contextMenu.add(R.string.open_in_background).setOnMenuItemClickListener {
2541                     // Lod the link URL in a new tab but do not move to it.
2542                     addNewTab(linkUrl, false)
2543
2544                     // Consume the event.
2545                     true
2546                 }
2547
2548                 // Add an open image in new tab entry.
2549                 contextMenu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener {
2550                     // Load the image in a new tab and move to it.
2551                     addNewTab(imageUrl, true)
2552
2553                     // Consume the event.
2554                     true
2555                 }
2556
2557                 // Add an open with app entry.
2558                 contextMenu.add(R.string.open_with_app).setOnMenuItemClickListener {
2559                     // Open the link URL with an external app.
2560                     openWithApp(linkUrl)
2561
2562                     // Consume the event.
2563                     true
2564                 }
2565
2566                 // Add an open with browser entry.
2567                 contextMenu.add(R.string.open_with_browser).setOnMenuItemClickListener {
2568                     // Open the link URL with an external browser.
2569                     openWithBrowser(linkUrl)
2570
2571                     // Consume the event.
2572                     true
2573                 }
2574
2575                 // Add a view image entry.
2576                 contextMenu.add(R.string.view_image).setOnMenuItemClickListener {
2577                     // View the image in the current tab.
2578                     loadUrl(currentWebView!!, imageUrl)
2579
2580                     // Consume the event.
2581                     true
2582                 }
2583
2584                 // Add a Save Image entry.
2585                 contextMenu.add(R.string.save_image).setOnMenuItemClickListener {
2586                     // Check the download preference.
2587                     if (downloadWithExternalApp)  // Download with an external app.
2588                         downloadUrlWithExternalApp(imageUrl)
2589                     else  // Handle the download inside of Privacy Browser.  The dialog will be displayed once the file size and the content disposition have been acquired.
2590                         PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, imageUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
2591
2592                     // Consume the event.
2593                     true
2594                 }
2595
2596                 // Add a copy URL entry.
2597                 contextMenu.add(R.string.copy_url).setOnMenuItemClickListener {
2598                     // Save the link URL in a clip data.
2599                     val srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl)
2600
2601                     // Set the clip data as the clipboard's primary clip.
2602                     clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData)
2603
2604                     // Consume the event.
2605                     true
2606                 }
2607
2608                 // Add a save URL entry.
2609                 contextMenu.add(R.string.save_url).setOnMenuItemClickListener {
2610                     // Check the download preference.
2611                     if (downloadWithExternalApp)  // Download with an external app.
2612                         downloadUrlWithExternalApp(linkUrl)
2613                     else  // Handle the download inside of Privacy Browser.  The dialog will be displayed once the file size and the content disposition have been acquired.
2614                         PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, linkUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
2615
2616                     // Consume the event.
2617                     true
2618                 }
2619
2620                 // Add an empty cancel entry, which by default closes the context menu.
2621                 contextMenu.add(R.string.cancel)
2622             }
2623
2624             WebView.HitTestResult.EMAIL_TYPE -> {
2625                 // Get the target URL.
2626                 linkUrl = hitTestResult.extra
2627
2628                 // Set the target URL as the title of the context menu.
2629                 contextMenu.setHeaderTitle(linkUrl)
2630
2631                 // Add a write email entry.
2632                 contextMenu.add(R.string.write_email).setOnMenuItemClickListener {
2633                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2634                     val emailIntent = Intent(Intent.ACTION_SENDTO)
2635
2636                     // Parse the url and set it as the data for the intent.
2637                     emailIntent.data = Uri.parse("mailto:$linkUrl")
2638
2639                     // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2640                     emailIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2641
2642                     try {
2643                         // Make it so.
2644                         startActivity(emailIntent)
2645                     } catch (exception: ActivityNotFoundException) {
2646                         // Display a snackbar.
2647                         Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
2648                     }
2649
2650                     // Consume the event.
2651                     true
2652                 }
2653
2654                 // Add a copy email address entry.
2655                 contextMenu.add(R.string.copy_email_address).setOnMenuItemClickListener {
2656                     // Save the email address in a clip data.
2657                     val srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl)
2658
2659                     // Set the clip data as the clipboard's primary clip.
2660                     clipboardManager.setPrimaryClip(srcEmailTypeClipData)
2661
2662                     // Consume the event.
2663                     true
2664                 }
2665
2666                 // Add an empty cancel entry, which by default closes the context menu.
2667                 contextMenu.add(R.string.cancel)
2668             }
2669         }
2670     }
2671
2672     override fun onCreateBookmark(dialogFragment: DialogFragment, favoriteIconBitmap: Bitmap) {
2673         // Get the dialog.
2674         val dialog = dialogFragment.dialog!!
2675
2676         // Get the views from the dialog fragment.
2677         val createBookmarkNameEditText = dialog.findViewById<EditText>(R.id.create_bookmark_name_edittext)
2678         val createBookmarkUrlEditText = dialog.findViewById<EditText>(R.id.create_bookmark_url_edittext)
2679
2680         // Extract the strings from the edit texts.
2681         val bookmarkNameString = createBookmarkNameEditText.text.toString()
2682         val bookmarkUrlString = createBookmarkUrlEditText.text.toString()
2683
2684         // Create a favorite icon byte array output stream.
2685         val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
2686
2687         // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2688         favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
2689
2690         // Convert the favorite icon byte array stream to a byte array.
2691         val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
2692
2693         // Display the new bookmark below the current items in the (0 indexed) list.
2694         val newBookmarkDisplayOrder = bookmarksListView.count
2695
2696         // Create the bookmark.
2697         bookmarksDatabaseHelper!!.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray)
2698
2699         // Update the bookmarks cursor with the current contents of this folder.
2700         bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolder)
2701
2702         // Update the list view.
2703         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
2704
2705         // Scroll to the new bookmark.
2706         bookmarksListView.setSelection(newBookmarkDisplayOrder)
2707     }
2708
2709     override fun onCreateBookmarkFolder(dialogFragment: DialogFragment, favoriteIconBitmap: Bitmap) {
2710         // Get the dialog.
2711         val dialog = dialogFragment.dialog!!
2712
2713         // Get handles for the views in the dialog fragment.
2714         val folderNameEditText = dialog.findViewById<EditText>(R.id.folder_name_edittext)
2715         val defaultIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_icon_radiobutton)
2716         val defaultIconImageView = dialog.findViewById<ImageView>(R.id.default_icon_imageview)
2717
2718         // Get new folder name string.
2719         val folderNameString = folderNameEditText.text.toString()
2720
2721         // Set the folder icon bitmap according to the dialog.
2722         val folderIconBitmap: Bitmap = if (defaultIconRadioButton.isChecked) {  // Use the default folder icon.
2723             // Get the default folder icon drawable.
2724             val folderIconDrawable = defaultIconImageView.drawable
2725
2726             // Convert the folder icon drawable to a bitmap drawable.
2727             val folderIconBitmapDrawable = folderIconDrawable as BitmapDrawable
2728
2729             // Convert the folder icon bitmap drawable to a bitmap.
2730             folderIconBitmapDrawable.bitmap
2731         } else {  // Use the WebView favorite icon.
2732             // Copy the favorite icon bitmap to the folder icon bitmap.
2733             favoriteIconBitmap
2734         }
2735
2736         // Create a folder icon byte array output stream.
2737         val folderIconByteArrayOutputStream = ByteArrayOutputStream()
2738
2739         // Convert the folder icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
2740         folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream)
2741
2742         // Convert the folder icon byte array stream to a byte array.
2743         val folderIconByteArray = folderIconByteArrayOutputStream.toByteArray()
2744
2745         // Move all the bookmarks down one in the display order.
2746         for (i in 0 until bookmarksListView.count) {
2747             // Get the bookmark database id.
2748             val databaseId = bookmarksListView.getItemIdAtPosition(i).toInt()
2749
2750             // Move the bookmark down one slot.
2751             bookmarksDatabaseHelper!!.updateDisplayOrder(databaseId, i + 1)
2752         }
2753
2754         // Create the folder, which will be placed at the top of the list view.
2755         bookmarksDatabaseHelper!!.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray)
2756
2757         // Update the bookmarks cursor with the current contents of this folder.
2758         bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolder)
2759
2760         // Update the list view.
2761         bookmarksCursorAdapter.changeCursor(bookmarksCursor)
2762
2763         // Scroll to the new folder.
2764         bookmarksListView.setSelection(0)
2765     }
2766
2767     private fun loadUrlFromTextBox() {
2768         // Get the text from URL text box and convert it to a string.  trim() removes white spaces from the beginning and end of the string.
2769         var unformattedUrlString = urlEditText.text.toString().trim { it <= ' ' }
2770
2771         // Create the formatted URL string.
2772         var urlString = ""
2773
2774         // Check to see if the unformatted URL string is a valid URL.  Otherwise, convert it into a search.
2775         if (unformattedUrlString.startsWith("content://")) {  // This is a content URL.
2776             // Load the entire content URL.
2777             urlString = unformattedUrlString
2778         } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
2779             unformattedUrlString.startsWith("file://")) {  // This is a standard URL.
2780
2781             // Add `https://` at the beginning if there is no protocol.  Otherwise the app will segfault.
2782             if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://"))
2783                 unformattedUrlString = "https://$unformattedUrlString"
2784
2785             // Initialize the unformatted URL.
2786             var unformattedUrl: URL? = null
2787
2788             // Convert the unformatted URL string to a URL.
2789             try {
2790                 unformattedUrl = URL(unformattedUrlString)
2791             } catch (exception: MalformedURLException) {
2792                 exception.printStackTrace()
2793             }
2794
2795             // Get the components of the URL.
2796             val scheme = unformattedUrl?.protocol
2797             val authority = unformattedUrl?.authority
2798             val path = unformattedUrl?.path
2799             val query = unformattedUrl?.query
2800             val fragment = unformattedUrl?.ref
2801
2802             // Create a URI.
2803             val uri = Uri.Builder()
2804
2805             // Build the URI from the components of the URL.
2806             uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment)
2807
2808             // Decode the URI as a UTF-8 string in.
2809             try {
2810                 urlString = URLDecoder.decode(uri.build().toString(), "UTF-8")
2811             } catch (exception: UnsupportedEncodingException) {
2812                 // Do nothing.  The formatted URL string will remain blank.
2813             }
2814         } else if (unformattedUrlString.isNotEmpty()) {  // This is not a URL, but rather a search string.
2815             // Sanitize the search input.
2816             val encodedSearchString = try {
2817                 URLEncoder.encode(unformattedUrlString, "UTF-8")
2818             } catch (exception: UnsupportedEncodingException) {
2819                 ""
2820             }
2821
2822             // Add the base search URL.
2823             urlString = searchURL + encodedSearchString
2824         }
2825
2826         // Clear the focus from the URL edit text.  Otherwise, proximate typing in the box will retain the colorized formatting instead of being reset during refocus.
2827         urlEditText.clearFocus()
2828
2829         // Make it so.
2830         loadUrl(currentWebView!!, urlString)
2831     }
2832
2833     private fun loadUrl(nestedScrollWebView: NestedScrollWebView, url: String) {
2834         // Sanitize the URL.
2835         val urlString = sanitizeUrl(url)
2836
2837         // Apply the domain settings and load the URL.
2838         applyDomainSettings(nestedScrollWebView, urlString, resetTab = true, reloadWebsite = false, loadUrl = true)
2839     }
2840
2841     // The view parameter cannot be removed because it is called from the layout onClick.
2842     fun findPreviousOnPage(@Suppress("UNUSED_PARAMETER")view: View?) {
2843         // Go to the previous highlighted phrase on the page.  `false` goes backwards instead of forwards.
2844         currentWebView!!.findNext(false)
2845     }
2846
2847     // The view parameter cannot be removed because it is called from the layout onClick.
2848     fun findNextOnPage(@Suppress("UNUSED_PARAMETER")view: View?) {
2849         // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
2850         currentWebView!!.findNext(true)
2851     }
2852
2853     // The view parameter cannot be removed because it is called from the layout onClick.
2854     fun closeFindOnPage(@Suppress("UNUSED_PARAMETER")view: View?) {
2855         // Delete the contents of the find on page edit text.
2856         findOnPageEditText.text = null
2857
2858         // Clear the highlighted phrases if the WebView is not null.
2859         currentWebView?.clearMatches()
2860
2861         // Hide the find on page linear layout.
2862         findOnPageLinearLayout.visibility = View.GONE
2863
2864         // Show the toolbar.
2865         toolbar.visibility = View.VISIBLE
2866
2867         // Get a handle for the input method manager.
2868         val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
2869
2870         // Hide the keyboard.
2871         inputMethodManager.hideSoftInputFromWindow(toolbar.windowToken, 0)
2872     }
2873
2874     override fun onApplyNewFontSize(dialogFragment: DialogFragment) {
2875         // Get the dialog.
2876         val dialog = dialogFragment.dialog!!
2877
2878         // Get a handle for the font size edit text.
2879         val fontSizeEditText = dialog.findViewById<EditText>(R.id.font_size_edittext)
2880
2881         // Initialize the new font size variable with the current font size.
2882         var newFontSize = currentWebView!!.settings.textZoom
2883
2884         // Get the font size from the edit text.
2885         try {
2886             newFontSize = fontSizeEditText.text.toString().toInt()
2887         } catch (exception: Exception) {
2888             // If the edit text does not contain a valid font size do nothing.
2889         }
2890
2891         // Apply the new font size.
2892         currentWebView!!.settings.textZoom = newFontSize
2893     }
2894
2895     override fun onOpen(dialogFragment: DialogFragment) {
2896         // Get the dialog.
2897         val dialog = dialogFragment.dialog!!
2898
2899         // Get handles for the views.
2900         val fileNameEditText = dialog.findViewById<EditText>(R.id.file_name_edittext)
2901         val mhtCheckBox = dialog.findViewById<CheckBox>(R.id.mht_checkbox)
2902
2903         // Get the file path string.
2904         val openFilePath = fileNameEditText.text.toString()
2905
2906         // Apply the domain settings.  This resets the favorite icon and removes any domain settings.
2907         applyDomainSettings(currentWebView!!, openFilePath, resetTab = true, reloadWebsite = false, loadUrl = false)
2908
2909         // Open the file according to the type.
2910         if (mhtCheckBox.isChecked) {  // Force opening of an MHT file.
2911             try {
2912                 // Get the MHT file input stream.
2913                 val mhtFileInputStream = contentResolver.openInputStream(Uri.parse(openFilePath))
2914
2915                 // Create a temporary MHT file.
2916                 val temporaryMhtFile = File.createTempFile(TEMPORARY_MHT_FILE, ".mht", cacheDir)
2917
2918                 // Get a file output stream for the temporary MHT file.
2919                 val temporaryMhtFileOutputStream = FileOutputStream(temporaryMhtFile)
2920
2921                 // Create a transfer byte array.
2922                 val transferByteArray = ByteArray(1024)
2923
2924                 // Create an integer to track the number of bytes read.
2925                 var bytesRead: Int
2926
2927                 // Copy the temporary MHT file input stream to the MHT output stream.
2928                 while (mhtFileInputStream!!.read(transferByteArray).also { bytesRead = it } > 0)
2929                     temporaryMhtFileOutputStream.write(transferByteArray, 0, bytesRead)
2930
2931                 // Flush the temporary MHT file output stream.
2932                 temporaryMhtFileOutputStream.flush()
2933
2934                 // Close the streams.
2935                 temporaryMhtFileOutputStream.close()
2936                 mhtFileInputStream.close()
2937
2938                 // Load the temporary MHT file.
2939                 currentWebView!!.loadUrl(temporaryMhtFile.toString())
2940             } catch (exception: Exception) {
2941                 // Display a snackbar.
2942                 Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
2943             }
2944         } else {  // Let the WebView handle opening of the file.
2945             // Open the file.
2946             currentWebView!!.loadUrl(openFilePath)
2947         }
2948     }
2949
2950     private fun downloadUrlWithExternalApp(url: String) {
2951         // Create a download intent.  Not specifying the action type will display the maximum number of options.
2952         val downloadIntent = Intent()
2953
2954         // Set the URI and the mime type.
2955         downloadIntent.setDataAndType(Uri.parse(url), "text/html")
2956
2957         // Flag the intent to open in a new task.
2958         downloadIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2959
2960         // Show the chooser.
2961         startActivity(Intent.createChooser(downloadIntent, getString(R.string.download_with_external_app)))
2962     }
2963
2964     override fun onSaveUrl(originalUrlString: String, fileNameString: String, dialogFragment: DialogFragment) {
2965         // Store the URL.  This will be used in the save URL activity result launcher.
2966         saveUrlString = if (originalUrlString.startsWith("data:")) {
2967             // Save the original URL.
2968             originalUrlString
2969         } else {
2970             // Get the dialog.
2971             val dialog = dialogFragment.dialog!!
2972
2973             // Get a handle for the dialog URL edit text.
2974             val dialogUrlEditText = dialog.findViewById<EditText>(R.id.url_edittext)
2975
2976             // Get the URL from the edit text, which may have been modified.
2977             dialogUrlEditText.text.toString()
2978         }
2979
2980         // Open the file picker.
2981         saveUrlActivityResultLauncher.launch(fileNameString)
2982     }
2983
2984     // Remove the warning that `OnTouchListener()` needs to override `performClick()`, as the only purpose of setting the `OnTouchListener()` is to make it do nothing.
2985     @SuppressLint("ClickableViewAccessibility")
2986     private fun initializeApp() {
2987         // Get a handle for the input method.
2988         val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
2989
2990         // Initialize the color spans for highlighting the URLs.
2991         initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
2992         finalGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
2993         redColorSpan = ForegroundColorSpan(getColor(R.color.red_text))
2994
2995         // Remove the formatting from the URL edit text when the user is editing the text.
2996         urlEditText.onFocusChangeListener = View.OnFocusChangeListener { _: View?, hasFocus: Boolean ->
2997             if (hasFocus) {  // The user is editing the URL text box.
2998                 // Remove the syntax highlighting.
2999                 urlEditText.text.removeSpan(redColorSpan)
3000                 urlEditText.text.removeSpan(initialGrayColorSpan)
3001                 urlEditText.text.removeSpan(finalGrayColorSpan)
3002             } else {  // The user has stopped editing the URL text box.
3003                 // Move to the beginning of the string.
3004                 urlEditText.setSelection(0)
3005
3006                 // Reapply the syntax highlighting.
3007                 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
3008             }
3009         }
3010
3011         // Set the go button on the keyboard to load the URL in url text box.
3012         urlEditText.setOnKeyListener { _: View?, keyCode: Int, keyEvent: KeyEvent ->
3013             // If the event is a key-down event on the `enter` button, load the URL.
3014             if ((keyEvent.action == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {  // The enter key was pressed.
3015                 // Load the URL.
3016                 loadUrlFromTextBox()
3017
3018                 // Consume the event.
3019                 return@setOnKeyListener true
3020             } else {  // Some other key was pressed.
3021                 // Do not consume the event.
3022                 return@setOnKeyListener false
3023             }
3024         }
3025
3026         // Create an Orbot status broadcast receiver.
3027         orbotStatusBroadcastReceiver = object : BroadcastReceiver() {
3028             override fun onReceive(context: Context, intent: Intent) {
3029                 // Get the content of the status message.
3030                 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS")!!
3031
3032                 // If Privacy Browser is waiting on the proxy, load the website now that Orbot is connected.
3033                 if ((orbotStatus == ProxyHelper.ORBOT_STATUS_ON) && waitingForProxy) {
3034                     // Reset the waiting for proxy status.
3035                     waitingForProxy = false
3036
3037                     // Get a list of the current fragments.
3038                     val fragmentList = supportFragmentManager.fragments
3039
3040                     // Check each fragment to see if it is a waiting for proxy dialog.  Sometimes more than one is displayed.
3041                     for (i in fragmentList.indices) {
3042                         // Get the fragment tag.
3043                         val fragmentTag = fragmentList[i].tag
3044
3045                         // Check to see if it is the waiting for proxy dialog.
3046                         if (fragmentTag != null && fragmentTag == getString(R.string.waiting_for_proxy_dialog)) {
3047                             // Dismiss the waiting for proxy dialog.
3048                             (fragmentList[i] as DialogFragment).dismiss()
3049                         }
3050                     }
3051
3052                     // Reload existing URLs and load any URLs that are waiting for the proxy.
3053                     for (i in 0 until webViewPagerAdapter!!.count) {
3054                         // Get the WebView tab fragment.
3055                         val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
3056
3057                         // Get the fragment view.
3058                         val fragmentView = webViewTabFragment.view
3059
3060                         // Only process the WebViews if they exist.
3061                         if (fragmentView != null) {
3062                             // Get the nested scroll WebView from the tab fragment.
3063                             val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
3064
3065                             // Get the waiting for proxy URL string.
3066                             val waitingForProxyUrlString = nestedScrollWebView.waitingForProxyUrlString
3067
3068                             // Load the pending URL if it exists.
3069                             if (waitingForProxyUrlString.isNotEmpty()) {  // A URL is waiting to be loaded.
3070                                 // Load the URL.
3071                                 loadUrl(nestedScrollWebView, waitingForProxyUrlString)
3072
3073                                 // Reset the waiting for proxy URL string.
3074                                 nestedScrollWebView.waitingForProxyUrlString = ""
3075                             } else {  // No URL is waiting to be loaded.
3076                                 // Reload the existing URL.
3077                                 nestedScrollWebView.reload()
3078                             }
3079                         }
3080                     }
3081                 }
3082             }
3083         }
3084
3085         // Register the Orbot status broadcast receiver.
3086         registerReceiver(orbotStatusBroadcastReceiver, IntentFilter("org.torproject.android.intent.action.STATUS"))
3087
3088         // Get handles for views that need to be modified.
3089         val bookmarksHeaderLinearLayout = findViewById<LinearLayout>(R.id.bookmarks_header_linearlayout)
3090         val launchBookmarksActivityFab = findViewById<FloatingActionButton>(R.id.launch_bookmarks_activity_fab)
3091         val createBookmarkFolderFab = findViewById<FloatingActionButton>(R.id.create_bookmark_folder_fab)
3092         val createBookmarkFab = findViewById<FloatingActionButton>(R.id.create_bookmark_fab)
3093
3094         // Update the WebView pager every time a tab is modified.
3095         webViewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
3096             override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
3097
3098             override fun onPageSelected(position: Int) {
3099                 // Close the find on page bar if it is open.
3100                 closeFindOnPage(null)
3101
3102                 // Set the current WebView.
3103                 setCurrentWebView(position)
3104
3105                 // Select the corresponding tab if it does not match the currently selected page.  This will happen if the page was scrolled by creating a new tab.
3106                 if (tabLayout.selectedTabPosition != position) {
3107                     // Wait until the new tab has been created.
3108                     tabLayout.post {
3109                         // Get a handle for the tab.
3110                         val tab = tabLayout.getTabAt(position)!!
3111
3112                         // Select the tab.
3113                         tab.select()
3114                     }
3115                 }
3116             }
3117
3118             override fun onPageScrollStateChanged(state: Int) {}
3119         })
3120
3121         // Handle tab selections.
3122         tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
3123             override fun onTabSelected(tab: TabLayout.Tab) {
3124                 // Select the same page in the view pager.
3125                 webViewPager.currentItem = tab.position
3126             }
3127
3128             override fun onTabUnselected(tab: TabLayout.Tab) {}
3129
3130             override fun onTabReselected(tab: TabLayout.Tab) {
3131                 // Instantiate the View SSL Certificate dialog.
3132                 val viewSslCertificateDialogFragment: DialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView!!.webViewFragmentId, currentWebView!!.getFavoriteIcon())
3133
3134                 // Display the View SSL Certificate dialog.
3135                 viewSslCertificateDialogFragment.show(supportFragmentManager, getString(R.string.view_ssl_certificate))
3136             }
3137         })
3138
3139         // Set a touch listener on the bookmarks header linear layout so that touches don't pass through to the button underneath.
3140         bookmarksHeaderLinearLayout.setOnTouchListener { _: View?, _: MotionEvent? -> true }
3141
3142         // Set the launch bookmarks activity floating action button to launch the bookmarks activity.
3143         launchBookmarksActivityFab.setOnClickListener {
3144             // Get a copy of the favorite icon bitmap.
3145             val currentFavoriteIconBitmap = currentWebView!!.getFavoriteIcon()
3146
3147             // Create a favorite icon byte array output stream.
3148             val currentFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
3149
3150             // Convert the favorite icon bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
3151             currentFavoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, currentFavoriteIconByteArrayOutputStream)
3152
3153             // Convert the favorite icon byte array stream to a byte array.
3154             val currentFavoriteIconByteArray = currentFavoriteIconByteArrayOutputStream.toByteArray()
3155
3156             // Create an intent to launch the bookmarks activity.
3157             val bookmarksIntent = Intent(applicationContext, BookmarksActivity::class.java)
3158
3159             // Add the extra information to the intent.
3160             bookmarksIntent.putExtra(CURRENT_FOLDER, currentBookmarksFolder)
3161             bookmarksIntent.putExtra(CURRENT_TITLE, currentWebView!!.title)
3162             bookmarksIntent.putExtra(CURRENT_URL, currentWebView!!.url)
3163             bookmarksIntent.putExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY, currentFavoriteIconByteArray)
3164
3165             // Make it so.
3166             startActivity(bookmarksIntent)
3167         }
3168
3169         // Set the create new bookmark folder floating action button to display an alert dialog.
3170         createBookmarkFolderFab.setOnClickListener {
3171             // Create a create bookmark folder dialog.
3172             val createBookmarkFolderDialog: DialogFragment = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView!!.getFavoriteIcon())
3173
3174             // Show the create bookmark folder dialog.
3175             createBookmarkFolderDialog.show(supportFragmentManager, getString(R.string.create_folder))
3176         }
3177
3178         // Set the create new bookmark floating action button to display an alert dialog.
3179         createBookmarkFab.setOnClickListener {
3180             // Instantiate the create bookmark dialog.
3181             val createBookmarkDialog: DialogFragment = CreateBookmarkDialog.createBookmark(currentWebView!!.url!!, currentWebView!!.title!!, currentWebView!!.getFavoriteIcon())
3182
3183             // Display the create bookmark dialog.
3184             createBookmarkDialog.show(supportFragmentManager, getString(R.string.create_bookmark))
3185         }
3186
3187         // Search for the string on the page whenever a character changes in the find on page edit text.
3188         findOnPageEditText.addTextChangedListener(object : TextWatcher {
3189             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
3190
3191             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
3192
3193             override fun afterTextChanged(s: Editable) {
3194                 // Search for the text in the WebView if it is not null.  Sometimes on resume after a period of non-use the WebView will be null.
3195                 currentWebView?.findAllAsync(findOnPageEditText.text.toString())
3196             }
3197         })
3198
3199         // Set the `check mark` button for the find on page edit text keyboard to close the soft keyboard.
3200         findOnPageEditText.setOnKeyListener { _: View?, keyCode: Int, keyEvent: KeyEvent ->
3201             if ((keyEvent.action == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {  // The `enter` key was pressed.
3202                 // Hide the soft keyboard.
3203                 inputMethodManager.hideSoftInputFromWindow(currentWebView!!.windowToken, 0)
3204
3205                 // Consume the event.
3206                 return@setOnKeyListener true
3207             } else {  // A different key was pressed.
3208                 // Do not consume the event.
3209                 return@setOnKeyListener false
3210             }
3211         }
3212
3213         // Implement swipe to refresh.
3214         swipeRefreshLayout.setOnRefreshListener {
3215             // Reload the website.
3216             currentWebView!!.reload()
3217         }
3218
3219         // Store the default progress view offsets.
3220         defaultProgressViewStartOffset = swipeRefreshLayout.progressViewStartOffset
3221         defaultProgressViewEndOffset = swipeRefreshLayout.progressViewEndOffset
3222
3223         // Set the refresh color scheme according to the theme.
3224         swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
3225
3226         // Initialize a color background typed value.
3227         val colorBackgroundTypedValue = TypedValue()
3228
3229         // Get the color background from the theme.
3230         theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
3231
3232         // Get the color background int from the typed value.
3233         val colorBackgroundInt = colorBackgroundTypedValue.data
3234
3235         // Set the swipe refresh background color.
3236         swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
3237
3238         // Set the drawer titles, which identify the drawer layouts in accessibility mode.
3239         drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer))
3240         drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks))
3241
3242         // Load the bookmarks folder.
3243         loadBookmarksFolder()
3244
3245         // Handle clicks on bookmarks.
3246         bookmarksListView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
3247             // Convert the id from long to int to match the format of the bookmarks database.
3248             val databaseId = id.toInt()
3249
3250             // Get the bookmark cursor for this ID.
3251             val bookmarkCursor = bookmarksDatabaseHelper!!.getBookmark(databaseId)
3252
3253             // Move the bookmark cursor to the first row.
3254             bookmarkCursor.moveToFirst()
3255
3256             // Act upon the bookmark according to the type.
3257             if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) {  // The selected bookmark is a folder.
3258                 // Store the folder name.
3259                 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
3260
3261                 // Load the new folder.
3262                 loadBookmarksFolder()
3263             } else {  // The selected bookmark is not a folder.
3264                 // Load the bookmark URL.
3265                 loadUrl(currentWebView!!, bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)))
3266
3267                 // Close the bookmarks drawer if it is not pinned.
3268                 if (!bookmarksDrawerPinned)
3269                     drawerLayout.closeDrawer(GravityCompat.END)
3270             }
3271
3272             // Close the cursor.
3273             bookmarkCursor.close()
3274         }
3275
3276         // Handle long-presses on bookmarks.
3277         bookmarksListView.onItemLongClickListener = AdapterView.OnItemLongClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
3278             // Convert the database ID from `long` to `int`.
3279             val databaseId = id.toInt()
3280
3281             // Run the commands associated with the type.
3282             if (bookmarksDatabaseHelper!!.isFolder(databaseId)) {  // The bookmark is a folder.
3283                 // Get a cursor of all the bookmarks in the folder.
3284                 val bookmarksCursor = bookmarksDatabaseHelper!!.getFolderBookmarks(databaseId)
3285
3286                 // Move to the first entry in the cursor.
3287                 bookmarksCursor.moveToFirst()
3288
3289                 // Open each bookmark
3290                 for (i in 0 until bookmarksCursor.count) {
3291                     // Load the bookmark in a new tab, moving to the tab for the first bookmark if the drawer is not pinned.
3292                     addNewTab(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), !bookmarksDrawerPinned && (i == 0))
3293
3294                     // Move to the next bookmark.
3295                     bookmarksCursor.moveToNext()
3296                 }
3297
3298                 // Close the cursor.
3299                 bookmarksCursor.close()
3300             } else {  // The bookmark is not a folder.
3301                 // Get the bookmark cursor for this ID.
3302                 val bookmarkCursor = bookmarksDatabaseHelper!!.getBookmark(databaseId)
3303
3304                 // Move the bookmark cursor to the first row.
3305                 bookmarkCursor.moveToFirst()
3306
3307                 // Load the bookmark in a new tab and move to the tab if the drawer is not pinned.
3308                 addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), !bookmarksDrawerPinned)
3309
3310                 // Close the cursor.
3311                 bookmarkCursor.close()
3312             }
3313
3314             // Close the bookmarks drawer if it is not pinned.
3315             if (!bookmarksDrawerPinned)
3316                 drawerLayout.closeDrawer(GravityCompat.END)
3317
3318             // Consume the event.
3319             true
3320         }
3321
3322         // The drawer listener is used to update the navigation menu.
3323         drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener {
3324             override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
3325
3326             override fun onDrawerOpened(drawerView: View) {}
3327
3328             override fun onDrawerClosed(drawerView: View) {
3329                 // Reset the drawer icon when the drawer is closed.  Otherwise, it remains an arrow if the drawer is open when the app is restarted.
3330                 actionBarDrawerToggle!!.syncState()
3331             }
3332
3333             override fun onDrawerStateChanged(newState: Int) {
3334                 if (newState == DrawerLayout.STATE_SETTLING || newState == DrawerLayout.STATE_DRAGGING) {  // A drawer is opening or closing.
3335                     // Update the navigation menu items if the WebView is not null.
3336                     if (currentWebView != null) {
3337                         navigationBackMenuItem.isEnabled = currentWebView!!.canGoBack()
3338                         navigationForwardMenuItem.isEnabled = currentWebView!!.canGoForward()
3339                         navigationHistoryMenuItem.isEnabled = currentWebView!!.canGoBack() || currentWebView!!.canGoForward()
3340                         navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + currentWebView!!.getRequestsCount(BLOCKED_REQUESTS)
3341
3342                         // Hide the keyboard (if displayed).
3343                         inputMethodManager.hideSoftInputFromWindow(currentWebView!!.windowToken, 0)
3344                     }
3345
3346                     // Clear the focus from from the URL text box.  This removes any text selection markers and context menus, which otherwise draw above the open drawers.
3347                     urlEditText.clearFocus()
3348
3349                     // Clear the focus from from the WebView if it is not null, which can happen if a user opens a drawer while the browser is being resumed.
3350                     // Clearing the focus from the WebView removes any text selection markers and context menus, which otherwise draw above the open drawers.
3351                     currentWebView?.clearFocus()
3352                 }
3353             }
3354         })
3355
3356         // Inflate a bare WebView to get the default user agent.  It is not used to render content on the screen.
3357         @SuppressLint("InflateParams") val webViewLayout = layoutInflater.inflate(R.layout.bare_webview, null, false)
3358
3359         // Get a handle for the WebView.
3360         val bareWebView = webViewLayout.findViewById<WebView>(R.id.bare_webview)
3361
3362         // Store the default user agent.
3363         webViewDefaultUserAgent = bareWebView.settings.userAgentString
3364
3365         // Destroy the bare WebView.
3366         bareWebView.destroy()
3367
3368         // Update the domains settings set.
3369         updateDomainsSettingsSet()
3370
3371         // Instantiate the blocklist helper.
3372         blocklistHelper = BlocklistHelper()
3373     }
3374
3375     private fun applyAppSettings() {
3376         // Store the values from the shared preferences in variables.
3377         incognitoModeEnabled = sharedPreferences.getBoolean(getString(R.string.incognito_mode_key), false)
3378         sanitizeTrackingQueries = sharedPreferences.getBoolean(getString(R.string.tracking_queries_key), true)
3379         sanitizeAmpRedirects = sharedPreferences.getBoolean(getString(R.string.amp_redirects_key), true)
3380         proxyMode = sharedPreferences.getString(getString(R.string.proxy_key), getString(R.string.proxy_default_value))!!
3381         fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean(getString(R.string.full_screen_browsing_mode_key), false)
3382         hideAppBar = sharedPreferences.getBoolean(getString(R.string.hide_app_bar_key), true)
3383         downloadWithExternalApp = sharedPreferences.getBoolean(getString(R.string.download_with_external_app_key), false)
3384         scrollAppBar = sharedPreferences.getBoolean(getString(R.string.scroll_app_bar_key), true)
3385
3386         // Apply the saved proxy mode if the app has been restarted.
3387         if (savedProxyMode != null) {
3388             // Apply the saved proxy mode.
3389             proxyMode = savedProxyMode!!
3390
3391             // Reset the saved proxy mode.
3392             savedProxyMode = null
3393         }
3394
3395         // Get the search string.
3396         val searchString = sharedPreferences.getString(getString(R.string.search_key), getString(R.string.search_default_value))!!
3397
3398         // Set the search string, using the custom search URL if specified.
3399         searchURL = if (searchString == getString(R.string.custom_url_item))
3400             sharedPreferences.getString(getString(R.string.search_custom_url_key), getString(R.string.search_custom_url_default_value))!!
3401         else
3402             searchString
3403
3404         // Apply the proxy.
3405         applyProxy(false)
3406
3407         // Adjust the layout and scrolling parameters according to the position of the app bar.
3408         if (bottomAppBar) {  // The app bar is on the bottom.
3409             // Adjust the UI.
3410             if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) {  // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
3411                 // Reset the WebView padding to fill the available space.
3412                 swipeRefreshLayout.setPadding(0, 0, 0, 0)
3413             } else {  // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
3414                 // Move the WebView above the app bar layout.
3415                 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight)
3416
3417                 // Show the app bar if it is scrolled off the screen.
3418                 if (appBarLayout.translationY != 0f) {
3419                     // Animate the bottom app bar onto the screen.
3420                     objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0f)
3421
3422                     // Make it so.
3423                     objectAnimator.start()
3424                 }
3425             }
3426         } else {  // The app bar is on the top.
3427             // Get the current layout parameters.  Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command.
3428             val swipeRefreshLayoutParams = swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams
3429             val toolbarLayoutParams = toolbar.layoutParams as AppBarLayout.LayoutParams
3430             val findOnPageLayoutParams = findOnPageLinearLayout.layoutParams as AppBarLayout.LayoutParams
3431             val tabsLayoutParams = tabsLinearLayout.layoutParams as AppBarLayout.LayoutParams
3432
3433             // Add the scrolling behavior to the layout parameters.
3434             if (scrollAppBar) {
3435                 // Enable scrolling of the app bar.
3436                 swipeRefreshLayoutParams.behavior = AppBarLayout.ScrollingViewBehavior()
3437                 toolbarLayoutParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS or AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
3438                 findOnPageLayoutParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS or AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
3439                 tabsLayoutParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS or AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
3440             } else {
3441                 // Disable scrolling of the app bar.
3442                 swipeRefreshLayoutParams.behavior = null
3443                 toolbarLayoutParams.scrollFlags = 0
3444                 findOnPageLayoutParams.scrollFlags = 0
3445                 tabsLayoutParams.scrollFlags = 0
3446
3447                 // Expand the app bar if it is currently collapsed.
3448                 appBarLayout.setExpanded(true)
3449             }
3450
3451             // Set the app bar scrolling for each WebView.
3452             for (i in 0 until webViewPagerAdapter!!.count) {
3453                 // Get the WebView tab fragment.
3454                 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
3455
3456                 // Get the fragment view.
3457                 val fragmentView = webViewTabFragment.view
3458
3459                 // Only modify the WebViews if they exist.
3460                 if (fragmentView != null) {
3461                     // Get the nested scroll WebView from the tab fragment.
3462                     val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
3463
3464                     // Set the app bar scrolling.
3465                     nestedScrollWebView.isNestedScrollingEnabled = scrollAppBar
3466                 }
3467             }
3468         }
3469
3470         // Update the full screen browsing mode settings.
3471         if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
3472             // Update the visibility of the app bar, which might have changed in the settings.
3473             if (hideAppBar) {
3474                 // Hide the tab linear layout.
3475                 tabsLinearLayout.visibility = View.GONE
3476
3477                 // Hide the app bar.
3478                 appBar.hide()
3479             } else {
3480                 // Show the tab linear layout.
3481                 tabsLinearLayout.visibility = View.VISIBLE
3482
3483                 // Show the app bar.
3484                 appBar.show()
3485             }
3486
3487             /* Hide the system bars.
3488              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3489              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3490              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3491              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3492              */
3493
3494             // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
3495             @Suppress("DEPRECATION")
3496             rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
3497         } else {  // Privacy Browser is not in full screen browsing mode.
3498             // Reset the full screen tracker, which could be true if Privacy Browser was in full screen mode before entering settings and full screen browsing was disabled.
3499             inFullScreenBrowsingMode = false
3500
3501             // Show the tab linear layout.
3502             tabsLinearLayout.visibility = View.VISIBLE
3503
3504             // Show the app bar.
3505             appBar.show()
3506
3507             // Remove the `SYSTEM_UI` flags from the root frame layout.  The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
3508             @Suppress("DEPRECATION")
3509             rootFrameLayout.systemUiVisibility = 0
3510         }
3511     }
3512
3513     override fun navigateHistory(url: String, steps: Int) {
3514         // Apply the domain settings.
3515         applyDomainSettings(currentWebView!!, url, resetTab = false, reloadWebsite = false, loadUrl = false)
3516
3517         // Load the history entry.
3518         currentWebView!!.goBackOrForward(steps)
3519     }
3520
3521     override fun pinnedErrorGoBack() {
3522         // Get the current web back forward list.
3523         val webBackForwardList = currentWebView!!.copyBackForwardList()
3524
3525         // Get the previous entry URL.
3526         val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url
3527
3528         // Apply the domain settings.
3529         applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
3530
3531         // Go back.
3532         currentWebView!!.goBack()
3533     }
3534
3535     // `reloadWebsite` is used if returning from the Domains activity.  Otherwise JavaScript might not function correctly if it is newly enabled.
3536     @SuppressLint("SetJavaScriptEnabled")
3537     private fun applyDomainSettings(nestedScrollWebView: NestedScrollWebView, url: String?, resetTab: Boolean, reloadWebsite: Boolean, loadUrl: Boolean) {
3538         // Store the current URL.
3539         nestedScrollWebView.currentUrl = url!!
3540
3541         // Parse the URL into a URI.
3542         val uri = Uri.parse(url)
3543
3544         // Extract the domain from the URI.
3545         var newHostName = uri.host
3546
3547         // Strings don't like to be null.
3548         if (newHostName == null)
3549             newHostName = ""
3550
3551         // Apply the domain settings if a new domain is being loaded or if the new domain is blank.  This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc.
3552         if (nestedScrollWebView.currentDomainName != newHostName || newHostName == "") {
3553             // Set the new host name as the current domain name.
3554             nestedScrollWebView.currentDomainName = newHostName
3555
3556             // Reset the ignoring of pinned domain information.
3557             nestedScrollWebView.ignorePinnedDomainInformation = false
3558
3559             // Clear any pinned SSL certificate or IP addresses.
3560             nestedScrollWebView.clearPinnedSslCertificate()
3561             nestedScrollWebView.pinnedIpAddresses = ""
3562
3563             // Reset the favorite icon if specified.
3564             if (resetTab) {
3565                 // Initialize the favorite icon.
3566                 nestedScrollWebView.initializeFavoriteIcon()
3567
3568                 // Get the current page position.
3569                 val currentPagePosition = webViewPagerAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
3570
3571                 // Get the corresponding tab.
3572                 val tab = tabLayout.getTabAt(currentPagePosition)
3573
3574                 // Update the tab if it isn't null, which sometimes happens when restarting from the background.
3575                 if (tab != null) {
3576                     // Get the tab custom view.
3577                     val tabCustomView = tab.customView!!
3578
3579                     // Get the tab views.
3580                     val tabFavoriteIconImageView = tabCustomView.findViewById<ImageView>(R.id.favorite_icon_imageview)
3581                     val tabTitleTextView = tabCustomView.findViewById<TextView>(R.id.title_textview)
3582
3583                     // Set the default favorite icon as the favorite icon for this tab.
3584                     tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteIcon(), 64, 64, true))
3585
3586                     // Set the loading title text.
3587                     tabTitleTextView.setText(R.string.loading)
3588                 }
3589             }
3590
3591             // Initialize the domain name in database variable.
3592             var domainNameInDatabase: String? = null
3593
3594             // Check the hostname against the domain settings set.
3595             if (domainsSettingsSet.contains(newHostName)) {  // The hostname is contained in the domain settings set.
3596                 // Record the domain name in the database.
3597                 domainNameInDatabase = newHostName
3598
3599                 // Set the domain settings applied tracker to true.
3600                 nestedScrollWebView.domainSettingsApplied = true
3601             } else {  // The hostname is not contained in the domain settings set.
3602                 // Set the domain settings applied tracker to false.
3603                 nestedScrollWebView.domainSettingsApplied = false
3604             }
3605
3606             // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3607             while (!nestedScrollWebView.domainSettingsApplied && newHostName!!.contains(".")) {  // Stop checking if domain settings are already applied or there are no more `.` in the hostname.
3608                 if (domainsSettingsSet.contains("*.$newHostName")) {  // Check the host name prepended by `*.`.
3609                     // Set the domain settings applied tracker to true.
3610                     nestedScrollWebView.domainSettingsApplied = true
3611
3612                     // Store the applied domain names as it appears in the database.
3613                     domainNameInDatabase = "*.$newHostName"
3614                 }
3615
3616                 // Strip out the lowest subdomain of of the host name.
3617                 newHostName = newHostName.substring(newHostName.indexOf(".") + 1)
3618             }
3619
3620             // Store the general preference information.
3621             val defaultFontSizeString = sharedPreferences.getString(getString(R.string.font_size_key), getString(R.string.font_size_default_value))
3622             val defaultUserAgentName = sharedPreferences.getString(getString(R.string.user_agent_key), getString(R.string.user_agent_default_value))
3623             val defaultSwipeToRefresh = sharedPreferences.getBoolean(getString(R.string.swipe_to_refresh_key), true)
3624             val webViewTheme = sharedPreferences.getString(getString(R.string.webview_theme_key), getString(R.string.webview_theme_default_value))
3625             val wideViewport = sharedPreferences.getBoolean(getString(R.string.wide_viewport_key), true)
3626             val displayWebpageImages = sharedPreferences.getBoolean(getString(R.string.display_webpage_images_key), true)
3627
3628             // Get the WebView theme entry values string array.
3629             val webViewThemeEntryValuesStringArray = resources.getStringArray(R.array.webview_theme_entry_values)
3630
3631             // Initialize the user agent array adapter and string array.
3632             val userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item)
3633             val userAgentDataArray = resources.getStringArray(R.array.user_agent_data)
3634
3635             // Apply either the domain settings for the default settings.
3636             if (nestedScrollWebView.domainSettingsApplied) {  // The url has custom domain settings.
3637                 // Get a cursor for the current host.
3638                 val currentDomainSettingsCursor = domainsDatabaseHelper!!.getCursorForDomainName(domainNameInDatabase!!)
3639
3640                 // Move to the first position.
3641                 currentDomainSettingsCursor.moveToFirst()
3642
3643                 // Get the settings from the cursor.
3644                 nestedScrollWebView.domainSettingsDatabaseId = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ID))
3645                 nestedScrollWebView.settings.javaScriptEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1
3646                 nestedScrollWebView.acceptCookies = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.COOKIES)) == 1
3647                 nestedScrollWebView.settings.domStorageEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1
3648                 // Form data can be removed once the minimum API >= 26.
3649                 val saveFormData = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1
3650                 nestedScrollWebView.easyListEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1
3651                 nestedScrollWebView.easyPrivacyEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1
3652                 nestedScrollWebView.fanboysAnnoyanceListEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1
3653                 nestedScrollWebView.fanboysSocialBlockingListEnabled =
3654                     currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1
3655                 nestedScrollWebView.ultraListEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ULTRALIST)) == 1
3656                 nestedScrollWebView.ultraPrivacyEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1
3657                 nestedScrollWebView.blockAllThirdPartyRequests = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1
3658                 val userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.USER_AGENT))
3659                 val fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.FONT_SIZE))
3660                 val swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SWIPE_TO_REFRESH))
3661                 val webViewThemeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WEBVIEW_THEME))
3662                 val wideViewportInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WIDE_VIEWPORT))
3663                 val displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DISPLAY_IMAGES))
3664                 val pinnedSslCertificate = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1
3665                 val pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME))
3666                 val pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION))
3667                 val pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT))
3668                 val pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME))
3669                 val pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION))
3670                 val pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT))
3671                 val pinnedSslStartDate = Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_START_DATE)))
3672                 val pinnedSslEndDate = Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_END_DATE)))
3673                 val pinnedIpAddresses = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1
3674                 val pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.IP_ADDRESSES))
3675
3676                 // Close the current host domain settings cursor.
3677                 currentDomainSettingsCursor.close()
3678
3679                 // If there is a pinned SSL certificate, store it in the WebView.
3680                 if (pinnedSslCertificate)
3681                     nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3682                         pinnedSslStartDate, pinnedSslEndDate)
3683
3684                 // If there is a pinned IP address, store it in the WebView.
3685                 if (pinnedIpAddresses)
3686                     nestedScrollWebView.pinnedIpAddresses = pinnedHostIpAddresses
3687
3688                 // Apply the cookie domain settings.
3689                 cookieManager.setAcceptCookie(nestedScrollWebView.acceptCookies)
3690
3691                 // Apply the form data setting if the API < 26.
3692                 @Suppress("DEPRECATION")
3693                 if (Build.VERSION.SDK_INT < 26)
3694                     nestedScrollWebView.settings.saveFormData = saveFormData
3695
3696                 // Apply the font size.
3697                 try {  // Try the specified font size to see if it is valid.
3698                     if (fontSize == 0) {  // Apply the default font size.
3699                         // Set the font size from the value in the app settings.
3700                         nestedScrollWebView.settings.textZoom = defaultFontSizeString!!.toInt()
3701                     } else {  // Apply the font size from domain settings.
3702                         nestedScrollWebView.settings.textZoom = fontSize
3703                     }
3704                 } catch (exception: Exception) {  // The specified font size is invalid
3705                     // Set the font size to be 100%
3706                     nestedScrollWebView.settings.textZoom = 100
3707                 }
3708
3709                 // Set the user agent.
3710                 if (userAgentName == getString(R.string.system_default_user_agent)) {  // Use the system default user agent.
3711                     // Set the user agent according to the system default.
3712                     when (val defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName)) {
3713                         // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3714                         UNRECOGNIZED_USER_AGENT -> nestedScrollWebView.settings.userAgentString = defaultUserAgentName
3715
3716                         // Set the user agent to `""`, which uses the default value.
3717                         SETTINGS_WEBVIEW_DEFAULT_USER_AGENT -> nestedScrollWebView.settings.userAgentString = ""
3718
3719                         // Set the default custom user agent.
3720                         SETTINGS_CUSTOM_USER_AGENT -> nestedScrollWebView.settings.userAgentString =
3721                             sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value))
3722
3723                         // Get the user agent string from the user agent data array
3724                         else -> nestedScrollWebView.settings.userAgentString = userAgentDataArray[defaultUserAgentArrayPosition]
3725                     }
3726                 } else {  // Set the user agent according to the stored name.
3727                     // Set the user agent.
3728                     when (val userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName)) {
3729                         // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3730                         UNRECOGNIZED_USER_AGENT ->
3731                             nestedScrollWebView.settings.userAgentString = userAgentName
3732
3733                         // Set the user agent to `""`, which uses the default value.
3734                         SETTINGS_WEBVIEW_DEFAULT_USER_AGENT ->
3735                             nestedScrollWebView.settings.userAgentString = ""
3736
3737                         // Get the user agent string from the user agent data array.
3738                         else ->
3739                             nestedScrollWebView.settings.userAgentString = userAgentDataArray[userAgentArrayPosition]
3740                     }
3741                 }
3742
3743                 // Set swipe to refresh.
3744                 when (swipeToRefreshInt) {
3745                     DomainsDatabaseHelper.SYSTEM_DEFAULT -> {
3746                         // Store the swipe to refresh status in the nested scroll WebView.
3747                         nestedScrollWebView.swipeToRefresh = defaultSwipeToRefresh
3748
3749                         // Update the swipe refresh layout.
3750                         if (defaultSwipeToRefresh) {  // Swipe to refresh is enabled.
3751                             // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null).
3752                             if (currentWebView != null) {
3753                                 // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
3754                                 swipeRefreshLayout.isEnabled = (currentWebView!!.scrollY == 0)
3755                             }
3756                         } else {  // Swipe to refresh is disabled.
3757                             // Disable the swipe refresh layout.
3758                             swipeRefreshLayout.isEnabled = false
3759                         }
3760                     }
3761
3762                     DomainsDatabaseHelper.ENABLED -> {
3763                         // Store the swipe to refresh status in the nested scroll WebView.
3764                         nestedScrollWebView.swipeToRefresh = true
3765
3766                         // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null).
3767                         if (currentWebView != null) {
3768                             // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
3769                             swipeRefreshLayout.isEnabled = (currentWebView!!.scrollY == 0)
3770                         }
3771                     }
3772
3773                     DomainsDatabaseHelper.DISABLED -> {
3774                         // Store the swipe to refresh status in the nested scroll WebView.
3775                         nestedScrollWebView.swipeToRefresh = false
3776
3777                         // Disable swipe to refresh.
3778                         swipeRefreshLayout.isEnabled = false
3779                     }
3780                 }
3781
3782                 // Set the WebView theme if device is running API >= 29 and algorithmic darkening is supported.
3783                 if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
3784                     // Set the WebView theme.
3785                     when (webViewThemeInt) {
3786                         // Set the WebView theme.
3787                         DomainsDatabaseHelper.SYSTEM_DEFAULT ->
3788                             when (webViewTheme) {
3789                                 // The light theme is selected.  Turn off algorithmic darkening.
3790                                 webViewThemeEntryValuesStringArray[1] -> WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
3791
3792                                 // The dark theme is selected.  Turn on algorithmic darkening.
3793                                 webViewThemeEntryValuesStringArray[2] -> WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
3794
3795                                 // The system default theme is selected.
3796                                 else -> {
3797                                     // Get the current system theme status.
3798                                     val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
3799
3800                                     // Set the algorithmic darkening according to the current system theme status.
3801                                     WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, currentThemeStatus == Configuration.UI_MODE_NIGHT_YES)
3802                                 }
3803                             }
3804
3805                         // Turn off algorithmic darkening.
3806                         DomainsDatabaseHelper.LIGHT_THEME -> WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
3807
3808                         // Turn on algorithmic darkening.
3809                         DomainsDatabaseHelper.DARK_THEME -> WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
3810                     }
3811                 }
3812
3813                 // Set the wide viewport status.
3814                 when (wideViewportInt) {
3815                     DomainsDatabaseHelper.SYSTEM_DEFAULT -> nestedScrollWebView.settings.useWideViewPort = wideViewport
3816                     DomainsDatabaseHelper.ENABLED -> nestedScrollWebView.settings.useWideViewPort = true
3817                     DomainsDatabaseHelper.DISABLED -> nestedScrollWebView.settings.useWideViewPort = false
3818                 }
3819
3820                 // Set the display webpage images status.
3821                 when (displayWebpageImagesInt) {
3822                     DomainsDatabaseHelper.SYSTEM_DEFAULT -> nestedScrollWebView.settings.loadsImagesAutomatically = displayWebpageImages
3823                     DomainsDatabaseHelper.ENABLED -> nestedScrollWebView.settings.loadsImagesAutomatically = true
3824                     DomainsDatabaseHelper.DISABLED -> nestedScrollWebView.settings.loadsImagesAutomatically = false
3825                 }
3826
3827                 // Set a background on the URL relative layout to indicate that custom domain settings are being used.
3828                 urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.drawable.domain_settings_url_background)
3829             } else {  // The new URL does not have custom domain settings.  Load the defaults.
3830                 // Store the values from the shared preferences.
3831                 nestedScrollWebView.settings.javaScriptEnabled = sharedPreferences.getBoolean(getString(R.string.javascript_key), false)
3832                 nestedScrollWebView.acceptCookies = sharedPreferences.getBoolean(getString(R.string.cookies_key), false)
3833                 nestedScrollWebView.settings.domStorageEnabled = sharedPreferences.getBoolean(getString(R.string.dom_storage_key), false)
3834                 val saveFormData = sharedPreferences.getBoolean(getString(R.string.save_form_data_key), false) // Form data can be removed once the minimum API >= 26.
3835                 nestedScrollWebView.easyListEnabled = sharedPreferences.getBoolean(getString(R.string.easylist_key), true)
3836                 nestedScrollWebView.easyPrivacyEnabled = sharedPreferences.getBoolean(getString(R.string.easyprivacy_key), true)
3837                 nestedScrollWebView.fanboysAnnoyanceListEnabled = sharedPreferences.getBoolean(getString(R.string.fanboys_annoyance_list_key), true)
3838                 nestedScrollWebView.fanboysSocialBlockingListEnabled = sharedPreferences.getBoolean(getString(R.string.fanboys_social_blocking_list_key), true)
3839                 nestedScrollWebView.ultraListEnabled = sharedPreferences.getBoolean(getString(R.string.ultralist_key), true)
3840                 nestedScrollWebView.ultraPrivacyEnabled = sharedPreferences.getBoolean(getString(R.string.ultraprivacy_key), true)
3841                 nestedScrollWebView.blockAllThirdPartyRequests = sharedPreferences.getBoolean(getString(R.string.block_all_third_party_requests_key), false)
3842
3843                 // Apply the default cookie setting.
3844                 cookieManager.setAcceptCookie(nestedScrollWebView.acceptCookies)
3845
3846                 // Apply the default font size setting.
3847                 try {
3848                     // Try to set the font size from the value in the app settings.
3849                     nestedScrollWebView.settings.textZoom = defaultFontSizeString!!.toInt()
3850                 } catch (exception: Exception) {
3851                     // If the app settings value is invalid, set the font size to 100%.
3852                     nestedScrollWebView.settings.textZoom = 100
3853                 }
3854
3855                 // Apply the form data setting if the API < 26.
3856                 if (Build.VERSION.SDK_INT < 26)
3857                     @Suppress("DEPRECATION")
3858                     nestedScrollWebView.settings.saveFormData = saveFormData
3859
3860                 // Store the swipe to refresh status in the nested scroll WebView.
3861                 nestedScrollWebView.swipeToRefresh = defaultSwipeToRefresh
3862
3863                 // Update the swipe refresh layout.
3864                 if (defaultSwipeToRefresh) {  // Swipe to refresh is enabled.
3865                     // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null).
3866                     if (currentWebView != null) {
3867                         // Only enable the swipe refresh layout if the WebView is scrolled to the top.  It is updated every time the scroll changes.
3868                         swipeRefreshLayout.isEnabled = currentWebView!!.scrollY == 0
3869                     }
3870                 } else {  // Swipe to refresh is disabled.
3871                     // Disable the swipe refresh layout.
3872                     swipeRefreshLayout.isEnabled = false
3873                 }
3874
3875                 // Reset the domain settings database ID.
3876                 nestedScrollWebView.domainSettingsDatabaseId = -1
3877
3878                 // Set the user agent.
3879                 when (val userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName)) {
3880                     // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3881                     UNRECOGNIZED_USER_AGENT -> nestedScrollWebView.settings.userAgentString = defaultUserAgentName
3882
3883                     // Set the user agent to `""`, which uses the default value.
3884                     SETTINGS_WEBVIEW_DEFAULT_USER_AGENT -> nestedScrollWebView.settings.userAgentString = ""
3885
3886                     // Set the default custom user agent.
3887                     SETTINGS_CUSTOM_USER_AGENT -> nestedScrollWebView.settings.userAgentString =
3888                         sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value))
3889
3890                     // Get the user agent string from the user agent data array
3891                     else -> nestedScrollWebView.settings.userAgentString = userAgentDataArray[userAgentArrayPosition]
3892                 }
3893
3894                 // Set the WebView theme if the device is running API >= 29 and algorithmic darkening is supported.
3895                 if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
3896                     // Set the WebView theme.
3897                     when (webViewTheme) {
3898                         // The light theme is selected.  Turn off algorithmic darkening.
3899                         webViewThemeEntryValuesStringArray[1] -> WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
3900
3901                         // The dark theme is selected.  Turn on algorithmic darkening.
3902                         webViewThemeEntryValuesStringArray[2] -> WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
3903
3904                         // The system default theme is selected.  Get the current system theme status.
3905                         else -> {
3906                             // Get the current theme status.
3907                             val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
3908
3909                             // Set the algorithmic darkening according to the current system theme status.
3910                             WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, currentThemeStatus == Configuration.UI_MODE_NIGHT_YES)
3911                         }
3912                     }
3913                 }
3914
3915                 // Set the viewport.
3916                 nestedScrollWebView.settings.useWideViewPort = wideViewport
3917
3918                 // Set the loading of webpage images.
3919                 nestedScrollWebView.settings.loadsImagesAutomatically = displayWebpageImages
3920
3921                 // Set a transparent background on the URL relative layout.
3922                 urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.color.transparent)
3923             }
3924
3925             // Update the privacy icons.
3926             updatePrivacyIcons(true)
3927         }
3928
3929         // Reload the website if returning from the Domains activity.
3930         if (reloadWebsite)
3931             nestedScrollWebView.reload()
3932
3933         // Load the URL if directed.  This makes sure that the domain settings are properly loaded before the URL.  By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent.
3934         if (loadUrl)
3935             nestedScrollWebView.loadUrl(url)
3936     }
3937
3938     private fun applyProxy(reloadWebViews: Boolean) {
3939         // Set the proxy according to the mode.
3940         proxyHelper.setProxy(applicationContext, appBarLayout, proxyMode)
3941
3942         // Reset the waiting for proxy tracker.
3943         waitingForProxy = false
3944
3945         // Set the proxy.
3946         when (proxyMode) {
3947             ProxyHelper.NONE -> {
3948                 // Initialize a color background typed value.
3949                 val colorBackgroundTypedValue = TypedValue()
3950
3951                 // Get the color background from the theme.
3952                 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
3953
3954                 // Get the color background int from the typed value.
3955                 val colorBackgroundInt = colorBackgroundTypedValue.data
3956
3957                 // Set the default app bar layout background.
3958                 appBarLayout.setBackgroundColor(colorBackgroundInt)
3959             }
3960
3961             ProxyHelper.TOR -> {
3962                 // Set the app bar background to indicate proxying is enabled.
3963                 appBarLayout.setBackgroundResource(R.color.proxy_appbar_background)
3964
3965                 // Check to see if Orbot is installed.
3966                 try {
3967                     // Get the package manager.
3968                     val packageManager = packageManager
3969
3970                     // Check to see if Orbot is in the list.  This will throw an error and drop to the catch section if it isn't installed.  The deprecated method must be used until the minimum API >= 33.
3971                     @Suppress("DEPRECATION")
3972                     packageManager.getPackageInfo("org.torproject.android", 0)
3973
3974                     // Check to see if the proxy is ready.
3975                     if (orbotStatus != ProxyHelper.ORBOT_STATUS_ON) {  // Orbot is not ready.
3976                         // Set the waiting for proxy status.
3977                         waitingForProxy = true
3978
3979                         // Show the waiting for proxy dialog if it isn't already displayed.
3980                         if (supportFragmentManager.findFragmentByTag(getString(R.string.waiting_for_proxy_dialog)) == null) {
3981                             // Get a handle for the waiting for proxy alert dialog.
3982                             val waitingForProxyDialogFragment = WaitingForProxyDialog()
3983
3984                             // Try to show the dialog.  Sometimes the window is not yet active if returning from Settings.
3985                             try {
3986                                 // Show the waiting for proxy alert dialog.
3987                                 waitingForProxyDialogFragment.show(supportFragmentManager, getString(R.string.waiting_for_proxy_dialog))
3988                             } catch (waitingForTorException: Exception) {
3989                                 // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
3990                                 pendingDialogsArrayList.add(PendingDialogDataClass(waitingForProxyDialogFragment, getString(R.string.waiting_for_proxy_dialog)))
3991                             }
3992                         }
3993                     }
3994                 } catch (exception: PackageManager.NameNotFoundException) {  // Orbot is not installed.
3995                     // Show the Orbot not installed dialog if it is not already displayed.
3996                     if (supportFragmentManager.findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
3997                         // Get a handle for the Orbot not installed alert dialog.
3998                         val orbotNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode)
3999
4000                         // Try to show the dialog.  Sometimes the window is not yet active if returning from Settings.
4001                         try {
4002                             // Display the Orbot not installed alert dialog.
4003                             orbotNotInstalledDialogFragment.show(supportFragmentManager, getString(R.string.proxy_not_installed_dialog))
4004                         } catch (orbotNotInstalledException: Exception) {
4005                             // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
4006                             pendingDialogsArrayList.add(PendingDialogDataClass(orbotNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)))
4007                         }
4008                     }
4009                 }
4010             }
4011
4012             ProxyHelper.I2P -> {
4013                 // Set the app bar background to indicate proxying is enabled.
4014                 appBarLayout.setBackgroundResource(R.color.proxy_appbar_background)
4015
4016                 // Check to see if I2P is installed.
4017                 try {
4018                     // Check to see if the F-Droid flavor is installed.  This will throw an error and drop to the catch section if it isn't installed.
4019                     // The deprecated method must be used until the minimum API >= 33.
4020                     @Suppress("DEPRECATION")
4021                     packageManager.getPackageInfo("net.i2p.android.router", 0)
4022                 } catch (fdroidException: PackageManager.NameNotFoundException) {  // The F-Droid flavor is not installed.
4023                     try {
4024                         // Check to see if the Google Play flavor is installed.  This will throw an error and drop to the catch section if it isn't installed.
4025                         // The deprecated method must be used until the minimum API >= 33.
4026                         @Suppress("DEPRECATION")
4027                         packageManager.getPackageInfo("net.i2p.android", 0)
4028                     } catch (googlePlayException: PackageManager.NameNotFoundException) {  // The Google Play flavor is not installed.
4029                         // Sow the I2P not installed dialog if it is not already displayed.
4030                         if (supportFragmentManager.findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
4031                             // Get a handle for the waiting for proxy alert dialog.
4032                             val i2pNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode)
4033
4034                             // Try to show the dialog.  Sometimes the window is not yet active if returning from Settings.
4035                             try {
4036                                 // Display the I2P not installed alert dialog.
4037                                 i2pNotInstalledDialogFragment.show(supportFragmentManager, getString(R.string.proxy_not_installed_dialog))
4038                             } catch (i2pNotInstalledException: Exception) {
4039                                 // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
4040                                 pendingDialogsArrayList.add(PendingDialogDataClass(i2pNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)))
4041                             }
4042                         }
4043                     }
4044                 }
4045             }
4046
4047             ProxyHelper.CUSTOM ->
4048                 // Set the app bar background to indicate proxying is enabled.
4049                 appBarLayout.setBackgroundResource(R.color.proxy_appbar_background)
4050         }
4051
4052         // Reload the WebViews if requested and not waiting for the proxy.
4053         if (reloadWebViews && !waitingForProxy) {
4054             // Reload the WebViews.
4055             for (i in 0 until webViewPagerAdapter!!.count) {
4056                 // Get the WebView tab fragment.
4057                 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
4058
4059                 // Get the fragment view.
4060                 val fragmentView = webViewTabFragment.view
4061
4062                 // Only reload the WebViews if they exist.
4063                 if (fragmentView != null) {
4064                     // Get the nested scroll WebView from the tab fragment.
4065                     val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
4066
4067                     // Reload the WebView.
4068                     nestedScrollWebView.reload()
4069                 }
4070             }
4071         }
4072     }
4073
4074     private fun updatePrivacyIcons(runInvalidateOptionsMenu: Boolean) {
4075         // Only update the privacy icons if the options menu and the current WebView have already been populated.
4076         if ((optionsMenu != null) && (currentWebView != null)) {
4077             // Update the privacy icon.
4078             if (currentWebView!!.settings.javaScriptEnabled)  // JavaScript is enabled.
4079                 optionsPrivacyMenuItem.setIcon(R.drawable.javascript_enabled)
4080             else if (currentWebView!!.acceptCookies)  // JavaScript is disabled but cookies are enabled.
4081                 optionsPrivacyMenuItem.setIcon(R.drawable.warning)
4082             else  // All the dangerous features are disabled.
4083                 optionsPrivacyMenuItem.setIcon(R.drawable.privacy_mode)
4084
4085             // Update the cookies icon.
4086             if (currentWebView!!.acceptCookies)
4087                 optionsCookiesMenuItem.setIcon(R.drawable.cookies_enabled)
4088             else
4089                 optionsCookiesMenuItem.setIcon(R.drawable.cookies_disabled)
4090
4091             // Update the refresh icon.
4092             if (optionsRefreshMenuItem.title == getString(R.string.refresh))  // The refresh icon is displayed.
4093                 optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled)
4094             else  // The stop icon is displayed.
4095                 optionsRefreshMenuItem.setIcon(R.drawable.close_blue)
4096
4097             // `invalidateOptionsMenu()` calls `onPrepareOptionsMenu()` and redraws the icons in the app bar.
4098             if (runInvalidateOptionsMenu)
4099                 invalidateOptionsMenu()
4100         }
4101     }
4102
4103     private fun loadBookmarksFolder() {
4104         // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4105         bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolder)
4106
4107         // Populate the bookmarks cursor adapter.
4108         bookmarksCursorAdapter = object : CursorAdapter(this, bookmarksCursor, false) {
4109             override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
4110                 // Inflate the individual item layout.
4111                 return layoutInflater.inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false)
4112             }
4113
4114             override fun bindView(view: View, context: Context, cursor: Cursor) {
4115                 // Get handles for the views.
4116                 val bookmarkFavoriteIcon = view.findViewById<ImageView>(R.id.bookmark_favorite_icon)
4117                 val bookmarkNameTextView = view.findViewById<TextView>(R.id.bookmark_name)
4118
4119                 // Get the favorite icon byte array from the cursor.
4120                 val favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
4121
4122                 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
4123                 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
4124
4125                 // Display the bitmap in the bookmark favorite icon.
4126                 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap)
4127
4128                 // Display the bookmark name from the cursor in the bookmark name text view.
4129                 bookmarkNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
4130
4131                 // Make the font bold for folders.
4132                 if (cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1)
4133                     bookmarkNameTextView.typeface = Typeface.DEFAULT_BOLD
4134                 else  // Reset the font to default for normal bookmarks.
4135                     bookmarkNameTextView.typeface = Typeface.DEFAULT
4136             }
4137         }
4138
4139         // Populate the list view with the adapter.
4140         bookmarksListView.adapter = bookmarksCursorAdapter
4141
4142         // Set the bookmarks drawer title.
4143         if (currentBookmarksFolder.isEmpty())
4144             bookmarksTitleTextView.setText(R.string.bookmarks)
4145         else
4146             bookmarksTitleTextView.text = currentBookmarksFolder
4147     }
4148
4149     private fun openWithApp(url: String) {
4150         // Create an open with app intent with `ACTION_VIEW`.
4151         val openWithAppIntent = Intent(Intent.ACTION_VIEW)
4152
4153         // Set the URI but not the MIME type.  This should open all available apps.
4154         openWithAppIntent.data = Uri.parse(url)
4155
4156         // Flag the intent to open in a new task.
4157         openWithAppIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
4158
4159         // Try the intent.
4160         try {
4161             // Show the chooser.
4162             startActivity(openWithAppIntent)
4163         } catch (exception: ActivityNotFoundException) {  // There are no apps available to open the URL.
4164             // Show a snackbar with the error.
4165             Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
4166         }
4167     }
4168
4169     private fun openWithBrowser(url: String) {
4170
4171         // Create an open with browser intent with `ACTION_VIEW`.
4172         val openWithBrowserIntent = Intent(Intent.ACTION_VIEW)
4173
4174         // Set the URI and the MIME type.  `"text/html"` should load browser options.
4175         openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html")
4176
4177         // Flag the intent to open in a new task.
4178         openWithBrowserIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
4179
4180         // Try the intent.
4181         try {
4182             // Show the chooser.
4183             startActivity(openWithBrowserIntent)
4184         } catch (exception: ActivityNotFoundException) {  // There are no browsers available to open the URL.
4185             // Show a snackbar with the error.
4186             Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
4187         }
4188     }
4189
4190     private fun sanitizeUrl(urlString: String): String {
4191         // Initialize a sanitized URL string.
4192         var sanitizedUrlString = urlString
4193
4194         // Sanitize tracking queries.
4195         if (sanitizeTrackingQueries)
4196             sanitizedUrlString = SanitizeUrlHelper.sanitizeTrackingQueries(sanitizedUrlString)
4197
4198         // Sanitize AMP redirects.
4199         if (sanitizeAmpRedirects)
4200             sanitizedUrlString = SanitizeUrlHelper.sanitizeAmpRedirects(sanitizedUrlString)
4201
4202         // Return the sanitized URL string.
4203         return sanitizedUrlString
4204     }
4205
4206     override fun finishedPopulatingBlocklists(combinedBlocklists: ArrayList<ArrayList<List<Array<String>>>>) {
4207         // Store the blocklists.
4208         easyList = combinedBlocklists[0]
4209         easyPrivacy = combinedBlocklists[1]
4210         fanboysAnnoyanceList = combinedBlocklists[2]
4211         fanboysSocialList = combinedBlocklists[3]
4212         ultraList = combinedBlocklists[4]
4213         ultraPrivacy = combinedBlocklists[5]
4214
4215         // Check to see if the activity has been restarted with a saved state.
4216         if ((savedStateArrayList == null) || (savedStateArrayList!!.size == 0)) {  // The activity has not been restarted or it was restarted on start to change the theme.
4217             // Add the first tab.
4218             addNewTab("", true)
4219         } else {  // The activity has been restarted.
4220             // Restore each tab.
4221             for (i in savedStateArrayList!!.indices) {
4222                 // Add a new tab.
4223                 tabLayout.addTab(tabLayout.newTab())
4224
4225                 // Get the new tab.
4226                 val newTab = tabLayout.getTabAt(i)!!
4227
4228                 // Set a custom view on the new tab.
4229                 newTab.setCustomView(R.layout.tab_custom_view)
4230
4231                 // Add the new page.
4232                 webViewPagerAdapter!!.restorePage(savedStateArrayList!![i], savedNestedScrollWebViewStateArrayList!![i])
4233             }
4234
4235             // Reset the saved state variables.
4236             savedStateArrayList = null
4237             savedNestedScrollWebViewStateArrayList = null
4238
4239             // Restore the selected tab position.
4240             if (savedTabPosition == 0) {  // The first tab is selected.
4241                 // Set the first page as the current WebView.
4242                 setCurrentWebView(0)
4243             } else {  // The first tab is not selected.
4244                 // Move to the selected tab.
4245                 webViewPager.currentItem = savedTabPosition
4246             }
4247
4248             // Get the intent that started the app.
4249             val intent = intent
4250
4251             // Reset the intent.  This prevents a duplicate tab from being created on restart.
4252             setIntent(Intent())
4253
4254             // Get the information from the intent.
4255             val intentAction = intent.action
4256             val intentUriData = intent.data
4257             val intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT)
4258
4259             // Determine if this is a web search.
4260             val isWebSearch = (intentAction != null) && (intentAction == Intent.ACTION_WEB_SEARCH)
4261
4262             // Only process the URI if it contains data or it is a web search.  If the user pressed the desktop icon after the app was already running the URI will be null.
4263             if ((intentUriData != null) || (intentStringExtra != null) || isWebSearch) {
4264                 // Get the URL string.
4265                 val urlString = if (isWebSearch) {  // The intent is a web search.
4266                     // Sanitize the search input.
4267                     val encodedSearchString: String = try {
4268                         URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8")
4269                     } catch (exception: UnsupportedEncodingException) {
4270                         ""
4271                     }
4272
4273                     // Add the base search URL.
4274                     searchURL + encodedSearchString
4275                 } else { // The intent contains a URL formatted as a URI or a URL in the string extra.
4276                     // Get the URL string.
4277                     intentUriData?.toString() ?: intentStringExtra!!
4278                 }
4279
4280                 // Add a new tab if specified in the preferences.
4281                 if (sharedPreferences.getBoolean(getString(R.string.open_intents_in_new_tab_key), true)) {  // Load the URL in a new tab.
4282                     // Set the loading new intent flag.
4283                     loadingNewIntent = true
4284
4285                     // Add a new tab.
4286                     addNewTab(urlString, true)
4287                 } else {  // Load the URL in the current tab.
4288                     // Make it so.
4289                     loadUrl(currentWebView!!, urlString)
4290                 }
4291             }
4292         }
4293     }
4294
4295     // The view parameter cannot be removed because it is called from the layout onClick.
4296     fun addTab(@Suppress("UNUSED_PARAMETER")view: View?) {
4297         // Add a new tab with a blank URL.
4298         addNewTab("", true)
4299     }
4300
4301     private fun addNewTab(urlString: String, moveToTab: Boolean) {
4302         // Clear the focus from the URL edit text, so that it will be populated with the information from the new tab.
4303         urlEditText.clearFocus()
4304
4305         // Get the new page number.  The page numbers are 0 indexed, so the new page number will match the current count.
4306         val newTabNumber = tabLayout.tabCount
4307
4308         // Add a new tab.
4309         tabLayout.addTab(tabLayout.newTab())
4310
4311         // Get the new tab.
4312         val newTab = tabLayout.getTabAt(newTabNumber)!!
4313
4314         // Set a custom view on the new tab.
4315         newTab.setCustomView(R.layout.tab_custom_view)
4316
4317         // Add the new WebView page.
4318         webViewPagerAdapter!!.addPage(newTabNumber, webViewPager, urlString, moveToTab)
4319
4320         // Show the app bar if it is at the bottom of the screen and the new tab is taking focus.
4321         if (bottomAppBar && moveToTab && appBarLayout.translationY != 0f) {
4322             // Animate the bottom app bar onto the screen.
4323             objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0f)
4324
4325             // Make it so.
4326             objectAnimator.start()
4327         }
4328     }
4329
4330     // The view parameter cannot be removed because it is called from the layout onClick.
4331     fun closeTab(@Suppress("UNUSED_PARAMETER")view: View?) {
4332         // Run the command according to the number of tabs.
4333         if (tabLayout.tabCount > 1)  // There is more than one tab open.
4334             closeCurrentTab()
4335         else  // There is only one tab open.
4336             clearAndExit()
4337     }
4338
4339     private fun closeCurrentTab() {
4340         // Get the current tab number.
4341         val currentTabNumber = tabLayout.selectedTabPosition
4342
4343         // Delete the current tab.
4344         tabLayout.removeTabAt(currentTabNumber)
4345
4346         // Delete the current page.  If the selected page number did not change during the delete (because the newly selected tab has has same number as the previously deleted tab), it will return true,
4347         // meaning that the current WebView must be reset.  Otherwise it will happen automatically as the selected tab number changes.
4348         if (webViewPagerAdapter!!.deletePage(currentTabNumber, webViewPager))
4349             setCurrentWebView(currentTabNumber)
4350     }
4351
4352     private fun exitFullScreenVideo() {
4353         // Re-enable the screen timeout.
4354         fullScreenVideoFrameLayout.keepScreenOn = false
4355
4356         // Unset the full screen video flag.
4357         displayingFullScreenVideo = false
4358
4359         // Remove all the views from the full screen video frame layout.
4360         fullScreenVideoFrameLayout.removeAllViews()
4361
4362         // Hide the full screen video frame layout.
4363         fullScreenVideoFrameLayout.visibility = View.GONE
4364
4365         // Enable the sliding drawers.
4366         drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
4367
4368         // Show the coordinator layout.
4369         coordinatorLayout.visibility = View.VISIBLE
4370
4371         // Apply the appropriate full screen mode flags.
4372         if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) {  // Privacy Browser is currently in full screen browsing mode.
4373             // Hide the app bar if specified.
4374             if (hideAppBar) {
4375                 // Hide the tab linear layout.
4376                 tabsLinearLayout.visibility = View.GONE
4377
4378                 // Hide the app bar.
4379                 appBar.hide()
4380             }
4381
4382             /* Hide the system bars.
4383              * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4384              * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4385              * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4386              * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4387              */
4388
4389             // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4390             @Suppress("DEPRECATION")
4391             rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
4392         } else {  // Switch to normal viewing mode.
4393             // Remove the `SYSTEM_UI` flags from the root frame layout.  The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4394             @Suppress("DEPRECATION")
4395             rootFrameLayout.systemUiVisibility = 0
4396         }
4397     }
4398
4399     private fun clearAndExit() {
4400         // Close the bookmarks cursor if it exists.
4401         bookmarksCursor?.close()
4402
4403         // Close the databases helpers if they exist.
4404         bookmarksDatabaseHelper?.close()
4405         domainsDatabaseHelper?.close()
4406
4407         // Get the status of the clear everything preference.
4408         val clearEverything = sharedPreferences.getBoolean(getString(R.string.clear_everything_key), true)
4409
4410         // Get a handle for the runtime.
4411         val runtime = Runtime.getRuntime()
4412
4413         // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
4414         // which links to `/data/data/com.stoutner.privacybrowser.standard`.
4415         val privateDataDirectoryString = applicationInfo.dataDir
4416
4417         // Clear cookies.
4418         if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_cookies_key), true)) {
4419             // Ass the cookie manager to delete all the cookies.
4420             cookieManager.removeAllCookies(null)
4421
4422             // Ask the cookie manager to flush the cookie database.
4423             cookieManager.flush()
4424
4425             // Manually delete the cookies database, as the cookie manager sometimes will not flush its changes to disk before system exit is run.
4426             try {
4427                 // Two commands must be used because `Runtime.exec()` does not like `*`.
4428                 val deleteCookiesProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/Cookies")
4429                 val deleteCookiesJournalProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/Cookies-journal")
4430
4431                 // Wait until the processes have finished.
4432                 deleteCookiesProcess.waitFor()
4433                 deleteCookiesJournalProcess.waitFor()
4434             } catch (exception: Exception) {
4435                 // Do nothing if an error is thrown.
4436             }
4437         }
4438
4439         // Clear DOM storage.
4440         if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_dom_storage_key), true)) {
4441             // Ask web storage to clear the DOM storage.
4442             WebStorage.getInstance().deleteAllData()
4443
4444             // Manually delete the DOM storage files and directories, as web storage sometimes will not flush its changes to disk before system exit is run.
4445             try {
4446                 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4447                 val deleteLocalStorageProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Local Storage/"))
4448
4449                 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
4450                 val deleteIndexProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/IndexedDB")
4451                 val deleteQuotaManagerProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager")
4452                 val deleteQuotaManagerJournalProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager-journal")
4453                 val deleteDatabaseProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/databases")
4454
4455                 // Wait until the processes have finished.
4456                 deleteLocalStorageProcess.waitFor()
4457                 deleteIndexProcess.waitFor()
4458                 deleteQuotaManagerProcess.waitFor()
4459                 deleteQuotaManagerJournalProcess.waitFor()
4460                 deleteDatabaseProcess.waitFor()
4461             } catch (exception: Exception) {
4462                 // Do nothing if an error is thrown.
4463             }
4464         }
4465
4466         // Clear form data if the API < 26.
4467         if (Build.VERSION.SDK_INT < 26 && (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_form_data_key), true))) {
4468             // Ask the WebView database to clear the form data.
4469             @Suppress("DEPRECATION")
4470             WebViewDatabase.getInstance(this).clearFormData()
4471
4472             // Manually delete the form data database, as the WebView database sometimes will not flush its changes to disk before system exit is run.
4473             try {
4474                 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
4475                 val deleteWebDataProcess = runtime.exec(arrayOf("rm", "-f", "$privateDataDirectoryString/app_webview/Web Data"))
4476                 val deleteWebDataJournalProcess = runtime.exec(arrayOf("rm", "-f", "$privateDataDirectoryString/app_webview/Web Data-journal"))
4477
4478                 // Wait until the processes have finished.
4479                 deleteWebDataProcess.waitFor()
4480                 deleteWebDataJournalProcess.waitFor()
4481             } catch (exception: Exception) {
4482                 // Do nothing if an error is thrown.
4483             }
4484         }
4485
4486         // Clear the logcat.
4487         if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_logcat_key), true)) {
4488             try {
4489                 // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
4490                 val process = Runtime.getRuntime().exec("logcat -b all -c")
4491
4492                 // Wait for the process to finish.
4493                 process.waitFor()
4494             } catch (exception: IOException) {
4495                 // Do nothing.
4496             } catch (exception: InterruptedException) {
4497                 // Do nothing.
4498             }
4499         }
4500
4501         // Clear the cache.
4502         if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_cache_key), true)) {
4503             // Clear the cache from each WebView.
4504             for (i in 0 until webViewPagerAdapter!!.count) {
4505                 // Get the WebView tab fragment.
4506                 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
4507
4508                 // Get the WebView fragment view.
4509                 val webViewFragmentView = webViewTabFragment.view
4510
4511                 // Only clear the cache if the WebView exists.
4512                 if (webViewFragmentView != null) {
4513                     // Get the nested scroll WebView from the tab fragment.
4514                     val nestedScrollWebView = webViewFragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
4515
4516                     // Clear the cache for this WebView.
4517                     nestedScrollWebView.clearCache(true)
4518                 }
4519             }
4520
4521             // Manually delete the cache directories.
4522             try {
4523                 // Delete the main cache directory.
4524                 val deleteCacheProcess = runtime.exec("rm -rf $privateDataDirectoryString/cache")
4525
4526                 // Delete the secondary `Service Worker` cache directory.
4527                 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4528                 val deleteServiceWorkerProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Service Worker/"))
4529
4530                 // Wait until the processes have finished.
4531                 deleteCacheProcess.waitFor()
4532                 deleteServiceWorkerProcess.waitFor()
4533             } catch (exception: Exception) {
4534                 // Do nothing if an error is thrown.
4535             }
4536         }
4537
4538         // Wipe out each WebView.
4539         for (i in 0 until webViewPagerAdapter!!.count) {
4540             // Get the WebView tab fragment.
4541             val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
4542
4543             // Get the WebView frame layout.
4544             val webViewFrameLayout = webViewTabFragment.view as FrameLayout?
4545
4546             // Only wipe out the WebView if it exists.
4547             if (webViewFrameLayout != null) {
4548                 // Get the nested scroll WebView from the tab fragment.
4549                 val nestedScrollWebView = webViewFrameLayout.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
4550
4551                 // Clear SSL certificate preferences for this WebView.
4552                 nestedScrollWebView.clearSslPreferences()
4553
4554                 // Clear the back/forward history for this WebView.
4555                 nestedScrollWebView.clearHistory()
4556
4557                 // Remove all the views from the frame layout.
4558                 webViewFrameLayout.removeAllViews()
4559
4560                 // Destroy the internal state of the WebView.
4561                 nestedScrollWebView.destroy()
4562             }
4563         }
4564
4565         // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
4566         // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
4567         if (clearEverything) {
4568             try {
4569                 // Delete the folder.
4570                 val deleteAppWebviewProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview")
4571
4572                 // Wait until the process has finished.
4573                 deleteAppWebviewProcess.waitFor()
4574             } catch (exception: Exception) {
4575                 // Do nothing if an error is thrown.
4576             }
4577         }
4578
4579         // Close Privacy Browser.  `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
4580         finishAndRemoveTask()
4581
4582         // Remove the terminated program from RAM.  The status code is `0`.
4583         exitProcess(0)
4584     }
4585
4586     // The view parameter cannot be removed because it is called from the layout onClick.
4587     fun bookmarksBack(@Suppress("UNUSED_PARAMETER")view: View?) {
4588         if (currentBookmarksFolder.isEmpty()) {  // The home folder is displayed.
4589             // close the bookmarks drawer.
4590             drawerLayout.closeDrawer(GravityCompat.END)
4591         } else {  // A subfolder is displayed.
4592             // Set the former parent folder as the current folder.
4593             currentBookmarksFolder = bookmarksDatabaseHelper!!.getParentFolderName(currentBookmarksFolder)
4594
4595             // Load the new folder.
4596             loadBookmarksFolder()
4597         }
4598     }
4599
4600         // The view parameter cannot be removed because it is called from the layout onClick.
4601     fun toggleBookmarksDrawerPinned(@Suppress("UNUSED_PARAMETER")view: View?) {
4602         // Toggle the bookmarks drawer pinned tracker.
4603         bookmarksDrawerPinned = !bookmarksDrawerPinned
4604
4605         // Update the bookmarks drawer pinned image view.
4606         updateBookmarksDrawerPinnedImageView()
4607     }
4608
4609     private fun updateBookmarksDrawerPinnedImageView() {
4610         // Set the current icon.
4611         if (bookmarksDrawerPinned)
4612             bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin_selected)
4613         else
4614             bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin)
4615     }
4616
4617     private fun setCurrentWebView(pageNumber: Int) {
4618         // Stop the swipe to refresh indicator if it is running
4619         swipeRefreshLayout.isRefreshing = false
4620
4621         // Get the WebView tab fragment.
4622         val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(pageNumber)
4623
4624         // Get the fragment view.
4625         val webViewFragmentView = webViewTabFragment.view
4626
4627         // Set the current WebView if the fragment view is not null.
4628         if (webViewFragmentView != null) {  // The fragment has been populated.
4629             // Store the current WebView.
4630             currentWebView = webViewFragmentView.findViewById(R.id.nestedscroll_webview)
4631
4632             // Update the status of swipe to refresh.
4633             if (currentWebView!!.swipeToRefresh) {  // Swipe to refresh is enabled.
4634                 // Enable the swipe refresh layout if the WebView is scrolled all the way to the top.  It is updated every time the scroll changes.
4635                 swipeRefreshLayout.isEnabled = (currentWebView!!.scrollY == 0)
4636             } else {  // Swipe to refresh is disabled.
4637                 // Disable the swipe refresh layout.
4638                 swipeRefreshLayout.isEnabled = false
4639             }
4640
4641             // Set the cookie status.
4642             cookieManager.setAcceptCookie(currentWebView!!.acceptCookies)
4643
4644             // Update the privacy icons.  `true` redraws the icons in the app bar.
4645             updatePrivacyIcons(true)
4646
4647             // Get a handle for the input method manager.
4648             val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
4649
4650             // Get the current URL.
4651             val urlString = currentWebView!!.url
4652
4653             // Update the URL edit text if not loading a new intent.  Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`.
4654             if (!loadingNewIntent) {  // A new intent is not being loaded.
4655                 if ((urlString == null) || (urlString == "about:blank")) {  // The WebView is blank.
4656                     // Display the hint in the URL edit text.
4657                     urlEditText.setText("")
4658
4659                     // Request focus for the URL text box.
4660                     urlEditText.requestFocus()
4661
4662                     // Display the keyboard.
4663                     inputMethodManager.showSoftInput(urlEditText, 0)
4664                 } else {  // The WebView has a loaded URL.
4665                     // Clear the focus from the URL text box.
4666                     urlEditText.clearFocus()
4667
4668                     // Hide the soft keyboard.
4669                     inputMethodManager.hideSoftInputFromWindow(currentWebView!!.windowToken, 0)
4670
4671                     // Display the current URL in the URL text box.
4672                     urlEditText.setText(urlString)
4673
4674                     // Highlight the URL syntax.
4675                     UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
4676                 }
4677             } else {  // A new intent is being loaded.
4678                 // Reset the loading new intent flag.
4679                 loadingNewIntent = false
4680             }
4681
4682             // Set the background to indicate the domain settings status.
4683             if (currentWebView!!.domainSettingsApplied) {
4684                 // Set a background on the URL relative layout to indicate that custom domain settings are being used.
4685                 urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.drawable.domain_settings_url_background)
4686             } else {
4687                 // Remove any background on the URL relative layout.
4688                 urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.color.transparent)
4689             }
4690         } else if (pageNumber == savedTabPosition) {  // The app is being restored but the saved tab position fragment has not been populated yet.  Try again in 100 milliseconds.
4691             // Create a handler to set the current WebView.
4692             val setCurrentWebViewHandler = Handler(Looper.getMainLooper())
4693
4694             // Create a runnable to set the current WebView.
4695             val setCurrentWebWebRunnable = Runnable {
4696                 // Set the current WebView.
4697                 setCurrentWebView(pageNumber)
4698             }
4699
4700             // Try setting the current WebView again after 100 milliseconds.
4701             setCurrentWebViewHandler.postDelayed(setCurrentWebWebRunnable, 100)
4702         }
4703     }
4704
4705     @SuppressLint("ClickableViewAccessibility")
4706     override fun initializeWebView(nestedScrollWebView: NestedScrollWebView, pageNumber: Int, progressBar: ProgressBar, urlString: String, restoringState: Boolean) {
4707         // Get the WebView theme.
4708         val webViewTheme = sharedPreferences.getString(getString(R.string.webview_theme_key), getString(R.string.webview_theme_default_value))
4709
4710         // Get the WebView theme entry values string array.
4711         val webViewThemeEntryValuesStringArray = resources.getStringArray(R.array.webview_theme_entry_values)
4712
4713         // Set the WebView theme if device is running API >= 29 and algorithmic darkening is supported.
4714         if (Build.VERSION.SDK_INT >= 29 && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
4715             // Set the WebView them.  A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
4716             if (webViewTheme == webViewThemeEntryValuesStringArray[1]) {  // The light theme is selected.
4717                 // Turn off algorithmic darkening.
4718                 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
4719
4720                 // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
4721                 // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
4722                 nestedScrollWebView.visibility = View.VISIBLE
4723             } else if (webViewTheme == webViewThemeEntryValuesStringArray[2]) {  // The dark theme is selected.
4724                 // Turn on algorithmic darkening.
4725                 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
4726             } else {  // The system default theme is selected.
4727                 // Get the current theme status.
4728                 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
4729
4730                 // Set the algorithmic darkening according to the current system theme status.
4731                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {  // The system is in day mode.
4732                     // Turn off algorithmic darkening.
4733                     WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
4734
4735                     // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
4736                     // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
4737                     nestedScrollWebView.visibility = View.VISIBLE
4738                 } else {  // The system is in night mode.
4739                     // Turn on algorithmic darkening.
4740                     WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
4741                 }
4742             }
4743         }
4744
4745         // Get a handle for the input method manager.
4746         val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
4747
4748         // Set the app bar scrolling.
4749         nestedScrollWebView.isNestedScrollingEnabled = scrollAppBar
4750
4751         // Allow pinch to zoom.
4752         nestedScrollWebView.settings.builtInZoomControls = true
4753
4754         // Hide zoom controls.
4755         nestedScrollWebView.settings.displayZoomControls = false
4756
4757         // Don't allow mixed content (HTTP and HTTPS) on the same website.
4758         nestedScrollWebView.settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
4759
4760         // Set the WebView to load in overview mode (zoomed out to the maximum width).
4761         nestedScrollWebView.settings.loadWithOverviewMode = true
4762
4763         // Explicitly disable geolocation.
4764         nestedScrollWebView.settings.setGeolocationEnabled(false)
4765
4766         // Allow loading of file:// URLs.  This is necessary for opening MHT web archives, which are copied into a temporary cache location.
4767         nestedScrollWebView.settings.allowFileAccess = true
4768
4769         // Create a double-tap gesture detector to toggle full-screen mode.
4770         val doubleTapGestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
4771             // Override `onDoubleTap()`.  All other events are handled using the default settings.
4772             override fun onDoubleTap(motionEvent: MotionEvent): Boolean {
4773                 return if (fullScreenBrowsingModeEnabled) {  // Only process the double-tap if full screen browsing mode is enabled.
4774                     // Toggle the full screen browsing mode tracker.
4775                     inFullScreenBrowsingMode = !inFullScreenBrowsingMode
4776
4777                     // Toggle the full screen browsing mode.
4778                     if (inFullScreenBrowsingMode) {  // Switch to full screen mode.
4779                         // Hide the app bar if specified.
4780                         if (hideAppBar) {  // App bar hiding is enabled.
4781                             // Close the find on page bar if it is visible.
4782                             closeFindOnPage(null)
4783
4784                             // Hide the tab linear layout.
4785                             tabsLinearLayout.visibility = View.GONE
4786
4787                             // Hide the app bar.
4788                             appBar.hide()
4789
4790                             // Set layout and scrolling parameters according to the position of the app bar.
4791                             if (bottomAppBar) {  // The app bar is at the bottom.
4792                                 // Reset the WebView padding to fill the available space.
4793                                 swipeRefreshLayout.setPadding(0, 0, 0, 0)
4794                             } else {  // The app bar is at the top.
4795                                 // Check to see if the app bar is normally scrolled.
4796                                 if (scrollAppBar) {  // The app bar is scrolled when it is displayed.
4797                                     // Get the swipe refresh layout parameters.
4798                                     val swipeRefreshLayoutParams = swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams
4799
4800                                     // Remove the off-screen scrolling layout.
4801                                     swipeRefreshLayoutParams.behavior = null
4802                                 } else {  // The app bar is not scrolled when it is displayed.
4803                                     // Remove the padding from the top of the swipe refresh layout.
4804                                     swipeRefreshLayout.setPadding(0, 0, 0, 0)
4805
4806                                     // The swipe refresh circle must be moved above the now removed status bar location.
4807                                     swipeRefreshLayout.setProgressViewOffset(false, -200, defaultProgressViewEndOffset)
4808                                 }
4809                             }
4810                         } else {  // App bar hiding is not enabled.
4811                             // Adjust the UI for the bottom app bar.
4812                             if (bottomAppBar) {
4813                                 // Adjust the UI according to the scrolling of the app bar.
4814                                 if (scrollAppBar) {
4815                                     // Reset the WebView padding to fill the available space.
4816                                     swipeRefreshLayout.setPadding(0, 0, 0, 0)
4817                                 } else {
4818                                     // Move the WebView above the app bar layout.
4819                                     swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight)
4820                                 }
4821                             }
4822                         }
4823
4824                         /* Hide the system bars.
4825                          * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4826                          * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4827                          * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4828                          * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4829                          */
4830
4831                         // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4832                         @Suppress("DEPRECATION")
4833                         rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
4834                                 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
4835                     } else {  // Switch to normal viewing mode.
4836                         // Show the app bar if it was hidden.
4837                         if (hideAppBar) {
4838                             // Show the tab linear layout.
4839                             tabsLinearLayout.visibility = View.VISIBLE
4840
4841                             // Show the app bar.
4842                             appBar.show()
4843                         }
4844
4845                         // Set layout and scrolling parameters according to the position of the app bar.
4846                         if (bottomAppBar) {  // The app bar is at the bottom.
4847                             // Adjust the UI.
4848                             if (scrollAppBar) {
4849                                 // Reset the WebView padding to fill the available space.
4850                                 swipeRefreshLayout.setPadding(0, 0, 0, 0)
4851                             } else {
4852                                 // Move the WebView above the app bar layout.
4853                                 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight)
4854                             }
4855                         } else {  // The app bar is at the top.
4856                             // Check to see if the app bar is normally scrolled.
4857                             if (scrollAppBar) {  // The app bar is scrolled when it is displayed.
4858                                 // Get the swipe refresh layout parameters.
4859                                 val swipeRefreshLayoutParams = swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams
4860
4861                                 // Add the off-screen scrolling layout.
4862                                 swipeRefreshLayoutParams.behavior = AppBarLayout.ScrollingViewBehavior()
4863                             } else {  // The app bar is not scrolled when it is displayed.
4864                                 // The swipe refresh layout must be manually moved below the app bar layout.
4865                                 swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0)
4866
4867                                 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
4868                                 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight)
4869                             }
4870                         }
4871
4872                         // Remove the `SYSTEM_UI` flags from the root frame layout.  The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4873                         @Suppress("DEPRECATION")
4874                         rootFrameLayout.systemUiVisibility = 0
4875                     }
4876
4877                     // Consume the double-tap.
4878                     true
4879                 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4880                     // Return false.
4881                     false
4882                 }
4883             }
4884
4885             override fun onFling(motionEvent1: MotionEvent, motionEvent2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
4886                 // Scroll the bottom app bar if enabled.
4887                 if (bottomAppBar && scrollAppBar && !objectAnimator.isRunning) {
4888                     // Calculate the Y change.
4889                     val motionY = motionEvent2.y - motionEvent1.y
4890
4891                     // Scroll the app bar if the change is greater than 50 pixels.
4892                     if (motionY > 50) {
4893                         // Animate the bottom app bar onto the screen.
4894                         objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0f)
4895                     } else if (motionY < -50) {
4896                         // Animate the bottom app bar off the screen.
4897                         objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", appBarLayout.height.toFloat())
4898                     }
4899
4900                     // Make it so.
4901                     objectAnimator.start()
4902                 }
4903
4904                 // Do not consume the event.
4905                 return false
4906             }
4907         })
4908
4909         // Pass all touch events on the WebView through the double-tap gesture detector.
4910         nestedScrollWebView.setOnTouchListener { view: View, motionEvent: MotionEvent? ->
4911             // Call `performClick()` on the view, which is required for accessibility.
4912             view.performClick()
4913
4914             // Check for double-taps.
4915             doubleTapGestureDetector.onTouchEvent(motionEvent!!)
4916         }
4917
4918         // Register the WebView for a context menu.  This is used to see link targets and download images.
4919         registerForContextMenu(nestedScrollWebView)
4920
4921         // Allow the downloading of files.
4922         nestedScrollWebView.setDownloadListener { downloadUrlString: String?, userAgent: String?, contentDisposition: String?, mimetype: String?, contentLength: Long ->
4923             // Check the download preference.
4924             if (downloadWithExternalApp) {  // Download with an external app.
4925                 downloadUrlWithExternalApp(downloadUrlString!!)
4926             } else {  // Handle the download inside of Privacy Browser.
4927                 // Define a formatted file size string.
4928
4929                 // Process the content length if it contains data.
4930                 val formattedFileSizeString = if (contentLength > 0) {  // The content length is greater than 0.
4931                     // Format the content length as a string.
4932                     NumberFormat.getInstance().format(contentLength) + " " + getString(R.string.bytes)
4933                 } else {  // The content length is not greater than 0.
4934                     // Set the formatted file size string to be `unknown size`.
4935                     getString(R.string.unknown_size)
4936                 }
4937
4938                 // Get the file name from the content disposition.
4939                 val fileNameString = UrlHelper.getFileName(this, contentDisposition, mimetype, downloadUrlString!!)
4940
4941                 // Instantiate the save dialog.
4942                 val saveDialogFragment = SaveDialog.saveUrl(downloadUrlString, fileNameString, formattedFileSizeString, userAgent!!, nestedScrollWebView.acceptCookies)
4943
4944                 // Try to show the dialog.  The download listener continues to function even when the WebView is paused.  Attempting to display a dialog in that state leads to a crash.
4945                 try {
4946                     // Show the save dialog.
4947                     saveDialogFragment.show(supportFragmentManager, getString(R.string.save_dialog))
4948                 } catch (exception: Exception) {  // The dialog could not be shown.
4949                     // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
4950                     pendingDialogsArrayList.add(PendingDialogDataClass(saveDialogFragment, getString(R.string.save_dialog)))
4951                 }
4952             }
4953         }
4954
4955         // Update the find on page count.
4956         nestedScrollWebView.setFindListener { activeMatchOrdinal, numberOfMatches, isDoneCounting ->
4957             if (isDoneCounting && (numberOfMatches == 0)) {  // There are no matches.
4958                 // Set the find on page count text view to be `0/0`.
4959                 findOnPageCountTextView.setText(R.string.zero_of_zero)
4960             } else if (isDoneCounting) {  // There are matches.
4961                 // The active match ordinal is zero-based.
4962                 val activeMatch = activeMatchOrdinal + 1
4963
4964                 // Build the match string.
4965                 val matchString = "$activeMatch/$numberOfMatches"
4966
4967                 // Update the find on page count text view.
4968                 findOnPageCountTextView.text = matchString
4969             }
4970         }
4971
4972         // Process scroll changes.
4973         nestedScrollWebView.setOnScrollChangeListener { _: View?, _: Int, _: Int, _: Int, _: Int ->
4974             // Set the swipe to refresh status.
4975             if (nestedScrollWebView.swipeToRefresh)  // Only enable swipe to refresh if the WebView is scrolled to the top.
4976                 swipeRefreshLayout.isEnabled = nestedScrollWebView.scrollY == 0
4977             else  // Disable swipe to refresh.
4978                 swipeRefreshLayout.isEnabled = false
4979
4980             // Reinforce the system UI visibility flags if in full screen browsing mode.
4981             // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard.
4982             if (inFullScreenBrowsingMode) {
4983                 /* Hide the system bars.
4984                  * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4985                  * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4986                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4987                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4988                  */
4989
4990                 // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4991                 @Suppress("DEPRECATION")
4992                 rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
4993             }
4994         }
4995
4996         // Set the web chrome client.
4997         nestedScrollWebView.webChromeClient = object : WebChromeClient() {
4998             // Update the progress bar when a page is loading.
4999             override fun onProgressChanged(view: WebView, progress: Int) {
5000                 // Update the progress bar.
5001                 progressBar.progress = progress
5002
5003                 // Set the visibility of the progress bar.
5004                 if (progress < 100) {
5005                     // Show the progress bar.
5006                     progressBar.visibility = View.VISIBLE
5007                 } else {
5008                     // Hide the progress bar.
5009                     progressBar.visibility = View.GONE
5010
5011                     //Stop the swipe to refresh indicator if it is running
5012                     swipeRefreshLayout.isRefreshing = false
5013
5014                     // Make the current WebView visible.  If this is a new tab, the current WebView would have been created invisible in `webview_framelayout` to prevent a white background splash in night mode.
5015                     nestedScrollWebView.visibility = View.VISIBLE
5016                 }
5017             }
5018
5019             // Set the favorite icon when it changes.
5020             override fun onReceivedIcon(view: WebView, icon: Bitmap) {
5021                 // Only update the favorite icon if the website has finished loading and the new favorite icon height is greater than the current favorite icon height.
5022                 // This prevents low resolution icons from replacing high resolution one.
5023                 // The check for the visibility of the progress bar can possibly be removed once https://redmine.stoutner.com/issues/747 is fixed.
5024                 if ((progressBar.visibility == View.GONE) && (icon.height > nestedScrollWebView.getFavoriteIconHeight())) {
5025                     // Store the new favorite icon.
5026                     nestedScrollWebView.setFavoriteIcon(icon)
5027
5028                     // Get the current page position.
5029                     val currentPosition = webViewPagerAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
5030
5031                     // Get the current tab.
5032                     val tab = tabLayout.getTabAt(currentPosition)
5033
5034                     // Check to see if the tab has been populated.
5035                     if (tab != null) {
5036                         // Get the custom view from the tab.
5037                         val tabView = tab.customView
5038
5039                         // Check to see if the custom tab view has been populated.
5040                         if (tabView != null) {
5041                             // Get the favorite icon image view from the tab.
5042                             val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
5043
5044                             // Display the favorite icon in the tab.
5045                             tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true))
5046                         }
5047                     }
5048                 }
5049             }
5050
5051             // Save a copy of the title when it changes.
5052             override fun onReceivedTitle(view: WebView, title: String) {
5053                 // Get the current page position.
5054                 val currentPosition = webViewPagerAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
5055
5056                 // Get the current tab.
5057                 val tab = tabLayout.getTabAt(currentPosition)
5058
5059                 // Only populate the title text view if the tab has been fully created.
5060                 if (tab != null) {
5061                     // Get the custom view from the tab.
5062                     val tabView = tab.customView
5063
5064                     // Only populate the title text view if the tab view has been fully populated.
5065                     if (tabView != null) {
5066                         // Get the title text view from the tab.
5067                         val tabTitleTextView = tabView.findViewById<TextView>(R.id.title_textview)
5068
5069                         // Set the title according to the URL.
5070                         if (title == "about:blank") {
5071                             // Set the title to indicate a new tab.
5072                             tabTitleTextView.setText(R.string.new_tab)
5073                         } else {
5074                             // Set the title as the tab text.
5075                             tabTitleTextView.text = title
5076                         }
5077                     }
5078                 }
5079             }
5080
5081             // Enter full screen video.
5082             override fun onShowCustomView(video: View, callback: CustomViewCallback) {
5083                 // Set the full screen video flag.
5084                 displayingFullScreenVideo = true
5085
5086                 // Hide the keyboard.
5087                 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.windowToken, 0)
5088
5089                 // Hide the coordinator layout.
5090                 coordinatorLayout.visibility = View.GONE
5091
5092                 /* Hide the system bars.
5093                  * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5094                  * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5095                  * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5096                  * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5097                  */
5098
5099                 // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
5100                 @Suppress("DEPRECATION")
5101                 rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
5102
5103                 // Disable the sliding drawers.
5104                 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
5105
5106                 // Add the video view to the full screen video frame layout.
5107                 fullScreenVideoFrameLayout.addView(video)
5108
5109                 // Show the full screen video frame layout.
5110                 fullScreenVideoFrameLayout.visibility = View.VISIBLE
5111
5112                 // Disable the screen timeout while the video is playing.  YouTube does this automatically, but not all other videos do.
5113                 fullScreenVideoFrameLayout.keepScreenOn = true
5114             }
5115
5116             // Exit full screen video.
5117             override fun onHideCustomView() {
5118                 // Exit the full screen video.
5119                 exitFullScreenVideo()
5120             }
5121
5122             // Upload files.
5123             override fun onShowFileChooser(webView: WebView, filePathCallback: ValueCallback<Array<Uri>>, fileChooserParams: FileChooserParams): Boolean {
5124                 // Store the file path callback.
5125                 fileChooserCallback = filePathCallback
5126
5127                 // Create an intent to open a chooser based on the file chooser parameters.
5128                 val fileChooserIntent = fileChooserParams.createIntent()
5129
5130                 // Check to see if the file chooser intent resolves to an installed package.
5131                 if (fileChooserIntent.resolveActivity(packageManager) != null) {  // The file chooser intent is fine.
5132                     // Launch the file chooser intent.
5133                     browseFileUploadActivityResultLauncher.launch(fileChooserIntent)
5134                 } else {  // The file chooser intent will cause a crash.
5135                     // Create a generic intent to open a chooser.
5136                     val genericFileChooserIntent = Intent(Intent.ACTION_GET_CONTENT)
5137
5138                     // Request an openable file.
5139                     genericFileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE)
5140
5141                     // Set the file type to everything.
5142                     genericFileChooserIntent.type = "*/*"
5143
5144                     // Launch the generic file chooser intent.
5145                     browseFileUploadActivityResultLauncher.launch(genericFileChooserIntent)
5146                 }
5147
5148                 // Handle the event.
5149                 return true
5150             }
5151         }
5152         nestedScrollWebView.webViewClient = object : WebViewClient() {
5153             // `shouldOverrideUrlLoading` makes this WebView the default handler for URLs inside the app, so that links are not kicked out to other apps.
5154             override fun shouldOverrideUrlLoading(view: WebView, webResourceRequest: WebResourceRequest): Boolean {
5155                 // Get the URL from the web resource request.
5156                 var requestUrlString = webResourceRequest.url.toString()
5157
5158                 // Sanitize the url.
5159                 requestUrlString = sanitizeUrl(requestUrlString)
5160
5161                 // Handle the URL according to the type.
5162                 return if (requestUrlString.startsWith("http")) {  // Load the URL in Privacy Browser.
5163                     // Load the URL.  By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent.
5164                     loadUrl(nestedScrollWebView, requestUrlString)
5165
5166                     // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
5167                     // Custom headers cannot be added if false is returned and the WebView handles the loading of the URL.
5168                     true
5169                 } else if (requestUrlString.startsWith("mailto:")) {  // Load the email address in an external email program.
5170                     // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
5171                     val emailIntent = Intent(Intent.ACTION_SENDTO)
5172
5173                     // Parse the url and set it as the data for the intent.
5174                     emailIntent.data = Uri.parse(requestUrlString)
5175
5176                     // Open the email program in a new task instead of as part of Privacy Browser.
5177                     emailIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
5178
5179                     try {
5180                         // Make it so.
5181                         startActivity(emailIntent)
5182                     } catch (exception: ActivityNotFoundException) {
5183                         // Display a snackbar.
5184                         Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
5185                     }
5186
5187                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5188                     true
5189                 } else if (requestUrlString.startsWith("tel:")) {  // Load the phone number in the dialer.
5190                     // Create a dial intent.
5191                     val dialIntent = Intent(Intent.ACTION_DIAL)
5192
5193                     // Add the phone number to the intent.
5194                     dialIntent.data = Uri.parse(requestUrlString)
5195
5196                     // Open the dialer in a new task instead of as part of Privacy Browser.
5197                     dialIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
5198
5199                     try {
5200                         // Make it so.
5201                         startActivity(dialIntent)
5202                     } catch (exception: ActivityNotFoundException) {
5203                         // Display a snackbar.
5204                         Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
5205                     }
5206
5207                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5208                     true
5209                 } else {  // Load a system chooser to select an app that can handle the URL.
5210                     // Create a generic intent to open an app.
5211                     val genericIntent = Intent(Intent.ACTION_VIEW)
5212
5213                     // Add the URL to the intent.
5214                     genericIntent.data = Uri.parse(requestUrlString)
5215
5216                     // List all apps that can handle the URL instead of just opening the first one.
5217                     genericIntent.addCategory(Intent.CATEGORY_BROWSABLE)
5218
5219                     // Open the app in a new task instead of as part of Privacy Browser.
5220                     genericIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
5221
5222                     try {
5223                         // Make it so.
5224                         startActivity(genericIntent)
5225                     } catch (exception: ActivityNotFoundException) {
5226                         // Display a snackbar.
5227                         Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url, requestUrlString), Snackbar.LENGTH_SHORT).show()
5228                     }
5229
5230                     // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5231                     true
5232                 }
5233             }
5234
5235             // Check requests against the block lists.
5236             override fun shouldInterceptRequest(view: WebView, webResourceRequest: WebResourceRequest): WebResourceResponse? {
5237                 // Get the URL.
5238                 val requestUrlString = webResourceRequest.url.toString()
5239
5240                 // Check to see if the resource request is for the main URL.
5241                 if (requestUrlString == nestedScrollWebView.currentUrl) {
5242                     // `return null` loads the resource request, which should never be blocked if it is the main URL.
5243                     return null
5244                 }
5245
5246                 // Wait until the blocklists have been populated.  When Privacy Browser is being resumed after having the process killed in the background it will try to load the URLs immediately.
5247                 while (ultraPrivacy == null) {
5248                     try {
5249                         // Check to see if the blocklists have been populated after 100 ms.
5250                         Thread.sleep(100)
5251                     } catch (exception: InterruptedException) {
5252                         // Do nothing.
5253                     }
5254                 }
5255
5256                 // Create an empty web resource response to be used if the resource request is blocked.
5257                 val emptyWebResourceResponse = WebResourceResponse("text/plain", "utf8", ByteArrayInputStream("".toByteArray()))
5258
5259                 // Initialize the variables.
5260                 var whitelistResultStringArray: Array<String>? = null
5261                 var isThirdPartyRequest = false
5262
5263                 // Get the current URL.  `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
5264                 var currentBaseDomain = nestedScrollWebView.currentDomainName
5265
5266                 // Store a copy of the current domain for use in later requests.
5267                 val currentDomain = currentBaseDomain
5268
5269                 // Get the request host name.
5270                 var requestBaseDomain = webResourceRequest.url.host
5271
5272                 // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
5273                 if (currentBaseDomain.isNotEmpty() && (requestBaseDomain != null)) {
5274                     // Determine the current base domain.
5275                     while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) {  // There is at least one subdomain.
5276                         // Remove the first subdomain.
5277                         currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1)
5278                     }
5279
5280                     // Determine the request base domain.
5281                     while (requestBaseDomain!!.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) {  // There is at least one subdomain.
5282                         // Remove the first subdomain.
5283                         requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1)
5284                     }
5285
5286                     // Update the third party request tracker.
5287                     isThirdPartyRequest = currentBaseDomain != requestBaseDomain
5288                 }
5289
5290                 // Get the current WebView page position.
5291                 val webViewPagePosition = webViewPagerAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
5292
5293                 // Determine if the WebView is currently displayed.
5294                 val webViewDisplayed = webViewPagePosition == tabLayout.selectedTabPosition
5295
5296                 // Block third-party requests if enabled.
5297                 if (isThirdPartyRequest && nestedScrollWebView.blockAllThirdPartyRequests) {
5298                     // Add the result to the resource requests.
5299                     nestedScrollWebView.addResourceRequest(arrayOf(BlocklistHelper.REQUEST_THIRD_PARTY, requestUrlString))
5300
5301                     // Increment the blocked requests counters.
5302                     nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5303                     nestedScrollWebView.incrementRequestsCount(THIRD_PARTY_REQUESTS)
5304
5305                     // Update the titles of the blocklist menu items if the WebView is currently displayed.
5306                     if (webViewDisplayed) {
5307                         // Updating the UI must be run from the UI thread.
5308                         runOnUiThread {
5309                             // Update the menu item titles.
5310                             navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5311
5312                             // Update the options menu if it has been populated.
5313                             if (optionsMenu != null) {
5314                                 optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5315                                 optionsBlockAllThirdPartyRequestsMenuItem.title =
5316                                     nestedScrollWebView.getRequestsCount(THIRD_PARTY_REQUESTS).toString() + " - " + getString(R.string.block_all_third_party_requests)
5317                             }
5318                         }
5319                     }
5320
5321                     // The resource request was blocked.  Return an empty web resource response.
5322                     return emptyWebResourceResponse
5323                 }
5324
5325                 // Check UltraList if it is enabled.
5326                 if (nestedScrollWebView.ultraListEnabled) {
5327                     // Check the URL against UltraList.
5328                     val ultraListResults = blocklistHelper.checkBlocklist(currentDomain, requestUrlString, isThirdPartyRequest, ultraList)
5329
5330                     // Process the UltraList results.
5331                     if (ultraListResults[0] == BlocklistHelper.REQUEST_BLOCKED) {  // The resource request matched UltraList's blacklist.
5332                         // Add the result to the resource requests.
5333                         nestedScrollWebView.addResourceRequest(arrayOf(ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]))
5334
5335                         // Increment the blocked requests counters.
5336                         nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5337                         nestedScrollWebView.incrementRequestsCount(ULTRALIST)
5338
5339                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5340                         if (webViewDisplayed) {
5341                             // Updating the UI must be run from the UI thread.
5342                             runOnUiThread {
5343                                 // Update the menu item titles.
5344                                 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5345
5346                                 // Update the options menu if it has been populated.
5347                                 if (optionsMenu != null) {
5348                                     optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5349                                     optionsUltraListMenuItem.title = nestedScrollWebView.getRequestsCount(ULTRALIST).toString() + " - " + getString(R.string.ultralist)
5350                                 }
5351                             }
5352                         }
5353
5354                         // The resource request was blocked.  Return an empty web resource response.
5355                         return emptyWebResourceResponse
5356                     } else if (ultraListResults[0] == BlocklistHelper.REQUEST_ALLOWED) {  // The resource request matched UltraList's whitelist.
5357                         // Add a whitelist entry to the resource requests array.
5358                         nestedScrollWebView.addResourceRequest(arrayOf(ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]))
5359
5360                         // The resource request has been allowed by UltraPrivacy.  `return null` loads the requested resource.
5361                         return null
5362                     }
5363                 }
5364
5365                 // Check UltraPrivacy if it is enabled.
5366                 if (nestedScrollWebView.ultraPrivacyEnabled) {
5367                     // Check the URL against UltraPrivacy.
5368                     val ultraPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, requestUrlString, isThirdPartyRequest, ultraPrivacy!!)
5369
5370                     // Process the UltraPrivacy results.
5371                     if (ultraPrivacyResults[0] == BlocklistHelper.REQUEST_BLOCKED) {  // The resource request matched UltraPrivacy's blacklist.
5372                         // Add the result to the resource requests.
5373                         nestedScrollWebView.addResourceRequest(arrayOf(ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5374                             ultraPrivacyResults[5]))
5375
5376                         // Increment the blocked requests counters.
5377                         nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5378                         nestedScrollWebView.incrementRequestsCount(ULTRAPRIVACY)
5379
5380                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5381                         if (webViewDisplayed) {
5382                             // Updating the UI must be run from the UI thread.
5383                             runOnUiThread {
5384                                 // Update the menu item titles.
5385                                 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5386
5387                                 // Update the options menu if it has been populated.
5388                                 if (optionsMenu != null) {
5389                                     optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5390                                     optionsUltraPrivacyMenuItem.title = nestedScrollWebView.getRequestsCount(ULTRAPRIVACY).toString() + " - " + getString(R.string.ultraprivacy)
5391                                 }
5392                             }
5393                         }
5394
5395                         // The resource request was blocked.  Return an empty web resource response.
5396                         return emptyWebResourceResponse
5397                     } else if (ultraPrivacyResults[0] == BlocklistHelper.REQUEST_ALLOWED) {  // The resource request matched UltraPrivacy's whitelist.
5398                         // Add a whitelist entry to the resource requests array.
5399                         nestedScrollWebView.addResourceRequest(arrayOf(ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5400                             ultraPrivacyResults[5]))
5401
5402                         // The resource request has been allowed by UltraPrivacy.  `return null` loads the requested resource.
5403                         return null
5404                     }
5405                 }
5406
5407                 // Check EasyList if it is enabled.
5408                 if (nestedScrollWebView.easyListEnabled) {
5409                     // Check the URL against EasyList.
5410                     val easyListResults = blocklistHelper.checkBlocklist(currentDomain, requestUrlString, isThirdPartyRequest, easyList)
5411
5412                     // Process the EasyList results.
5413                     if (easyListResults[0] == BlocklistHelper.REQUEST_BLOCKED) {  // The resource request matched EasyList's blacklist.
5414                         // Add the result to the resource requests.
5415                         nestedScrollWebView.addResourceRequest(arrayOf(easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]))
5416
5417                         // Increment the blocked requests counters.
5418                         nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5419                         nestedScrollWebView.incrementRequestsCount(EASYLIST)
5420
5421                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5422                         if (webViewDisplayed) {
5423                             // Updating the UI must be run from the UI thread.
5424                             runOnUiThread {
5425                                 // Update the menu item titles.
5426                                 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5427
5428                                 // Update the options menu if it has been populated.
5429                                 if (optionsMenu != null) {
5430                                     optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5431                                     optionsEasyListMenuItem.title = nestedScrollWebView.getRequestsCount(EASYLIST).toString() + " - " + getString(R.string.easylist)
5432                                 }
5433                             }
5434                         }
5435
5436                         // The resource request was blocked.  Return an empty web resource response.
5437                         return emptyWebResourceResponse
5438                     } else if (easyListResults[0] == BlocklistHelper.REQUEST_ALLOWED) {  // The resource request matched EasyList's whitelist.
5439                         // Update the whitelist result string array tracker.
5440                         whitelistResultStringArray = arrayOf(easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5])
5441                     }
5442                 }
5443
5444                 // Check EasyPrivacy if it is enabled.
5445                 if (nestedScrollWebView.easyPrivacyEnabled) {
5446                     // Check the URL against EasyPrivacy.
5447                     val easyPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, requestUrlString, isThirdPartyRequest, easyPrivacy)
5448
5449                     // Process the EasyPrivacy results.
5450                     if (easyPrivacyResults[0] == BlocklistHelper.REQUEST_BLOCKED) {  // The resource request matched EasyPrivacy's blacklist.
5451                         // Add the result to the resource requests.
5452                         nestedScrollWebView.addResourceRequest(arrayOf(easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]))
5453
5454                         // Increment the blocked requests counters.
5455                         nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5456                         nestedScrollWebView.incrementRequestsCount(EASYPRIVACY)
5457
5458                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5459                         if (webViewDisplayed) {
5460                             // Updating the UI must be run from the UI thread.
5461                             runOnUiThread {
5462                                 // Update the menu item titles.
5463                                 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5464
5465                                 // Update the options menu if it has been populated.
5466                                 if (optionsMenu != null) {
5467                                     optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5468                                     optionsEasyPrivacyMenuItem.title = nestedScrollWebView.getRequestsCount(EASYPRIVACY).toString() + " - " + getString(R.string.easyprivacy)
5469                                 }
5470                             }
5471                         }
5472
5473                         // The resource request was blocked.  Return an empty web resource response.
5474                         return emptyWebResourceResponse
5475                     } else if (easyPrivacyResults[0] == BlocklistHelper.REQUEST_ALLOWED) {  // The resource request matched EasyPrivacy's whitelist.
5476                         // Update the whitelist result string array tracker.
5477                         whitelistResultStringArray = arrayOf(easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5])
5478                     }
5479                 }
5480
5481                 // Check Fanboy’s Annoyance List if it is enabled.
5482                 if (nestedScrollWebView.fanboysAnnoyanceListEnabled) {
5483                     // Check the URL against Fanboy's Annoyance List.
5484                     val fanboysAnnoyanceListResults = blocklistHelper.checkBlocklist(currentDomain, requestUrlString, isThirdPartyRequest, fanboysAnnoyanceList)
5485
5486                     // Process the Fanboy's Annoyance List results.
5487                     if (fanboysAnnoyanceListResults[0] == BlocklistHelper.REQUEST_BLOCKED) {  // The resource request matched Fanboy's Annoyance List's blacklist.
5488                         // Add the result to the resource requests.
5489                         nestedScrollWebView.addResourceRequest(arrayOf(fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5490                                 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]))
5491
5492                         // Increment the blocked requests counters.
5493                         nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5494                         nestedScrollWebView.incrementRequestsCount(FANBOYS_ANNOYANCE_LIST)
5495
5496                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5497                         if (webViewDisplayed) {
5498                             // Updating the UI must be run from the UI thread.
5499                             runOnUiThread {
5500                                 // Update the menu item titles.
5501                                 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5502
5503                                 // Update the options menu if it has been populated.
5504                                 if (optionsMenu != null) {
5505                                     optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5506                                     optionsFanboysAnnoyanceListMenuItem.title = nestedScrollWebView.getRequestsCount(FANBOYS_ANNOYANCE_LIST).toString() + " - " + getString(R.string.fanboys_annoyance_list)
5507                                 }
5508                             }
5509                         }
5510
5511                         // The resource request was blocked.  Return an empty web resource response.
5512                         return emptyWebResourceResponse
5513                     } else if (fanboysAnnoyanceListResults[0] == BlocklistHelper.REQUEST_ALLOWED) {  // The resource request matched Fanboy's Annoyance List's whitelist.
5514                         // Update the whitelist result string array tracker.
5515                         whitelistResultStringArray = arrayOf(fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5516                             fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5])
5517                     }
5518                 } else if (nestedScrollWebView.fanboysSocialBlockingListEnabled) {  // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5519                     // Check the URL against Fanboy's Annoyance List.
5520                     val fanboysSocialListResults = blocklistHelper.checkBlocklist(currentDomain, requestUrlString, isThirdPartyRequest, fanboysSocialList)
5521
5522                     // Process the Fanboy's Social Blocking List results.
5523                     if (fanboysSocialListResults[0] == BlocklistHelper.REQUEST_BLOCKED) {  // The resource request matched Fanboy's Social Blocking List's blacklist.
5524                         // Add the result to the resource requests.
5525                         nestedScrollWebView.addResourceRequest(arrayOf(fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5526                             fanboysSocialListResults[4], fanboysSocialListResults[5]))
5527
5528                         // Increment the blocked requests counters.
5529                         nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5530                         nestedScrollWebView.incrementRequestsCount(FANBOYS_SOCIAL_BLOCKING_LIST)
5531
5532                         // Update the titles of the blocklist menu items if the WebView is currently displayed.
5533                         if (webViewDisplayed) {
5534                             // Updating the UI must be run from the UI thread.
5535                             runOnUiThread {
5536                                 // Update the menu item titles.
5537                                 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5538
5539                                 // Update the options menu if it has been populated.
5540                                 if (optionsMenu != null) {
5541                                     optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5542                                     optionsFanboysSocialBlockingListMenuItem.title =
5543                                         nestedScrollWebView.getRequestsCount(FANBOYS_SOCIAL_BLOCKING_LIST).toString() + " - " + getString(R.string.fanboys_social_blocking_list)
5544                                 }
5545                             }
5546                         }
5547
5548                         // The resource request was blocked.  Return an empty web resource response.
5549                         return emptyWebResourceResponse
5550                     } else if (fanboysSocialListResults[0] == BlocklistHelper.REQUEST_ALLOWED) {  // The resource request matched Fanboy's Social Blocking List's whitelist.
5551                         // Update the whitelist result string array tracker.
5552                         whitelistResultStringArray = arrayOf(fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3], fanboysSocialListResults[4],
5553                             fanboysSocialListResults[5])
5554                     }
5555                 }
5556
5557                 // Add the request to the log because it hasn't been processed by any of the previous checks.
5558                 if (whitelistResultStringArray != null) {  // The request was processed by a whitelist.
5559                     nestedScrollWebView.addResourceRequest(whitelistResultStringArray)
5560                 } else {  // The request didn't match any blocklist entry.  Log it as a default request.
5561                     nestedScrollWebView.addResourceRequest(arrayOf(BlocklistHelper.REQUEST_DEFAULT, requestUrlString))
5562                 }
5563
5564                 // The resource request has not been blocked.  `return null` loads the requested resource.
5565                 return null
5566             }
5567
5568             // Handle HTTP authentication requests.
5569             override fun onReceivedHttpAuthRequest(view: WebView, handler: HttpAuthHandler, host: String, realm: String) {
5570                 // Store the handler.
5571                 nestedScrollWebView.httpAuthHandler = handler
5572
5573                 // Instantiate an HTTP authentication dialog.
5574                 val httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.webViewFragmentId)
5575
5576                 // Show the HTTP authentication dialog.
5577                 httpAuthenticationDialogFragment.show(supportFragmentManager, getString(R.string.http_authentication))
5578             }
5579
5580             override fun onPageStarted(webView: WebView, url: String, favicon: Bitmap?) {
5581                 // Get the app bar layout height.  This can't be done in `applyAppSettings()` because the app bar is not yet populated there.
5582                 // This should only be populated if it is greater than 0 because otherwise it will be reset to 0 if the app bar is hidden in full screen browsing mode.
5583                 if (appBarLayout.height > 0)
5584                     appBarHeight = appBarLayout.height
5585
5586                 // Set the padding and layout settings according to the position of the app bar.
5587                 if (bottomAppBar) {  // The app bar is on the bottom.
5588                     // Adjust the UI.
5589                     if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) {  // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
5590                         // Reset the WebView padding to fill the available space.
5591                         swipeRefreshLayout.setPadding(0, 0, 0, 0)
5592                     } else {  // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
5593                         // Move the WebView above the app bar layout.
5594                         swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight)
5595                     }
5596                 } else {  // The app bar is on the top.
5597                     // Set the top padding of the swipe refresh layout according to the app bar scrolling preference.  This can't be done in `appAppSettings()` because the app bar is not yet populated there.
5598                     if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) {  // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
5599                         // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior.
5600                         swipeRefreshLayout.setPadding(0, 0, 0, 0)
5601
5602                         // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5603                         swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset)
5604                     } else {  // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
5605                         // The swipe refresh layout must be manually moved below the app bar layout.
5606                         swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0)
5607
5608                         // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5609                         swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight)
5610                     }
5611                 }
5612
5613                 // Reset the list of resource requests.
5614                 nestedScrollWebView.clearResourceRequests()
5615
5616                 // Reset the requests counters.
5617                 nestedScrollWebView.resetRequestsCounters()
5618
5619                 // Get the current page position.
5620                 val currentPagePosition = webViewPagerAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
5621
5622                 // Update the URL text bar if the page is currently selected and the URL edit text is not currently being edited.
5623                 if ((tabLayout.selectedTabPosition == currentPagePosition) && !urlEditText.hasFocus()) {
5624                     // Display the formatted URL text.
5625                     urlEditText.setText(url)
5626
5627                     // Highlight the URL syntax.
5628                     UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
5629
5630                     // Hide the keyboard.
5631                     inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.windowToken, 0)
5632                 }
5633
5634                 // Reset the list of host IP addresses.
5635                 nestedScrollWebView.currentIpAddresses = ""
5636
5637                 // Get a URI for the current URL.
5638                 val currentUri = Uri.parse(url)
5639
5640                 // Get the current domain name.
5641                 val currentDomainName = currentUri.host
5642
5643                 // Get the IP addresses for the current domain.
5644                 if ((currentDomainName != null) && currentDomainName.isNotEmpty())
5645                     GetHostIpAddressesCoroutine.checkPinnedMismatch(currentDomainName, nestedScrollWebView, supportFragmentManager, getString(R.string.pinned_mismatch))
5646
5647                 // Replace Refresh with Stop if the options menu has been created and the WebView is currently displayed.  (The first WebView typically begins loading before the menu items are instantiated.)
5648                 if ((optionsMenu != null) && (webView == currentWebView)) {
5649                     // Set the title.
5650                     optionsRefreshMenuItem.setTitle(R.string.stop)
5651
5652                     // Set the icon if it is displayed in the AppBar.
5653                     if (displayAdditionalAppBarIcons)
5654                         optionsRefreshMenuItem.setIcon(R.drawable.close_blue)
5655                 }
5656             }
5657
5658             override fun onPageFinished(webView: WebView, url: String) {
5659                 // Flush any cookies to persistent storage.  The cookie manager has become very lazy about flushing cookies in recent versions.
5660                 if (nestedScrollWebView.acceptCookies)
5661                     cookieManager.flush()
5662
5663                 // Update the Refresh menu item if the options menu has been created and the WebView is currently displayed.
5664                 if (optionsMenu != null && (webView == currentWebView)) {
5665                     // Reset the Refresh title.
5666                     optionsRefreshMenuItem.setTitle(R.string.refresh)
5667
5668                     // Reset the icon if it is displayed in the app bar.
5669                     if (displayAdditionalAppBarIcons)
5670                         optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled)
5671                 }
5672
5673                 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
5674                 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
5675                 val privateDataDirectoryString = applicationInfo.dataDir
5676
5677                 // Clear the cache, history, and logcat if Incognito Mode is enabled.
5678                 if (incognitoModeEnabled) {
5679                     // Clear the cache.  `true` includes disk files.
5680                     nestedScrollWebView.clearCache(true)
5681
5682                     // Clear the back/forward history.
5683                     nestedScrollWebView.clearHistory()
5684
5685                     // Manually delete cache folders.
5686                     try {
5687                         // Delete the main cache directory.
5688                         Runtime.getRuntime().exec("rm -rf $privateDataDirectoryString/cache")
5689                     } catch (exception: IOException) {
5690                         // Do nothing if an error is thrown.
5691                     }
5692
5693                     // Clear the logcat.
5694                     try {
5695                         // Clear the logcat.  `-c` clears the logcat.  `-b all` clears all the buffers (instead of just crash, main, and system).
5696                         Runtime.getRuntime().exec("logcat -b all -c")
5697                     } catch (exception: IOException) {
5698                         // Do nothing.
5699                     }
5700                 }
5701
5702                 // Clear the `Service Worker` directory.
5703                 try {
5704                     // A string array must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5705                     Runtime.getRuntime().exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Service Worker/"))
5706                 } catch (exception: IOException) {
5707                     // Do nothing.
5708                 }
5709
5710                 // Get the current page position.
5711                 val currentPagePosition = webViewPagerAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
5712
5713                 // Get the current URL from the nested scroll WebView.  This is more accurate than using the URL passed into the method, which is sometimes not the final one.
5714                 val currentUrl = nestedScrollWebView.url
5715
5716                 // Get the current tab.
5717                 val tab = tabLayout.getTabAt(currentPagePosition)
5718
5719                 // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text.
5720                 // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point.
5721                 // Probably some sort of race condition when Privacy Browser is being resumed.
5722                 if ((tabLayout.selectedTabPosition == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) {
5723                     // Check to see if the URL is `about:blank`.
5724                     if (currentUrl == "about:blank") {  // The WebView is blank.
5725                         // Display the hint in the URL edit text.
5726                         urlEditText.setText("")
5727
5728                         // Request focus for the URL text box.
5729                         urlEditText.requestFocus()
5730
5731                         // Display the keyboard.
5732                         inputMethodManager.showSoftInput(urlEditText, 0)
5733
5734                         // Apply the domain settings.  This clears any settings from the previous domain.
5735                         applyDomainSettings(nestedScrollWebView, "", resetTab = true, reloadWebsite = false, loadUrl = false)
5736
5737                         // Only populate the title text view if the tab has been fully created.
5738                         if (tab != null) {
5739                             // Get the custom view from the tab.
5740                             val tabView = tab.customView!!
5741
5742                             // Get the title text view from the tab.
5743                             val tabTitleTextView = tabView.findViewById<TextView>(R.id.title_textview)
5744
5745                             // Set the title as the tab text.
5746                             tabTitleTextView.setText(R.string.new_tab)
5747                         }
5748                     } else {  // The WebView has loaded a webpage.
5749                         // Update the URL edit text if it is not currently being edited.
5750                         if (!urlEditText.hasFocus()) {
5751                             // Sanitize the current URL.  This removes unwanted URL elements that were added by redirects, so that they won't be included if the URL is shared.
5752                             val sanitizedUrl = sanitizeUrl(currentUrl)
5753
5754                             // Display the final URL.  Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
5755                             urlEditText.setText(sanitizedUrl)
5756
5757                             // Highlight the URL syntax.
5758                             UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
5759                         }
5760
5761                         // Only populate the title text view if the tab has been fully created.
5762                         if (tab != null) {
5763                             // Get the custom view from the tab.
5764                             val tabView = tab.customView!!
5765
5766                             // Get the title text view from the tab.
5767                             val tabTitleTextView = tabView.findViewById<TextView>(R.id.title_textview)
5768
5769                             // Set the title as the tab text.  Sometimes `onReceivedTitle()` is not called, especially when navigating history.
5770                             tabTitleTextView.text = nestedScrollWebView.title
5771                         }
5772                     }
5773                 }
5774             }
5775
5776             // Handle SSL Certificate errors.  Suppress the lint warning that ignoring the error might be dangerous.
5777             @SuppressLint("WebViewClientOnReceivedSslError")
5778             override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
5779                 // Get the current website SSL certificate.
5780                 val currentWebsiteSslCertificate = error.certificate
5781
5782                 // Extract the individual pieces of information from the current website SSL certificate.
5783                 val currentWebsiteIssuedToCName = currentWebsiteSslCertificate.issuedTo.cName
5784                 val currentWebsiteIssuedToOName = currentWebsiteSslCertificate.issuedTo.oName
5785                 val currentWebsiteIssuedToUName = currentWebsiteSslCertificate.issuedTo.uName
5786                 val currentWebsiteIssuedByCName = currentWebsiteSslCertificate.issuedBy.cName
5787                 val currentWebsiteIssuedByOName = currentWebsiteSslCertificate.issuedBy.oName
5788                 val currentWebsiteIssuedByUName = currentWebsiteSslCertificate.issuedBy.uName
5789                 val currentWebsiteSslStartDate = currentWebsiteSslCertificate.validNotBeforeDate
5790                 val currentWebsiteSslEndDate = currentWebsiteSslCertificate.validNotAfterDate
5791
5792                 // Get the pinned SSL certificate.
5793                 val (pinnedSslCertificateStringArray, pinnedSslCertificateDateArray) = nestedScrollWebView.getPinnedSslCertificate()
5794
5795                 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5796                 if (nestedScrollWebView.hasPinnedSslCertificate() &&
5797                     (currentWebsiteIssuedToCName == pinnedSslCertificateStringArray[0]) &&
5798                     (currentWebsiteIssuedToOName == pinnedSslCertificateStringArray[1]) &&
5799                     (currentWebsiteIssuedToUName == pinnedSslCertificateStringArray[2]) &&
5800                     (currentWebsiteIssuedByCName == pinnedSslCertificateStringArray[3]) &&
5801                     (currentWebsiteIssuedByOName == pinnedSslCertificateStringArray[4]) &&
5802                     (currentWebsiteIssuedByUName == pinnedSslCertificateStringArray[5]) &&
5803                     (currentWebsiteSslStartDate == pinnedSslCertificateDateArray[0]) &&
5804                     (currentWebsiteSslEndDate == pinnedSslCertificateDateArray[1])) {
5805
5806                     // An SSL certificate is pinned and matches the current domain certificate.  Proceed to the website without displaying an error.
5807                     handler.proceed()
5808                 } else {  // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5809                     // Store the SSL error handler.
5810                     nestedScrollWebView.sslErrorHandler = handler
5811
5812                     // Instantiate an SSL certificate error alert dialog.
5813                     val sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.webViewFragmentId)
5814
5815                     // Try to show the dialog.  The SSL error handler continues to function even when the WebView is paused.  Attempting to display a dialog in that state leads to a crash.
5816                     try {
5817                         // Show the SSL certificate error dialog.
5818                         sslCertificateErrorDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate_error))
5819                     } catch (exception: Exception) {
5820                         // Add the dialog to the pending dialog array list.  It will be displayed in `onStart()`.
5821                         pendingDialogsArrayList.add(PendingDialogDataClass(sslCertificateErrorDialogFragment, getString(R.string.ssl_certificate_error)))
5822                     }
5823                 }
5824             }
5825         }
5826
5827         // Check to see if the state is being restored.
5828         if (restoringState) {  // The state is being restored.
5829             // Resume the nested scroll WebView JavaScript timers.
5830             nestedScrollWebView.resumeTimers()
5831         } else if (pageNumber == 0) {  // The first page is being loaded.
5832             // Set this nested scroll WebView as the current WebView.
5833             currentWebView = nestedScrollWebView
5834
5835             // Get the intent that started the app.
5836             val launchingIntent = intent
5837
5838             // Reset the intent.  This prevents a duplicate tab from being created on restart.
5839             intent = Intent()
5840
5841             // Get the information from the intent.
5842             val launchingIntentAction = launchingIntent.action
5843             val launchingIntentUriData = launchingIntent.data
5844             val launchingIntentStringExtra = launchingIntent.getStringExtra(Intent.EXTRA_TEXT)
5845
5846             // Parse the launching intent URL.  Suppress the suggestions of using elvis expressions as they make the logic very difficult to follow.
5847             @Suppress("IfThenToElvis") val urlToLoadString = if ((launchingIntentAction != null) && (launchingIntentAction == Intent.ACTION_WEB_SEARCH)) {  // The intent contains a search string.
5848                 // Sanitize the search input and convert it to a search.
5849                 val encodedSearchString = try {
5850                     URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8")
5851                 } catch (exception: UnsupportedEncodingException) {
5852                     ""
5853                 }
5854
5855                 // Add the search URL to the encodedSearchString
5856                 searchURL + encodedSearchString
5857             } else if (launchingIntentUriData != null) {  // The launching intent contains a URL formatted as a URI.
5858                 // Get the URL from the URI.
5859                 launchingIntentUriData.toString()
5860             } else if (launchingIntentStringExtra != null) {  // The launching intent contains text that might be a URL.
5861                 // Get the URL from the string extra.
5862                 launchingIntentStringExtra
5863             } else if (urlString != "") {  // The activity has been restarted.
5864                 // Load the saved URL.
5865                 urlString
5866             } else {  // The is no saved URL and there is no URL in the intent.
5867                 // Load the homepage.
5868                 sharedPreferences.getString("homepage", getString(R.string.homepage_default_value))
5869             }
5870
5871             // Load the website if not waiting for the proxy.
5872             if (waitingForProxy) {  // Store the URL to be loaded in the Nested Scroll WebView.
5873                 nestedScrollWebView.waitingForProxyUrlString = urlToLoadString!!
5874             } else {  // Load the URL.
5875                 loadUrl(nestedScrollWebView, urlToLoadString!!)
5876             }
5877
5878             // Reset the intent.  This prevents a duplicate tab from being created on a subsequent restart if loading an link from a new intent on restart.
5879             // For example, this prevents a duplicate tab if a link is loaded from the Guide after changing the theme in the guide and then changing the theme again in the main activity.
5880             intent = Intent()
5881         } else {  // This is not the first tab.
5882             // Load the URL.
5883             loadUrl(nestedScrollWebView, urlString)
5884
5885             // Set the focus and display the keyboard if the URL is blank.
5886             if (urlString == "") {
5887                 // Request focus for the URL text box.
5888                 urlEditText.requestFocus()
5889
5890                 // Create a display keyboard handler.
5891                 val displayKeyboardHandler = Handler(Looper.getMainLooper())
5892
5893                 // Create a display keyboard runnable.
5894                 val displayKeyboardRunnable = Runnable {
5895                     // Display the keyboard.
5896                     inputMethodManager.showSoftInput(urlEditText, 0)
5897                 }
5898
5899                 // Display the keyboard after 100 milliseconds, which leaves enough time for the tab to transition.
5900                 displayKeyboardHandler.postDelayed(displayKeyboardRunnable, 100)
5901             }
5902         }
5903     }
5904
5905     private fun updateDomainsSettingsSet() {
5906         // Reset the domains settings set.
5907         domainsSettingsSet = HashSet()
5908
5909         // Get a domains cursor.
5910         val domainsCursor = domainsDatabaseHelper!!.domainNameCursorOrderedByDomain
5911
5912         // Get the current count of domains.
5913         val domainsCount = domainsCursor.count
5914
5915         // Get the domain name column index.
5916         val domainNameColumnIndex = domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME)
5917
5918         // Populate the domain settings set.
5919         for (i in 0 until domainsCount) {
5920             // Move the domains cursor to the current row.
5921             domainsCursor.moveToPosition(i)
5922
5923             // Store the domain name in the domain settings set.
5924             domainsSettingsSet.add(domainsCursor.getString(domainNameColumnIndex))
5925         }
5926
5927         // Close the domains cursor.
5928         domainsCursor.close()
5929     }
5930 }