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