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