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