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